Bug 103863 - Support encapsulation in SWT layout mechanism
Summary: Support encapsulation in SWT layout mechanism
Status: RESOLVED FIXED
Alias: None
Product: Platform
Classification: Eclipse Project
Component: SWT (show other bugs)
Version: 3.1   Edit
Hardware: PC Windows XP
: P3 enhancement (vote)
Target Milestone: 3.6 M3   Edit
Assignee: Silenio Quarti CLA
QA Contact:
URL:
Whiteboard:
Keywords:
Depends on:
Blocks:
 
Reported: 2005-07-14 13:29 EDT by Stefan Xenos CLA
Modified: 2015-11-12 16:08 EST (History)
14 users (show)

See Also:


Attachments
Suggested algorithm (implemented through JFace) (38.64 KB, patch)
2005-07-14 13:37 EDT, Stefan Xenos CLA
no flags Details | Diff
Standalone SWT applications that demonstrate the problem (23.11 KB, application/octet-stream)
2005-07-14 13:47 EDT, Stefan Xenos CLA
no flags Details
SWT-only snippet (5.22 KB, application/octet-stream)
2007-06-11 16:31 EDT, Boris Bokowski CLA
no flags Details
Improved SWT-only example that demonstrates the extra repaints (5.27 KB, text/x-java)
2008-06-13 16:39 EDT, Stefan Xenos CLA
no flags Details
Tests a layout with an excessive number of widgets (6.23 KB, text/x-java)
2008-06-13 18:27 EDT, Stefan Xenos CLA
no flags Details

Note You need to log in before you can comment on or make changes to this bug.
Description Stefan Xenos CLA 2005-07-14 13:29:52 EDT
Currently, SWT layouts work like this:

- Some event occurs in the app
- Method X updates a set of widgets
- Method X notifies the wigdet's layout of the change (by calling changed() and
layout())

This prevents writing any black box object that knows how to update a set of
wigdets without depending on where those wigdets are used. This is creating a
severe maintnance problem within the Eclipse UI.

If code modifies a widget, it needs to:
1. Know about every other piece of code that calls that widget's computeSize method.
2. Know which object to trigger a layout on.
3. Know when to trigger a layout. (Or when to defer and undefer layouts)

Issue 1 creates very large (and complicated) update methods.

Issue 2 means that the spaghetti code is always specific to a particular layout
and cannot be reused. Even minor layout changes often require severe refactoring.

Issue 3 is impossible to get right. In our world, everything is triggered by
listeners. Even though the layout only needs to happen after the last listener
has done its thing and control returns to the event loop, there is no way to
know that "this will be the last change to this widget before control returns to
the event loop". We work around this by heuristically batching updates together,
and making individual update methods responsible for as many widgets as
possible. Our current heuristics work well, but the code is extremely
complicated and brittle.

This PR requests the following:

a. Provide a way to flag a widget as "changed", such that any dependant layout
will be notified and triggered when control returns to the event loop. (The
client code MUST NOT be responsible for calling layout(...) or setLayoutDeferred)

b. Provide a "preferred size changed" listener, so that a widget can have its
position managed by something other than its immediate parent (aka proxy controls).

c. Provide a way for layouts to indicate whether a change to a particular child
will affect their own preferred size. This will be needed for SWT to correctly
compute the effects of a "changed" event.

Not sure whether to flag as "blocker" or "enhancement". It is really an
enhancement, but it is blocking further work on the Eclipse layouts. Flagging as
major as a compromise. :-)
Comment 1 Stefan Xenos CLA 2005-07-14 13:37:02 EDT
Created attachment 24773 [details]
Suggested algorithm (implemented through JFace)

This patch suggests a new layout protocol.

It is currently implemented as a bunch of static methods and coding conventions
in JFace, but the same pattern would work if the static methods were moved to
the Control class and the coding conventions happened automatically.

The patch also addresses an independant issue involving computeSize (in other
words, feel free to ignore the new compute*Size methods).
Comment 2 Stefan Xenos CLA 2005-07-14 13:47:59 EDT
Created attachment 24775 [details]
Standalone SWT applications that demonstrate the problem

