Repository: zoofIO/flexx
Pull request #113 (2016-05-11 08:35:05)

Tweaks related to new event system
Continuing #108

* improve mouse events
* make that in `JS.init()` you have access to sub models created in `Py.init()`
* session is implicitly inherited by models when used as a context manager. 
* There is a func to get the currently active (as in being "in context") model, that can be used in subclasses
* props can deal with numpy arrays



Patch:
diff --git a/docs/app/api.rst b/docs/app/api.rst
index f826620..ca7dd91 100644
--- a/docs/app/api.rst
+++ b/docs/app/api.rst
@@ -29,6 +29,8 @@ Functions related to us Model classes as apps
 
 .. autofunction:: flexx.app.get_model_classes
 
+.. autofunction:: flexx.app.get_active_model
+
 
 The Model class
 ---------------
diff --git a/examples/app/parent_children.py b/examples/app/parent_children.py
index d0123a3..2933a3f 100644
--- a/examples/app/parent_children.py
+++ b/examples/app/parent_children.py
@@ -61,8 +61,11 @@ class Node(app.Model):
 
 
 n1 = app.launch(Node, 'xul')
-n2 = Node(session=n1.session)
-n3 = Node(session=n1.session)
+
+# Create other nodes in context of n1, so they share the same session
+with n1:
+    n2 = Node()
+    n3 = Node()
 
 # This is intended to be run interactively (e.g. in Pyzo), so that you
 # change the parent-children relationships dynamically.
diff --git a/examples/ui/chatroom.py b/examples/ui/chatroom.py
index 6adba26..3ebfec5 100644
--- a/examples/ui/chatroom.py
+++ b/examples/ui/chatroom.py
@@ -49,7 +49,8 @@ class ChatRoom(ui.Widget):
     
     def _push_info(self, *events):
         if self.session.status:
-            self.emit('new_message', events[-1])
+            for ev in events:
+                self.emit('new_message', ev)
     
     def _update_participants(self):
         if not self.session.status:
diff --git a/examples/ui/colab_painting.py b/examples/ui/colab_painting.py
new file mode 100644
index 0000000..9e99a78
--- /dev/null
+++ b/examples/ui/colab_painting.py
@@ -0,0 +1,123 @@
+"""
+A web app that allows multiple people to colaborate in painting. People
+connecting later will not see the paint that was added earlier. The
+paint slowly fades (like memory) as more paint is added, so the
+appearance will be more similar as people are connected longer (people
+"align" as they work together). Each person is assigned a random color
+(his/her "skill") which might affect how that person can best contribute
+to the painting.
+"""
+
+import random
+
+from flexx import app, ui, event
+
+COLORS = ('#eee', '#999', '#555', '#111', 
+          '#f00', '#0f0', '#00f', '#ff0', '#f0f', '#0ff',
+          '#a44', '#4a4', '#44a', '#aa4', '#afa', '#4aa',
+          )
+
+
+class Relay(event.HasEvents):
+    """ Global object to relay paint events to all participants.
+    """
+    @event.emitter
+    def new_dot(self, pos, color):
+        return dict(pos=pos, color=color)
+
+
+@app.serve
+class ColabPainting(ui.Widget):
+    """ Web app for colaborative painting.
+    """
+    
+    CSS = """
+    .flx-Widget { background: #ddd; }
+    .flx-CanvasWidget {
+        background: #fff;
+        border: 10px solid #000;
+        min-width: 400px; max-width: 400px;
+        min-height: 400px; max-height: 400px;
+    }
+    """
+    
+    def init(self):
+        
+        # Select random color
+        #rgb = [str(random.randint(0, 200)) for i in range(3)]
+        #self.color = 'rgb(%s)' % (', '.join(rgb))
+        self.color = random.choice(COLORS)
+        
+        # App layout
+        with ui.VBox():
+            self.people = ui.Label(flex=0)
+            ui.Widget(flex=1)
+            with ui.HBox(flex=2):
+                ui.Widget(flex=1)
+                self.canvas = ui.CanvasWidget(flex=0)
+                ui.Widget(flex=1)
+            ui.Widget(flex=1)
+        
+        # Pipe messages send by the relay into this app
+        relay.connect(self._push_info, 'new_dot:' + self.id)
+        
+        # Start people-count-updater
+        self._update_participants()
+    
+    def _push_info(self, *events):
+        if self.session.status:
+            for ev in events:
+                self.emit('new_dot', ev)
+    
+    def _update_participants(self):
+        if not self.session.status:
+            relay.disconnect('new_dot:' + self.id)
+            return  # and dont't invoke a new call
+        proxies = app.manager.get_connections(self.__class__.__name__)
+        n = len(proxies)
+        del proxies
+        self.people.text = '%i persons are painting' % n
+        app.call_later(3, self._update_participants)
+    
+    @event.connect('canvas.mouse_down')
+    def _user_adds_paint(self, *events):
+        for ev in events:
+            relay.new_dot(ev.pos, self.color)
+    
+    @event.prop
+    def color(self, color='#000'):
+        """ Your color.
+        """
+        return str(color)
+    
+    class JS:
+        
+        def init(self):
+            super().init()
+            self._ctx = self.canvas.node.getContext('2d')
+        
+        @event.connect('color')
+        def _update_color(self, *events):
+            self.canvas.style = 'border: 10px solid ' + events[-1].new_value
+        
+        @event.connect('new_dot')
+        def _paint_dot(self, *events):
+            for ev in events:
+                # Slowly hide old paint
+                self._ctx.globalAlpha = 0.01
+                self._ctx.fillStyle = '#fff'
+                self._ctx.fillRect(0, 0, 400, 400)
+                # Add new dot
+                self._ctx.globalAlpha = 0.8
+                self._ctx.beginPath()
+                self._ctx.fillStyle = ev.color
+                self._ctx.arc(ev.pos[0], ev.pos[1], 5, 0, 6.2831)
+                self._ctx.fill()
+
+
+# Create global relay
+relay = Relay()
+
+if __name__ == '__main__':
+    m = app.launch(ColabPainting)  # for use during development
+    app.start()
diff --git a/examples/ui/drawing.py b/examples/ui/drawing.py
index a1538a9..98ff1bd 100644
--- a/examples/ui/drawing.py
+++ b/examples/ui/drawing.py
@@ -11,22 +11,37 @@ class Drawing(ui.CanvasWidget):
         
         def init(self):
             super().init()
-            self.ctx = self.canvas.getContext('2d')
+            self.ctx = self.node.getContext('2d')
+            self._last_ev = None
         
         def on_mouse_move(self, *events):
             for ev in events:
