enaml - defer triggering event until next UI event loop cycle?

classic Classic list List threaded Threaded
5 messages Options
Reply | Threaded
Open this post in threaded view
|

enaml - defer triggering event until next UI event loop cycle?

Matthew Scott
Please try the code snippet at https://gist.github.com/3021983

You'll see an empty text editor, a "regenerate" button, and an "increment" button.  In the terminal you'll see:
widget.main_component.counter == 1
widget.main_component.initialized == False

Click "increment", and the text editor will show "2" and this will appear in the terminal:
widget.main_component.counter == 2
widget.main_component.initialized == True

Click "regenerate", and it will recreate the text editor widget, which will show up blank, and this will appear in the terminal:
widget.main_component.counter == 2
widget.main_component.initialized == False


I think this is happening because too many things are happening in a single UI event loop cycle.

Is it possible to defer the triggering of an enaml event until the next cycle?


Thanks!


-- 
Matthew Scott
ElevenCraft Inc.


_______________________________________________
Enthought-Dev mailing list
[hidden email]
https://mail.enthought.com/mailman/listinfo/enthought-dev
Reply | Threaded
Open this post in threaded view
|

Re: enaml - defer triggering event until next UI event loop cycle?

Chris Colbert


On Fri, Jun 29, 2012 at 11:16 PM, Matthew Scott <[hidden email]> wrote:
Please try the code snippet at https://gist.github.com/3021983

You'll see an empty text editor, a "regenerate" button, and an "increment" button.  In the terminal you'll see:
widget.main_component.counter == 1
widget.main_component.initialized == False

Click "increment", and the text editor will show "2" and this will appear in the terminal:
widget.main_component.counter == 2
widget.main_component.initialized == True

Click "regenerate", and it will recreate the text editor widget, which will show up blank, and this will appear in the terminal:
widget.main_component.counter == 2
widget.main_component.initialized == False


I think this is happening because too many things are happening in a single UI event loop cycle.

Is it possible to defer the triggering of an enaml event until the next cycle?


Thanks!



The problem you are running into is that when you modify the components in an include, the new components to not get initialized until the next event loop cycle. That is, changing the include components triggers a relayout request under the covers, which is a deferred request. This is done on purpose because multiple relayout requests will be collapsed into a single relayout. There are many things in Enaml which, when changed, require the UI to relayout. The three most common things are 1) changing the constraints on a component, 2) changes the items in an Include, 3) changing an attribute on a component which causes its size hint to become invalid.

Usually, these changes come in groups where changes to constraints are accompanied by changes in size hint. You wouldn't want a relayout to occur for each one of these things as that would be slow and wasteful and result if visible resizing/layout transients. The only proper way to handle this then, is to handle a relayout request just like a paint event: deferred and collapsed.

So, when you fire off that event which causes your list of components in the Include to update, the Include requests a relayout then immediately returns, at which point you fire the next event which prints to the shell. Unlike GUI toolkit events, Enaml "events" dispatch immediately and synchronously. If more than one listener is attached to an event, the order of their execution is not guaranteed.

So, what to do?

In your case, you need to queue up a function to be called *after* the relayout has occurred. Enaml provides several ways of doing this, and they all involve the toolkit application object.

The Enaml Toolkit object has an "app" property which returns an instance of AbstractTkApplication:

This application object is implemented by whichever GUI toolkit is in use (Qt or Wx) and provides three ways of deferring work until later: "schedule", "call_on_main", and "timer". The docs on these methods explain their functionality well.

Lemme know if you need more info,

Chris

_______________________________________________
Enthought-Dev mailing list
[hidden email]
https://mail.enthought.com/mailman/listinfo/enthought-dev
Reply | Threaded
Open this post in threaded view
|

Re: enaml - defer triggering event until next UI event loop cycle?

Chris Colbert


On Fri, Jun 29, 2012 at 11:36 PM, Chris Colbert <[hidden email]> wrote:


On Fri, Jun 29, 2012 at 11:16 PM, Matthew Scott <[hidden email]> wrote:
Please try the code snippet at https://gist.github.com/3021983

You'll see an empty text editor, a "regenerate" button, and an "increment" button.  In the terminal you'll see:
widget.main_component.counter == 1
widget.main_component.initialized == False

Click "increment", and the text editor will show "2" and this will appear in the terminal:
widget.main_component.counter == 2
widget.main_component.initialized == True

Click "regenerate", and it will recreate the text editor widget, which will show up blank, and this will appear in the terminal:
widget.main_component.counter == 2
widget.main_component.initialized == False


I think this is happening because too many things are happening in a single UI event loop cycle.

