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