-                self.ctx.fillStyle = '#ff0'
-                self.ctx.fillRect(ev.x-2, ev.y-2, 4, 4)
+                last_ev = self._last_ev
+                if 1 in ev.buttons and last_ev is not None:
+                    self.ctx.beginPath()
+                    self.ctx.strokeStyle = '#080'
+                    self.ctx.lineWidth = 3
+                    self.ctx.lineCap = 'round'
+                    self.ctx.moveTo(*last_ev.pos)
+                    self.ctx.lineTo(*ev.pos)
+                    self.ctx.stroke()
+                    self._last_ev = ev
         
         def on_mouse_down(self, *events):
             for ev in events:
+                self.ctx.beginPath()
                 self.ctx.fillStyle = '#f00'
-                self.ctx.fillRect(ev.x-10, ev.y-10, 20, 20)
+                self.ctx.arc(ev.pos[0], ev.pos[1], 3, 0, 6.2831)
+                self.ctx.fill()
+                self._last_ev = ev
         
         def on_mouse_up(self, *events):
             for ev in events:
+                self.ctx.beginPath()
                 self.ctx.fillStyle = '#00f'
-                self.ctx.fillRect(ev.x-10, ev.y-10, 20, 20)
+                self.ctx.arc(ev.pos[0], ev.pos[1], 3, 0, 6.2831)
+                self.ctx.fill()
+            self._last_ev = None
 
 
 class Main(ui.Widget):
@@ -35,7 +50,7 @@ class Main(ui.Widget):
     
     CSS = """
     .flx-Widget { background: #aaf;}
-    .flx-Drawing {background: #fff;}
+    .flx-Drawing {background: #fff; border: 5px solid #000;}
     """
     
     def init(self):
@@ -48,6 +63,6 @@ class Main(ui.Widget):
                 ui.Widget(flex=1)
             ui.Widget(flex=1)
 
-
-m = app.launch(Main, 'xul')
-app.run()
+if __name__ == '__main__':
+    m = app.launch(Main, 'xul')
+    app.start()
diff --git a/flexx/app/__init__.py b/flexx/app/__init__.py
index 2e7cfcd..fb3b21a 100644
--- a/flexx/app/__init__.py
+++ b/flexx/app/__init__.py
@@ -79,7 +79,8 @@ Overview of classes:
 """
 
 from .session import manager, Session  # noqa
-from .model import Model, get_instance_by_id, get_model_classes  # noqa
+from .model import Model, get_active_model  # noqa
+from .model import get_instance_by_id, get_model_classes  # noqa
 from .funcs import run, start, stop, call_later  # noqa
 from .funcs import init_notebook, serve, launch, export  # noqa
 from .assetstore import assets  # noqa
diff --git a/flexx/app/model.py b/flexx/app/model.py
index 57861fd..7c025c0 100644
--- a/flexx/app/model.py
+++ b/flexx/app/model.py
@@ -68,6 +68,7 @@ attributes and handlers can be created here.
 import json
 import weakref
 import logging
+import threading
 
 from .. import event
 from ..event._hasevents import (with_metaclass, new_type, HasEventsMeta,
@@ -100,6 +101,36 @@ def get_instance_by_id(id):
         return None  # Could we revive it? ... probably not a good idea
 
 
+# Keep track of a stack of "active" models for use within context
+# managers. We have one list for each thread. Note that we should limit
+# its use to context managers, and execution should never be handed back
+# to the Tornado event loop while inside a context.
+_active_models_per_thread = {}  # dict of threadid -> list
+
+def _get_active_models():
+    """ Get list that represents the stack of "active" models.
+    Each thread has its own stack. Should only be used inside a Model
+    context manager.
+    """
+    # Get thread id
+    if hasattr(threading, 'current_thread'):
+        tid = id(threading.current_thread())
+    else:
+        tid = id(threading.currentThread())
+    # Get list of parents for this thread
+    return _active_models_per_thread.setdefault(tid, [])
+
+
+def get_active_model():
+    """ If the execution is now in a Model context manager, return the
+    corresponding object, and None otherwise. Can be used by subclasses
+    to implement parenting-like behaviour using the with-statement.
+    """
+    models = _get_active_models()
+    if models:
+        return models[-1]
+
+
 def stub_prop_func(self, v):
     return v
 
@@ -243,6 +274,10 @@ class Model(with_metaclass(ModelMeta, event.HasEvents)):
     values, but handlers are not yet initialized and events are not yet
     emitted.
     
+    Models can be used as a context manager to make new Model objects
+    created inside such a context to share the same session. The ``init()``
+    method is invoked in the context of the object itself.
+    
     Parameters:
         session (Session, None): the session object that connects this
             instance to a JS client.
@@ -300,6 +335,10 @@ class Model(with_metaclass(ModelMeta, event.HasEvents)):
         Model._instances[self._id] = self
         
         # Init session
+        if session is None:
+            active_model = get_active_model()
+            if active_model is not None:
+                session = active_model.session
         if session is None:
             from .session import manager
             session = manager.get_default_session()
@@ -333,23 +372,35 @@ class Model(with_metaclass(ModelMeta, event.HasEvents)):
         
         # Initialize the model further, e.g. Widgets can create
         # subwidgets etc. This is done here, at the point where the
-        # properties are initialized, but the handlers not yet. Handlers
-        # are initialized after this, so that they can connect to newly
-        # created sub Models. This class implements a stub contect manager.
+        # properties are initialized, but the handlers not yet.
         with self:
             self.init()
+        self._session._exec('flexx.instances.%s.init();' % self._id)
         
-        # Initialize handlers for Python and for JS
+        # Initialize handlers for Python and for JS. Done after init()
+        # so that they can connect to newly created sub Models.
         self._init_handlers()
         self._session._exec('flexx.instances.%s._init_handlers();' % self._id)
     
-    # Can be used as a context manager, but is a no-op for base Model class.
     def __enter__(self):
-        pass
+        # Note that __exit__ is guaranteed to be called, so there is
+        # no need to use weak refs for items stored in active_models
+        active_models = _get_active_models()
+        active_models.append(self)
+        call_later(0, self.__check_not_active)
+        return self
     
     def __exit__(self, type, value, traceback):
-        pass
-        
+        active_models = _get_active_models()
+        assert self is active_models.pop(-1)
+    
+    def __check_not_active(self):
+        active_models = _get_active_models()
+        if self in active_models:
+            raise RuntimeError('It seems that the event loop is processing '
+                               'events while a Model is active. This has a '
+                               'high risk on race conditions.')
+    
     def __repr__(self):
         clsname = self.__class__.__name__
         return "<%s object '%s' at 0x%x>" % (clsname, self._id, id(self))
@@ -493,11 +544,8 @@ class Model(with_metaclass(ModelMeta, event.HasEvents)):
             # Init HasEvents, but delay initialization of handlers
             super().__init__(False)
             
-            # Initialize when properties are initialized, but before handlers
-            # are initialized.
-            self.init()
-            
-            # self._init_handlers() -> called from py after pys init is done
+            # self.init() -> called from py
+            # self._init_handlers() -> called from py
         
         def init(self):
             """ Can be overloaded by subclasses to initialize the model.
