diff --git a/enaml/qt/q_window_base.py b/enaml/qt/q_window_base.py index 25b2dbdf9..605f9772d 100644 --- a/enaml/qt/q_window_base.py +++ b/enaml/qt/q_window_base.py @@ -5,7 +5,7 @@ # # The full license is in the file COPYING.txt, distributed with this software. #------------------------------------------------------------------------------ -from .QtCore import Qt, QSize, Signal +from .QtCore import Qt, QSize from .QtGui import QWidget, QLayout from .q_single_widget_layout import QSingleWidgetLayout @@ -55,11 +55,6 @@ class QWindowBase(QWidget): normally be computed by the layout. """ - #: A signal which can be emitted when the window is closed. The - #: decision as to when and if to emit this signal is left up to - #: the derived classes. - closed = Signal() - def __init__(self, parent=None, flags=Qt.WindowFlags(0)): """ Initialize a QWindowBase. @@ -68,6 +63,9 @@ def __init__(self, parent=None, flags=Qt.WindowFlags(0)): parent : QWidget, optional The parent of the window. + flags : Qt.WindowFlags, optional + The window flags to pass to the parent constructor. + """ super(QWindowBase, self).__init__(parent, flags) self._expl_min_size = QSize() diff --git a/enaml/qt/qt_dialog.py b/enaml/qt/qt_dialog.py index 8640695c8..2b2a9f7d4 100644 --- a/enaml/qt/qt_dialog.py +++ b/enaml/qt/qt_dialog.py @@ -5,37 +5,57 @@ # # The full license is in the file COPYING.txt, distributed with this software. #------------------------------------------------------------------------------ -from atom.api import Typed +from atom.api import Typed, atomref from enaml.widgets.dialog import ProxyDialog +from enaml.widgets.window import CloseEvent -from .QtCore import Qt, Signal +from .QtCore import Qt from .QtGui import QDialog +from .q_deferred_caller import deferredCall from .q_window_base import QWindowBase -from .qt_window import QtWindow +from .qt_window import QtWindow, finalize_close class QWindowDialog(QDialog, QWindowBase): """ A window base subclass which implements dialog behavior. """ - # This signal must be redefined, or the QtWindow base class will - # not be able to connect to it. This is a quirk of using multiple - # inheritance with PyQt. Note that this signal is never emitted - # it is here only for API compatibility with the base class. - closed = Signal() - - def __init__(self, parent=None, flags=Qt.Widget): + def __init__(self, proxy, parent=None, flags=Qt.Widget): """ Initialize a QWindowDialog. Parameters ---------- + proxy : QtDialog + The proxy object which owns this dialog. Only an atomref + will be maintained to this object. + parent : QWidget, optional The parent of the dialog. + flags : Qt.WindowFlags, optional + The window flags to pass to the parent constructor. + """ super(QWindowDialog, self).__init__(parent, flags) + self._proxy_ref = atomref(proxy) + + def closeEvent(self, event): + """ Handle the close event for the dialog. + + """ + event.accept() + if not self._proxy_ref: + return + proxy = self._proxy_ref() + d = proxy.declaration + d_event = CloseEvent() + d.closing(d_event) + if d_event.is_accepted(): + super(QWindowDialog, self).closeEvent(event) + else: + event.ignore() class QtDialog(QtWindow, ProxyDialog): @@ -49,7 +69,7 @@ def create_widget(self): """ flags = self.creation_flags() - self.widget = QWindowDialog(self.parent_widget(), flags) + self.widget = QWindowDialog(self, self.parent_widget(), flags) def init_widget(self): """ Initialize the underlying widget. @@ -73,7 +93,7 @@ def on_finished(self): d.accepted() else: d.rejected() - d._handle_close() + deferredCall(finalize_close, d) #-------------------------------------------------------------------------- # ProxyDialog API diff --git a/enaml/qt/qt_main_window.py b/enaml/qt/qt_main_window.py index f6f952833..a01220b0e 100644 --- a/enaml/qt/qt_main_window.py +++ b/enaml/qt/qt_main_window.py @@ -7,11 +7,12 @@ #------------------------------------------------------------------------------ import sys -from atom.api import Typed +from atom.api import Typed, atomref from enaml.widgets.main_window import ProxyMainWindow +from enaml.widgets.window import CloseEvent -from .QtCore import Qt, Signal +from .QtCore import Qt from .QtGui import QMainWindow from .q_deferred_caller import deferredCall @@ -20,25 +21,47 @@ from .qt_menu_bar import QtMenuBar from .qt_status_bar import QtStatusBar from .qt_tool_bar import QtToolBar -from .qt_window import QtWindow +from .qt_window import QtWindow, finalize_close class QCustomMainWindow(QMainWindow): """ A custom QMainWindow which adds some Enaml specific features. """ - #: A signal emitted when the window is closed by the user - closed = Signal() + def __init__(self, proxy, parent=None, flags=Qt.Widget): + """ Initialize a QCustomMainWindow. + + Parameters + ---------- + proxy : QtMainWindow + The proxy object which owns this window. Only an atomref + will be maintained to this object. + + parent : QWidget, optional + The parent of the window. + + flags : Qt.WindowFlags, optional + The window flags to pass to the parent constructor. + + """ + super(QCustomMainWindow, self).__init__(parent, Qt.Window | flags) + self._proxy_ref = atomref(proxy) - #-------------------------------------------------------------------------- - # Private API - #-------------------------------------------------------------------------- def closeEvent(self, event): - """ A close event handler which emits the 'closed' signal. + """ Handle the close event for the window. """ - super(QCustomMainWindow, self).closeEvent(event) - self.closed.emit() + event.accept() + if not self._proxy_ref: + return + proxy = self._proxy_ref() + d = proxy.declaration + d_event = CloseEvent() + d.closing(d_event) + if d_event.is_accepted(): + deferredCall(finalize_close, d) + else: + event.ignore() #-------------------------------------------------------------------------- # Public API @@ -109,7 +132,7 @@ def create_widget(self): """ flags = self.creation_flags() - widget = QCustomMainWindow(self.parent_widget(), flags) + widget = QCustomMainWindow(self, self.parent_widget(), flags) widget.setDocumentMode(True) widget.setDockNestingEnabled(True) self.widget = widget diff --git a/enaml/qt/qt_window.py b/enaml/qt/qt_window.py index 2aecbe231..2f112dde1 100644 --- a/enaml/qt/qt_window.py +++ b/enaml/qt/qt_window.py @@ -7,14 +7,15 @@ #------------------------------------------------------------------------------ import sys -from atom.api import Typed +from atom.api import Typed, atomref from enaml.layout.geometry import Pos, Rect, Size -from enaml.widgets.window import ProxyWindow +from enaml.widgets.window import ProxyWindow, CloseEvent from .QtCore import Qt, QPoint, QRect, QSize from .QtGui import QApplication, QIcon +from .q_deferred_caller import deferredCall from .q_resource_helpers import get_cached_qicon from .q_window_base import QWindowBase from .qt_container import QtContainer @@ -28,6 +29,19 @@ } +def finalize_close(d): + """ Finalize the closing of the declaration object. + + This is performed as a deferred call so that the window may fully + close before the declaration is potentially destroyed. + + """ + d.visible = False + d.closed() + if d.destroy_on_close: + d.destroy() + + class QWindow(QWindowBase): """ A window base subclass which handles the close event. @@ -35,26 +49,40 @@ class QWindow(QWindowBase): on its central widget, unless the user explicitly changes them. """ - def __init__(self, parent=None, flags=Qt.Widget): + def __init__(self, proxy, parent=None, flags=Qt.Widget): """ Initialize a QWindow. Parameters ---------- + proxy : QtWindow + The proxy object which owns this window. Only an atomref + will be maintained to this object. + parent : QWidget, optional The parent of the window. + flags : Qt.WindowFlags, optional + The window flags to pass to the parent constructor. + """ super(QWindow, self).__init__(parent, Qt.Window | flags) + self._proxy_ref = atomref(proxy) def closeEvent(self, event): - """ Handle the QCloseEvent from the window system. - - By default, this handler calls the superclass' method to close - the window and then emits the 'closed' signal. + """ Handle the close event for the window. """ - super(QWindow, self).closeEvent(event) - self.closed.emit() + event.accept() + if not self._proxy_ref: + return + proxy = self._proxy_ref() + d = proxy.declaration + d_event = CloseEvent() + d.closing(d_event) + if d_event.is_accepted(): + deferredCall(finalize_close, d) + else: + event.ignore() class QtWindow(QtWidget, ProxyWindow): @@ -81,7 +109,7 @@ def create_widget(self): """ flags = self.creation_flags() - self.widget = QWindow(self.parent_widget(), flags) + self.widget = QWindow(self, self.parent_widget(), flags) def init_widget(self): """ Initialize the widget. @@ -99,7 +127,6 @@ def init_widget(self): self.set_modality(d.modality) if d.icon: self.set_icon(d.icon) - self.widget.closed.connect(self.on_closed) def init_layout(self): """ Initialize the widget layout. @@ -125,14 +152,6 @@ def central_widget(self): if d is not None: return d.proxy.widget - def on_closed(self): - """ The signal handler for the 'closed' signal. - - This method will fire the 'closed' event on the declaration. - - """ - self.declaration._handle_close() - #-------------------------------------------------------------------------- # Child Events #-------------------------------------------------------------------------- diff --git a/enaml/widgets/window.py b/enaml/widgets/window.py index b60979811..c0cf7f2c5 100644 --- a/enaml/widgets/window.py +++ b/enaml/widgets/window.py @@ -6,11 +6,10 @@ # The full license is in the file COPYING.txt, distributed with this software. #------------------------------------------------------------------------------ from atom.api import ( - Unicode, Enum, Bool, Event, Coerced, Typed, ForwardTyped, observe, + Atom, Unicode, Enum, Bool, Event, Coerced, Typed, ForwardTyped, observe, set_default ) -from enaml.application import deferred_call from enaml.core.declarative import d_ from enaml.icon import Icon from enaml.layout.geometry import Pos, Rect, Size @@ -90,6 +89,40 @@ def close(self): raise NotImplementedError +class CloseEvent(Atom): + """ An payload object carried by a window 'closing' event. + + User code can manipulate this object to veto a close event. + + """ + #: The internal accepted state. + _accepted = Bool(True) + + def is_accepted(self): + """ Get whether or not the event is accepted. + + Returns + ------- + result : bool + True if the event is accepted, False otherwise. The + default is True. + + """ + return self._accepted + + def accept(self): + """ Accept the close event and allow the window to be closed. + + """ + self._accepted = True + + def ignore(self): + """ Reject the close event and prevent the window from closing. + + """ + self._accepted = False + + class Window(Widget): """ A top-level Window component. @@ -134,8 +167,14 @@ class Window(Widget): #: Changes to this value after the window is shown will be ignored. always_on_top = d_(Bool(False)) - #: An event fired when the window is closed. This event is triggered - #: by the proxy object when the window is closed. + #: An event fired when the user request the window to be closed. + #: This will happen when the user clicks on the "X" button in the + #: title bar button, or when the 'close' method is called. The + #: payload will be a CloseEvent object which will allow code to + #: veto the close event and prevent the window from closing. + closing = d_(Event(CloseEvent), writable=False) + + #: An event fired when the window is closed. closed = d_(Event(), writable=False) #: Windows are invisible by default. @@ -387,15 +426,3 @@ def _update_proxy(self, change): """ # The superclass handler implementation is sufficient. super(Window, self)._update_proxy(change) - - #-------------------------------------------------------------------------- - # Private API - #-------------------------------------------------------------------------- - def _handle_close(self): - """ Handle the close event from the proxy widget. - - """ - self.visible = False - self.closed() - if self.destroy_on_close: - deferred_call(self.destroy) diff --git a/enaml/wx/wx_window.py b/enaml/wx/wx_window.py index 4336df0a4..d0d3fb746 100644 --- a/enaml/wx/wx_window.py +++ b/enaml/wx/wx_window.py @@ -10,7 +10,7 @@ from atom.api import Typed from enaml.layout.geometry import Pos, Rect, Size -from enaml.widgets.window import ProxyWindow +from enaml.widgets.window import ProxyWindow, CloseEvent from .wx_action import wxAction from .wx_container import WxContainer @@ -19,6 +19,19 @@ from .wx_widget import WxWidget +def finalize_close(d): + """ Finalize the closing of the declaration object. + + This is performed as a deferred call so that the window may fully + close before the declaration is potentially destroyed. + + """ + d.visible = False + d.closed() + if d.destroy_on_close: + d.destroy() + + class wxCustomWindow(wx.Frame): """ A custom wxFrame which manages a central widget. @@ -206,11 +219,17 @@ def on_close(self, event): """ The event handler for the EVT_CLOSE event. """ - event.Skip() - # Make sure the frame is not modal when closing, or no other - # windows will be unblocked. - self.widget.MakeModal(False) - self.declaration._handle_close() + d = self.declaration + d_event = CloseEvent() + d.closing(d_event) + if d_event.is_accepted(): + event.Skip() + # Make sure the frame is not modal when closing, or no other + # windows will be unblocked. + self.widget.MakeModal(False) + wx.CallAfter(finalize_close, d) + else: + event.Veto() def on_layout_requested(self, event): """ Handle the layout request event from the central widget.