Is it possible to defer the triggering of an enaml event until the next cycle?


Thanks!



The problem you are running into is that when you modify the components in an include, the new components to not get initialized until the next event loop cycle. That is, changing the include components triggers a relayout request under the covers, which is a deferred request. This is done on purpose because multiple relayout requests will be collapsed into a single relayout. There are many things in Enaml which, when changed, require the UI to relayout. The three most common things are 1) changing the constraints on a component, 2) changes the items in an Include, 3) changing an attribute on a component which causes its size hint to become invalid.

Usually, these changes come in groups where changes to constraints are accompanied by changes in size hint. You wouldn't want a relayout to occur for each one of these things as that would be slow and wasteful and result if visible resizing/layout transients. The only proper way to handle this then, is to handle a relayout request just like a paint event: deferred and collapsed.

So, when you fire off that event which causes your list of components in the Include to update, the Include requests a relayout then immediately returns, at which point you fire the next event which prints to the shell. Unlike GUI toolkit events, Enaml "events" dispatch immediately and synchronously. If more than one listener is attached to an event, the order of their execution is not guaranteed.

So, what to do?

In your case, you need to queue up a function to be called *after* the relayout has occurred. Enaml provides several ways of doing this, and they all involve the toolkit application object.

The Enaml Toolkit object has an "app" property which returns an instance of AbstractTkApplication:

This application object is implemented by whichever GUI toolkit is in use (Qt or Wx) and provides three ways of deferring work until later: "schedule", "call_on_main", and "timer". The docs on these methods explain their functionality well.

Lemme know if you need more info,

Chris


Of course I would claim those methods are well documented only to realize the docs for the "timer" method are fallacious. The "ms" arg in "timer" is the timer timeout in milliseconds.

_______________________________________________
Enthought-Dev mailing list
[hidden email]
https://mail.enthought.com/mailman/listinfo/enthought-dev
Reply | Threaded
Open this post in threaded view
|

Re: enaml - defer triggering event until next UI event loop cycle?

Matthew Scott
In reply to this post by Chris Colbert
On Friday, June 29, 2012 at 22:36, Chris Colbert wrote:
So, what to do?

In your case, you need to queue up a function to be called *after* the relayout has occurred. Enaml provides several ways of doing this, and they all involve the toolkit application object.

The Enaml Toolkit object has an "app" property which returns an instance of AbstractTkApplication:

This application object is implemented by whichever GUI toolkit is in use (Qt or Wx) and provides three ways of deferring work until later: "schedule", "call_on_main", and "timer". The docs on these methods explain their functionality well.

Thank you for the detail about how Include does its magic and how that affects relayout and refresh!

Quick version:  I discovered that both request_refresh_task(callback) and toolkit.app.schedule(callback) have the desired effect.

Question:  Is request_refresh_task(callback) appropriate in the scenario I'm using it in?  It seems like it is but I want to get your opinion.


I've updated https://gist.github.com/3021983 with some detail on this, and here are some more notes:


- enaml made it insanely easy to wrap some behavior controls around test scripts like this

The one minor setback I had is that inside the various lambda statements created as part of the default value for the trigger_methods attr, they don't know about the enamldef namespace such as the 'main' name I'd assigned to the MainWindow components.  So I had to accept a 'main' argument, and pass in 'main' when calling.


- widget_needs_updating(): failed in 100% of cases

This is due to not allowing event loop to perform relayout as discussed.


- toolkit.app.call_on_main(widget_needs_updating): failed in 100% of cases

I'm guessing that implementation wise, this ends up being synchronous when called from the main thread itself.


- toolkit.app.timer(0, widget_needs_updating): succeeded in > 50% of cases, but < 100%

Rather than using priorities, this uses wall time, which even with 0ms (but especially with anything non-zero) it seems to be a misplaced use of wall time.


- toolkit.app.schedule(widget_needs_updating, priority=50): succeded in 100% of cases
- toolkit.app.schedule(widget_needs_updating, priority=49): failed in 100% of cases

The only other relevant calls to .schedule from within enaml itself appear to be in components/layout_task_handler.py:
  101 :             self._relayout_task = self.toolkit.app.schedule(self.relayout)
  111 :             self._refresh_task = self.toolkit.app.schedule(self.refresh)

Because the default priority for schedule() is 50, this leads me to believe that it is an implementation detail where tasks of the same priority get executed in FIFO order... that is, the relayout and refresh tasks were queued first, so even though widget_needs_updating is priority 50, it always gets executed after relayout and refresh.


- request_refresh_task(widget_needs_updating): succeeded in 100% of cases