diff --git a/flexx/event/_handler.py b/flexx/event/_handler.py
index 76c3c3a..901df98 100644
--- a/flexx/event/_handler.py
+++ b/flexx/event/_handler.py
@@ -137,6 +137,8 @@ class Handler:
             self._func_is_method = inspect.getargspec(func)[0][0] in ('self', 'this')
         except (TypeError, IndexError):
             self._func_is_method = False
+        if getattr(func, '__self__', None) is not None:
+            self._func_is_method = False  # already bound
         
         self._init(connection_strings)
     
diff --git a/flexx/event/_hasevents.py b/flexx/event/_hasevents.py
index 90c3538..bbf0ccc 100644
--- a/flexx/event/_hasevents.py
+++ b/flexx/event/_hasevents.py
@@ -305,7 +305,14 @@ class HasEvents(with_metaclass(HasEventsMeta, object)):
             return True
         # Otherwise only set if value has changed
         old = getattr(self, private_name)
-        if value2 != old:
+        if this_is_js():
+            is_equal = old == value2
+        elif hasattr(old, 'dtype') and hasattr(value2, 'dtype'):
+            import numpy as np
+            is_equal = np.array_equal(old, value2)
+        else:
+            is_equal = type(old) == type(value2) and old == value2
+        if not is_equal:
             setattr(self, private_name, value2)
             self.emit(prop_name, dict(new_value=value2, old_value=old))
             return True
diff --git a/flexx/ui/_widget.py b/flexx/ui/_widget.py
index 3e876c6..f5d5d74 100644
--- a/flexx/ui/_widget.py
+++ b/flexx/ui/_widget.py
@@ -9,32 +9,11 @@
 
 """
 
-import threading
-
 from .. import event
-from ..app import Model, call_later
+from ..app import Model, call_later, get_active_model
 from ..pyscript import undefined, window
 
 
-# Keep track of stack of default parents when using widgets
-# as context managers. Have one list for each thread.
-
-_default_parents_per_thread = {}  # dict of threadid -> list
-
-def _get_default_parents():
-    """ Get list that represents the stack of default parents.
-    Each thread has its own stack.
-    """
-    # Get thread id
-    if hasattr(threading, 'current_thread'):
-        tid = id(threading.current_thread())
-    else:
-        tid = id(threading.currentThread())
-    # Get list of parents for this thread
-    return _default_parents_per_thread.setdefault(tid, [])
-
-
-
 class LiveKeeper:
     """ This little utility keeps objects alive for a set period of
     time. This is used to prevent Widget objects from being cleaned up
@@ -91,12 +70,12 @@ class Widget(Model):
     
     def __init__(self, **kwargs):
         
-        # Apply default parent?
+        # Handle parent
         parent = kwargs.pop('parent', None)
         if parent is None:
-            default_parents = _get_default_parents()
-            if default_parents:
-                parent = default_parents[-1]
+            active_model = get_active_model()
+            if isinstance(active_model, Widget):
+                parent = active_model
         kwargs['parent'] = parent
         
         # Use parent session unless session was given
@@ -148,17 +127,6 @@ class Widget(Model):
         for child in children:
             child.dispose()
     
-    def __enter__(self):
-        # Note that __exit__ is guaranteed to be called, so there is
-        # no need to use weak refs for items stored in default_parents
-        default_parents = _get_default_parents()
-        default_parents.append(self)
-        return self
-    
-    def __exit__(self, type, value, traceback):
-        default_parents = _get_default_parents()
-        assert self is default_parents.pop(-1)
-    
     @event.prop(both=True)
     def title(self, v=''):
         """ The title of this widget. This is used to mark the widget
@@ -290,17 +258,13 @@ class Widget(Model):
         def __init__(self, *args):
             super().__init__(*args)
             
-            # Get phosphor widget that is set in init()
-            if not self.phosphor:
-                self.phosphor = window.phosphor.panel.Panel()
-            self.node = self.phosphor.node
+            # Let widget create Phoshor and DOM nodes
+            self._init_phosphor_and_node()
+            # Set outernode. Usually, but not always equal to self.node
+            self.outernode = self.phosphor.node
             
-            # Connect same standard events
-            self.node.addEventListener('mousedown', self.mouse_down, 0)
-            self.node.addEventListener('mouseup', self.mouse_up, 0)
-            self.node.addEventListener('keydown', self.key_down, 0)
-            self.node.addEventListener('keyup', self.key_up, 0)
-            self.node.addEventListener('keypress', self.key_press, 0)
+            # Setup JS events to enter Flexx' event system
+            self._init_events()
             
             # Keep track of size
             that = self
@@ -315,7 +279,7 @@ class Widget(Model):
             # Derive css class name
             cls_name = self._class_name
             for i in range(32):  # i.e. a safe while-loop
-                self.node.classList.add('flx-' + cls_name)
+                self.outernode.classList.add('flx-' + cls_name)
                 cls = window.flexx.classes[cls_name]
                 if not cls:
                     break
@@ -325,6 +289,12 @@ class Widget(Model):
             else:
                 raise RuntimeError('Error while determining class names')
         
+        def _init_phosphor_and_node(self):
+            """ Overload this in sub widgets.
+            """
+            self.phosphor = window.phosphor.panel.Panel()
+            self.node = self.phosphor.node
+        
         @event.connect('style')
         def __style_changed(self, *events):
             """ Emits when the style signal changes, and provides a dict with
@@ -333,6 +303,11 @@ class Widget(Model):
             # self.node.style = style  # forbidden in strict mode,
             # plus it clears all previously set style
             
+            # Note that styling is applied to the outer node, just like
+            # the styling defined via the CSS attribute. In most cases
+            # the inner and outer node are the same, but not always
+            # (e.g. CanvasWidget).
+            
             # Set style elements, keep track in a dict
             d = {}
             for ev in events:
@@ -342,7 +317,7 @@ class Widget(Model):
                         if ':' in part:
                             key, val = part.split(':')
                             key, val = key.trim(), val.trim()
-                            self.node.style[key] = val
+                            self.outernode.style[key] = val
                             d[key] = val
             
             # Did we change style related to sizing?
@@ -354,10 +329,10 @@ class Widget(Model):
             
             if size_limits_changed:
                 # Clear phosphor's limit cache (no need for getComputedStyle())
-                values = [self.node.style[k] for k in size_limits_keys]
+                values = [self.outernode.style[k] for k in size_limits_keys]
                 # todo: do I need a variant of self.phosphor.clearSizeLimits()?
                 for k, v in zip(size_limits_keys, values):
-                    self.node.style[k] = v
+                    self.outernode.style[k] = v
                 # Allow parent to re-layout
                 parent = self.parent
                 if parent:
@@ -379,18 +354,18 @@ class Widget(Model):
             """
             return v[0], v[1]
         
-        @event.connect('container', 'parent', 'children')
+        @event.connect('container', 'parent.size', 'children')
         def __update_size(self, *events):
             # Check size in *next* event loop iter to give the DOM a
-            # chance to settle
+            # chance to settle.
             window.setTimeout(self._check_real_size, 0)
         
         def _check_real_size(self):
             """ Check whether the current size has changed.
             """
-            n = self.node
+            n = self.outernode
             cursize = self.size
-            if cursize[0] != n.clientWidth or cursize[1] !=n.clientHeight:
+            if cursize[0] != n.clientWidth or cursize[1] != n.clientHeight:
                 self._set_prop('size', [n.clientWidth, n.clientHeight])
         
         def _set_size(self, prefix, w, h):
@@ -404,28 +379,28 @@ class Widget(Model):
                     size[i] = size[i] + 'px'
                 else:
                     size[i] = size[i] * 100 + '%'
-            self.node.style[prefix + 'width'] = size[0]
-            self.node.style[prefix + 'height'] = size[1]
+            self.outernode.style[prefix + 'width'] = size[0]
+            self.outernode.style[prefix + 'height'] = size[1]
         
         ## Parenting
         
         @event.connect('container')
         def __container_changed(self, *events):
             id = events[-1].new_value
-            self.node.classList.remove('flx-main-widget')
+            self.outernode.classList.remove('flx-main-widget')
             if self.parent:
                 return 
             if id:
                 el = window.document.getElementById(id)
                 if self.phosphor.isAttached:
                     self.phosphor.detach()
-                if self.node.parentNode is not None:  # detachWidget not enough
-                    self.node.parentNode.removeChild(self.node)
+                if self.outernode.parentNode is not None:  # detachWidget not enough
+                    self.outernode.parentNode.removeChild(self.outernode)
                 self.phosphor.attach(el)
                 window.addEventListener('resize', lambda: (self.phosphor.update(), 
                                                            self._check_real_size()))
             if id == 'body':
-                self.node.classList.add('flx-main-widget')
+                self.outernode.classList.add('flx-main-widget')
         
         @event.connect('children')
         def __children_changed(self, *events):
@@ -442,8 +417,6 @@ class Widget(Model):
                     break
                 i_ok = i
             
-            if new_children:
-                print('adding children on ', self.id, [c.id for c in new_children])
             for child in old_children[i_ok:]:
                 self._remove_child(child)
             for child in new_children[i_ok:]:
@@ -463,19 +436,76 @@ class Widget(Model):
         
         ## Events
         
-        # todo: events: mouse_move?,focus, enter, leave ...
+        # todo: events: focus, enter, leave ... ?
+        
+        CAPTURE_MOUSE = False
+        
+        def _init_events(self):
+            # Connect some standard events
+            self.node.addEventListener('mousedown', self.mouse_down, 0)
+            self.node.addEventListener('wheel', self.mouse_wheel, 0)
+            self.node.addEventListener('keydown', self.key_down, 0)
+            self.node.addEventListener('keyup', self.key_up, 0)
+            self.node.addEventListener('keypress', self.key_press, 0)
+            
+            # Disable context menu so we can handle RMB clicks
+            _context_menu = lambda ev: ev.preventDefault() and False
+            self.node.addEventListener('contextmenu', _context_menu, 0)
+            
+            # Implement mouse capturing. When a mouse is pressed down on
+            # a widget, it "captures" the mouse, and will continue to receive 
+            # move and up events, even if the mouse is not over the widget.
+            
+            self._capture_flag = None
+            
+            def capture(e):
+                # On FF, capture so we get events when outside browser viewport
+                if self.CAPTURE_MOUSE and self.node.setCapture:  
+                    self.node.setCapture()
+                self._capture_flag = 2
+                window.document.addEventListener("mousemove", mouse_outside, True)
+                window.document.addEventListener("mouseup", mouse_outside, True)
+            
+            def release():
+                self._capture_flag = 1
+                window.document.removeEventListener("mousemove", mouse_outside, True)
+                window.document.removeEventListener("mouseup", mouse_outside, True)
+            
+            def mouse_inside(e):
+                if self._capture_flag == 1:
+                    self._capture_flag = 0
+                elif not self._capture_flag:
+                    if e.type == 'mousemove':
+                        self.mouse_move(e)
+                    elif e.type == 'mouseup':
+                        self.mouse_up(e)
+            
+            def mouse_outside(e):
+                if self._capture_flag:  # Should actually always be 0
+                    e = window.event if window.event else e
+                    if e.type == 'mousemove':
+                        self.mouse_move(e)
+                    elif e.type == 'mouseup':
+                        release()
+                        self.mouse_up(e)
+            
+            # Setup capturing and releasing
+            self.node.addEventListener('mousedown', capture, True)
+            self.node.addEventListener("losecapture", release)
+            # Subscribe to normal mouse events
+            self.node.addEventListener("mousemove", mouse_inside, False)
+            self.node.addEventListener("mouseup", mouse_inside, False)
         
         @event.emitter
         def mouse_down(self, e):
             """ Event emitted when the mouse is pressed down.
             
             A mouse event has the following attributes:
-            * x: the x location, in pixels, relative to this widget
-            * y: the y location, in pixels, relative to this widget
-            * pageX: the x location relative to the page
-            * pageY: the y location relative to the page
-            * button: what button the event is about, 1, 2, 3 are left, middle,
-              right, respectively.
+            
+            * pos: the mouse position, in pixels, relative to this widget
+            * page_pos: the mouse position relative to the page
+            * button: what button the event is about, 1, 2, 3 are left, right,
+              middle, respectively. 0 indicates no button.
             * buttons: what buttons where pressed at the time of the event.
             * modifiers: list of strings "Alt", "Shift", "Ctrl", "Meta" for
               modifier keys pressed down at the time of the event.