This project contains the same SWT application coded in three ways:

1. Using the pattern currently used by Eclipse: a big update method that does
everything. Good (but non-optimal) performance, complicated code.

2. Using a pattern suggested by Veronika: call Shell.layout(changedControl)
every time any control changes. Very poor performance, well encapsulated code.

3. Using the proposed pattern. Controls are dirtied whenever they change, and
any necessary layouts happen when control returns to the event loop. Optimal
performance, well encapsulated code.


The app implements a simple model/view/controller pattern, where there are two
buttons to add and remove elements from a set and a bunch of "views" that
display the size of the set as asterisks. 

A second shell monitors the number of times the last label is moved or resized.
Comment 3 Stefan Xenos CLA 2005-07-14 14:09:13 EDT
CC'ing all the people I promised to keep informed about this issue. :-)
Comment 4 Stefan Xenos CLA 2005-07-14 16:03:23 EDT
I still haven't tested this algorithm with the ProxyControl pattern used
commonly in the workbench. In order to fully clean up the mess, the code that
changes the code being proxied should not need to know that the control is being
proxied, clipping should be computed correctly (and should update if one of the
ancestors contributing to the clipping region changes), and the target should
not move more than once per pass through the event loop.
Comment 5 Veronika Irvine CLA 2005-07-14 17:06:29 EDT
Proxy'd widgets aside, the mechanism I would propose for minimizing the number 
of layouts is shown in the following example.

The controlChanged method could be a utility function in JFace.

In the example, any button you click on will update two labels, one which is a 
peer of the button and one which is a peer of the button's parent.

To count the number of layouts performed, I hacked FillLayout and GridLayout 
to print out each time layout was called.  I also hacked Button and Label to 
print out each time computeSize was called on the widgets.

public class DeferredLayoutExample {
	static int INDEX;
public static void main (String [] args) {
	Display display = new Display ();
	Shell shell = new Shell (display);
	shell.setLayout(new GridLayout());
	doit(shell);
	shell.pack();
	shell.open();
	while (!shell.isDisposed ()) {
		if (!display.readAndDispatch ()) display.sleep ();
	}
	display.dispose ();
}
static void doit(Composite parent) {
	if (INDEX == 10) return;
	Composite c = new Composite(parent, SWT.BORDER);
	c.setLayout(new GridLayout(3, false));
	Button b  = new Button(c, SWT.PUSH);
	b.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false));
	b.setText("Button "+INDEX++);
	controlChanged(b);
	// wrapping label
	final Label l1 = new Label(c, SWT.WRAP);	
	GridData data = new GridData(SWT.LEFT, SWT.TOP, false, false);
	data.widthHint = 100;
	l1.setLayoutData(data);
	final Label l2 = new Label(parent, SWT.WRAP);	
	data = new GridData(SWT.LEFT, SWT.TOP, true, false, 2, 1);
	data.widthHint = 100;
	l2.setLayoutData(data);
	b.addListener(SWT.Selection, new Listener() {
		public void handleEvent(Event e) {
			l1.setText(l1.getText()+"* ");
			controlChanged(l1);
			l2.setText(l2.getText()+"x ");
			controlChanged(l2);
		}
	});
	doit(c);
}
static void controlChanged(Control c) {
	final Shell shell = c.getShell();
	shell.setLayoutDeferred(true);
	shell.layout(new Control[] {c});
	shell.getDisplay().asyncExec(new Runnable() {
		public void run() {
			shell.setLayoutDeferred(false);
		}
	});
}
}
Comment 6 Stefan Xenos CLA 2005-07-15 01:03:42 EDT
There are several problems with comment 5:

1. The layouts do not occur in the same pass through the event loop. Although
the total number of layouts is the same, there could be considerable flicker
because SWT may repaint the widgets between each *syncExec. In order to be
flicker-free, the layouts must occur in the same pass through the event loop as
they were dirtied. 

