diff --git a/src/common/sizer.cpp b/src/common/sizer.cpp index a5a68e5c07..872145da95 100644 --- a/src/common/sizer.cpp +++ b/src/common/sizer.cpp @@ -32,6 +32,7 @@ #endif // WX_PRECOMP #include "wx/display.h" +#include "wx/vector.h" #include "wx/listimpl.cpp" @@ -1987,6 +1988,50 @@ wxSizerItem *wxBoxSizer::AddSpacer(int size) return IsVertical() ? Add(0, size) : Add(size, 0); } +namespace +{ + +/* + Helper of RecalcSizes(): checks if there is enough remaining space for the + min size of the given item and returns its min size or the entire remaining + space depending on which one is greater. + + This function updates the remaining space parameter to account for the size + effectively allocated to the item. + */ +int +GetMinOrRemainingSize(int orient, const wxSizerItem *item, int *remainingSpace_) +{ + int& remainingSpace = *remainingSpace_; + + wxCoord size; + if ( remainingSpace > 0 ) + { + const wxSize sizeMin = item->GetMinSizeWithBorder(); + size = orient == wxHORIZONTAL ? sizeMin.x : sizeMin.y; + + if ( size >= remainingSpace ) + { + // truncate the item to fit in the remaining space, this is better + // than showing it only partially in general, even if both choices + // are bad -- but there is nothing else we can do + size = remainingSpace; + } + + remainingSpace -= size; + } + else // no remaining space + { + // no space at all left, no need to even query the item for its min + // size as we can't give it to it anyhow + size = 0; + } + + return size; +} + +} // anonymous namespace + void wxBoxSizer::RecalcSizes() { if ( m_children.empty() ) @@ -1999,14 +2044,17 @@ void wxBoxSizer::RecalcSizes() // stretchable items (i.e. those with non zero proportion) int delta = totalMajorSize - GetSizeInMajorDir(m_minSize); + // declare loop variables used below: + wxSizerItemList::const_iterator i; // iterator in m_children list + unsigned n = 0; // item index in majorSizes array - // Inform child items about the size in minor direction, that can - // change how much free space we have in major dir and how to distribute it. - int majorMinSum = 0; - wxSizerItemList::const_iterator i ; - for ( i = m_children.begin(); - i != m_children.end(); - ++i ) + + // First, inform item about the available size in minor direction as this + // can change their size in the major direction. Also compute the number of + // visible items and sum of their min sizes in major direction. + + int minMajorSize = 0; + for ( i = m_children.begin(); i != m_children.end(); ++i ) { wxSizerItem * const item = *i; @@ -2023,65 +2071,175 @@ void wxBoxSizer::RecalcSizes() // take too much, so delta should not become negative. delta -= deltaChange; } - majorMinSum += GetSizeInMajorDir(item->GetMinSizeWithBorder()); + minMajorSize += GetSizeInMajorDir(item->GetMinSizeWithBorder()); } - // And update our min size - SizeInMajorDir(m_minSize) = majorMinSum; + + // update our min size and delta which may have changed + SizeInMajorDir(m_minSize) = minMajorSize; + delta = totalMajorSize - minMajorSize; - // might have a new delta now - delta = totalMajorSize - GetSizeInMajorDir(m_minSize); + // space and sum of proportions for the remaining items, both may change + // below + wxCoord remaining = totalMajorSize; + int totalProportion = m_totalProportion; + + // size of the (visible) items in major direction, -1 means "not fixed yet" + wxVector majorSizes(GetItemCount(), wxDefaultCoord); + + + // Check for the degenerated case when we don't have enough space for even + // the min sizes of all the items: in this case we really can't do much + // more than to to allocate the min size to as many of fixed size items as + // possible (on the assumption that variable size items such as text zones + // or list boxes may use scrollbars to show their content even if their + // size is less than min size but that fixed size items such as buttons + // will suffer even more if we don't give them their min size) + if ( totalMajorSize < minMajorSize ) + { + // Second degenerated case pass: allocate min size to all fixed size + // items. + for ( i = m_children.begin(), n = 0; i != m_children.end(); ++i, ++n ) + { + wxSizerItem * const item = *i; + + if ( !item->IsShown() ) + continue; + + // deal with fixed size items only during this pass + if ( item->GetProportion() ) + continue; + + majorSizes[n] = GetMinOrRemainingSize(m_orient, item, &remaining); + } + + + // Third degenerated case pass: allocate min size to all the remaining, + // i.e. non-fixed size, items. + for ( i = m_children.begin(), n = 0; i != m_children.end(); ++i, ++n ) + { + wxSizerItem * const item = *i; + + if ( !item->IsShown() ) + continue; + + // we've already dealt with fixed size items above + if ( !item->GetProportion() ) + continue; + + majorSizes[n] = GetMinOrRemainingSize(m_orient, item, &remaining); + } + } + else // we do have enough space to give at least min sizes to all items + { + // Second and maybe more passes in the non-degenerated case: deal with + // fixed size items and items whose min size is greater than what we + // would allocate to them taking their proportion into account. For + // both of them, we will just use their min size, but for the latter we + // also need to reexamine all the items as the items which fitted + // before we adjusted their size upwards might not fit any more. This + // does make for a quadratic algorithm but it's not obvious how to + // avoid it and hopefully it's not a huge problem in practice as the + // sizers don't have many items usually (and, of course, the algorithm + // still reduces into a linear one if there is enough space for all the + // min sizes). + bool nonFixedSpaceChanged = false; + for ( i = m_children.begin(), n = 0; ; ++i, ++n ) + { + if ( nonFixedSpaceChanged ) + { + i = m_children.begin(); + n = 0; + nonFixedSpaceChanged = false; + } + + // check for the end of the loop only after the check above as + // otherwise we wouldn't do another pass if the last child resulted + // in non fixed space reduction + if ( i == m_children.end() ) + break; + + wxSizerItem * const item = *i; + + if ( !item->IsShown() ) + continue; + + // don't check the item which we had already dealt with during a + // previous pass (this is more than an optimization, the code + // wouldn't work correctly if we kept adjusting for the same item + // over and over again) + if ( majorSizes[n] != wxDefaultCoord ) + continue; + + const wxCoord + minMajor = GetSizeInMajorDir(item->GetMinSizeWithBorder()); + const int propItem = item->GetProportion(); + if ( propItem ) + { + // is the desired size of this item big enough? + if ( (remaining*propItem)/totalProportion >= minMajor ) + { + // yes, it is, we'll determine the real size of this + // item later, for now just leave it as wxDefaultCoord + continue; + } + + // the proportion of this item won't count, it has + // effectively become fixed + totalProportion -= propItem; + } + + // we can already allocate space for this item + majorSizes[n] = minMajor; + + // change the amount of the space remaining to the other items, + // as this can result in not being able to satisfy their + // proportions any more we will need to redo another loop + // iteration + remaining -= minMajor; + + nonFixedSpaceChanged = true; + } + + + // Last by one pass: distribute the remaining space among the non-fixed + // items whose size weren't fixed yet according to their proportions. + for ( i = m_children.begin(), n = 0; i != m_children.end(); ++i, ++n ) + { + wxSizerItem * const item = *i; + + if ( !item->IsShown() ) + continue; + + if ( majorSizes[n] == wxDefaultCoord ) + { + const int propItem = item->GetProportion(); + majorSizes[n] = (remaining*propItem)/totalProportion; + + remaining -= majorSizes[n]; + totalProportion -= propItem; + } + } + } + // the position at which we put the next child wxPoint pt(m_position); - // space remaining for the items - wxCoord majorRemaining = totalMajorSize; - int totalProportion = m_totalProportion; - for ( i = m_children.begin(); - i != m_children.end(); - ++i ) + // Final pass: finally do position the items correctly using their sizes as + // determined above. + for ( i = m_children.begin(), n = 0; i != m_children.end(); ++i, ++n ) { wxSizerItem * const item = *i; if ( !item->IsShown() ) continue; + const int majorSize = majorSizes[n]; + const wxSize sizeThis(item->GetMinSizeWithBorder()); - // adjust the size in the major direction using the proportion - wxCoord majorSize = GetSizeInMajorDir(sizeThis); - - if ( delta > 0 ) - { - // distribute extra space among the items respecting their - // proportions - const int propItem = item->GetProportion(); - if ( propItem ) - { - const int deltaItem = (delta * propItem) / totalProportion; - - majorSize += deltaItem; - - delta -= deltaItem; - totalProportion -= propItem; - } - } - else // delta < 0 - { - // we're not going to have enough space for making all items even - // of their minimal size, check if this item still fits at all and - // truncate it if it doesn't -- even if it means giving it 0 size - // and thus making it invisible because we just can't do anything - // else - if ( majorSize > majorRemaining ) - majorSize = majorRemaining; - - majorRemaining -= majorSize; - } - - // apply the alignment in the minor direction wxPoint posChild(pt); diff --git a/tests/sizers/boxsizer.cpp b/tests/sizers/boxsizer.cpp index c86e319549..3087f6f07c 100644 --- a/tests/sizers/boxsizer.cpp +++ b/tests/sizers/boxsizer.cpp @@ -42,9 +42,11 @@ public: private: CPPUNIT_TEST_SUITE( BoxSizerTestCase ); CPPUNIT_TEST( Size1 ); + CPPUNIT_TEST( Size3 ); CPPUNIT_TEST_SUITE_END(); void Size1(); + void Size3(); wxWindow *m_win; wxSizer *m_sizer; @@ -93,7 +95,7 @@ void BoxSizerTestCase::Size1() m_sizer->Add(child); m_win->Layout(); CPPUNIT_ASSERT_EQUAL( sizeChild, child->GetSize() ); -; + m_sizer->Clear(); m_sizer->Add(child, wxSizerFlags(1)); m_win->Layout(); @@ -117,3 +119,131 @@ void BoxSizerTestCase::Size1() CPPUNIT_ASSERT_EQUAL( sizeTotal, child->GetSize() ); } +void BoxSizerTestCase::Size3() +{ + // check that various combinations of minimal sizes and proportions work as + // expected for different window sizes + static const struct LayoutTestData + { + // proportions of the elements + int prop[3]; + + // minimal sizes of the elements in the sizer direction + int minsize[3]; + + // total size and the expected sizes of the elements + int x, + sizes[3]; + + // if true, don't try the permutations of our test data + bool dontPermute; + + + // Add the given window to the sizer with the corresponding parameters + void AddToSizer(wxSizer *sizer, wxWindow *win, int n) const + { + sizer->Add(win, wxSizerFlags(prop[n])); + sizer->SetItemMinSize(win, wxSize(minsize[n], -1)); + } + + } layoutTestData[] = + { + // some really simple cases (no need to permute those, they're + // symmetrical anyhow) + { { 1, 1, 1, }, { 50, 50, 50, }, 150, { 50, 50, 50, }, true }, + { { 2, 2, 2, }, { 50, 50, 50, }, 600, { 200, 200, 200, }, true }, + + // items with different proportions and min sizes when there is enough + // space to lay them out + { { 1, 2, 3, }, { 0, 0, 0, }, 600, { 100, 200, 300, } }, + { { 1, 2, 3, }, { 100, 100, 100, }, 600, { 100, 200, 300, } }, + { { 1, 2, 3, }, { 100, 50, 50, }, 600, { 100, 200, 300, } }, + { { 0, 1, 1, }, { 200, 100, 100, }, 600, { 200, 200, 200, } }, + { { 0, 1, 2, }, { 300, 100, 100, }, 600, { 300, 100, 200, } }, + { { 0, 1, 1, }, { 100, 50, 50, }, 300, { 100, 100, 100, } }, + { { 0, 1, 2, }, { 100, 50, 50, }, 400, { 100, 100, 200, } }, + + // cases when there is not enough space to lay out the items correctly + // while still respecting their min sizes + { { 0, 1, 1, }, { 100, 150, 50, }, 300, { 100, 150, 50, } }, + { { 1, 2, 3, }, { 100, 100, 100, }, 300, { 100, 100, 100, } }, + { { 1, 2, 3, }, { 100, 50, 50, }, 300, { 100, 80, 120, } }, + { { 1, 2, 3, }, { 100, 10, 10, }, 150, { 100, 20, 30, } }, + + // cases when there is not enough space even for the min sizes (don't + // permute in these cases as the layout does depend on the item order + // because the first ones have priority) + { { 1, 2, 3, }, { 100, 50, 50, }, 150, { 100, 50, 0, }, true }, + { { 1, 2, 3, }, { 100, 100, 100, }, 200, { 100, 100, 0, }, true }, + { { 1, 2, 3, }, { 100, 100, 100, }, 150, { 100, 50, 0, }, true }, + { { 1, 2, 3, }, { 100, 100, 100, }, 50, { 50, 0, 0, }, true }, + { { 1, 2, 3, }, { 100, 100, 100, }, 0, { 0, 0, 0, }, true }, + }; + + wxWindow *child[3]; + child[0] = new wxWindow(m_win, wxID_ANY); + child[1] = new wxWindow(m_win, wxID_ANY); + child[2] = new wxWindow(m_win, wxID_ANY); + + int j; + for ( unsigned i = 0; i < WXSIZEOF(layoutTestData); i++ ) + { + LayoutTestData ltd = layoutTestData[i]; + + // the results shouldn't depend on the order of items except in the + // case when there is not enough space for even the fixed width items + // (in which case the first ones might get enough of it but not the + // last ones) so test a couple of permutations of test data unless + // specifically disabled for this test case + for ( unsigned p = 0; p < 3; p++) + { + switch ( p ) + { + case 0: + // nothing to do, use original data + break; + + case 1: + // exchange first and last elements + wxSwap(ltd.prop[0], ltd.prop[2]); + wxSwap(ltd.minsize[0], ltd.minsize[2]); + wxSwap(ltd.sizes[0], ltd.sizes[2]); + break; + + case 2: + // exchange the original third and second elements + wxSwap(ltd.prop[0], ltd.prop[1]); + wxSwap(ltd.minsize[0], ltd.minsize[1]); + wxSwap(ltd.sizes[0], ltd.sizes[1]); + break; + } + + m_sizer->Clear(); + for ( j = 0; j < WXSIZEOF(child); j++ ) + ltd.AddToSizer(m_sizer, child[j], j); + + m_win->SetClientSize(ltd.x, -1); + m_win->Layout(); + + for ( j = 0; j < WXSIZEOF(child); j++ ) + { + WX_ASSERT_EQUAL_MESSAGE + ( + ( + "test %lu, permutation #%d: wrong size for child #%d " + "for total size %d", + static_cast(i), + static_cast(p), + j, + ltd.x + ), + ltd.sizes[j], child[j]->GetSize().x + ); + } + + // don't try other permutations if explicitly disabled + if ( ltd.dontPermute ) + break; + } + } +}