@@ -488,27 +518,62 @@ class Widget(Model):
             
             See mouse_down() for a description of the event object.
             """
-            return self._create_mouse_event(e)
+            ev = self._create_mouse_event(e)
+            return ev
+        
+        @event.emitter
+        def mouse_move(self, e):
+            """ Event fired when the mouse is moved inside the canvas.
+            See mouse_down for details.
+            """
+            
+            ev = self._create_mouse_event(e)
+            ev.button = 0
+            return ev
+        
+        @event.emitter
+        def mouse_wheel(self, e):
+            """ Event emitted when the mouse wheel is used.
+            
+            See mouse_down() for a description of the event object.
+            Additional event attributes:
+            
+            * hscroll: amount of scrolling in horizontal direction
+            * vscroll: amount of scrolling in vertical direction
+            """
+            # Note: wheel event gets generated also for parent widgets
+            # I think this makes sense, but there might be cases
+            # where we want to prevent propagation.
+            ev = self._create_mouse_event(e)
+            ev.button = 0
+            ev.hscroll = e.deltaX * [1, 16, 600][e.deltaMode]
+            ev.vscroll = e.deltaY * [1, 16, 600][e.deltaMode]
+            return ev
         
         def _create_mouse_event(self, e):
             # note: our button has a value as in JS "which"
             modifiers = [n for n in ('Alt', 'Shift', 'Ctrl', 'Meta')
                          if e[n.lower()+'Key']]
+            # Fix position
             pos = e.clientX, e.clientY
             rect = self.node.getBoundingClientRect()
             offset = rect.left, rect.top
-            x, y = float(pos[0] - offset[0]), float(pos[1] - offset[1])
-            return dict(x=x, y=y, pageX=e.pageX, pageY=e.pageY,
-                        button=e.button+1, buttons=[b+1 for b in e.buttons],
+            pos = float(pos[0] - offset[0]), float(pos[1] - offset[1])
+            # Fix buttons
+            buttons_mask = reversed([c for c in e.buttons.toString(2)]).join('')
+            buttons = [i+1 for i in range(5) if buttons_mask[i] == '1']
+            button = {0: 1, 1: 3, 2: 2, 3: 4, 4: 5}[e.button]
+            # Create event dict
+            return dict(pos=pos, page_pos=(e.pageX, e.pageY),
+                        button=button, buttons=buttons,
                         modifiers=modifiers,
                         )
         
         @event.emitter
         def key_down(self, e):
-            """ Event emitted when a key is pressed down while
-            this widget has focus..
+            """ Event emitted when a key is pressed down while this
+            widget has focus. A key event has the following attributes:
             
-            A key event has the following attributes:
             * key: the character corresponding to the key being pressed, or
               a key name like "Escape", "Alt", "Enter".
             * modifiers: list of strings "Alt", "Shift", "Ctrl", "Meta" for
@@ -561,17 +626,17 @@ class Widget(Model):
         @event.connect('children')
         def __update_css(self, *events):
             children = events[-1].new_value
-            if 'flx-Layout' not in self.node.className:
+            if 'flx-Layout' not in self.outernode.className:
                 # Ok, no layout, so maybe we need to take care of CSS.
                 # If we have a child that is a hbox/vbox, we need to be a
                 # flex container.
-                self.node.style['display'] = ''
-                self.node.style['flex-flow'] = ''
+                self.outernode.style['display'] = ''
+                self.outernode.style['flex-flow'] = ''
                 if len(children) == 1:
-                    subClassName = children[0].node.className
+                    subClassName = children[0].outernode.className
                     if 'flx-hbox' in subClassName:
-                        self.node.style['display'] = 'flex'
-                        self.node.style['flex-flow'] = 'row'
+                        self.outernode.style['display'] = 'flex'
+                        self.outernode.style['flex-flow'] = 'row'
                     elif 'flx-vbox' in subClassName:
-                        self.node.style['display'] = 'flex'
-                        self.node.style['flex-flow'] = 'column'
+                        self.outernode.style['display'] = 'flex'
+                        self.outernode.style['flex-flow'] = 'column'
diff --git a/flexx/ui/layouts/_box.py b/flexx/ui/layouts/_box.py
index ac48cc1..81ca580 100644
--- a/flexx/ui/layouts/_box.py
+++ b/flexx/ui/layouts/_box.py
@@ -221,15 +221,12 @@ class BoxLayout(BaseBoxLayout):
         
         _DEFAULT_ORIENTATION = 'h'
         
-        def init(self):
-            self.phosphor = window.phosphor.panel.Panel()
-        
         @event.connect('orientation', 'children.*.flex')
         def __set_flexes(self, *events):
             ori = self.orientation
             i = 0 if ori in (0, 'h', 'hr') else 1
             for widget in self.children:
-                self._applyBoxStyle(widget.node, 'flex-grow', widget.flex[i])
+                self._applyBoxStyle(widget.outernode, 'flex-grow', widget.flex[i])
             for widget in self.children:
                 widget._check_real_size()
         
@@ -241,28 +238,28 @@ class BoxLayout(BaseBoxLayout):
             children = self.children
             # Reset
             for child in children:
-                child.node.style['margin-top'] = ''
-                child.node.style['margin-left'] = ''
+                child.outernode.style['margin-top'] = ''
+                child.outernode.style['margin-left'] = ''
             for child in old_children:
-                child.node.style['margin-top'] = ''
-                child.node.style['margin-left'] = ''
+                child.outernode.style['margin-top'] = ''
+                child.outernode.style['margin-left'] = ''
             # Set
             margin = 'margin-top' if ori in (1, 'v', 'vr') else 'margin-left'
             if children.length:
                 if ori in ('vr', 'hr'):
-                    children[-1].node.style[margin] = '0px'
+                    children[-1].outernode.style[margin] = '0px'
                     for child in children[:-1]:
-                        child.node.style[margin] = self.spacing + 'px'
+                        child.outernode.style[margin] = self.spacing + 'px'
                 else:
-                    children[0].node.style[margin] = '0px'
+                    children[0].outernode.style[margin] = '0px'
                     for child in children[1:]:
-                        child.node.style[margin] = self.spacing + 'px'
+                        child.outernode.style[margin] = self.spacing + 'px'
             for widget in children:
                 widget._check_real_size()
         
         @event.connect('padding')
         def __padding_changed(self, *events):
-            self.node.style['padding'] = self.padding + 'px'
+            self.outernode.style['padding'] = self.padding + 'px'
             for widget in self.children:
                 widget._check_real_size()
         
@@ -270,15 +267,15 @@ class BoxLayout(BaseBoxLayout):
         def __orientation_changed(self, *events):
             ori = self.orientation
             for name in ('hbox', 'vbox', 'hboxr', 'vboxr'):
-                self.node.classList.remove('flx-'+name)
+                self.outernode.classList.remove('flx-'+name)
             if ori == 0 or ori == 'h':
-                self.node.classList.add('flx-hbox')
+                self.outernode.classList.add('flx-hbox')
             elif ori == 1 or ori == 'v':
-                self.node.classList.add('flx-vbox')
+                self.outernode.classList.add('flx-vbox')
             elif ori == 'hr':
-                self.node.classList.add('flx-hboxr')
+                self.outernode.classList.add('flx-hboxr')
             elif ori == 'vr':
-                self.node.classList.add('flx-vboxr')
+                self.outernode.classList.add('flx-vboxr')
             else:
                 raise ValueError('Invalid box orientation: ' + ori)
             for widget in self.children:
@@ -322,8 +319,9 @@ class BoxPanel(BaseBoxLayout):
         
         _DEFAULT_ORIENTATION = 'h'
         
-        def init(self):
+        def _init_phosphor_and_node(self):
             self.phosphor = window.phosphor.boxpanel.BoxPanel()
+            self.node = self.phosphor.node
         
         @event.connect('orientation', 'children.*.flex')
         def __set_flexes(self, *events):
diff --git a/flexx/ui/layouts/_dock.py b/flexx/ui/layouts/_dock.py
index 62a46f2..5d2fd03 100644
--- a/flexx/ui/layouts/_dock.py
+++ b/flexx/ui/layouts/_dock.py
@@ -63,8 +63,9 @@ class DockPanel(Layout):
     
     class JS:
         
-        def init(self):
+        def _init_phosphor_and_node(self):
             self.phosphor = window.phosphor.dockpanel.DockPanel()
+            self.node = self.phosphor.node
         
         def _add_child(self, widget):
             self.phosphor.insertRight(widget.phosphor)
diff --git a/flexx/ui/layouts/_form.py b/flexx/ui/layouts/_form.py
index 181d0be..71b6412 100644
--- a/flexx/ui/layouts/_form.py
+++ b/flexx/ui/layouts/_form.py
@@ -174,8 +174,9 @@ class FormLayout(BaseTableLayout):
     
     class JS:
         
-        def init(self):
+        def _init_phosphor_and_node(self):
             self.phosphor = window.phosphor.createWidget('table')
+            self.node = self.phosphor.node
         
         def _apply_cell_layout(self, row, col, vflex, hflex, cum_vflex, cum_hflex):
             AUTOFLEX = 729
@@ -208,14 +209,14 @@ class FormLayout(BaseTableLayout):
             # Create element for widget
             td = window.document.createElement("td")
             row.appendChild(td)
-            td.appendChild(widget.node)
+            td.appendChild(widget.outernode)
             #
-            widget.node.hflex = 1
-            widget.node.vflex = widget.flex[1]
+            widget.outernode.hflex = 1
+            widget.outernode.vflex = widget.flex[1]
             self._apply_table_layout()
         
         def _remove_child(self, widget):
-            row = widget.node.parentNode.parentNode
+            row = widget.outernode.parentNode.parentNode
             self.node.removeChild(row)
             if widget._title_elem:
                 del widget._title_elem
@@ -223,7 +224,7 @@ class FormLayout(BaseTableLayout):
         @event.connect('children.*.flex')
         def __update_flexes(self, *events):
             for widget in self.children:
-                widget.node.vflex = widget.flex[1]
+                widget.outernode.vflex = widget.flex[1]
             self._apply_table_layout()
         
         @event.connect('children.*.title')
diff --git a/flexx/ui/layouts/_grid.py b/flexx/ui/layouts/_grid.py
index 1b35469..9742049 100644
--- a/flexx/ui/layouts/_grid.py
+++ b/flexx/ui/layouts/_grid.py
@@ -73,8 +73,9 @@ class GridPanel(Layout):
     
     class JS:
         
-        def init(self):
+        def _init_phosphor_and_node(self):
             self.phosphor = window.phosphor.gridpanel.GridPanel()
+            self.node = self.phosphor.node
             
             that = self
             class LayoutNotifier:
diff --git a/flexx/ui/layouts/_pinboard.py b/flexx/ui/layouts/_pinboard.py
index bfb0631..d12d6fc 100644
--- a/flexx/ui/layouts/_pinboard.py
+++ b/flexx/ui/layouts/_pinboard.py
@@ -37,14 +37,15 @@ class PinboardLayout(Layout):
     """
     
     class JS:
-        def init(self):
+        def _init_phosphor_and_node(self):
             self.phosphor = window.phosphor.panel.Panel()
+            self.node = self.phosphor.node
         
         @event.connect('children.*.pos')
         def __pos_changed(self, *events):
             for child in self.children:
                 pos = child.pos
-                st = child.node.style
+                st = child.outernode.style
                 st.left = pos[0] + "px" if (pos[0] > 1) else pos[0] * 100 + "%"
                 st.top = pos[1] + "px" if (pos[1] > 1) else pos[1] * 100 + "%"
         
diff --git a/flexx/ui/layouts/_split.py b/flexx/ui/layouts/_split.py
index d4ac9e3..c96d3ac 100644
--- a/flexx/ui/layouts/_split.py
+++ b/flexx/ui/layouts/_split.py
@@ -53,8 +53,9 @@ class SplitPanel(Layout):
         
         _DEFAULT_ORIENTATION = 'h'
         
-        def init(self):
+        def _init_phosphor_and_node(self):
             self.phosphor = window.phosphor.splitpanel.SplitPanel()
+            self.node = self.phosphor.node
         
         @event.connect('orientation')
         def __orientation_changed(self, *events):
diff --git a/flexx/ui/layouts/_stack.py b/flexx/ui/layouts/_stack.py
index 319944c..962eac6 100644
--- a/flexx/ui/layouts/_stack.py
+++ b/flexx/ui/layouts/_stack.py
@@ -45,8 +45,9 @@ class StackedPanel(Layout):
     
     class JS:
         
-        def init(self):
+        def _init_phosphor_and_node(self):
             self.phosphor = window.phosphor.stackedpanel.StackedPanel()
+            self.node = self.phosphor.node
         
         @event.connect('current')
         def __set_current_widget(self, *events):
diff --git a/flexx/ui/layouts/_tabs.py b/flexx/ui/layouts/_tabs.py
index b1fdad6..934a96c 100644
--- a/flexx/ui/layouts/_tabs.py
+++ b/flexx/ui/layouts/_tabs.py
@@ -21,8 +21,9 @@ from . import Layout
 #     """ A widget containing tabs.
 #     """
 #     
-#     def init(self):
+#     def _init_phosphor_and_node(self):
 #         self.phosphor = window.phosphor.tabs.TabBar()
+#         self.node = self.phosphor.node
 # 
 #     def _add_child(self, widget):
 #         raise ValueError('A TabBar cannot have children.')
@@ -106,5 +107,6 @@ class TabPanel(Layout):
     
     class JS:
         
-        def init(self):
+        def _init_phosphor_and_node(self):
             self.phosphor = window.phosphor.tabs.TabPanel()
