1 /* 2 * Copyright 2006-2009, Ingo Weinhold <ingo_weinhold@gmx.de>. 3 * All rights reserved. Distributed under the terms of the MIT License. 4 */ 5 6 7 #include "SplitLayout.h" 8 9 #include <new> 10 #include <stdio.h> 11 12 #include <ControlLook.h> 13 #include <LayoutItem.h> 14 #include <LayoutUtils.h> 15 #include <Message.h> 16 #include <View.h> 17 18 #include "OneElementLayouter.h" 19 #include "SimpleLayouter.h" 20 21 22 using std::nothrow; 23 24 25 // archivng constants 26 namespace { 27 const char* const kItemCollapsibleField = "BSplitLayout:item:collapsible"; 28 const char* const kItemWeightField = "BSplitLayout:item:weight"; 29 const char* const kSpacingField = "BSplitLayout:spacing"; 30 const char* const kSplitterSizeField = "BSplitLayout:splitterSize"; 31 const char* const kIsVerticalField = "BSplitLayout:vertical"; 32 const char* const kInsetsField = "BSplitLayout:insets"; 33 } 34 35 36 class BSplitLayout::ItemLayoutInfo { 37 public: 38 float weight; 39 BRect layoutFrame; 40 BSize min; 41 BSize max; 42 bool isVisible; 43 bool isCollapsible; 44 45 ItemLayoutInfo() 46 : 47 weight(1.0f), 48 layoutFrame(0, 0, -1, -1), 49 min(), 50 max(), 51 isVisible(true), 52 isCollapsible(true) 53 { 54 } 55 }; 56 57 58 class BSplitLayout::ValueRange { 59 public: 60 int32 sumValue; // including spacing 61 int32 previousMin; 62 int32 previousMax; 63 int32 previousSize; 64 int32 nextMin; 65 int32 nextMax; 66 int32 nextSize; 67 }; 68 69 70 class BSplitLayout::SplitterItem : public BLayoutItem { 71 public: 72 SplitterItem(BSplitLayout* layout) 73 : 74 fLayout(layout), 75 fFrame() 76 { 77 } 78 79 80 virtual BSize MinSize() 81 { 82 if (fLayout->Orientation() == B_HORIZONTAL) 83 return BSize(fLayout->SplitterSize() - 1, -1); 84 else 85 return BSize(-1, fLayout->SplitterSize() - 1); 86 } 87 88 virtual BSize MaxSize() 89 { 90 if (fLayout->Orientation() == B_HORIZONTAL) 91 return BSize(fLayout->SplitterSize() - 1, B_SIZE_UNLIMITED); 92 else 93 return BSize(B_SIZE_UNLIMITED, fLayout->SplitterSize() - 1); 94 } 95 96 virtual BSize PreferredSize() 97 { 98 return MinSize(); 99 } 100 101 virtual BAlignment Alignment() 102 { 103 return BAlignment(B_ALIGN_HORIZONTAL_CENTER, B_ALIGN_VERTICAL_CENTER); 104 } 105 106 virtual void SetExplicitMinSize(BSize size) 107 { 108 // not allowed 109 } 110 111 virtual void SetExplicitMaxSize(BSize size) 112 { 113 // not allowed 114 } 115 116 virtual void SetExplicitPreferredSize(BSize size) 117 { 118 // not allowed 119 } 120 121 virtual void SetExplicitAlignment(BAlignment alignment) 122 { 123 // not allowed 124 } 125 126 virtual bool IsVisible() 127 { 128 return true; 129 } 130 131 virtual void SetVisible(bool visible) 132 { 133 // not allowed 134 } 135 136 137 virtual BRect Frame() 138 { 139 return fFrame; 140 } 141 142 virtual void SetFrame(BRect frame) 143 { 144 fFrame = frame; 145 } 146 147 private: 148 BSplitLayout* fLayout; 149 BRect fFrame; 150 }; 151 152 153 // #pragma mark - 154 155 156 BSplitLayout::BSplitLayout(orientation orientation, float spacing) 157 : 158 fOrientation(orientation), 159 fLeftInset(0), 160 fRightInset(0), 161 fTopInset(0), 162 fBottomInset(0), 163 fSplitterSize(6), 164 fSpacing(BControlLook::ComposeSpacing(spacing)), 165 166 fSplitterItems(), 167 fVisibleItems(), 168 fMin(), 169 fMax(), 170 fPreferred(), 171 172 fHorizontalLayouter(NULL), 173 fVerticalLayouter(NULL), 174 fHorizontalLayoutInfo(NULL), 175 fVerticalLayoutInfo(NULL), 176 177 fHeightForWidthItems(), 178 fHeightForWidthVerticalLayouter(NULL), 179 fHeightForWidthHorizontalLayoutInfo(NULL), 180 181 fLayoutValid(false), 182 183 fCachedHeightForWidthWidth(-2), 184 fHeightForWidthVerticalLayouterWidth(-2), 185 fCachedMinHeightForWidth(-1), 186 fCachedMaxHeightForWidth(-1), 187 fCachedPreferredHeightForWidth(-1), 188 189 fDraggingStartPoint(), 190 fDraggingStartValue(0), 191 fDraggingCurrentValue(0), 192 fDraggingSplitterIndex(-1) 193 { 194 } 195 196 197 BSplitLayout::BSplitLayout(BMessage* from) 198 : 199 BAbstractLayout(BUnarchiver::PrepareArchive(from)), 200 fOrientation(B_HORIZONTAL), 201 fLeftInset(0), 202 fRightInset(0), 203 fTopInset(0), 204 fBottomInset(0), 205 fSplitterSize(6), 206 fSpacing(be_control_look->DefaultItemSpacing()), 207 208 fSplitterItems(), 209 fVisibleItems(), 210 fMin(), 211 fMax(), 212 fPreferred(), 213 214 fHorizontalLayouter(NULL), 215 fVerticalLayouter(NULL), 216 fHorizontalLayoutInfo(NULL), 217 fVerticalLayoutInfo(NULL), 218 219 fHeightForWidthItems(), 220 fHeightForWidthVerticalLayouter(NULL), 221 fHeightForWidthHorizontalLayoutInfo(NULL), 222 223 fLayoutValid(false), 224 225 fCachedHeightForWidthWidth(-2), 226 fHeightForWidthVerticalLayouterWidth(-2), 227 fCachedMinHeightForWidth(-1), 228 fCachedMaxHeightForWidth(-1), 229 fCachedPreferredHeightForWidth(-1), 230 231 fDraggingStartPoint(), 232 fDraggingStartValue(0), 233 fDraggingCurrentValue(0), 234 fDraggingSplitterIndex(-1) 235 { 236 BUnarchiver unarchiver(from); 237 238 bool isVertical; 239 status_t err = from->FindBool(kIsVerticalField, &isVertical); 240 if (err != B_OK) { 241 unarchiver.Finish(err); 242 return; 243 } 244 fOrientation = (isVertical) ? B_VERTICAL : B_HORIZONTAL ; 245 246 BRect insets; 247 err = from->FindRect(kInsetsField, &insets); 248 if (err != B_OK) { 249 unarchiver.Finish(err); 250 return; 251 } 252 SetInsets(insets.left, insets.top, insets.right, insets.bottom); 253 254 err = from->FindFloat(kSplitterSizeField, &fSplitterSize); 255 if (err == B_OK) 256 err = from->FindFloat(kSpacingField, &fSpacing); 257 258 unarchiver.Finish(err); 259 } 260 261 262 BSplitLayout::~BSplitLayout() 263 { 264 } 265 266 267 void 268 BSplitLayout::SetInsets(float left, float top, float right, float bottom) 269 { 270 fLeftInset = left; 271 fTopInset = top; 272 fRightInset = right; 273 fBottomInset = bottom; 274 275 InvalidateLayout(); 276 } 277 278 279 void 280 BSplitLayout::GetInsets(float* left, float* top, float* right, 281 float* bottom) const 282 { 283 if (left) 284 *left = fLeftInset; 285 if (top) 286 *top = fTopInset; 287 if (right) 288 *right = fRightInset; 289 if (bottom) 290 *bottom = fBottomInset; 291 } 292 293 294 float 295 BSplitLayout::Spacing() const 296 { 297 return fSpacing; 298 } 299 300 301 void 302 BSplitLayout::SetSpacing(float spacing) 303 { 304 spacing = BControlLook::ComposeSpacing(spacing); 305 if (spacing != fSpacing) { 306 fSpacing = spacing; 307 308 InvalidateLayout(); 309 } 310 } 311 312 313 orientation 314 BSplitLayout::Orientation() const 315 { 316 return fOrientation; 317 } 318 319 320 void 321 BSplitLayout::SetOrientation(orientation orientation) 322 { 323 if (orientation != fOrientation) { 324 fOrientation = orientation; 325 326 InvalidateLayout(); 327 } 328 } 329 330 331 float 332 BSplitLayout::SplitterSize() const 333 { 334 return fSplitterSize; 335 } 336 337 338 void 339 BSplitLayout::SetSplitterSize(float size) 340 { 341 if (size != fSplitterSize) { 342 fSplitterSize = size; 343 344 InvalidateLayout(); 345 } 346 } 347 348 349 BLayoutItem* 350 BSplitLayout::AddView(BView* child) 351 { 352 return BAbstractLayout::AddView(child); 353 } 354 355 356 BLayoutItem* 357 BSplitLayout::AddView(int32 index, BView* child) 358 { 359 return BAbstractLayout::AddView(index, child); 360 } 361 362 363 BLayoutItem* 364 BSplitLayout::AddView(BView* child, float weight) 365 { 366 return AddView(-1, child, weight); 367 } 368 369 370 BLayoutItem* 371 BSplitLayout::AddView(int32 index, BView* child, float weight) 372 { 373 BLayoutItem* item = AddView(index, child); 374 if (item) 375 SetItemWeight(item, weight); 376 377 return item; 378 } 379 380 381 bool 382 BSplitLayout::AddItem(BLayoutItem* item) 383 { 384 return BAbstractLayout::AddItem(item); 385 } 386 387 388 bool 389 BSplitLayout::AddItem(int32 index, BLayoutItem* item) 390 { 391 return BAbstractLayout::AddItem(index, item); 392 } 393 394 395 bool 396 BSplitLayout::AddItem(BLayoutItem* item, float weight) 397 { 398 return AddItem(-1, item, weight); 399 } 400 401 402 bool 403 BSplitLayout::AddItem(int32 index, BLayoutItem* item, float weight) 404 { 405 bool success = AddItem(index, item); 406 if (success) 407 SetItemWeight(item, weight); 408 409 return success; 410 } 411 412 413 float 414 BSplitLayout::ItemWeight(int32 index) const 415 { 416 if (index < 0 || index >= CountItems()) 417 return 0; 418 419 return ItemWeight(ItemAt(index)); 420 } 421 422 423 float 424 BSplitLayout::ItemWeight(BLayoutItem* item) const 425 { 426 if (ItemLayoutInfo* info = _ItemLayoutInfo(item)) 427 return info->weight; 428 return 0; 429 } 430 431 432 void 433 BSplitLayout::SetItemWeight(int32 index, float weight, bool invalidateLayout) 434 { 435 if (index < 0 || index >= CountItems()) 436 return; 437 438 BLayoutItem* item = ItemAt(index); 439 SetItemWeight(item, weight); 440 441 if (fHorizontalLayouter) { 442 int32 visibleIndex = fVisibleItems.IndexOf(item); 443 if (visibleIndex >= 0) { 444 if (fOrientation == B_HORIZONTAL) 445 fHorizontalLayouter->SetWeight(visibleIndex, weight); 446 else 447 fVerticalLayouter->SetWeight(visibleIndex, weight); 448 } 449 } 450 451 if (invalidateLayout) 452 InvalidateLayout(); 453 } 454 455 456 void 457 BSplitLayout::SetItemWeight(BLayoutItem* item, float weight) 458 { 459 if (ItemLayoutInfo* info = _ItemLayoutInfo(item)) 460 info->weight = weight; 461 } 462 463 464 bool 465 BSplitLayout::IsCollapsible(int32 index) const 466 { 467 return _ItemLayoutInfo(ItemAt(index))->isCollapsible; 468 } 469 470 471 void 472 BSplitLayout::SetCollapsible(bool collapsible) 473 { 474 SetCollapsible(0, CountItems() - 1, collapsible); 475 } 476 477 478 void 479 BSplitLayout::SetCollapsible(int32 index, bool collapsible) 480 { 481 SetCollapsible(index, index, collapsible); 482 } 483 484 485 void 486 BSplitLayout::SetCollapsible(int32 first, int32 last, bool collapsible) 487 { 488 for (int32 i = first; i <= last; i++) 489 _ItemLayoutInfo(ItemAt(i))->isCollapsible = collapsible; 490 } 491 492 493 bool 494 BSplitLayout::IsItemCollapsed(int32 index) const 495 { 496 return _ItemLayoutInfo(ItemAt(index))->isVisible; 497 } 498 499 500 void 501 BSplitLayout::SetItemCollapsed(int32 index, bool collapsed) 502 { 503 ItemAt(index)->SetVisible(collapsed); 504 505 InvalidateLayout(true); 506 } 507 508 509 BSize 510 BSplitLayout::BaseMinSize() 511 { 512 _ValidateMinMax(); 513 514 return _AddInsets(fMin); 515 } 516 517 518 BSize 519 BSplitLayout::BaseMaxSize() 520 { 521 _ValidateMinMax(); 522 523 return _AddInsets(fMax); 524 } 525 526 527 BSize 528 BSplitLayout::BasePreferredSize() 529 { 530 _ValidateMinMax(); 531 532 return _AddInsets(fPreferred); 533 } 534 535 536 BAlignment 537 BSplitLayout::BaseAlignment() 538 { 539 return BAbstractLayout::BaseAlignment(); 540 } 541 542 543 bool 544 BSplitLayout::HasHeightForWidth() 545 { 546 _ValidateMinMax(); 547 548 return !fHeightForWidthItems.IsEmpty(); 549 } 550 551 552 void 553 BSplitLayout::GetHeightForWidth(float width, float* min, float* max, 554 float* preferred) 555 { 556 if (!HasHeightForWidth()) 557 return; 558 559 float innerWidth = _SubtractInsets(BSize(width, 0)).width; 560 _InternalGetHeightForWidth(innerWidth, false, min, max, preferred); 561 _AddInsets(min, max, preferred); 562 } 563 564 565 void 566 BSplitLayout::LayoutInvalidated(bool children) 567 { 568 delete fHorizontalLayouter; 569 delete fVerticalLayouter; 570 delete fHorizontalLayoutInfo; 571 delete fVerticalLayoutInfo; 572 573 fHorizontalLayouter = NULL; 574 fVerticalLayouter = NULL; 575 fHorizontalLayoutInfo = NULL; 576 fVerticalLayoutInfo = NULL; 577 578 _InvalidateCachedHeightForWidth(); 579 580 fLayoutValid = false; 581 } 582 583 584 void 585 BSplitLayout::DoLayout() 586 { 587 _ValidateMinMax(); 588 589 // layout the elements 590 BSize size = _SubtractInsets(LayoutArea().Size()); 591 fHorizontalLayouter->Layout(fHorizontalLayoutInfo, size.width); 592 593 Layouter* verticalLayouter; 594 if (HasHeightForWidth()) { 595 float minHeight, maxHeight, preferredHeight; 596 _InternalGetHeightForWidth(size.width, true, &minHeight, &maxHeight, 597 &preferredHeight); 598 size.height = max_c(size.height, minHeight); 599 verticalLayouter = fHeightForWidthVerticalLayouter; 600 } else 601 verticalLayouter = fVerticalLayouter; 602 603 verticalLayouter->Layout(fVerticalLayoutInfo, size.height); 604 605 float xOffset = fLeftInset; 606 float yOffset = fTopInset; 607 float splitterWidth = 0; // pixel counts, no distances 608 float splitterHeight = 0; // 609 float xSpacing = 0; 610 float ySpacing = 0; 611 if (fOrientation == B_HORIZONTAL) { 612 splitterWidth = fSplitterSize; 613 splitterHeight = size.height + 1; 614 xSpacing = fSpacing; 615 } else { 616 splitterWidth = size.width + 1; 617 splitterHeight = fSplitterSize; 618 ySpacing = fSpacing; 619 } 620 621 int itemCount = CountItems(); 622 for (int i = 0; i < itemCount; i++) { 623 // layout the splitter 624 if (i > 0) { 625 SplitterItem* splitterItem = _SplitterItemAt(i - 1); 626 627 _LayoutItem(splitterItem, BRect(xOffset, yOffset, 628 xOffset + splitterWidth - 1, yOffset + splitterHeight - 1), 629 true); 630 631 if (fOrientation == B_HORIZONTAL) 632 xOffset += splitterWidth + xSpacing; 633 else 634 yOffset += splitterHeight + ySpacing; 635 } 636 637 // layout the item 638 BLayoutItem* item = ItemAt(i); 639 int32 visibleIndex = fVisibleItems.IndexOf(item); 640 if (visibleIndex < 0) { 641 _LayoutItem(item, BRect(), false); 642 continue; 643 } 644 645 // get the dimensions of the item 646 float width = fHorizontalLayoutInfo->ElementSize(visibleIndex); 647 float height = fVerticalLayoutInfo->ElementSize(visibleIndex); 648 649 // place the component 650 _LayoutItem(item, BRect(xOffset, yOffset, xOffset + width, 651 yOffset + height), true); 652 653 if (fOrientation == B_HORIZONTAL) 654 xOffset += width + xSpacing + 1; 655 else 656 yOffset += height + ySpacing + 1; 657 } 658 659 fLayoutValid = true; 660 } 661 662 663 BRect 664 BSplitLayout::SplitterItemFrame(int32 index) const 665 { 666 if (SplitterItem* item = _SplitterItemAt(index)) 667 return item->Frame(); 668 return BRect(); 669 } 670 671 672 bool 673 BSplitLayout::IsAboveSplitter(const BPoint& point) const 674 { 675 return _SplitterItemAt(point) != NULL; 676 } 677 678 679 bool 680 BSplitLayout::StartDraggingSplitter(BPoint point) 681 { 682 StopDraggingSplitter(); 683 684 // Layout must be valid. Bail out, if it isn't. 685 if (!fLayoutValid) 686 return false; 687 688 // Things shouldn't be draggable, if we have a >= max layout. 689 BSize size = _SubtractInsets(LayoutArea().Size()); 690 if ((fOrientation == B_HORIZONTAL && size.width >= fMax.width) 691 || (fOrientation == B_VERTICAL && size.height >= fMax.height)) { 692 return false; 693 } 694 695 int32 index = -1; 696 if (_SplitterItemAt(point, &index) != NULL) { 697 fDraggingStartPoint = Owner()->ConvertToScreen(point); 698 fDraggingStartValue = _SplitterValue(index); 699 fDraggingCurrentValue = fDraggingStartValue; 700 fDraggingSplitterIndex = index; 701 702 return true; 703 } 704 705 return false; 706 } 707 708 709 bool 710 BSplitLayout::DragSplitter(BPoint point) 711 { 712 if (fDraggingSplitterIndex < 0) 713 return false; 714 715 point = Owner()->ConvertToScreen(point); 716 717 int32 valueDiff; 718 if (fOrientation == B_HORIZONTAL) 719 valueDiff = int32(point.x - fDraggingStartPoint.x); 720 else 721 valueDiff = int32(point.y - fDraggingStartPoint.y); 722 723 return _SetSplitterValue(fDraggingSplitterIndex, 724 fDraggingStartValue + valueDiff); 725 } 726 727 728 bool 729 BSplitLayout::StopDraggingSplitter() 730 { 731 if (fDraggingSplitterIndex < 0) 732 return false; 733 734 // update the item weights 735 _UpdateSplitterWeights(); 736 737 fDraggingSplitterIndex = -1; 738 739 return true; 740 } 741 742 743 int32 744 BSplitLayout::DraggedSplitter() const 745 { 746 return fDraggingSplitterIndex; 747 } 748 749 750 status_t 751 BSplitLayout::Archive(BMessage* into, bool deep) const 752 { 753 BArchiver archiver(into); 754 status_t err = BAbstractLayout::Archive(into, deep); 755 756 if (err == B_OK) 757 err = into->AddBool(kIsVerticalField, fOrientation == B_VERTICAL); 758 759 if (err == B_OK) { 760 BRect insets(fLeftInset, fTopInset, fRightInset, fBottomInset); 761 err = into->AddRect(kInsetsField, insets); 762 } 763 764 if (err == B_OK) 765 err = into->AddFloat(kSplitterSizeField, fSplitterSize); 766 767 if (err == B_OK) 768 err = into->AddFloat(kSpacingField, fSpacing); 769 770 return archiver.Finish(err); 771 } 772 773 774 BArchivable* 775 BSplitLayout::Instantiate(BMessage* from) 776 { 777 if (validate_instantiation(from, "BSplitLayout")) 778 return new(std::nothrow) BSplitLayout(from); 779 return NULL; 780 } 781 782 783 status_t 784 BSplitLayout::ItemArchived(BMessage* into, BLayoutItem* item, int32 index) const 785 { 786 ItemLayoutInfo* info = _ItemLayoutInfo(item); 787 788 status_t err = into->AddFloat(kItemWeightField, info->weight); 789 if (err == B_OK) 790 err = into->AddBool(kItemCollapsibleField, info->isCollapsible); 791 792 return err; 793 } 794 795 796 status_t 797 BSplitLayout::ItemUnarchived(const BMessage* from, 798 BLayoutItem* item, int32 index) 799 { 800 ItemLayoutInfo* info = _ItemLayoutInfo(item); 801 status_t err = from->FindFloat(kItemWeightField, index, &info->weight); 802 803 if (err == B_OK) { 804 bool* collapsible = &info->isCollapsible; 805 err = from->FindBool(kItemCollapsibleField, index, collapsible); 806 } 807 return err; 808 } 809 810 811 bool 812 BSplitLayout::ItemAdded(BLayoutItem* item, int32 atIndex) 813 { 814 ItemLayoutInfo* itemInfo = new(nothrow) ItemLayoutInfo(); 815 if (!itemInfo) 816 return false; 817 818 if (CountItems() > 1) { 819 SplitterItem* splitter = new(nothrow) SplitterItem(this); 820 ItemLayoutInfo* splitterInfo = new(nothrow) ItemLayoutInfo(); 821 if (!splitter || !splitterInfo || !fSplitterItems.AddItem(splitter)) { 822 delete itemInfo; 823 delete splitter; 824 delete splitterInfo; 825 return false; 826 } 827 splitter->SetLayoutData(splitterInfo); 828 SetItemWeight(splitter, 0); 829 } 830 831 item->SetLayoutData(itemInfo); 832 SetItemWeight(item, 1); 833 return true; 834 } 835 836 837 void 838 BSplitLayout::ItemRemoved(BLayoutItem* item, int32 atIndex) 839 { 840 if (fSplitterItems.CountItems() > 0) { 841 SplitterItem* splitterItem = (SplitterItem*)fSplitterItems.RemoveItem( 842 fSplitterItems.CountItems() - 1); 843 delete _ItemLayoutInfo(splitterItem); 844 delete splitterItem; 845 } 846 847 delete _ItemLayoutInfo(item); 848 item->SetLayoutData(NULL); 849 } 850 851 852 void 853 BSplitLayout::_InvalidateCachedHeightForWidth() 854 { 855 delete fHeightForWidthVerticalLayouter; 856 delete fHeightForWidthHorizontalLayoutInfo; 857 858 fHeightForWidthVerticalLayouter = NULL; 859 fHeightForWidthHorizontalLayoutInfo = NULL; 860 861 fCachedHeightForWidthWidth = -2; 862 fHeightForWidthVerticalLayouterWidth = -2; 863 } 864 865 866 BSplitLayout::SplitterItem* 867 BSplitLayout::_SplitterItemAt(const BPoint& point, int32* index) const 868 { 869 int32 splitterCount = fSplitterItems.CountItems(); 870 for (int32 i = 0; i < splitterCount; i++) { 871 SplitterItem* splitItem = _SplitterItemAt(i); 872 BRect frame = splitItem->Frame(); 873 if (frame.Contains(point)) { 874 if (index != NULL) 875 *index = i; 876 return splitItem; 877 } 878 } 879 return NULL; 880 } 881 882 883 BSplitLayout::SplitterItem* 884 BSplitLayout::_SplitterItemAt(int32 index) const 885 { 886 return (SplitterItem*)fSplitterItems.ItemAt(index); 887 } 888 889 890 void 891 BSplitLayout::_GetSplitterValueRange(int32 index, ValueRange& range) 892 { 893 ItemLayoutInfo* previousInfo = _ItemLayoutInfo(ItemAt(index)); 894 ItemLayoutInfo* nextInfo = _ItemLayoutInfo(ItemAt(index + 1)); 895 if (fOrientation == B_HORIZONTAL) { 896 range.previousMin = (int32)previousInfo->min.width + 1; 897 range.previousMax = (int32)previousInfo->max.width + 1; 898 range.previousSize = previousInfo->layoutFrame.IntegerWidth() + 1; 899 range.nextMin = (int32)nextInfo->min.width + 1; 900 range.nextMax = (int32)nextInfo->max.width + 1; 901 range.nextSize = nextInfo->layoutFrame.IntegerWidth() + 1; 902 } else { 903 range.previousMin = (int32)previousInfo->min.height + 1; 904 range.previousMax = (int32)previousInfo->max.height + 1; 905 range.previousSize = previousInfo->layoutFrame.IntegerHeight() + 1; 906 range.nextMin = (int32)nextInfo->min.height + 1; 907 range.nextMax = (int32)nextInfo->max.height + 1; 908 range.nextSize = (int32)nextInfo->layoutFrame.IntegerHeight() + 1; 909 } 910 911 range.sumValue = range.previousSize + range.nextSize; 912 if (previousInfo->isVisible) 913 range.sumValue += (int32)fSpacing; 914 if (nextInfo->isVisible) 915 range.sumValue += (int32)fSpacing; 916 } 917 918 919 int32 920 BSplitLayout::_SplitterValue(int32 index) const 921 { 922 ItemLayoutInfo* info = _ItemLayoutInfo(ItemAt(index)); 923 if (info && info->isVisible) { 924 if (fOrientation == B_HORIZONTAL) 925 return info->layoutFrame.IntegerWidth() + 1 + (int32)fSpacing; 926 else 927 return info->layoutFrame.IntegerHeight() + 1 + (int32)fSpacing; 928 } else 929 return 0; 930 } 931 932 933 void 934 BSplitLayout::_LayoutItem(BLayoutItem* item, BRect frame, bool visible) 935 { 936 // update the layout frame 937 ItemLayoutInfo* info = _ItemLayoutInfo(item); 938 info->isVisible = visible; 939 if (visible) 940 info->layoutFrame = frame; 941 else 942 info->layoutFrame = BRect(0, 0, -1, -1); 943 944 // update min/max 945 info->min = item->MinSize(); 946 info->max = item->MaxSize(); 947 948 if (item->HasHeightForWidth()) { 949 BSize size = _SubtractInsets(LayoutArea().Size()); 950 float minHeight, maxHeight; 951 item->GetHeightForWidth(size.width, &minHeight, &maxHeight, NULL); 952 info->min.height = max_c(info->min.height, minHeight); 953 info->max.height = min_c(info->max.height, maxHeight); 954 } 955 956 // layout the item 957 if (visible) 958 item->AlignInFrame(frame); 959 } 960 961 962 void 963 BSplitLayout::_LayoutItem(BLayoutItem* item, ItemLayoutInfo* info) 964 { 965 // update the visibility of the item 966 bool isVisible = item->IsVisible(); 967 bool visibilityChanged = (info->isVisible != isVisible); 968 if (visibilityChanged) 969 item->SetVisible(info->isVisible); 970 971 // nothing more to do, if the item is not visible 972 if (!info->isVisible) 973 return; 974 975 item->AlignInFrame(info->layoutFrame); 976 977 // if the item became visible, we need to update its internal layout 978 if (visibilityChanged && 979 (fOrientation != B_HORIZONTAL || !HasHeightForWidth())) { 980 item->Relayout(true); 981 } 982 } 983 984 985 bool 986 BSplitLayout::_SetSplitterValue(int32 index, int32 value) 987 { 988 // if both items are collapsed, nothing can be dragged 989 BLayoutItem* previousItem = ItemAt(index); 990 BLayoutItem* nextItem = ItemAt(index + 1); 991 ItemLayoutInfo* previousInfo = _ItemLayoutInfo(previousItem); 992 ItemLayoutInfo* nextInfo = _ItemLayoutInfo(nextItem); 993 ItemLayoutInfo* splitterInfo = _ItemLayoutInfo(_SplitterItemAt(index)); 994 bool previousVisible = previousInfo->isVisible; 995 bool nextVisible = nextInfo->isVisible; 996 if (!previousVisible && !nextVisible) 997 return false; 998 999 ValueRange range; 1000 _GetSplitterValueRange(index, range); 1001 1002 value = max_c(min_c(value, range.sumValue), -(int32)fSpacing); 1003 1004 int32 previousSize = value - (int32)fSpacing; 1005 int32 nextSize = range.sumValue - value - (int32)fSpacing; 1006 1007 // Note: While this collapsed-check is mathmatically correct (i.e. we 1008 // collapse an item, if it would become smaller than half its minimum 1009 // size), we might want to change it, since for the user it looks like 1010 // collapsing happens earlier. The reason being that the only visual mark 1011 // the user has is the mouse cursor which indeed hasn't crossed the middle 1012 // of the item yet. 1013 bool previousCollapsed = (previousSize <= range.previousMin / 2) 1014 && previousInfo->isCollapsible; 1015 bool nextCollapsed = (nextSize <= range.nextMin / 2) 1016 && nextInfo->isCollapsible; 1017 if (previousCollapsed && nextCollapsed) { 1018 // we cannot collapse both items; we have to decide for one 1019 if (previousSize < nextSize) { 1020 // collapse previous 1021 nextCollapsed = false; 1022 nextSize = range.sumValue - (int32)fSpacing; 1023 } else { 1024 // collapse next 1025 previousCollapsed = false; 1026 previousSize = range.sumValue - (int32)fSpacing; 1027 } 1028 } 1029 1030 if (previousCollapsed || nextCollapsed) { 1031 // one collapsed item -- check whether that violates the constraints 1032 // of the other one 1033 int32 availableSpace = range.sumValue - (int32)fSpacing; 1034 if (previousCollapsed) { 1035 if (availableSpace < range.nextMin 1036 || availableSpace > range.nextMax) { 1037 // we cannot collapse the previous item 1038 previousCollapsed = false; 1039 } 1040 } else { 1041 if (availableSpace < range.previousMin 1042 || availableSpace > range.previousMax) { 1043 // we cannot collapse the next item 1044 nextCollapsed = false; 1045 } 1046 } 1047 } 1048 1049 if (!(previousCollapsed || nextCollapsed)) { 1050 // no collapsed item -- check whether there is a close solution 1051 previousSize = value - (int32)fSpacing; 1052 nextSize = range.sumValue - value - (int32)fSpacing; 1053 1054 if (range.previousMin + range.nextMin + 2 * fSpacing > range.sumValue) { 1055 // we don't have enough space to uncollapse both items 1056 int32 availableSpace = range.sumValue - (int32)fSpacing; 1057 if (previousSize < nextSize && availableSpace >= range.nextMin 1058 && availableSpace <= range.nextMax 1059 && previousInfo->isCollapsible) { 1060 previousCollapsed = true; 1061 } else if (availableSpace >= range.previousMin 1062 && availableSpace <= range.previousMax 1063 && nextInfo->isCollapsible) { 1064 nextCollapsed = true; 1065 } else if (availableSpace >= range.nextMin 1066 && availableSpace <= range.nextMax 1067 && previousInfo->isCollapsible) { 1068 previousCollapsed = true; 1069 } else { 1070 if (previousSize < nextSize && previousInfo->isCollapsible) { 1071 previousCollapsed = true; 1072 } else if (nextInfo->isCollapsible) { 1073 nextCollapsed = true; 1074 } else { 1075 // Neither item is collapsible although there's not enough 1076 // space: Give them both their minimum size. 1077 previousSize = range.previousMin; 1078 nextSize = range.nextMin; 1079 } 1080 } 1081 1082 } else { 1083 // there is enough space for both items 1084 // make sure the min constraints are satisfied 1085 if (previousSize < range.previousMin) { 1086 previousSize = range.previousMin; 1087 nextSize = range.sumValue - previousSize - 2 * (int32)fSpacing; 1088 } else if (nextSize < range.nextMin) { 1089 nextSize = range.nextMin; 1090 previousSize = range.sumValue - nextSize - 2 * (int32)fSpacing; 1091 } 1092 1093 // if we can, also satisfy the max constraints 1094 if (range.previousMax + range.nextMax + 2 * (int32)fSpacing 1095 >= range.sumValue) { 1096 if (previousSize > range.previousMax) { 1097 previousSize = range.previousMax; 1098 nextSize = range.sumValue - previousSize 1099 - 2 * (int32)fSpacing; 1100 } else if (nextSize > range.nextMax) { 1101 nextSize = range.nextMax; 1102 previousSize = range.sumValue - nextSize 1103 - 2 * (int32)fSpacing; 1104 } 1105 } 1106 } 1107 } 1108 1109 // compute the size for one collapsed item; for none collapsed item we 1110 // already have correct values 1111 if (previousCollapsed || nextCollapsed) { 1112 int32 availableSpace = range.sumValue - (int32)fSpacing; 1113 if (previousCollapsed) { 1114 previousSize = 0; 1115 nextSize = availableSpace; 1116 } else { 1117 previousSize = availableSpace; 1118 nextSize = 0; 1119 } 1120 } 1121 1122 int32 newValue = previousSize + (previousCollapsed ? 0 : (int32)fSpacing); 1123 if (newValue == fDraggingCurrentValue) { 1124 // nothing changed 1125 return false; 1126 } 1127 1128 // something changed: we need to recompute the layout 1129 int32 baseOffset = -fDraggingCurrentValue; 1130 // offset to the current splitter position 1131 int32 splitterOffset = baseOffset + newValue; 1132 int32 nextOffset = splitterOffset + (int32)fSplitterSize + (int32)fSpacing; 1133 1134 BRect splitterFrame(splitterInfo->layoutFrame); 1135 if (fOrientation == B_HORIZONTAL) { 1136 // horizontal layout 1137 // previous item 1138 float left = splitterFrame.left + baseOffset; 1139 previousInfo->layoutFrame.Set( 1140 left, 1141 splitterFrame.top, 1142 left + previousSize - 1, 1143 splitterFrame.bottom); 1144 1145 // next item 1146 left = splitterFrame.left + nextOffset; 1147 nextInfo->layoutFrame.Set( 1148 left, 1149 splitterFrame.top, 1150 left + nextSize - 1, 1151 splitterFrame.bottom); 1152 1153 // splitter 1154 splitterInfo->layoutFrame.left += splitterOffset; 1155 splitterInfo->layoutFrame.right += splitterOffset; 1156 } else { 1157 // vertical layout 1158 // previous item 1159 float top = splitterFrame.top + baseOffset; 1160 previousInfo->layoutFrame.Set( 1161 splitterFrame.left, 1162 top, 1163 splitterFrame.right, 1164 top + previousSize - 1); 1165 1166 // next item 1167 top = splitterFrame.top + nextOffset; 1168 nextInfo->layoutFrame.Set( 1169 splitterFrame.left, 1170 top, 1171 splitterFrame.right, 1172 top + nextSize - 1); 1173 1174 // splitter 1175 splitterInfo->layoutFrame.top += splitterOffset; 1176 splitterInfo->layoutFrame.bottom += splitterOffset; 1177 } 1178 1179 previousInfo->isVisible = !previousCollapsed; 1180 nextInfo->isVisible = !nextCollapsed; 1181 1182 bool heightForWidth = (fOrientation == B_HORIZONTAL && HasHeightForWidth()); 1183 1184 // If the item visibility is to be changed, we need to update the splitter 1185 // values now, since the visibility change will cause an invalidation. 1186 if (previousVisible != previousInfo->isVisible 1187 || nextVisible != nextInfo->isVisible || heightForWidth) { 1188 _UpdateSplitterWeights(); 1189 } 1190 1191 // If we have height for width items, we need to invalidate the previous 1192 // and the next item. Actually we would only need to invalidate height for 1193 // width items, but since non height for width items might be aligned with 1194 // height for width items, we need to trigger a layout that creates a 1195 // context that spans all aligned items. 1196 // We invalidate already here, so that changing the items' size won't cause 1197 // an immediate relayout. 1198 if (heightForWidth) { 1199 previousItem->InvalidateLayout(); 1200 nextItem->InvalidateLayout(); 1201 } 1202 1203 // do the layout 1204 _LayoutItem(previousItem, previousInfo); 1205 _LayoutItem(_SplitterItemAt(index), splitterInfo); 1206 _LayoutItem(nextItem, nextInfo); 1207 1208 fDraggingCurrentValue = newValue; 1209 1210 return true; 1211 } 1212 1213 1214 BSplitLayout::ItemLayoutInfo* 1215 BSplitLayout::_ItemLayoutInfo(BLayoutItem* item) const 1216 { 1217 return (ItemLayoutInfo*)item->LayoutData(); 1218 } 1219 1220 1221 void 1222 BSplitLayout::_UpdateSplitterWeights() 1223 { 1224 int32 count = CountItems(); 1225 for (int32 i = 0; i < count; i++) { 1226 float weight; 1227 if (fOrientation == B_HORIZONTAL) 1228 weight = _ItemLayoutInfo(ItemAt(i))->layoutFrame.Width() + 1; 1229 else 1230 weight = _ItemLayoutInfo(ItemAt(i))->layoutFrame.Height() + 1; 1231 1232 SetItemWeight(i, weight, false); 1233 } 1234 1235 // Just updating the splitter weights is fine in principle. The next 1236 // LayoutItems() will use the correct values. But, if our orientation is 1237 // vertical, the cached height for width info needs to be flushed, or the 1238 // obsolete cached values will be used. 1239 if (fOrientation == B_VERTICAL) 1240 _InvalidateCachedHeightForWidth(); 1241 } 1242 1243 1244 void 1245 BSplitLayout::_ValidateMinMax() 1246 { 1247 if (fHorizontalLayouter != NULL) 1248 return; 1249 1250 fLayoutValid = false; 1251 1252 fVisibleItems.MakeEmpty(); 1253 fHeightForWidthItems.MakeEmpty(); 1254 1255 _InvalidateCachedHeightForWidth(); 1256 1257 // filter the visible items 1258 int32 itemCount = CountItems(); 1259 for (int32 i = 0; i < itemCount; i++) { 1260 BLayoutItem* item = ItemAt(i); 1261 if (item->IsVisible()) 1262 fVisibleItems.AddItem(item); 1263 1264 // Add "height for width" items even, if they aren't visible. Otherwise 1265 // we may get our parent into trouble, since we could change from 1266 // "height for width" to "not height for width". 1267 if (item->HasHeightForWidth()) 1268 fHeightForWidthItems.AddItem(item); 1269 } 1270 itemCount = fVisibleItems.CountItems(); 1271 1272 // create the layouters 1273 Layouter* itemLayouter = new SimpleLayouter(itemCount, 0); 1274 1275 if (fOrientation == B_HORIZONTAL) { 1276 fHorizontalLayouter = itemLayouter; 1277 fVerticalLayouter = new OneElementLayouter(); 1278 } else { 1279 fHorizontalLayouter = new OneElementLayouter(); 1280 fVerticalLayouter = itemLayouter; 1281 } 1282 1283 // tell the layouters about our constraints 1284 if (itemCount > 0) { 1285 for (int32 i = 0; i < itemCount; i++) { 1286 BLayoutItem* item = (BLayoutItem*)fVisibleItems.ItemAt(i); 1287 BSize min = item->MinSize(); 1288 BSize max = item->MaxSize(); 1289 BSize preferred = item->PreferredSize(); 1290 1291 fHorizontalLayouter->AddConstraints(i, 1, min.width, max.width, 1292 preferred.width); 1293 fVerticalLayouter->AddConstraints(i, 1, min.height, max.height, 1294 preferred.height); 1295 1296 float weight = ItemWeight(item); 1297 fHorizontalLayouter->SetWeight(i, weight); 1298 fVerticalLayouter->SetWeight(i, weight); 1299 } 1300 } 1301 1302 fMin.width = fHorizontalLayouter->MinSize(); 1303 fMin.height = fVerticalLayouter->MinSize(); 1304 fMax.width = fHorizontalLayouter->MaxSize(); 1305 fMax.height = fVerticalLayouter->MaxSize(); 1306 fPreferred.width = fHorizontalLayouter->PreferredSize(); 1307 fPreferred.height = fVerticalLayouter->PreferredSize(); 1308 1309 fHorizontalLayoutInfo = fHorizontalLayouter->CreateLayoutInfo(); 1310 if (fHeightForWidthItems.IsEmpty()) 1311 fVerticalLayoutInfo = fVerticalLayouter->CreateLayoutInfo(); 1312 1313 ResetLayoutInvalidation(); 1314 } 1315 1316 1317 void 1318 BSplitLayout::_InternalGetHeightForWidth(float width, bool realLayout, 1319 float* minHeight, float* maxHeight, float* preferredHeight) 1320 { 1321 if ((realLayout && fHeightForWidthVerticalLayouterWidth != width) 1322 || (!realLayout && fCachedHeightForWidthWidth != width)) { 1323 // The general strategy is to clone the vertical layouter, which only 1324 // knows the general min/max constraints, do a horizontal layout for the 1325 // given width, and add the children's height for width constraints to 1326 // the cloned vertical layouter. If this method is invoked internally, 1327 // we keep the cloned vertical layouter, for it will be used for doing 1328 // the layout. Otherwise we just drop it after we've got the height for 1329 // width info. 1330 1331 // clone the vertical layouter and get the horizontal layout info to be used 1332 LayoutInfo* horizontalLayoutInfo = NULL; 1333 Layouter* verticalLayouter = fVerticalLayouter->CloneLayouter(); 1334 if (realLayout) { 1335 horizontalLayoutInfo = fHorizontalLayoutInfo; 1336 delete fHeightForWidthVerticalLayouter; 1337 fHeightForWidthVerticalLayouter = verticalLayouter; 1338 delete fVerticalLayoutInfo; 1339 fVerticalLayoutInfo = verticalLayouter->CreateLayoutInfo(); 1340 fHeightForWidthVerticalLayouterWidth = width; 1341 } else { 1342 if (fHeightForWidthHorizontalLayoutInfo == NULL) { 1343 delete fHeightForWidthHorizontalLayoutInfo; 1344 fHeightForWidthHorizontalLayoutInfo 1345 = fHorizontalLayouter->CreateLayoutInfo(); 1346 } 1347 horizontalLayoutInfo = fHeightForWidthHorizontalLayoutInfo; 1348 } 1349 1350 // do the horizontal layout (already done when doing this for the real 1351 // layout) 1352 if (!realLayout) 1353 fHorizontalLayouter->Layout(horizontalLayoutInfo, width); 1354 1355 // add the children's height for width constraints 1356 int32 count = fHeightForWidthItems.CountItems(); 1357 for (int32 i = 0; i < count; i++) { 1358 BLayoutItem* item = (BLayoutItem*)fHeightForWidthItems.ItemAt(i); 1359 int32 index = fVisibleItems.IndexOf(item); 1360 if (index >= 0) { 1361 float itemMinHeight, itemMaxHeight, itemPreferredHeight; 1362 item->GetHeightForWidth( 1363 horizontalLayoutInfo->ElementSize(index), 1364 &itemMinHeight, &itemMaxHeight, &itemPreferredHeight); 1365 verticalLayouter->AddConstraints(index, 1, itemMinHeight, 1366 itemMaxHeight, itemPreferredHeight); 1367 } 1368 } 1369 1370 // get the height for width info 1371 fCachedHeightForWidthWidth = width; 1372 fCachedMinHeightForWidth = verticalLayouter->MinSize(); 1373 fCachedMaxHeightForWidth = verticalLayouter->MaxSize(); 1374 fCachedPreferredHeightForWidth = verticalLayouter->PreferredSize(); 1375 } 1376 1377 if (minHeight) 1378 *minHeight = fCachedMinHeightForWidth; 1379 if (maxHeight) 1380 *maxHeight = fCachedMaxHeightForWidth; 1381 if (preferredHeight) 1382 *preferredHeight = fCachedPreferredHeightForWidth; 1383 } 1384 1385 1386 float 1387 BSplitLayout::_SplitterSpace() const 1388 { 1389 int32 splitters = fSplitterItems.CountItems(); 1390 float space = 0; 1391 if (splitters > 0) { 1392 space = (fVisibleItems.CountItems() + splitters - 1) * fSpacing 1393 + splitters * fSplitterSize; 1394 } 1395 1396 return space; 1397 } 1398 1399 1400 BSize 1401 BSplitLayout::_AddInsets(BSize size) 1402 { 1403 size.width = BLayoutUtils::AddDistances(size.width, 1404 fLeftInset + fRightInset - 1); 1405 size.height = BLayoutUtils::AddDistances(size.height, 1406 fTopInset + fBottomInset - 1); 1407 1408 float spacing = _SplitterSpace(); 1409 if (fOrientation == B_HORIZONTAL) 1410 size.width = BLayoutUtils::AddDistances(size.width, spacing - 1); 1411 else 1412 size.height = BLayoutUtils::AddDistances(size.height, spacing - 1); 1413 1414 return size; 1415 } 1416 1417 1418 void 1419 BSplitLayout::_AddInsets(float* minHeight, float* maxHeight, 1420 float* preferredHeight) 1421 { 1422 float insets = fTopInset + fBottomInset - 1; 1423 if (fOrientation == B_VERTICAL) 1424 insets += _SplitterSpace(); 1425 if (minHeight) 1426 *minHeight = BLayoutUtils::AddDistances(*minHeight, insets); 1427 if (maxHeight) 1428 *maxHeight = BLayoutUtils::AddDistances(*maxHeight, insets); 1429 if (preferredHeight) 1430 *preferredHeight = BLayoutUtils::AddDistances(*preferredHeight, insets); 1431 } 1432 1433 1434 BSize 1435 BSplitLayout::_SubtractInsets(BSize size) 1436 { 1437 size.width = BLayoutUtils::SubtractDistances(size.width, 1438 fLeftInset + fRightInset - 1); 1439 size.height = BLayoutUtils::SubtractDistances(size.height, 1440 fTopInset + fBottomInset - 1); 1441 1442 float spacing = _SplitterSpace(); 1443 if (fOrientation == B_HORIZONTAL) 1444 size.width = BLayoutUtils::SubtractDistances(size.width, spacing - 1); 1445 else 1446 size.height = BLayoutUtils::SubtractDistances(size.height, spacing - 1); 1447 1448 return size; 1449 } 1450 1451