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