+            self.node = self.phosphor.node
diff --git a/flexx/ui/widgets/_bokeh.py b/flexx/ui/widgets/_bokeh.py
index 08fe228..9cefad9 100644
--- a/flexx/ui/widgets/_bokeh.py
+++ b/flexx/ui/widgets/_bokeh.py
@@ -44,8 +44,7 @@ class BokehWidget(Widget):
     }
     """
     
-    def init(self, *args, **kwargs):
-        super().init(*args, **kwargs)
+    def init(self):
         
         # Handle client dependencies
         import bokeh
diff --git a/flexx/ui/widgets/_button.py b/flexx/ui/widgets/_button.py
index c58caea..2783a01 100644
--- a/flexx/ui/widgets/_button.py
+++ b/flexx/ui/widgets/_button.py
@@ -50,9 +50,10 @@ class Button(Widget):
     
     class JS:
         
-        def init(self):
+        def _init_phosphor_and_node(self):
             self.phosphor = window.phosphor.createWidget('button')
-            self.phosphor.node.addEventListener('click', self.mouse_click, 0)
+            self.node = self.phosphor.node
+            self.node.addEventListener('click', self.mouse_click, 0)
         
         @event.emitter
         def mouse_click(self, e):
diff --git a/flexx/ui/widgets/_canvas.py b/flexx/ui/widgets/_canvas.py
index 7fc4c20..5624779 100644
--- a/flexx/ui/widgets/_canvas.py
+++ b/flexx/ui/widgets/_canvas.py
@@ -20,31 +20,23 @@ class CanvasWidget(Widget):
     
     class JS:
         
-        def init(self):
+        CAPTURE_MOUSE = True
+        
+        def _init_phosphor_and_node(self):
             
             self.phosphor = window.phosphor.createWidget('div')
-            self.canvas = window.document.createElement('canvas')
-            self.phosphor.node.appendChild(self.canvas)
+            self.node = window.document.createElement('canvas')
+            
+            self.phosphor.node.appendChild(self.node)
             # Set position to absolute so that the canvas is not going
             # to be forcing a size on the container div.
-            self.canvas.style.position = 'absolute'
-            
-            self.canvas.addEventListener('mousemove', self.mouse_move, 0)
+            self.node.style.position = 'absolute'
         
         @event.connect('size')
         def _update_canvas_size(self, *events):
             size = events[-1].new_value
             if size[0] or size[1]:
-                self.canvas.width = size[0]
-                self.canvas.height = size[1]
-                self.canvas.style.width = size[0] + 'px'
-                self.canvas.style.height = size[1] + 'px'
-        
-        # todo: define mouse_move in widget?
-        # todo: mouse capturing (is a pain)
-        @event.emitter
-        def mouse_move(self, e):
-            """ Event fired when the mouse is moved inside the canvas.
-            See mouse_down for details.
-            """
-            return self._create_mouse_event(e)
+                self.node.width = size[0]
+                self.node.height = size[1]
+                self.node.style.width = size[0] + 'px'
+                self.node.style.height = size[1] + 'px'
diff --git a/flexx/ui/widgets/_group.py b/flexx/ui/widgets/_group.py
index e3c91c7..1ed045e 100644
--- a/flexx/ui/widgets/_group.py
+++ b/flexx/ui/widgets/_group.py
@@ -43,7 +43,7 @@ class GroupWidget(Widget):
     
     class JS:
         
-        def init(self):
+        def _init_phosphor_and_node(self):
             # class FieldsetPanel(phosphor.panel.Panel):
             #      def createNode():
             #          return document.createElement('fieldset')
@@ -61,6 +61,8 @@ class GroupWidget(Widget):
             #self.phosphor = phosphor.createWidget('fieldset')
             self._legend = window.document.createElement('legend')
             self.phosphor.node.appendChild(self._legend)
+            
+            self.node = self.phosphor.node
         
         @event.connect('title')
         def _title_changed(self, *events):
diff --git a/flexx/ui/widgets/_iframe.py b/flexx/ui/widgets/_iframe.py
index 32a9aad..b28a52e 100644
--- a/flexx/ui/widgets/_iframe.py
+++ b/flexx/ui/widgets/_iframe.py
@@ -35,8 +35,9 @@ class IFrame(Widget):
     
     class JS:
         
-        def init(self):
+        def _init_phosphor_and_node(self):
             self.phosphor = window.phosphor.createWidget('iframe')
+            self.node = self.phosphor.node
         
         @event.connect('url')
         def _update_url(self, *events):
diff --git a/flexx/ui/widgets/_label.py b/flexx/ui/widgets/_label.py
index 21f04a1..705be92 100644
--- a/flexx/ui/widgets/_label.py
+++ b/flexx/ui/widgets/_label.py
@@ -60,8 +60,9 @@ class Label(Widget):
     
     class JS:
         
-        def init(self):
+        def _init_phosphor_and_node(self):
             self.phosphor = window.phosphor.createWidget('div')
+            self.node = self.phosphor.node
         
         @event.connect('text')
         def _text_changed(self, *events):
diff --git a/flexx/ui/widgets/_lineedit.py b/flexx/ui/widgets/_lineedit.py
index 7a4399e..21eea34 100644
--- a/flexx/ui/widgets/_lineedit.py
+++ b/flexx/ui/widgets/_lineedit.py
@@ -63,27 +63,37 @@ class LineEdit(Widget):
     
     class JS:
     
-        def init(self):
-            self.phosphor = window.phosphor.createWidget('div')
-            self.phosphor.node.innerHTML = '<input type="text", list="%s" />' % self.id
-            self._node = self.phosphor.node.childNodes[0]
+        def _init_phosphor_and_node(self):
             
             # self.phosphor = window.phosphor.createWidget('input')
             # self.node = self.phosphor.node
-            # self.node.type = 'text'
-            # self.node.list = self.id
+            # self.node.type = 'text'  
+            # self.node.list = self.id  -> readonly!
+            
+            # Some hacking to create an element with custom HTML
+            # todo: Maybe we can make createWidget accept HTML instead
+            ori = window.phosphor.widget.Widget.createNode
+            def _():
+                d = window.document.createElement('div')
+                d.innerHTML = '<input type="text", list="%s" />' % self.id
+                return d.childNodes[0]
+            window.phosphor.widget.Widget.createNode = _
+            self.phosphor = window.phosphor.widget.Widget()
+            window.phosphor.widget.Widget.createNode = ori
+            
+            self.node = self.phosphor.node
             
             self._autocomp = window.document.createElement('datalist')
             self._autocomp.id = self.id
-            self._node.appendChild(self._autocomp)
+            self.node.appendChild(self._autocomp)
             
-            f1 = lambda ev: self._set_prop('user_text', self._node.value)
+            f1 = lambda ev: self._set_prop('user_text', self.node.value)
             f2 = lambda ev: self.submit() if ev.which == 13 else None
-            self._node.addEventListener('input', f1, False)
-            self._node.addEventListener('keydown', f2, False)
+            self.node.addEventListener('input', f1, False)
+            self.node.addEventListener('keydown', f2, False)
             #if IE10:
             #    this.node.addEventListener('change', f1, False)
-            
+        
         @event.readonly
         def user_text(self, v=None):
             """ The text set by the user (updates on each keystroke). """
@@ -100,11 +110,11 @@ class LineEdit(Widget):
         
         @event.connect('text')
         def __text_changed(self, *events):
-            self._node.value = self.text
+            self.node.value = self.text
         
         @event.connect('placeholder_text')
         def __placeholder_text_changed(self, *events):
-            self._node.placeholder = self.placeholder_text
+            self.node.placeholder = self.placeholder_text
         
         # todo: this works in Firefox but not in Xul
         @event.connect('autocomp')
diff --git a/flexx/ui/widgets/_plotwidget.py b/flexx/ui/widgets/_plotwidget.py
index 4b0e641..7a707c4 100644
--- a/flexx/ui/widgets/_plotwidget.py
+++ b/flexx/ui/widgets/_plotwidget.py
@@ -114,7 +114,7 @@ class PlotWidget(CanvasWidget):
         
         def init(self):
             super().init()
-            self._context = self.canvas.getContext('2d')
+            self._context = self.node.getContext('2d')
             
             # create tick units
             self._tick_units = []
diff --git a/flexx/ui/widgets/_progressbar.py b/flexx/ui/widgets/_progressbar.py
index 98ce4c7..f868130 100644
--- a/flexx/ui/widgets/_progressbar.py
+++ b/flexx/ui/widgets/_progressbar.py
@@ -57,9 +57,10 @@ class ProgressBar(Widget):
     
     class JS:
     
-        def init(self):
+        def _init_phosphor_and_node(self):
             self.phosphor = window.phosphor.createWidget('progress')
-    
+            self.node = self.phosphor.node
+        
         @event.connect('value')
         def __value_changed(self, *events):
             self.node.value = events[-1].new_value
diff --git a/flexx/ui/widgets/_slider.py b/flexx/ui/widgets/_slider.py
index 5c5fd66..5ce8552 100644
--- a/flexx/ui/widgets/_slider.py
+++ b/flexx/ui/widgets/_slider.py
@@ -61,12 +61,14 @@ class Slider(Widget):
         return float(v)
     
     class JS:
-    
-        def init(self):
+        
+        def _init_phosphor_and_node(self):
             self.phosphor = window.phosphor.createWidget('input')
-            self.phosphor.node.type = 'range'
+            self.node = self.phosphor.node
+            
+            self.node.type = 'range'
             f = lambda ev: self._set_prop('user_value', self.node.value)
-            self.phosphor.node.addEventListener('input', f, False)
+            self.node.addEventListener('input', f, False)
             #if IE10:
             #   this.node.addEventListener('change', f, False)
         
diff --git a/flexx/util/eventhelper.py b/flexx/util/eventhelper.py
new file mode 100644
index 0000000..7354496
--- /dev/null
+++ b/flexx/util/eventhelper.py
@@ -0,0 +1,63 @@
+import os
+
+
+def iter_filenames(dir, ignore):
+    for dirpath, dirnames, filenames in os.walk(dir):
+        for fname in filenames:
+            filename = os.path.join(dirpath, fname)
+            if fname.endswith('.py'):
+                if fname not in ignore and filename not in ignore:
+                    yield os.path.join(dirpath, fname)
+
+
+def event_helper(dir, ignore=()):
+    """ Helper function to convert from the old event system to the new one.
+    This function does some basic analysis of the code in the given directory
+    and gives locations where properties are used as functions, or where
+    handlers seem incorrect. When used inside Pyzo, you can just click on
+    the filename to go there and fix it.
+    """
+    
+    props = set()
+    
+    # Collect properties
+    for filename in iter_filenames(dir, ignore):
+        text = open(filename, 'rb').read().decode()
+        prevline = ''
+        for i, line in enumerate(text.splitlines()):
+            if prevline.strip().startswith(('@event.prop', '@event.readonly')):
+                funcname = line.strip().split('def ')[1].split('(')[0].strip()
+                props.add(funcname)
+            prevline = line
+        
+    print('Found props/readonlies:')
+    print(props)
+    
+    # Check correct use of properties
+    for filename in iter_filenames(dir, ignore):
+        text = open(filename, 'rb').read().decode()
+        for i, line in enumerate(text.splitlines()):
+            
+            for prop in props:
+                t = '.%s(' % prop
+                if t in line:
+                    print('Old use of prop %s in File "%s", line %i' %
+                          (prop, filename, i+1))
+    
+    # Check correct use of handlers
+    for filename in iter_filenames(dir, ignore):
+        text = open(filename, 'rb').read().decode()
+        prevline = ''
+        for i, line in enumerate(text.splitlines()):
+            if prevline.strip().startswith('@event.connect'):
+                if 'def' not in line:
+                    continue  # without setting prevline
+                if '*events' not in line:
+                    funcname = line.strip().split('def ')[1].split('(')[0].strip()
+                    print('Suspicious handler %s in File "%s", line %i' %
+                          (funcname, filename, i+1))
+            prevline = line
+
+
+if __name__ == '__main__':
+    event_helper(r'd:\dev\pylib\arbiter\arbiter\viz', ['transform.py'])


Test patch:
diff --git a/flexx/app/tests/test_live.py b/flexx/app/tests/test_live.py
index 12e8461..6454e16 100644
--- a/flexx/app/tests/test_live.py
+++ b/flexx/app/tests/test_live.py
@@ -159,9 +159,9 @@ class ModelD(ModelB):
             super().init()
             print(self.foo3, self.bar3)
             
-            assert self.foo3 is None
+            assert self.foo3 == 2
             self.foo3 = 10
-            assert self.foo3 is None
+            assert self.foo3 == 2
             
             assert self.bar3 == 2
             self.bar3 = 10
@@ -193,7 +193,7 @@ class ModelE(ModelA):
     
     def test_init(self):
         app.call_later(0.2, self._emit_foo)
-        app.call_later(0.9, lambda:self.call_js('set_result()'))
+        app.call_later(0.3, lambda:self.call_js('set_result()'))
     
     def _emit_foo(self):
         self.emit('foo', {})
@@ -272,5 +272,5 @@ def test_apps():
 
 # NOTE: beware future self: if running this in Pyzo, turn off GUI integration!
 
-# runner(ModelD)
+# runner(ModelE)
 run_tests_if_main()
diff --git a/flexx/app/tests/test_model.py b/flexx/app/tests/test_model.py
index 89df015..417452d 100644
--- a/flexx/app/tests/test_model.py
+++ b/flexx/app/tests/test_model.py
@@ -3,7 +3,10 @@
 
 from flexx.util.testing import run_tests_if_main, raises
 
-from flexx.app.model import Model
+import logging
+import tornado
+
+from flexx.app.model import Model, _get_active_models
 from flexx import event
 
 
@@ -100,4 +103,39 @@ def test_no_duplicate_code():
     assert '_red_func' in Foo4.JS.CODE
 
 
+def test_active_models():
+    
+    # Test that by default there are no active models
+    m = Model()
+    assert not _get_active_models()
+    
+    # Test that model is active in its context
+    with m:
+        assert _get_active_models() == [m]
+    
+    # Can do this
+    app = tornado.ioloop.IOLoop.instance()
+    app.run_sync(lambda x=None: None)
+    
+    
+    class PMHandler(logging.Handler):
+        def emit(self, record):
+            if record.exc_info:
+                self.last_type, self.last_value, self.last_traceback = record.exc_info
+            return record
+    
+    handler = PMHandler()
+    root_logger = logging.getLogger()
+    root_logger.addHandler(handler)
+    
+    # Test that we prevent going back to Tornado in context
+    handler.last_type = None
+    with m:
+        assert _get_active_models() == [m]
+        # This raises error, but gets caught by Tornado
+        app.run_sync(lambda x=None: None)
+    assert handler.last_type is RuntimeError
+    assert 'risk on race conditions' in str(handler.last_value)
+
+
 run_tests_if_main()