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 47static const float kLeftTabInset = 4; 48 49 50TabContainerView::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 67TabContainerView::~TabContainerView() 68{ 69} 70 71 72BSize 73TabContainerView::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 82void 83TabContainerView::MessageReceived(BMessage* message) 84{ 85 switch (message->what) { 86 default: 87 BGroupView::MessageReceived(message); 88 } 89} 90 91 92void 93TabContainerView::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 125void 126TabContainerView::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 150void 151TabContainerView::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 171void 172TabContainerView::MouseMoved(BPoint where, uint32 transit, 173 const BMessage* dragMessage) 174{ 175 _MouseMoved(where, transit, dragMessage); 176} 177 178 179void 180TabContainerView::DoLayout() 181{ 182 BGroupView::DoLayout(); 183 184 _ValidateTabVisibility(); 185 _SendFakeMouseMoved(); 186} 187 188void 189TabContainerView::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 201void 202TabContainerView::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 230TabView* 231TabContainerView::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 283TabView* 284TabContainerView::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 294int32 295TabContainerView::IndexOf(TabView* tab) const 296{ 297 return GroupLayout()->IndexOfItem(tab->LayoutItem()); 298} 299 300 301void 302TabContainerView::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 314void 315TabContainerView::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 342void 343TabContainerView::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 354void 355TabContainerView::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 370int32 371TabContainerView::FirstVisibleTabIndex() const 372{ 373 return fFirstVisibleTabIndex; 374} 375 376 377int32 378TabContainerView::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 406bool 407TabContainerView::CanScrollLeft() const 408{ 409 return fFirstVisibleTabIndex < MaxFirstVisibleTabIndex(); 410} 411 412 413bool 414TabContainerView::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 430TabView* 431TabContainerView::_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 450void 451TabContainerView::_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 477void 478TabContainerView::_ValidateTabVisibility() 479{ 480 if (fFirstVisibleTabIndex > MaxFirstVisibleTabIndex()) 481 SetFirstVisibleTabIndex(MaxFirstVisibleTabIndex()); 482 else 483 _UpdateTabVisibility(); 484} 485 486 487void 488TabContainerView::_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 518float 519TabContainerView::_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 533void 534TabContainerView::_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