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