This is why the change should go in SWT and not JFace. The best JFace can do is
rely on *syncExec (creating flicker) or use a specialized event loop (like my
example) which won't work when a non-conforming modal dialog is open. SWT can
add code to readAndDispatch, which ensures that the layouts always work and
never flicker.

2. It will not work with the proxy pattern, which is a firm requirement for the
workbench (a solution that does not address this cannot be used).

3. Layouts do not have the opportunity opt out of dirtying their parent when the
child changes. In both of our examples this makes no difference, but in the
Eclipse workbench this makes a huge difference.

4. The requirement to call controlChanged after any modification to any control
is cumbersome. Putting this method in SWT means that commonly-used setters could
be updated to call this themselves rather than push the responsibility onto
their callers. (Any method that auto-flags the object as changed could be
JavaDoc'd as such so that callers know when they need to flag the object themselves)

The patch I attached was mostly flicker-free (it flickered at most once when a
non-conforming event loop was running) and did not suffer from issues 2 and 3. I
am not looking for assistance with a JFace-based protocol (I have one): I am
looking for a more elegant SWT-based solution.
Comment 7 Stefan Xenos CLA 2005-07-18 12:11:03 EDT
See bug 46112, bug 104234, and bug 103863. These are the three biggest issues
that are complicating the workbench layouts.
Comment 8 Randy Hudson CLA 2005-07-18 12:46:31 EDT
GEF has solved these problems, if you replace "Control" with "Figure". We 
always layout one time and only layout the minimum number of figures required, 
regardless of how many changes you make.

Are you suggesting that SWT change such that calling..
Button#setText(String)
...walks up the parent composite chain, marking the appropriate composites as 
needing to be layed out, and they get layed out later via async? We call this 
process revalidate(). It works great for us but its a major change in SWT.

I think the request for proxies should be considered step 2. You may not need 
proxies if the API gives you the context of the layout request, allowing you to 
multiplex a bunch of SWT layouts in some placehold layout on the workbench 
page's composite.

What became of all the new layout API added in 3.1?
Comment 9 Stefan Xenos CLA 2005-07-18 13:54:43 EDT
> Are you suggesting that SWT change such that calling..
> Button#setText(String)
> ...walks up the parent composite chain, marking the appropriate composites
> as needing to be layed out, and they get layed out later via async?

I'd like to see a standard protocol for marking the composites for layout, that
would potentially permit setText to do as you describe in the future. For now,
manual marking is enough. One difference: if possible, the layout should happen
later in the same pass through the event loop rather than in a later async.
Doing it in an async means that the user may first see the button with new text
+ old position before seeing it with new text + new position.


> its a major change in SWT.

...but not a breaking one.


> I think the request for proxies should be considered step 2. 

Step 1 must not rule out step 2. The layout protocol needs to be spec'd in a
manner that allows us to continue to implement the current layouts. Right now,
the API on ViewForm and CTabFolder requires us to use proxies (they only report
the bounds of certain regions by setting the bounds of a child control - if we
need to know those bounds, we need to supply a proxy). Unless this changes, we
can't consider a solution that rules out the use of proxies. The reason we need
this change is precisely to simplify the code for managing those proxies.


> You may not need proxies if the API gives you the context of the 
> layout request, allowing you to multiplex a bunch of SWT layouts in 
> some placehold layout on the workbench page's composite.

I'm not sure I follow that. :-(


> What became of all the new layout API added in 3.1?

The new API makes it possible for a layout to flush cached information for a
single child without recomputing the entire layout. This was a necessary step
forward, but not a complete solution.
Comment 10 Randy Hudson CLA 2005-07-18 14:13:57 EDT
> manual marking is enough. One difference: if possible, the layout should 
happen
> later in the same pass through the event loop rather than in a later async.
> Doing it in an async means that the user may first see the button with new 
text
> + old position before seeing it with new text + new position.

You could hook painting and do a pre-check to see if any layouts are pending 
first. This is what we have to do in GEF.

But, we'd like to do it the way you suggest. Something like a 
Display#afterCurrentEventExec(Runnable). One problem we have with asyncExec is 
that its priority is not high enough and a stream of UI events can preempt it 
indefinitely.

Do events in the OS queue have priorities?
Comment 11 Stefan Xenos CLA 2005-07-18 16:10:38 EDT
> Display#afterCurrentEventExec(Runnable)

YES! That would be superb.
Comment 12 Stefan Xenos CLA 2005-07-18 16:18:09 EDT
Veronika suggested that if setParent() worked on all platforms, we would not
need the proxy pattern. I suspect that (with considerable refactoring) this is
true. However, as long as Eclipse supports any platform (Motif) which doesn't
support reparenting, we will need the proxies.
Comment 13 Stefan Xenos CLA 2006-09-14 11:47:27 EDT
Moving back to inbox (since veronika no longer owns this code AFAIK).
Comment 14 Stefan Xenos CLA 2006-09-14 11:53:58 EDT
Most of the criticism I've heard about this pattern regards the following:

> b. Provide a "preferred size changed" listener, so that a widget can have
> its position managed by something other than its immediate parent (aka 
> proxy controls).

...since its possible for the preferred size of a widget to change due to events beyond SWT's control and therefore no way to get an event every time. If, however, we called it a "layout requested" listener, this would work just as well without suggesting it will be called on every preferred size change.

Comment 15 Steve Northover CLA 2006-10-25 01:24:13 EDT
Veronika still owns the layout bugs (for now), even if she is not working on them.  That way they all stay in the same place.

Given the amount of work we are doing for 3.3, I can't see us looking into this.  I'm really sorry about this.
Comment 16 Stefan Xenos CLA 2007-02-27 14:46:36 EST
cc'ing boris. He was working on a databinding utility that would automatically update toolbars. However, without a solution to this bug, there would be no way to correctly update the layout containing the toolbar after the update.
Comment 17 Christophe Cornu CLA 2007-03-07 18:22:23 EST
In common cases, isn't it sufficent to schedule a call to layout(true) on the composite common to the set of widgets binded to a given model? Do things have to be updated at all times in quasi real time? I guess here we're precisely talking about supporting the difficult cases. I'll hide :-)
Comment 18 Randy Hudson CLA 2007-03-07 22:17:55 EST
(In reply to comment #17)
> In common cases, isn't it sufficent to schedule a call to layout(true) on the

I think the answer is yes, but the part missing is the schedule support. You can do an asyncExec, but things could paint in their intermediate states before the layout occurs (refer to setText() scenario). In a simple closed system you might know what the last change is and you could do the layout then.
Comment 19 Stefan Xenos CLA 2007-03-20 20:35:17 EDT
> In common cases, isn't it sufficent to schedule a call to layout(true) 
> on the composite common to the set of widgets binded to a given model? 

Well, if you knew what composite was common, and you knew when all the changes to those widgets were finished, then a call to layout(...) would do the trick.

The specific problem we're looking at here are the cases where you don't know which widget is common or when to call layout(...) on it.


> Do things have to be updated at all times in quasi real time? 

They just need to be updated before the next repaint, and only once. The cases we're talking about here are things like the workbench layout, or the Sections in the Forms editors, where a change to a widget modifies its enclosing layout.

Comment 20 Boris Bokowski CLA 2007-06-11 16:31:58 EDT
Created attachment 70900 [details]
SWT-only snippet

New snippet without any JFace.  This is for looking at the "layout only once" issue.
Comment 21 Stefan Xenos CLA 2008-06-12 17:22:50 EDT
This is still a problem. Steve, would it improve the chances of getting this fixed if I were to offer a patch?
Comment 22 Stefan Xenos CLA 2008-06-13 16:39:32 EDT
Created attachment 104919 [details]
Improved SWT-only example that demonstrates the extra repaints

I've attached an improved version of Boris' code, that does a better job demonstrating the flicker in case two.

Case 1 needs no modification. I can see the flicker on GTK, and you can see the redundant moves and resizes being written to the console.

In case 2, the flicker arises from the multiple repaints, so I've attached a paint listener that shows the widget repainting twice.
Comment 23 Stefan Xenos CLA 2008-06-13 18:27:19 EDT
Created attachment 104929 [details]
Tests a layout with an excessive number of widgets

I've attached a test that uses veronica's approach in a layout with an excessive number of widgets, most of which are not affected by the changes being made to the layout.

I suspected that this would prove that the depth-first-search being done in Composite.updateLayout would end up causing a performance problem... however what it actually proved is that you run out of widget handles long before you encounter a serious performance problem here. I'm attaching my test anyway.

This basically makes comment 6, issue 3 redundant. If we can fix the flicker and make it work with the proxy pattern, Veronica's approach would be acceptable.
Comment 24 Silenio Quarti CLA 2009-10-20 17:15:09 EDT
We have added new layout API that should address this problem. See:

Composite.layout(Control[] changed, int flags)
SWT.ALL
SWT.CHANGED
SWT.DEFER

Please try the new API out. If the DEFER flag is specified the layout will be deferred until just before the next event is handled.
Comment 25 Veronika Irvine CLA 2009-10-20 19:08:30 EDT
Hi Silenio,

Just curious.  How is the new API different from the following?

Control[] changed = new Control[]{child1, child2};
someComposite.setLayoutDeferred(true);
someComposite.layout(changed, SWT.ALL);
someComposite.getDisplay().asyncExec(new Runnable() {
    public void run() {
        someComposite.setLayoutDeferred(false);
    }
});

I see that the SWT.DEFER flag causes the deferred layouts to take place before the event loop is next run (as opposed to when the event loop is next idle which is what asyncExec() does). Is this timing difference required to stop the flickering that Stefan mentions?

Cheers,
Veronika
Comment 26 Silenio Quarti CLA 2009-10-21 10:16:51 EDT
Hi Vick, good to know you are still watching :-)

I think you answered your own question. When SWT.DEFER is used, the layouts are done before the event loop has a chance to process any event. With the asyncExec() approach, paint events could happen before the widgets were laied out. This does not happen that often on Windows, but it happens almost all the time on GTK and Cocoa. I am not sure why it does not happen on Windows more often. It seems that the app goes idle before it paints.

The new API has two other advantages. One is simplicity, there is no need to have a cake in jface that clients have to call. This would be necessary (with asyncExec()) to make sure all layouts happen in one single step in the event loop. And the second is that SWT is in control when layout happens. If running before the next event has issues, we have a chance of find a better place.

It is hard to see the simple app flickering. E4 workbench has some issues related to layout and it is probably a good candidate to use this API.
Comment 27 Stefan Xenos CLA 2009-10-21 16:49:51 EDT
Silenio, this is great news. It sounds like you've addressed the core issues here. I'll start tinkering with the new APIs shortly.

Is there any way to use the proxy pattern with this new API, or is there still some additional support needed?
Comment 28 Silenio Quarti CLA 2009-10-22 16:26:39 EDT
I do not think the new API addresses the ProxyControl problem. I am not sure how this could be addressed by SWT.

This might not be a issue to the E4 workbench, since I beleive it is using Control.setParent() instead of proxy controls.
Comment 29 Boris Bokowski CLA 2009-10-22 17:03:21 EDT
(In reply to comment #28)
> I do not think the new API addresses the ProxyControl problem. I am not sure
> how this could be addressed by SWT.
> 
> This might not be a issue to the E4 workbench, since I beleive it is using
> Control.setParent() instead of proxy controls.

Yes, e4 won't run on platforms that don't support setParent. With e4, the Workbench is no longer in the business of "the big lie" :-)
Comment 30 Stefan Xenos CLA 2009-10-22 17:34:30 EDT
If I understand correctly... after I change a widget, I should do the following to make it update its position:

widget.getParent().layout(new Control[] {widget}, SWT.DEFER);

...or am I supposed to do this?

widget.getShell().layout(new Control[] {widget}, SWT.DEFER);


Either way, it might be easier for programmers if there was a convenience API that looked like this:

widget.requestLayout();

...it's simpler and has less room for programmer error.
Comment 31 Stefan Xenos CLA 2009-10-22 17:39:54 EDT
> I do not think the new API addresses the ProxyControl problem. I am not 
> sure how this could be addressed by SWT.

FYI, if you wanted to support ProxyControl (though the new setParent changes may have eliminated the driving use case for the pattern), it could be done with an additional listener type that allowed other objects to observe when a target widget is dirtied in its layout.
Comment 32 Silenio Quarti CLA 2009-10-23 09:46:29 EDT
(In reply to comment #30)
> If I understand correctly... after I change a widget, I should do the following
> to make it update its position:
> widget.getParent().layout(new Control[] {widget}, SWT.DEFER);
> ...or am I supposed to do this?
> widget.getShell().layout(new Control[] {widget}, SWT.DEFER);
> Either way, it might be easier for programmers if there was a convenience API
> that looked like this:
> widget.requestLayout();
> ...it's simpler and has less room for programmer error.

I would prefer not to add new concepts. People are used to call layout() on a composite.

I believe widget.requestLayout() would also have one fallback. It would be hard for SWT to determine which parent of the widget it should call setLayoutDeffered(true). The simplest solution would be to do it for the whole shell. This would case a relayout on the whole shell (performance problem). The APP code knows this better. For example, the workbench can probably request layout on the root of a given view (ViewForm?) because views represent internal shells in a sense. SWT would have to add concepts like what is the root composite for layout purposes making the API more complicate I believe.
Comment 33 Eric Moffatt CLA 2009-10-26 16:24:18 EDT
I echo the sentiment that this is good news...

The scenario under which I expect this to be of the most use is in the 'trim' layout. The top trim in E4 will contain the toolbars and under a context switch (i.e. editor activation) many of them could change size (or become invisible...) but we'd only like one layout ultimately.

It's worth noting that this is also about the -only- case in the UI Structure where a 'computeSize' makes sense. The Window/Stack/Sash containers need not ever ask their children how big they'd like to be (since we ignore the answer anyways...;-).

Can you tell me if the deferred layout is smart enough to handle cases where we have a deferred layout on a Composite "A" and also on a child of "A" (in which case I'd expect the child's 'deferred' layout to be removed since it's inferred in the layout for "A"...? This means that if there's a layout pending for the Shell that -no other- deferred layouts need to be remembered...
Comment 34 Silenio Quarti CLA 2009-11-02 14:03:23 EST
(In reply to comment #33)
> Can you tell me if the deferred layout is smart enough to handle cases where we
> have a deferred layout on a Composite "A" and also on a child of "A" (in which
> case I'd expect the child's 'deferred' layout to be removed since it's inferred
> in the layout for "A"...? This means that if there's a layout pending for the
> Shell that -no other- deferred layouts need to be remembered...

No, the new API only affects the composite where the method is called (not the children). 

Calling:

   comp.layout(new Control[] {child}, SW.DEFER);

is essentially the same as:

   comp.setLayoutDeferred(true);
   comp.layout(new Control[] {child});
   comp.getDisplay().asyncExec(new Runnable() {
	public void run() {
	   comp.setLayoutDeferred(false);
       }
   });

but the asyncExec() is done in a special way.
Comment 35 Stefan Xenos CLA 2015-05-29 14:31:39 EDT
Reopening since - according to comment 32 - issue 2 in the original description is still not fixed.

A requirement of this bug was that the app should not need to know the root widget affected by any given change, and the only way offered to do so is this:

widget.getShell().layout(new Control[] {widget}, SWT.DEFER);

However, if the app shouldn't do this due to performance issues, then it's still not fixed.

Note: the original request also asks for support for the proxy pattern, but since SWT now supports reparenting everywhere, this is no longer necessary.
Comment 36 Stefan Xenos CLA 2015-05-29 14:38:43 EDT
Actually, I'll leave this closed and can file a new bug tracking the remaining work.
Comment 37 Stefan Xenos CLA 2015-05-29 15:03:20 EDT
Filed bug 468854 to request the missing functionality.