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