1 /* 2 * Copyright (C) 2010 Rene Gollent <rene@gollent.com> 3 * Copyright (C) 2010 Stephan Aßmus <superstippi@gmx.de> 4 * 5 * All rights reserved. Distributed under the terms of the MIT License. 6 */ 7 8 #include "TabContainerView.h" 9 10 #include <stdio.h> 11 12 #include <Application.h> 13 #include <AbstractLayoutItem.h> 14 #include <Bitmap.h> 15 #include <Button.h> 16 #include <CardLayout.h> 17 #include <Catalog.h> 18 #include <ControlLook.h> 19 #include <GroupView.h> 20 #include <SpaceLayoutItem.h> 21 #include <Window.h> 22 23 #include "TabView.h" 24 25 26 #undef B_TRANSLATION_CONTEXT 27 #define B_TRANSLATION_CONTEXT "Tab Manager" 28 29 30 static const float kLeftTabInset = 4; 31 32 33 TabContainerView::TabContainerView(Controller* controller) 34 : 35 BGroupView(B_HORIZONTAL, 0.0), 36 fLastMouseEventTab(NULL), 37 fMouseDown(false), 38 fClickCount(0), 39 fSelectedTab(NULL), 40 fController(controller), 41 fFirstVisibleTabIndex(0) 42 { 43 SetFlags(Flags() | B_WILL_DRAW | B_FULL_UPDATE_ON_RESIZE); 44 SetViewColor(B_TRANSPARENT_COLOR); 45 GroupLayout()->SetInsets(kLeftTabInset, 0, 0, 1); 46 GroupLayout()->AddItem(BSpaceLayoutItem::CreateGlue(), 0.0f); 47 } 48 49 50 TabContainerView::~TabContainerView() 51 { 52 } 53 54 55 BSize 56 TabContainerView::MinSize() 57 { 58 // Eventually, we want to be scrolling if the tabs don't fit. 59 BSize size(BGroupView::MinSize()); 60 size.width = 300; 61 return size; 62 } 63 64 65 void 66 TabContainerView::MessageReceived(BMessage* message) 67 { 68 switch (message->what) { 69 default: 70 BGroupView::MessageReceived(message); 71 } 72 } 73 74 75 void 76 TabContainerView::Draw(BRect updateRect) 77 { 78 rgb_color base = ui_color(B_PANEL_BACKGROUND_COLOR); 79 BRect frame(Bounds()); 80 81 // Draw empty area before first tab. 82 uint32 borders = BControlLook::B_TOP_BORDER | BControlLook::B_BOTTOM_BORDER; 83 BRect leftFrame(frame.left, frame.top, kLeftTabInset, frame.bottom); 84 be_control_look->DrawInactiveTab(this, leftFrame, updateRect, base, 0, 85 borders); 86 87 // Draw all tabs, keeping track of where they end. 88 BGroupLayout* layout = GroupLayout(); 89 int32 count = layout->CountItems() - 1; 90 for (int32 i = 0; i < count; i++) { 91 TabLayoutItem* item = dynamic_cast<TabLayoutItem*>( 92 layout->ItemAt(i)); 93 if (!item || !item->IsVisible()) 94 continue; 95 item->Parent()->Draw(updateRect); 96 frame.left = item->Frame().right + 1; 97 } 98 99 // Draw empty area after last tab. 100 be_control_look->DrawInactiveTab(this, frame, updateRect, base, 0, borders); 101 } 102 103 104 void 105 TabContainerView::MouseDown(BPoint where) 106 { 107 uint32 buttons; 108 if (Window()->CurrentMessage()->FindInt32("buttons", (int32*)&buttons) != B_OK) 109 buttons = B_PRIMARY_MOUSE_BUTTON; 110 uint32 clicks; 111 if (Window()->CurrentMessage()->FindInt32("clicks", (int32*)&clicks) != B_OK) 112 clicks = 1; 113 fMouseDown = true; 114 SetMouseEventMask(B_POINTER_EVENTS, B_LOCK_WINDOW_FOCUS); 115 if (fLastMouseEventTab) 116 fLastMouseEventTab->MouseDown(where, buttons); 117 else { 118 if ((buttons & B_TERTIARY_MOUSE_BUTTON) != 0) { 119 // Middle click outside tabs should always open a new tab. 120 fController->DoubleClickOutsideTabs(); 121 } else if (clicks > 1) 122 fClickCount++; 123 else 124 fClickCount = 1; 125 } 126 } 127 128 129 void 130 TabContainerView::MouseUp(BPoint where) 131 { 132 fMouseDown = false; 133 if (fLastMouseEventTab) { 134 fLastMouseEventTab->MouseUp(where); 135 fClickCount = 0; 136 } else if (fClickCount > 1) { 137 // NOTE: fClickCount is >= 1 only if the first click was outside 138 // any tab. So even if fLastMouseEventTab has been reset to NULL 139 // because this tab was removed during mouse down, we wouldn't 140 // run the "outside tabs" code below. 141 fController->DoubleClickOutsideTabs(); 142 fClickCount = 0; 143 } 144 // Always check the tab under the mouse again, since we don't update 145 // it with fMouseDown == true. 146 _SendFakeMouseMoved(); 147 } 148 149 150 void 151 TabContainerView::MouseMoved(BPoint where, uint32 transit, 152 const BMessage* dragMessage) 153 { 154 _MouseMoved(where, transit, dragMessage); 155 } 156 157 158 void 159 TabContainerView::DoLayout() 160 { 161 BGroupView::DoLayout(); 162 163 _ValidateTabVisibility(); 164 _SendFakeMouseMoved(); 165 } 166 167 void 168 TabContainerView::AddTab(const char* label, int32 index) 169 { 170 TabView* tab; 171 if (fController) 172 tab = fController->CreateTabView(); 173 else 174 tab = new TabView(); 175 tab->SetLabel(label); 176 AddTab(tab, index); 177 } 178 179 180 void 181 TabContainerView::AddTab(TabView* tab, int32 index) 182 { 183 tab->SetContainerView(this); 184 185 if (index == -1) 186 index = GroupLayout()->CountItems() - 1; 187 188 bool hasFrames = fController != NULL && fController->HasFrames(); 189 bool isFirst = index == 0 && hasFrames; 190 bool isLast = index == GroupLayout()->CountItems() - 1 && hasFrames; 191 bool isFront = fSelectedTab == NULL; 192 tab->Update(isFirst, isLast, isFront); 193 194 GroupLayout()->AddItem(index, tab->LayoutItem()); 195 196 if (isFront) 197 SelectTab(tab); 198 if (isLast) { 199 TabLayoutItem* item 200 = dynamic_cast<TabLayoutItem*>(GroupLayout()->ItemAt(index - 1)); 201 if (item) 202 item->Parent()->SetIsLast(false); 203 } 204 205 SetFirstVisibleTabIndex(MaxFirstVisibleTabIndex()); 206 _ValidateTabVisibility(); 207 } 208 209 210 TabView* 211 TabContainerView::RemoveTab(int32 index) 212 { 213 TabLayoutItem* item 214 = dynamic_cast<TabLayoutItem*>(GroupLayout()->RemoveItem(index)); 215 216 if (!item) 217 return NULL; 218 219 BRect dirty(Bounds()); 220 dirty.left = item->Frame().left; 221 TabView* removedTab = item->Parent(); 222 removedTab->SetContainerView(NULL); 223 224 if (removedTab == fLastMouseEventTab) 225 fLastMouseEventTab = NULL; 226 227 // Update tabs after or before the removed tab. 228 bool hasFrames = fController != NULL && fController->HasFrames(); 229 item = dynamic_cast<TabLayoutItem*>(GroupLayout()->ItemAt(index)); 230 if (item) { 231 // This tab is behind the removed tab. 232 TabView* tab = item->Parent(); 233 tab->Update(index == 0 && hasFrames, 234 index == GroupLayout()->CountItems() - 2 && hasFrames, 235 tab == fSelectedTab); 236 if (removedTab == fSelectedTab) { 237 fSelectedTab = NULL; 238 SelectTab(tab); 239 } else if (fController && tab == fSelectedTab) 240 fController->TabSelected(index); 241 } else { 242 // The removed tab was the last tab. 243 item = dynamic_cast<TabLayoutItem*>(GroupLayout()->ItemAt(index - 1)); 244 if (item) { 245 TabView* tab = item->Parent(); 246 tab->Update(index == 0 && hasFrames, 247 index == GroupLayout()->CountItems() - 2 && hasFrames, 248 tab == fSelectedTab); 249 if (removedTab == fSelectedTab) { 250 fSelectedTab = NULL; 251 SelectTab(tab); 252 } 253 } 254 } 255 256 Invalidate(dirty); 257 _ValidateTabVisibility(); 258 259 return removedTab; 260 } 261 262 263 TabView* 264 TabContainerView::TabAt(int32 index) const 265 { 266 TabLayoutItem* item = dynamic_cast<TabLayoutItem*>( 267 GroupLayout()->ItemAt(index)); 268 if (item) 269 return item->Parent(); 270 return NULL; 271 } 272 273 274 int32 275 TabContainerView::IndexOf(TabView* tab) const 276 { 277 return GroupLayout()->IndexOfItem(tab->LayoutItem()); 278 } 279 280 281 void 282 TabContainerView::SelectTab(int32 index) 283 { 284 TabView* tab = NULL; 285 TabLayoutItem* item = dynamic_cast<TabLayoutItem*>( 286 GroupLayout()->ItemAt(index)); 287 if (item) 288 tab = item->Parent(); 289 290 SelectTab(tab); 291 } 292 293 294 void 295 TabContainerView::SelectTab(TabView* tab) 296 { 297 if (tab == fSelectedTab) 298 return; 299 300 if (fSelectedTab) 301 fSelectedTab->SetIsFront(false); 302 303 fSelectedTab = tab; 304 305 if (fSelectedTab) 306 fSelectedTab->SetIsFront(true); 307 308 if (fController != NULL) { 309 int32 index = -1; 310 if (fSelectedTab != NULL) 311 index = GroupLayout()->IndexOfItem(tab->LayoutItem()); 312 313 if (!tab->LayoutItem()->IsVisible()) 314 SetFirstVisibleTabIndex(index); 315 316 fController->TabSelected(index); 317 } 318 } 319 320 321 void 322 TabContainerView::SetTabLabel(int32 tabIndex, const char* label) 323 { 324 TabLayoutItem* item = dynamic_cast<TabLayoutItem*>( 325 GroupLayout()->ItemAt(tabIndex)); 326 if (item == NULL) 327 return; 328 329 item->Parent()->SetLabel(label); 330 } 331 332 333 void 334 TabContainerView::SetFirstVisibleTabIndex(int32 index) 335 { 336 if (index < 0) 337 index = 0; 338 if (index > MaxFirstVisibleTabIndex()) 339 index = MaxFirstVisibleTabIndex(); 340 if (fFirstVisibleTabIndex == index) 341 return; 342 343 fFirstVisibleTabIndex = index; 344 345 _UpdateTabVisibility(); 346 } 347 348 349 int32 350 TabContainerView::FirstVisibleTabIndex() const 351 { 352 return fFirstVisibleTabIndex; 353 } 354 355 356 int32 357 TabContainerView::MaxFirstVisibleTabIndex() const 358 { 359 float availableWidth = _AvailableWidthForTabs(); 360 if (availableWidth < 0) 361 return 0; 362 float visibleTabsWidth = 0; 363 364 BGroupLayout* layout = GroupLayout(); 365 int32 i = layout->CountItems() - 2; 366 for (; i >= 0; i--) { 367 TabLayoutItem* item = dynamic_cast<TabLayoutItem*>( 368 layout->ItemAt(i)); 369 if (item == NULL) 370 continue; 371 372 float itemWidth = item->MinSize().width; 373 if (availableWidth >= visibleTabsWidth + itemWidth) 374 visibleTabsWidth += itemWidth; 375 else { 376 // The tab before this tab is the last one that can be visible. 377 return i + 1; 378 } 379 } 380 381 return 0; 382 } 383 384 385 bool 386 TabContainerView::CanScrollLeft() const 387 { 388 return fFirstVisibleTabIndex < MaxFirstVisibleTabIndex(); 389 } 390 391 392 bool 393 TabContainerView::CanScrollRight() const 394 { 395 BGroupLayout* layout = GroupLayout(); 396 int32 count = layout->CountItems() - 1; 397 if (count > 0) { 398 TabLayoutItem* item = dynamic_cast<TabLayoutItem*>( 399 layout->ItemAt(count - 1)); 400 return !item->IsVisible(); 401 } 402 return false; 403 } 404 405 406 // #pragma mark - 407 408 409 TabView* 410 TabContainerView::_TabAt(const BPoint& where) const 411 { 412 BGroupLayout* layout = GroupLayout(); 413 int32 count = layout->CountItems() - 1; 414 for (int32 i = 0; i < count; i++) { 415 TabLayoutItem* item = dynamic_cast<TabLayoutItem*>(layout->ItemAt(i)); 416 if (item == NULL || !item->IsVisible()) 417 continue; 418 // Account for the fact that the tab frame does not contain the 419 // visible bottom border. 420 BRect frame = item->Frame(); 421 frame.bottom++; 422 if (frame.Contains(where)) 423 return item->Parent(); 424 } 425 return NULL; 426 } 427 428 429 void 430 TabContainerView::_MouseMoved(BPoint where, uint32 _transit, 431 const BMessage* dragMessage) 432 { 433 TabView* tab = _TabAt(where); 434 if (fMouseDown) { 435 uint32 transit = tab == fLastMouseEventTab 436 ? B_INSIDE_VIEW : B_OUTSIDE_VIEW; 437 if (fLastMouseEventTab) 438 fLastMouseEventTab->MouseMoved(where, transit, dragMessage); 439 return; 440 } 441 442 if (fLastMouseEventTab != NULL && fLastMouseEventTab == tab) 443 fLastMouseEventTab->MouseMoved(where, B_INSIDE_VIEW, dragMessage); 444 else { 445 if (fLastMouseEventTab) 446 fLastMouseEventTab->MouseMoved(where, B_EXITED_VIEW, dragMessage); 447 fLastMouseEventTab = tab; 448 if (fLastMouseEventTab) 449 fLastMouseEventTab->MouseMoved(where, B_ENTERED_VIEW, dragMessage); 450 else { 451 fController->SetToolTip( 452 B_TRANSLATE("Double-click or middle-click to open new tab.")); 453 } 454 } 455 } 456 457 458 void 459 TabContainerView::_ValidateTabVisibility() 460 { 461 if (fFirstVisibleTabIndex > MaxFirstVisibleTabIndex()) 462 SetFirstVisibleTabIndex(MaxFirstVisibleTabIndex()); 463 else 464 _UpdateTabVisibility(); 465 } 466 467 468 void 469 TabContainerView::_UpdateTabVisibility() 470 { 471 float availableWidth = _AvailableWidthForTabs(); 472 if (availableWidth < 0) 473 return; 474 float visibleTabsWidth = 0; 475 476 bool canScrollTabsLeft = fFirstVisibleTabIndex > 0; 477 bool canScrollTabsRight = false; 478 479 BGroupLayout* layout = GroupLayout(); 480 int32 count = layout->CountItems() - 1; 481 for (int32 i = 0; i < count; i++) { 482 TabLayoutItem* item = dynamic_cast<TabLayoutItem*>( 483 layout->ItemAt(i)); 484 if (i < fFirstVisibleTabIndex) 485 item->SetVisible(false); 486 else { 487 float itemWidth = item->MinSize().width; 488 bool visible = availableWidth >= visibleTabsWidth + itemWidth; 489 item->SetVisible(visible && !canScrollTabsRight); 490 visibleTabsWidth += itemWidth; 491 if (!visible) 492 canScrollTabsRight = true; 493 } 494 } 495 fController->UpdateTabScrollability(canScrollTabsLeft, canScrollTabsRight); 496 } 497 498 499 float 500 TabContainerView::_AvailableWidthForTabs() const 501 { 502 float width = Bounds().Width() - 10; 503 // TODO: Don't really know why -10 is needed above. 504 505 float left; 506 float right; 507 GroupLayout()->GetInsets(&left, NULL, &right, NULL); 508 width -= left + right; 509 510 return width; 511 } 512 513 514 void 515 TabContainerView::_SendFakeMouseMoved() 516 { 517 BPoint where; 518 uint32 buttons; 519 GetMouse(&where, &buttons, false); 520 if (Bounds().Contains(where)) 521 _MouseMoved(where, B_INSIDE_VIEW, NULL); 522 } 523