diff --git a/pyface/i_widget.py b/pyface/i_widget.py index bdf2861f5..c30110654 100644 --- a/pyface/i_widget.py +++ b/pyface/i_widget.py @@ -12,6 +12,7 @@ from traits.api import Any, Bool, HasTraits, Interface +from traits.trait_base import not_none class IWidget(Interface): @@ -90,11 +91,21 @@ def _remove_event_listeners(self): """ Remove toolkit-specific bindings for events """ -class MWidget(object): +class MWidget(HasTraits): """ The mixin class that contains common code for toolkit specific implementations of the IWidget interface. """ + def destroy(self): + """ Destroy the control if it exists. + + Subclasses of this mixin should ensure that they call super() + if they override this method. + """ + if self.control is not None: + self._remove_event_listeners() + self.control = None + # ------------------------------------------------------------------------ # Protected 'IWidget' interface. # ------------------------------------------------------------------------ @@ -104,6 +115,9 @@ def _create(self): This method should create the control and assign it to the :py:attr:``control`` trait. + + Subclasses of this mixin should ensure that they call super() + if they override this method. """ self.control = self._create_control(self.parent) self._add_event_listeners() @@ -111,6 +125,8 @@ def _create(self): def _create_control(self, parent): """ Create toolkit specific control that represents the widget. + Subclasses of this mixin should implement this method. + Parameters ---------- parent : toolkit control @@ -125,9 +141,29 @@ def _create_control(self, parent): raise NotImplementedError() def _add_event_listeners(self): - """ Set up toolkit-specific bindings for events """ - pass + """ Set up toolkit-specific bindings for events and trait observers + + The default implementation sets up observers for all traits with + the `widget_observer` metadata. + + Subclasses should override with additional listeners, but must call + super() to ensure superclass listeners are also connected. + """ + for name, trait in self.traits(widget_observer=not_none).items(): + observer = getattr(self, trait.widget_observer) + self.observe(observer, name, dispatch='ui') def _remove_event_listeners(self): - """ Remove toolkit-specific bindings for events """ - pass + """ Remove toolkit-specific bindings for events and trait observers + + The default implementation removes up observers for all traits with + the `widget_observer` metadata. + + Subclasses should override with additional listeners, but must call + super() to ensure superclass listeners are also removed. + """ + for name, trait in self.traits(widget_observer=not_none).items(): + observer = trait.widget_observer + if isinstance(observer, str): + observer = getattr(self, observer) + self.observe(observer, name, dispatch='ui', remove=True) diff --git a/pyface/tests/test_widget.py b/pyface/tests/test_widget.py index 900acb84d..8106f1e2d 100644 --- a/pyface/tests/test_widget.py +++ b/pyface/tests/test_widget.py @@ -11,8 +11,10 @@ import unittest +from traits.api import Any, Bool from traits.testing.unittest_tools import UnittestTools +from ..application_window import ApplicationWindow from ..toolkit import toolkit_object from ..widget import Widget @@ -21,6 +23,10 @@ class ConcreteWidget(Widget): + + # a trait for testing connection of observeres + flag = Bool(widget_observer='_flag_updated') + def _create_control(self, parent): if toolkit_object.toolkit == "wx": import wx @@ -38,6 +44,9 @@ def _create_control(self, parent): control = None return control + def _flag_updated(self, event): + self.flag_event = event + class TestWidget(unittest.TestCase, UnittestTools): def setUp(self): @@ -83,6 +92,36 @@ def test_enabled(self): self.assertFalse(self.widget.enabled) + def test_widget_observers(self): + self.window = ApplicationWindow() + self.window._create() + + self.widget = ConcreteWidget(parent=self.window.control) + self.widget._create() + + try: + with self.assertTraitChanges(self.widget, "flag", count=1): + self.widget.flag = True + + flag_event = getattr(self.widget, 'flag_event', None) + self.assertIsNotNone(flag_event) + self.assertEqual(flag_event.object, self.widget) + self.assertEqual(flag_event.name, "flag") + self.assertEqual(flag_event.old, False) + self.assertEqual(flag_event.new, True) + + self.widget.flag_event = None + + self.widget.destroy() + + with self.assertTraitChanges(self.widget, "flag", count=1): + self.widget.flag = False + + self.assertIsNone(self.widget.flag_event) + + finally: + self.window.destroy() + @unittest.skipIf(no_gui_test_assistant, "No GuiTestAssistant") class TestConcreteWidget(unittest.TestCase, GuiTestAssistant): diff --git a/pyface/ui/qt4/widget.py b/pyface/ui/qt4/widget.py index dcf64b05b..3494cef9d 100644 --- a/pyface/ui/qt4/widget.py +++ b/pyface/ui/qt4/widget.py @@ -73,13 +73,13 @@ def enable(self, enabled): self.control.setEnabled(enabled) def destroy(self): - self._remove_event_listeners() if self.control is not None: self.control.hide() self.control.deleteLater() - self.control = None + super().destroy() def _add_event_listeners(self): + super()._add_event_listeners() self.control.installEventFilter(self._event_filter) def _remove_event_listeners(self): @@ -87,6 +87,7 @@ def _remove_event_listeners(self): if self.control is not None: self.control.removeEventFilter(self._event_filter) self._event_filter = None + super()._remove_event_listeners() # Trait change handlers -------------------------------------------------- diff --git a/pyface/ui/wx/widget.py b/pyface/ui/wx/widget.py index 6f29f7c90..c0cbeaeb3 100644 --- a/pyface/ui/wx/widget.py +++ b/pyface/ui/wx/widget.py @@ -70,4 +70,4 @@ def enable(self, enabled): def destroy(self): if self.control is not None: self.control.Destroy() - self.control = None + super().destroy()