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 // draw tab frame 79 BRect rect(Bounds()); 80 rgb_color base = ui_color(B_PANEL_BACKGROUND_COLOR); 81 uint32 borders = BControlLook::B_TOP_BORDER 82 | BControlLook::B_BOTTOM_BORDER; 83 be_control_look->DrawTabFrame(this, rect, updateRect, base, 0, 84 borders, B_NO_BORDER); 85 86 // draw tabs on top of frame 87 BGroupLayout* layout = GroupLayout(); 88 int32 count = layout->CountItems() - 1; 89 for (int32 i = 0; i < count; i++) { 90 TabLayoutItem* item = dynamic_cast<TabLayoutItem*>(layout->ItemAt(i)); 91 if (item == NULL || !item->IsVisible()) 92 continue; 93 item->Parent()->Draw(item->Frame()); 94 } 95 } 96 97 98 void 99 TabContainerView::MouseDown(BPoint where) 100 { 101 if (Window() == NULL) 102 return; 103 104 BMessage* currentMessage = Window()->CurrentMessage(); 105 if (currentMessage == NULL) 106 return; 107 108 uint32 buttons; 109 if (currentMessage->FindInt32("buttons", (int32*)&buttons) != B_OK) 110 buttons = B_PRIMARY_MOUSE_BUTTON; 111 112 uint32 clicks; 113 if (currentMessage->FindInt32("clicks", (int32*)&clicks) != B_OK) 114 clicks = 1; 115 116 fMouseDown = true; 117 SetMouseEventMask(B_POINTER_EVENTS, B_LOCK_WINDOW_FOCUS); 118 119 if (fLastMouseEventTab != NULL) 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 != NULL) 176 tab = fController->CreateTabView(); 177 else 178 tab = new TabView(); 179 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 tab->Update(); 194 195 GroupLayout()->AddItem(index, tab->LayoutItem()); 196 197 if (fSelectedTab == NULL) 198 SelectTab(tab); 199 200 bool isLast = index == GroupLayout()->CountItems() - 1; 201 if (isLast) { 202 TabLayoutItem* item 203 = dynamic_cast<TabLayoutItem*>(GroupLayout()->ItemAt(index - 1)); 204 if (item != NULL) 205 item->Parent()->Update(); 206 } 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 if (item == NULL) 220 return NULL; 221 222 BRect dirty(Bounds()); 223 dirty.left = item->Frame().left; 224 TabView* removedTab = item->Parent(); 225 removedTab->SetContainerView(NULL); 226 227 if (removedTab == fLastMouseEventTab) 228 fLastMouseEventTab = NULL; 229 230 // Update tabs after or before the removed tab. 231 item = dynamic_cast<TabLayoutItem*>(GroupLayout()->ItemAt(index)); 232 if (item != NULL) { 233 // This tab is behind the removed tab. 234 TabView* tab = item->Parent(); 235 tab->Update(); 236 if (removedTab == fSelectedTab) { 237 fSelectedTab = NULL; 238 SelectTab(tab); 239 } else if (fController != NULL && tab == fSelectedTab) 240 fController->UpdateSelection(index); 241 } else { 242 // The removed tab was the last tab. 243 item = dynamic_cast<TabLayoutItem*>(GroupLayout()->ItemAt(index - 1)); 244 if (item != NULL) { 245 TabView* tab = item->Parent(); 246 tab->Update(); 247 if (removedTab == fSelectedTab) { 248 fSelectedTab = NULL; 249 SelectTab(tab); 250 } 251 } 252 } 253 254 Invalidate(dirty); 255 _ValidateTabVisibility(); 256 257 return removedTab; 258 } 259 260 261 TabView* 262 TabContainerView::TabAt(int32 index) const 263 { 264 TabLayoutItem* item = dynamic_cast<TabLayoutItem*>( 265 GroupLayout()->ItemAt(index)); 266 if (item != NULL) 267 return item->Parent(); 268 269 return NULL; 270 } 271 272 273 int32 274 TabContainerView::IndexOf(TabView* tab) const 275 { 276 if (tab == NULL || GroupLayout() == NULL) 277 return -1; 278 279 return GroupLayout()->IndexOfItem(tab->LayoutItem()); 280 } 281 282 283 void 284 TabContainerView::SelectTab(int32 index) 285 { 286 TabView* tab = NULL; 287 TabLayoutItem* item = dynamic_cast<TabLayoutItem*>( 288 GroupLayout()->ItemAt(index)); 289 if (item != NULL) 290 tab = item->Parent(); 291 292 SelectTab(tab); 293 } 294 295 296 void 297 TabContainerView::SelectTab(TabView* tab) 298 { 299 if (tab == fSelectedTab) 300 return; 301 302 // update old selected tab 303 if (fSelectedTab != NULL) 304 fSelectedTab->Update(); 305 306 fSelectedTab = tab; 307 308 // update new selected tab 309 if (fSelectedTab != NULL) 310 fSelectedTab->Update(); 311 312 int32 index = -1; 313 if (fSelectedTab != NULL) 314 index = GroupLayout()->IndexOfItem(tab->LayoutItem()); 315 316 if (!tab->LayoutItem()->IsVisible()) 317 SetFirstVisibleTabIndex(index); 318 319 if (fController != NULL) 320 fController->UpdateSelection(index); 321 } 322 323 324 void 325 TabContainerView::SetTabLabel(int32 index, const char* label) 326 { 327 TabLayoutItem* item = dynamic_cast<TabLayoutItem*>( 328 GroupLayout()->ItemAt(index)); 329 if (item == NULL) 330 return; 331 332 item->Parent()->SetLabel(label); 333 } 334 335 336 void 337 TabContainerView::SetFirstVisibleTabIndex(int32 index) 338 { 339 if (index < 0) 340 index = 0; 341 if (index > MaxFirstVisibleTabIndex()) 342 index = MaxFirstVisibleTabIndex(); 343 if (fFirstVisibleTabIndex == index) 344 return; 345 346 fFirstVisibleTabIndex = index; 347 348 _UpdateTabVisibility(); 349 } 350 351 352 int32 353 TabContainerView::FirstVisibleTabIndex() const 354 { 355 return fFirstVisibleTabIndex; 356 } 357 358 359 int32 360 TabContainerView::MaxFirstVisibleTabIndex() const 361 { 362 float availableWidth = _AvailableWidthForTabs(); 363 if (availableWidth < 0) 364 return 0; 365 float visibleTabsWidth = 0; 366 367 BGroupLayout* layout = GroupLayout(); 368 int32 i = layout->CountItems() - 2; 369 for (; i >= 0; i--) { 370 TabLayoutItem* item = dynamic_cast<TabLayoutItem*>( 371 layout->ItemAt(i)); 372 if (item == NULL) 373 continue; 374 375 float itemWidth = item->MinSize().width; 376 if (availableWidth >= visibleTabsWidth + itemWidth) 377 visibleTabsWidth += itemWidth; 378 else { 379 // The tab before this tab is the last one that can be visible. 380 return i + 1; 381 } 382 } 383 384 return 0; 385 } 386 387 388 bool 389 TabContainerView::CanScrollLeft() const 390 { 391 return fFirstVisibleTabIndex < MaxFirstVisibleTabIndex(); 392 } 393 394 395 bool 396 TabContainerView::CanScrollRight() const 397 { 398 BGroupLayout* layout = GroupLayout(); 399 int32 count = layout->CountItems() - 1; 400 if (count > 0) { 401 TabLayoutItem* item = dynamic_cast<TabLayoutItem*>( 402 layout->ItemAt(count - 1)); 403 return !item->IsVisible(); 404 } 405 return false; 406 } 407 408 409 // #pragma mark - 410 411 412 TabView* 413 TabContainerView::_TabAt(const BPoint& where) const 414 { 415 BGroupLayout* layout = GroupLayout(); 416 int32 count = layout->CountItems() - 1; 417 for (int32 i = 0; i < count; i++) { 418 TabLayoutItem* item = dynamic_cast<TabLayoutItem*>(layout->ItemAt(i)); 419 if (item == NULL || !item->IsVisible()) 420 continue; 421 // Account for the fact that the tab frame does not contain the 422 // visible bottom border. 423 BRect frame = item->Frame(); 424 frame.bottom++; 425 if (frame.Contains(where)) 426 return item->Parent(); 427 } 428 return NULL; 429 } 430 431 432 void 433 TabContainerView::_MouseMoved(BPoint where, uint32 _transit, 434 const BMessage* dragMessage) 435 { 436 TabView* tab = _TabAt(where); 437 if (fMouseDown) { 438 uint32 transit = tab == fLastMouseEventTab 439 ? B_INSIDE_VIEW : B_OUTSIDE_VIEW; 440 if (fLastMouseEventTab) 441 fLastMouseEventTab->MouseMoved(where, transit, dragMessage); 442 return; 443 } 444 445 if (fLastMouseEventTab != NULL && fLastMouseEventTab == tab) 446 fLastMouseEventTab->MouseMoved(where, B_INSIDE_VIEW, dragMessage); 447 else { 448 if (fLastMouseEventTab) 449 fLastMouseEventTab->MouseMoved(where, B_EXITED_VIEW, dragMessage); 450 fLastMouseEventTab = tab; 451 if (fLastMouseEventTab) 452 fLastMouseEventTab->MouseMoved(where, B_ENTERED_VIEW, dragMessage); 453 else { 454 fController->SetToolTip( 455 B_TRANSLATE("Double-click or middle-click to open new tab.")); 456 } 457 } 458 } 459 460 461 void 462 TabContainerView::_ValidateTabVisibility() 463 { 464 if (fFirstVisibleTabIndex > MaxFirstVisibleTabIndex()) 465 SetFirstVisibleTabIndex(MaxFirstVisibleTabIndex()); 466 else 467 _UpdateTabVisibility(); 468 } 469 470 471 void 472 TabContainerView::_UpdateTabVisibility() 473 { 474 float availableWidth = _AvailableWidthForTabs(); 475 if (availableWidth < 0) 476 return; 477 float visibleTabsWidth = 0; 478 479 bool canScrollTabsLeft = fFirstVisibleTabIndex > 0; 480 bool canScrollTabsRight = false; 481 482 BGroupLayout* layout = GroupLayout(); 483 int32 count = layout->CountItems() - 1; 484 for (int32 i = 0; i < count; i++) { 485 TabLayoutItem* item = dynamic_cast<TabLayoutItem*>( 486 layout->ItemAt(i)); 487 if (i < fFirstVisibleTabIndex) 488 item->SetVisible(false); 489 else { 490 float itemWidth = item->MinSize().width; 491 bool visible = availableWidth >= visibleTabsWidth + itemWidth; 492 item->SetVisible(visible && !canScrollTabsRight); 493 visibleTabsWidth += itemWidth; 494 if (!visible) 495 canScrollTabsRight = true; 496 } 497 } 498 fController->UpdateTabScrollability(canScrollTabsLeft, canScrollTabsRight); 499 } 500 501 502 float 503 TabContainerView::_AvailableWidthForTabs() const 504 { 505 float width = Bounds().Width() - 10; 506 // TODO: Don't really know why -10 is needed above. 507 508 float left; 509 float right; 510 GroupLayout()->GetInsets(&left, NULL, &right, NULL); 511 width -= left + right; 512 513 return width; 514 } 515 516 517 void 518 TabContainerView::_SendFakeMouseMoved() 519 { 520 BPoint where; 521 uint32 buttons; 522 GetMouse(&where, &buttons, false); 523 if (Bounds().Contains(where)) 524 _MouseMoved(where, B_INSIDE_VIEW, NULL); 525 } 526