It took me a long time but finally I think I have the correct version of
wxBoxSizer layout code which respects both the proportions and the minimal
sizes of the items, i.e. fixes #11311 (and also fixes the problems of
#10008 although this is actually an independent issue).
I've put the full diff (including changes to the generated files for ease
of testing) at http://trac.wxwidgets.org/attachment/ticket/11311/boxsizer.diff
Please review it if you can and test it with your code. It does result in
the same behaviour as in 2.8 for the minimal sample modified by Julian's
patch to the same ticket provided there is enough space and it also passes
all the unit tests which you can see in the new boxsizer.cpp file which is
part of the diff. So I'm rather confident that this patch indeed implements
the right behaviour -- but please let me know if you find anything wrong
with it!
The rest of this message deals with explaining how the new code works and
can be skipped if you're not interested in it.
I'll start by reminding you what we want to do: recall that the main
problem was that 2.8 code distributed the entire space allocated to the
sizer among its children using their proportions. So if you had a sizer
with 3 children with proportions 1, 2 and 3 and the total size was 600 (I'm
only speaking of the major direction here), then the sizes of the items
would be 100, 200 and 300 respectively. We want to preserve this behaviour
if possible but sometimes it isn't possible to have both the right
proportions and respect the items minimal sizes. For example, if the
minimal size of the first item is 100 then it's easy to see that the
proportions may be only respected if the total size is >= 600. If the total
size is just 300, 2.8 version would yield (50, 100, 150) thus truncating
the first item while possibly leaving too much space for the 2 other ones.
With my patch the first item will still have its min size and the remaining
space will be distributed among the other items according to their
proportions (and min sizes, of course). So in this case, assuming there are
no other min size constraints, the sizes will be (100, 80, 120). IOW, the
first item is treated as being fixed (i.e. proportion == 0) if we can't
respect both its proportion and min size. As an aside, notice that this
fixes a nice bug in 2.8 version in which an item with proportion 1 (or 100
for that matter) could be smaller than an item with proportion 0. With the
patch, increasing an item proportion will never decrease its size which
looks obvious but wasn't actually true before.
To summarize, we want to:
1. Respect all items minimal sizes.
2. Respect their proportions if possible.
3. Lay items out irrespectively of their order.
Let's turn to implementation now. First, it's clear that even (1) alone is
not always possible simply because we may not have enough space for the sum
of min sizes of all the items. This explains the bug #10008 and is actually
not related to the proportions at all (i.e. both 2.8 and 2.9 without this
patch behave equally wrongly here). So we need to handle specifically a
degenerate case when there is simply not enough space for all the items.
E.g. in the example above if the total space is 50 there is no way the
first item can have the size of 100 and forget about the proportions. So
the code deals with this case specifically and simply allocates min size to
fixed size items while it can, then, if anything still remains, to the
other items starting from the first one, and sets the size of the rest of
the items to 0. This is not ideal but we can't work miracles and if there
is not enough space, not everything will be visible and there is nothing we
can do about it. To finish with this case, notice that it's the only one
which breaks (3) as well because the first item(s) will get their min size
while the last one(s) will not. In all the other cases the algorithm does
respect (3).
In the normal case we actually implement a quadratic, i.e. O(N^2) where N
is the number of items in the sizer, algorithm. This is usually a bad idea
but I couldn't find anything better and, also, the number of items in a
(box) sizer will hopefully rarely be much more than a dozen or so, so it
shouldn't be that bad in practice. And the algorithm also reduces to a
linear one if there is enough space for everything and there are no items
with proportion 0 (it could be optimized to remain linear even in presence
of items with proportion 0 and I might do it later if this turns out to be
a real problem). What the algorithm does is basically to assign their final
size to all items with proportion 0 and also to all items with non-zero
proportion whose size computed according to their proportion would turn out
to be less than their min size. Such items are, as mentioned above, handled
as if they were fixed (proportion == 0) and I'll call them pseudo-fixed
below.
The quadratic part comes from the fact that once we subtract the space
needed by a fixed or pseudo-fixed item we need to recheck all the non-fixed
items again as there might not be enough space for them any more even if
there was enough initially. So we restart the loop each time we fix a size
of any item. Clearly, the loop is still going to terminate sooner or later
-- but it may take N iterations for it to do so in the worst case.
Anyhow, once it does terminate we distribute the remaining (after
subtracting the fixed and pseudo-fixed items) part among all the items
using their proportions as usual. It's easy to see that (2) is satisfied if
possible because if there is enough space, the loop above will never set
any non-fixed items sizes and so the sizes of all items will be determined
according to their proportions.
I think the algorithm is finally simple enough. It's also potentially
rather inefficient but, once again, I couldn't find any one-pass algorithm
and I am not sure if it's going to be a problem in practice. I also hope to
have implemented it correctly and tried to write a relatively exhaustive
unit test to prove it. If you can think of other cases to test, please send
patches adding them or ask me to do it. FWIW I'm horrified by the number of
bugs in the original sizer algorithm which could be found by even simplest
unit tests (e.g. even laying out _1_ element didn't work correctly!). If
any further reasons to absolutely require unit tests for any new code were
needed, this would have clenched it.
Any comments and especially test results welcome!
VZ
Everything I've tried so far seems to be working correctly and as
expected. Good job.
--
Robin Dunn
Software Craftsman
http://wxPython.org
Many thanks, I'll compile DialogBlocks against 2.9 and do some testing.
Have to figure out how to work around 2.9 API changes first.
Regards,
Julian
--
Julian Smart, Anthemion Software Ltd.
28/5 Gillespie Crescent, Edinburgh, Midlothian, EH10 4HU
www.anthemion.co.uk | +44 (0)131 229 5306
Tools for writers: www.writerscafe.co.uk
wxWidgets RAD: www.dialogblocks.com
Blog: www.juliansmart.com
Ronny
RK> Overall this patch fixes a couple of layout problems I was having, so it
RK> definitely makes things better.
Thanks for testing it!
RK> But there is now a strange thing
RK> happening with the wxChoice widgets that I use in my wxAuiToolBar. When
RK> I start the application everything is fine. But when I undock the
RK> toolbar and dock it again the height of the wxChoice window is reduced
RK> to only a couple of pixels.
Can this problem be reproduced with a simple patch to the auidemo sample?
Also, notice that wxAUI still uses the old 2.8 wxBoxSizer version for
layout as it copied it into its own code after the changes to wxBoxSizer in
2.9. I did remove wxAuiProportionalBoxSizer and replaced it with wxBoxSizer
locally already as I think there should be no reason to use the former any
more but it's not in the patch. So could you please also try if just
renaming wxAuiProportionalBoxSizer to wxBoxSizer globally in
src/aui/framemanager.cpp change anything?
RK> The combo sample also shows an unusual behaviour. When the main window
RK> is resized the combo boxes get smaller as if they don't have any min
RK> size at all. The same goes for the check box in the top right. I don't
RK> know if these problems are just incorrect uses of sizers that were
RK> masked by the buggy sizer algorithm or if this is a real bug introduced
RK> with the patch. Or it could be a bug in wxChoice and wxCheckBox not
RK> supplying correct min size values.
It's either this or a bug in the sample layout which might not set the min
window size correctly. In any case I don't see any significant difference
here compared with the current trunk version. The only difference is that
the controls were just truncated before while now the last visible control
is always resized to fit (which is intentional, see #10008). However the
window can still be resized to completely hide the checkbox even without
the patch. Or did I miss any change in the behaviour due to the patch?
Thanks,
VZ
And yes, I can provide a simple patch to the aui demo that shows the
problem.
I have currently no access to my development computer to create a real
patch, so I hope the following will suffice.
auidemo.cpp(849)
tb4->SetCustomOverflowItems(prepend_items, append_items);
wxChoice* choice = new wxChoice(tb4, ID_SampleItem+35);
choice->AppendString(wxT("Choice 1"));
tb4->AddControl(choice);
tb4->Realize();
Add the three lines between the SetCustomOverflowItems() and the
Realize() call and you get a choice box on one of the toolbars. Now
undock that toolbar and redock it again to see the very tiny combobox
afterwards.
>
> RK> The combo sample also shows an unusual behaviour. When the main window
> RK> is resized the combo boxes get smaller as if they don't have any min
> RK> size at all. The same goes for the check box in the top right. I don't
> RK> know if these problems are just incorrect uses of sizers that were
> RK> masked by the buggy sizer algorithm or if this is a real bug introduced
> RK> with the patch. Or it could be a bug in wxChoice and wxCheckBox not
> RK> supplying correct min size values.
>
> It's either this or a bug in the sample layout which might not set the min
> window size correctly. In any case I don't see any significant difference
> here compared with the current trunk version. The only difference is that
> the controls were just truncated before while now the last visible control
> is always resized to fit (which is intentional, see #10008). However the
> window can still be resized to completely hide the checkbox even without
> the patch. Or did I miss any change in the behaviour due to the patch?
I have to admit that I did not check the behaviour of the combo sample
before the patch. If it does the same now as it did before the patch
then the problem is somewhere else. Or I am totally wrong in believing
that shrinking a checkbox to one pixel is not a good layout behaviour. :)
Best regards,
Ronny
RK> wxAuiToolBar uses the normal wxBoxSizer to layout its items.
RK> wxAuiProportionalBoxSizer is used for the layout of the pane windows.
Yes, indeed, sorry, I was wrong about this, I just remembered that
wxAuiProportionalBoxSizer was used somewhere in wxAUI but didn't realize it
wasn't used everywhere. Sorry again for the confusion.
RK> And yes, I can provide a simple patch to the aui demo that shows the
RK> problem.
RK> I have currently no access to my development computer to create a real
RK> patch, so I hope the following will suffice.
RK>
RK> auidemo.cpp(849)
RK> tb4->SetCustomOverflowItems(prepend_items, append_items);
RK>
RK> wxChoice* choice = new wxChoice(tb4, ID_SampleItem+35);
RK> choice->AppendString(wxT("Choice 1"));
RK> tb4->AddControl(choice);
RK>
RK> tb4->Realize();
RK>
RK> Add the three lines between the SetCustomOverflowItems() and the
RK> Realize() call and you get a choice box on one of the toolbars. Now
RK> undock that toolbar and redock it again to see the very tiny combobox
RK> afterwards.
Thanks, I do see the problem and this time it's definitely due to the
patch as it behaves correctly in master. I'll check it.
RK> I have to admit that I did not check the behaviour of the combo sample
RK> before the patch. If it does the same now as it did before the patch
RK> then the problem is somewhere else. Or I am totally wrong in believing
RK> that shrinking a checkbox to one pixel is not a good layout behaviour. :)
No, it isn't, but, again, I think it's just a bug in the sample which
doesn't set the min window size correctly. And, of course, if there is not
enough space there is not much else that you can do. Anyhow, it'd be nice
to fix this but as I don't know this sample code and it doesn't seem
related to my changes I won't do it right now.
Regards,
VZ
Regards,
Ronny
RK> > Thanks, I do see the problem and this time it's definitely due to the
RK> > patch as it behaves correctly in master. I'll check it.
RK> Good to hear. If you have another patch I'll be happy to test it.
The fix is below and changes wxChoice to avoid hysteresis which it has
now: if you call SetSize(-1, 10) and SetSize(-1, 23) on it (where 23 is the
default height on my system, yours may be different), it remains of height
10 and doesn't become 23 again. This is, of course, bad on its own and so I
fixed this and now the AUI sample behaves correctly.
However it was also very surprising (at least to me) that the wxChoice
size was reduced below its normal value in the first place. And the culprit
is the call to SetSize(1,1) in wxAuiManager::Update() which, according to
the comment there, "reduces flicker". To be honest I don't understand at
all why is this needed, it seems to me that it can only increase it because
of an extra layout. If it hid and then showed the window I could understand
it but reducing its size to 1 pixel...? Does anybody know if this call is
(still) needed? Under Win7 I don't see any flicker anyhow because of its
double buffering and I don't have time to test elsewhere right now but I
seriously doubt this resize could improve anything anywhere.
Anyhow, thanks again for reporting this bug!
VZ
P.S. The promised patch fixing wxChoice:
--- a/src/msw/choice.cpp
+++ b/src/msw/choice.cpp
@@ -553,8 +553,21 @@ void wxChoice::DoSetSize(int x, int y,
int width, int height,
int sizeFlags)
{
+ const int heightBest = GetBestSize().y;
+
// we need the real height below so get the current one if it's not given
- if ( height != wxDefaultCoord && height != GetBestSize().y )
+ if ( height == wxDefaultCoord )
+ {
+ // height not specified, use the same as before
+ DoGetSize(NULL, &height);
+ }
+ else if ( height == heightBest )
+ {
+ // we don't need to manually manage our height, let the system use the
+ // default one
+ m_heightOwn = wxDefaultCoord;
+ }
+ else // non-default height specified
{
// set our new own height but be careful not to make it too big: the
// native control apparently stores it as a single byte and so setting
@@ -568,10 +581,6 @@ void wxChoice::DoSetSize(int x, int y,
else if ( m_heightOwn < COMBO_HEIGHT_ADJ )
m_heightOwn = COMBO_HEIGHT_ADJ;
}
- else // height not specified
- {
- DoGetSize(NULL, &height);
- }
// the height which we must pass to Windows should be the total height of
Regards,
Ronny
MS> I expect my app's frame (dialog, etc.) to have the needed size
MS> (wxDefaultSize), so the proportions (given stretch factors) are
MS> fulfilled (maybe my expectation is wrong?).
Difficult question. On one hand, if you have 2 windows with min size of
100 it seems logical that the min size of them together should be 200, not
1100 if their proportions are 1 and 10. OTOH I guess you could make an
argument for 1100 as well and apparently it's more compatible with 2.8.
I think/hope however that applications actually relying on this should be
relatively rare and if you really absolutely do need this, it would be
enough to set the size of the second window to its proportion times min
size of the first one.
So I don't think we're going to change this unless there is a great public
outcry.
Regards,
VZ
JS> Vadim Zeitlin wrote:
JS> > On Mon, 10 May 2010 12:11:40 -0700 (PDT) Michael Stratmann <Stra...@MicSt.de> wrote:
JS> >
JS> > MS> I expect my app's frame (dialog, etc.) to have the needed size
JS> > MS> (wxDefaultSize), so the proportions (given stretch factors) are
JS> > MS> fulfilled (maybe my expectation is wrong?).
JS> >
JS> > Difficult question. On one hand, if you have 2 windows with min size of
JS> > 100 it seems logical that the min size of them together should be 200, not
JS> > 1100 if their proportions are 1 and 10. OTOH I guess you could make an
JS> > argument for 1100 as well and apparently it's more compatible with 2.8.
JS> >
JS> > I think/hope however that applications actually relying on this should be
JS> > relatively rare and if you really absolutely do need this, it would be
JS> > enough to set the size of the second window to its proportion times min
JS> > size of the first one.
JS> >
JS> > So I don't think we're going to change this unless there is a great public
JS> > outcry.
JS> >
JS> What I don't quite understand about this is, how can you get the
JS> required layout using stretch factors if you don't know what the dialog
JS> size should be?
Right now only by specifying the minimal sizes explicitly (which is what,
I guess, you mean by "kludging").
JS> So I guess it comes down to whether proportion takes priority over min
JS> size, or vice versa.
Not quite, min sizes must be always respected. It's rather the question of
whether the proportions are vital enough to make the window (possibly
significantly) bigger to accommodate them or not. Personally I really don't
know the right answer as I practically never use proportions other than 0
and 1 anyhow. In fact I'd be curious to know what people use any higher
proportions for.
JS> How about a flag wxPRIORITIZE_PROPORTION which makes the total min size
JS> take proportion into account?
Oh no, please not another flag. If you think/there is consensus that this
is the right behaviour let's just implement it like this. FWIW it shouldn't
be very difficult neither, wxBoxSizer::CalcMin() should just calculate the
least common multiple of proportions and then the min size would be easy to
compute. Which doesn't mean that I wouldn't appreciate if someone else than
me did it...
JS> Or, automatically make this the behaviour if any min sizes are violated
JS> by a lack of space?
Min sizes are never violated by a lack of space unless the available space
is less than the sum of min sizes in which case proportions are really the
last of your problems.
Just to be clear: we're not talking about the layout being wrong here. It
is correct now and this doesn't need to change. We're only talking about
the determination of the minimal (and so initial) size of the sizer.
Regards,
VZ
MS> That ruins a lot, when the app runs on a PC with bigger default font
MS> size. E.g. I want two columns 1:1. Considering T10N I can not even say
MS> for sure which column is bigger, or even which size is needed at all
MS> (French usually needs more space ...).
This is a very good point which I didn't at all think about. And it does
indeed prove that we should calculate the minimal sizer size taking the
proportions into account.
So fixing wxBoxSizer::CalcMin() becomes critical. I'll do it before 3.0
but, again, if anybody can help with this [sooner] it would certainly be
very welcome.
MS> > Min sizes are never violated by a lack of space unless the available space
MS> > is less than the sum of min sizes in which case proportions are really the
MS> > last of your problems.
MS>
MS> For dialogs I agree, but frame size is not adjusted (neither in 2.8),
MS> so min sizes are violated. Is that wanted?
Sorry, I still don't see what do you mean by this, could you please
explain?
Thanks,
VZ
MS> > but, again, if anybody can help with this [sooner] it would certainly be
MS> > very welcome.
MS>
MS> I do not feel qualified :-|
It really shouldn't be difficult, this is just plain C++ code, no GUI
involved. It just needs time to do and test it properly.
MS> > Sorry, I still don't see what do you mean by this, could you please
MS> > explain?
MS>
MS> At main panel size(-1, -1), a wxFrame always gets about 400x240 small.
MS> Independent from its panel's content (e.g. 500x300 big). See Julian's
MS> sample without the SetClientSize() line.
MS>
MS> I do not need that, just that I am irritated and curious, why frame
MS> size is not adjusted according to panel min size and thus sizers' min
MS> sizes are violated.
Sorry, still not clear what exactly is wrong here. If you use a sizer and
call SetSizerAndFit() the frame does adjust to the sizer min size. I don't
know what is Julian's sample but maybe you could make a tiny patch (please
see http://trac.wxwidgets.org/wiki/HowToSubmitPatches) to the minimal
sample showing what you mean?
Regards,
VZ