This seems to be more semantically appropriate to what I am doing, and from what I can tell in the implementation, rather than scheduling an arbitrary task in a general task queue as with schedule(), this queues up a function to occur on the next refresh, then requests a refresh.


_______________________________________________
Enthought-Dev mailing list
[hidden email]
https://mail.enthought.com/mailman/listinfo/enthought-dev
Reply | Threaded
Open this post in threaded view
|

Re: enaml - defer triggering event until next UI event loop cycle?

Chris Colbert


On Sat, Jun 30, 2012 at 1:50 PM, Matthew Scott <[hidden email]> wrote:
On Friday, June 29, 2012 at 22:36, Chris Colbert wrote:
So, what to do?

In your case, you need to queue up a function to be called *after* the relayout has occurred. Enaml provides several ways of doing this, and they all involve the toolkit application object.

The Enaml Toolkit object has an "app" property which returns an instance of AbstractTkApplication:

This application object is implemented by whichever GUI toolkit is in use (Qt or Wx) and provides three ways of deferring work until later: "schedule", "call_on_main", and "timer". The docs on these methods explain their functionality well.

Thank you for the detail about how Include does its magic and how that affects relayout and refresh!

Quick version:  I discovered that both request_refresh_task(callback) and toolkit.app.schedule(callback) have the desired effect.


Yes this will work, but request_*_task methods were intended more for internal use. If you want to guarantee that something executes *after* a relayout, use schedule with a "lower" priority than the relayout (anything > 50).

Question:  Is request_refresh_task(callback) appropriate in the scenario I'm using it in?  It seems like it is but I want to get your opinion.


I've updated https://gist.github.com/3021983 with some detail on this, and here are some more notes:


- enaml made it insanely easy to wrap some behavior controls around test scripts like this


Cool, I'm glad you're finding it easy to work with!
 
The one minor setback I had is that inside the various lambda statements created as part of the default value for the trigger_methods attr, they don't know about the enamldef namespace such as the 'main' name I'd assigned to the MainWindow components.  So I had to accept a 'main' argument, and pass in 'main' when calling.


Yeah, I should probably make the use of lambda a syntax error. The problem here is that I don't want to be in the business of modifying the bytecode of closures to implement Enaml's scoping rules, especially since closures can be nested arbitrarily deep. So, the behavior you see is what I would expect.


- widget_needs_updating(): failed in 100% of cases

This is due to not allowing event loop to perform relayout as discussed.


- toolkit.app.call_on_main(widget_needs_updating): failed in 100% of cases

I'm guessing that implementation wise, this ends up being synchronous when called from the main thread itself.


This is interesting, I would expect the relayout to occur first, and then this call to be dispatched. Can you make a simpler test case which demonstrates that behavior?
 

- toolkit.app.timer(0, widget_needs_updating): succeeded in > 50% of cases, but < 100%


This really isn't an appropriate use of timer and the behavior you see will entirely depend on the internal timer implementation of the toolkit.
 
Rather than using priorities, this uses wall time, which even with 0ms (but especially with anything non-zero) it seems to be a misplaced use of wall time.


- toolkit.app.schedule(widget_needs_updating, priority=50): succeded in 100% of cases
- toolkit.app.schedule(widget_needs_updating, priority=49): failed in 100% of cases
 
The only other relevant calls to .schedule from within enaml itself appear to be in components/layout_task_handler.py:
  101 :             self._relayout_task = self.toolkit.app.schedule(self.relayout)
  111 :             self._refresh_task = self.toolkit.app.schedule(self.refresh)

Because the default priority for schedule() is 50, this leads me to believe that it is an implementation detail where tasks of the same priority get executed in FIFO order... that is, the relayout and refresh tasks were queued first, so even though widget_needs_updating is priority 50, it always gets executed after relayout and refresh.



Exactly, the internal application object uses a priority heap to schedule tasks. Tasks with the same priority execute in FIFO order:
 
- request_refresh_task(widget_needs_updating): succeeded in 100% of cases


This seems to be more semantically appropriate to what I am doing, and from what I can tell in the implementation, rather than scheduling an arbitrary task in a general task queue as with schedule(), this queues up a function to occur on the next refresh, then requests a refresh.



As I mentioned before, I'm not sure whether I want to consider these methods as part of the public api, but assuming they are, there purpose is allow you to execute arbitrary code on the main thread, immediately followed by a relayout or refresh (a relayout rebuilds the constraints and constraints solver, a refresh just resolves the system and refreshes the positions of widgets). The code which processe the queue is here:





_______________________________________________
Enthought-Dev mailing list
[hidden email]
https://mail.enthought.com/mailman/listinfo/enthought-dev