def test_add_middle_mergeable(self): history = UndoHistory() example = SimpleExample(str_value='foo', value=10) undo_item = UndoItem( object=example, name='str_value', old_value='bar', new_value='baz', ) history.add( UndoItem( object=example, name='str_value', old_value='foo', new_value='bar', )) history.add( UndoItem( object=example, name='str_value', old_value='bar', new_value='wombat', ), ) history.undo() with self.assertTraitDoesNotChange(history, 'undoable'): with self.assertTraitChanges(history, 'redoable', count=1): history.add(undo_item) self.assertEqual(history.now, 1) self.assertTrue(history.can_undo) self.assertFalse(history.can_redo)
def test_add_end_extend(self): history = UndoHistory() example = SimpleExample(str_value='foo', value=10) undo_item = UndoItem( object=example, name='value', old_value=0, new_value=10, ) history.add( UndoItem( object=example, name='str_value', old_value='foo', new_value='bar', )) with self.assertTraitDoesNotChange(history, 'undoable'): with self.assertTraitDoesNotChange(history, 'redoable'): history.add(undo_item, extend=True) self.assertEqual(history.now, 1) self.assertTrue(history.can_undo) self.assertFalse(history.can_redo)
def test_add_end_extend_merge(self): history = UndoHistory() example = SimpleExample(str_value='foo', value=10) undo_item = UndoItem( object=example, name='str_value', old_value='foo', new_value='baz', ) history.add( UndoItem( object=example, name='str_value', old_value='foo', new_value='bar', )) with self.assertTraitDoesNotChange(history, 'undoable'): with self.assertTraitDoesNotChange(history, 'redoable'): history.add(undo_item, extend=True) self.assertEqual(history.now, 1) self.assertTrue(history.can_undo) self.assertFalse(history.can_redo) # XXX this is testing private state to ensure merge happened self.assertEqual(len(history.stack._stack), 1)
def test_general_command_do(self): history = UndoHistory() command = DummyCommand() history.add(command) self.assertEqual(command.data, "do")
def test_clear_end(self): history = UndoHistory() self._populate_history(history) with self.assertTraitDoesNotChange(history, 'redoable'): with self.assertTraitChanges(history, 'undoable', count=1): with self.assertTraitDoesNotChange(self._example, 'anytrait'): history.clear() self.assertEqual(history.now, 0) self.assertFalse(history.can_undo) self.assertFalse(history.can_redo)
def test_revert_end(self): history = UndoHistory() self._populate_history(history) with self.assertTraitChanges(history, 'redoable', count=2): with self.assertTraitChanges(history, 'undoable', count=1): with self.assertTraitChanges(self._example, 'anytrait', count=3): # noqa: E501 history.revert() self.assertEqual(history.now, 0) self.assertFalse(history.can_undo) self.assertFalse(history.can_redo)
def test_undo_last(self): history = UndoHistory() self._populate_history(history) with self.assertTraitDoesNotChange(history, 'undoable'): with self.assertTraitChanges(history, 'redoable', count=1): with self.assertTraitChanges(self._example, 'anytrait', count=1): # noqa: E501 history.undo() self.assertEqual(history.now, 2) self.assertTrue(history.can_undo) self.assertTrue(history.can_redo)
def __init__(self, ui, parent, allow_buttons): """ Initializes the object. """ self.ui = ui history = None view = ui.view title = view.title # Reset any existing history listeners: history = ui.history if history is not None: history.on_trait_change(self._on_undoable, "undoable", remove=True) history.on_trait_change(self._on_redoable, "redoable", remove=True) history.on_trait_change( self._on_revertable, "undoable", remove=True ) # Determine if we need any buttons or an 'undo' history: buttons = [self.coerce_button(button) for button in view.buttons] nbuttons = len(buttons) if nbuttons == 0: if view.undo: self.check_button(buttons, UndoButton) if view.revert: self.check_button(buttons, RevertButton) if view.help: self.check_button(buttons, HelpButton) if allow_buttons and (history is None): for button in buttons: if self.is_button(button, "Undo") or self.is_button( button, "Revert" ): history = UndoHistory() break ui.history = history # Create a container panel to put everything in: cpanel = getattr(self, "control", None) if cpanel is not None: cpanel.SetSizer(None) cpanel.DestroyChildren() else: self.control = cpanel = TraitsUIPanel(parent, -1) # Create the actual trait sheet panel and embed it in a scrollable # window (if requested): sw_sizer = wx.BoxSizer(wx.VERTICAL) if ui.scrollable: sizer = wx.BoxSizer(wx.VERTICAL) sw = TraitsUIScrolledPanel(cpanel) sizer.Add(panel(ui, sw), 1, wx.EXPAND) sw.SetSizerAndFit(sizer) sw.SetScrollRate(16, 16) else: sw = panel(ui, cpanel) if (title != "") and ( not isinstance(getattr(parent, "owner", None), DockWindow) ): sw_sizer.Add( heading_text(cpanel, text=title).control, 0, wx.EXPAND ) self.add_toolbar(sw_sizer) sw_sizer.Add(sw, 1, wx.EXPAND) if allow_buttons and ( (nbuttons != 1) or (not self.is_button(buttons[0], "")) ): # Add the special function buttons: sw_sizer.Add(wx.StaticLine(cpanel, -1), 0, wx.EXPAND) b_sizer = wx.BoxSizer(wx.HORIZONTAL) for button in buttons: if self.is_button(button, "Undo"): self.undo = self.add_button( button, b_sizer, self._on_undo, False ) self.redo = self.add_button( button, b_sizer, self._on_redo, False, "Redo" ) history.on_trait_change( self._on_undoable, "undoable", dispatch="ui" ) history.on_trait_change( self._on_redoable, "redoable", dispatch="ui" ) elif self.is_button(button, "Revert"): self.revert = self.add_button( button, b_sizer, self._on_revert, False ) history.on_trait_change( self._on_revertable, "undoable", dispatch="ui" ) elif self.is_button(button, "Help"): self.add_button(button, b_sizer, self._on_help) elif not self.is_button(button, ""): self.add_button(button, b_sizer) sw_sizer.Add(b_sizer, 0, wx.ALIGN_RIGHT | wx.ALL, 5) cpanel.SetSizerAndFit(sw_sizer)
def __init__(self, ui, parent, is_subpanel): """Initialise the object. """ self.ui = ui history = ui.history view = ui.view # Reset any existing history listeners. if history is not None: history.observe(self._on_undoable, "undoable", remove=True, dispatch="ui") history.observe(self._on_redoable, "redoable", remove=True, dispatch="ui") history.observe(self._on_revertable, "undoable", remove=True, dispatch="ui") # Determine if we need any buttons or an 'undo' history. buttons = [self.coerce_button(button) for button in view.buttons] nr_buttons = len(buttons) has_buttons = not is_subpanel and (nr_buttons != 1 or not self.is_button(buttons[0], "")) if nr_buttons == 0: if view.undo: self.check_button(buttons, UndoButton) if view.revert: self.check_button(buttons, RevertButton) if view.help: self.check_button(buttons, HelpButton) if not is_subpanel and history is None: for button in buttons: if self.is_button(button, "Undo") or self.is_button( button, "Revert"): history = ui.history = UndoHistory() break # Create the panel. self.control = panel(ui) # Suppress the title if this is a subpanel or if we think it should be # superceded by the title of an "outer" widget (eg. a dock widget). title = view.title if (is_subpanel or (isinstance(parent, QtGui.QMainWindow) and not isinstance(parent.parent(), QtGui.QDialog)) or isinstance(parent, QtGui.QTabWidget)): title = "" # Panels must be widgets as it is only the TraitsUI PyQt code that can # handle them being layouts as well. Therefore create a widget if the # panel is not a widget or if we need a title or buttons. if (not isinstance(self.control, QtGui.QWidget) or title != "" or has_buttons): w = QtGui.QWidget() layout = QtGui.QVBoxLayout(w) layout.setContentsMargins(0, 0, 0, 0) # Handle any view title. if title != "": layout.addWidget(heading_text(None, text=view.title).control) if isinstance(self.control, QtGui.QWidget): layout.addWidget(self.control) elif isinstance(self.control, QtGui.QLayout): layout.addLayout(self.control) self.control = w # Add any buttons. if has_buttons: # Add the horizontal separator separator = QtGui.QFrame() separator.setFrameStyle(QtGui.QFrame.Sunken | QtGui.QFrame.HLine) separator.setFixedHeight(2) layout.addWidget(separator) # Add the special function buttons bbox = QtGui.QDialogButtonBox(QtCore.Qt.Horizontal) for button in buttons: role = QtGui.QDialogButtonBox.ActionRole if self.is_button(button, "Undo"): self.undo = self.add_button(button, bbox, role, self._on_undo, False, "Undo") self.redo = self.add_button(button, bbox, role, self._on_redo, False, "Redo") history.observe(self._on_undoable, "undoable", dispatch="ui") history.observe(self._on_redoable, "redoable", dispatch="ui") elif self.is_button(button, "Revert"): role = QtGui.QDialogButtonBox.ResetRole self.revert = self.add_button(button, bbox, role, self._on_revert, False) history.observe(self._on_revertable, "undoable", dispatch="ui") elif self.is_button(button, "Help"): role = QtGui.QDialogButtonBox.HelpRole self.add_button(button, bbox, role, self._on_help) elif not self.is_button(button, ""): self.add_button(button, bbox, role) layout.addWidget(bbox) # If the UI has a toolbar, should add it to the panel too self._add_toolbar(parent) # Ensure the control has a size hint reflecting the View specification. # Yes, this is a hack, but it's too late to repair this convoluted # control building process, so we do what we have to... self.control.sizeHint = _size_hint_wrapper(self.control.sizeHint, ui)
def init(self, ui, parent, style): self.is_modal = style == self.MODAL window_style = 0 view = ui.view if view.resizable: window_style |= wx.RESIZE_BORDER title = view.title if title == "": title = DefaultTitle history = ui.history window = ui.control if window is not None: if history is not None: history.observe( self._on_undoable, "undoable", remove=True, dispatch="ui" ) history.observe( self._on_redoable, "redoable", remove=True, dispatch="ui" ) history.observe( self._on_revertable, "undoable", remove=True, dispatch="ui" ) window.SetSizer(None) ui.reset() else: self.ui = ui if style == self.MODAL: if view.resizable: window_style |= wx.MAXIMIZE_BOX | wx.MINIMIZE_BOX window = wx.Dialog( parent, -1, title, style=window_style | wx.DEFAULT_DIALOG_STYLE, ) elif style == self.NONMODAL: if parent is not None: window_style |= ( wx.FRAME_FLOAT_ON_PARENT | wx.FRAME_NO_TASKBAR ) window = wx.Frame( parent, -1, title, style=window_style | (wx.DEFAULT_FRAME_STYLE & (~wx.RESIZE_BORDER)), ) else: if window_style == 0: window_style = wx.SIMPLE_BORDER if parent is not None: window_style |= ( wx.FRAME_FLOAT_ON_PARENT | wx.FRAME_NO_TASKBAR ) if isinstance(parent, tuple): window = wx.Frame(None, -1, "", style=window_style) window._control_region = parent else: window = wx.Frame(parent, -1, "", style=window_style) window._kind = ui.view.kind self._monitor = MouseMonitor(ui) # Set the correct default window background color: window.SetBackgroundColour(WindowColor) self.control = window window.Bind(wx.EVT_CLOSE, self._on_close_page) window.Bind(wx.EVT_CHAR, self._on_key) self.set_icon(view.icon) buttons = [self.coerce_button(button) for button in view.buttons] nbuttons = len(buttons) no_buttons = (nbuttons == 1) and self.is_button(buttons[0], "") has_buttons = (not no_buttons) and ( (nbuttons > 0) or view.undo or view.revert or view.ok or view.cancel ) if has_buttons or (view.menubar is not None): if history is None: history = UndoHistory() else: history = None ui.history = history # Create the actual trait sheet panel and imbed it in a scrollable # window (if requested): sw_sizer = wx.BoxSizer(wx.VERTICAL) if ui.scrollable: sizer = wx.BoxSizer(wx.VERTICAL) sw = TraitsUIScrolledPanel(window) trait_sheet = panel(ui, sw) sizer.Add(trait_sheet, 1, wx.EXPAND) tsdx, tsdy = trait_sheet.GetSize() sw.SetScrollRate(16, 16) max_dy = (2 * SystemMetrics().screen_height) // 3 sw.SetSizer(sizer) sw.SetSize( wx.Size( tsdx + ((tsdy > max_dy) * scrollbar_dx), min(tsdy, max_dy) ) ) else: sw = panel(ui, window) sw_sizer.Add(sw, 1, wx.EXPAND) sw_sizer.SetMinSize(sw.GetSize()) # Check to see if we need to add any of the special function buttons: if (not no_buttons) and (has_buttons or view.help): sw_sizer.Add(wx.StaticLine(window, -1), 0, wx.EXPAND) b_sizer = wx.BoxSizer(wx.HORIZONTAL) # Convert all button flags to actual button actions if no buttons # were specified in the 'buttons' trait: if nbuttons == 0: if view.undo: self.check_button(buttons, UndoButton) if view.revert: self.check_button(buttons, RevertButton) if view.ok: self.check_button(buttons, OKButton) if view.cancel: self.check_button(buttons, CancelButton) if view.help: self.check_button(buttons, HelpButton) # Create a button for each button action: for raw_button, button in zip(view.buttons, buttons): button = self.coerce_button(button) default = raw_button == view.default_button if self.is_button(button, "Undo"): self.undo = self.add_button( button, b_sizer, self._on_undo, False, default=default ) self.redo = self.add_button( button, b_sizer, self._on_redo, False, "Redo" ) history.observe( self._on_undoable, "undoable", dispatch="ui" ) history.observe( self._on_redoable, "redoable", dispatch="ui" ) if history.can_undo: self._on_undoable(True) if history.can_redo: self._on_redoable(True) elif self.is_button(button, "Revert"): self.revert = self.add_button( button, b_sizer, self._on_revert, False, default=default, ) history.observe( self._on_revertable, "undoable", dispatch="ui" ) if history.can_undo: self._on_revertable(True) elif self.is_button(button, "OK"): self.ok = self.add_button( button, b_sizer, self._on_ok, default=default ) ui.observe(self._on_error, "errors", dispatch="ui") elif self.is_button(button, "Cancel"): self.add_button( button, b_sizer, self._on_cancel, default=default ) elif self.is_button(button, "Help"): self.add_button( button, b_sizer, self._on_help, default=default ) elif not self.is_button(button, ""): self.add_button(button, b_sizer, default=default) sw_sizer.Add(b_sizer, 0, wx.ALIGN_RIGHT | wx.ALL, 5) # Add the menu bar, tool bar and status bar (if any): self.add_menubar() self.add_toolbar() self.add_statusbar() # Lay all of the dialog contents out: window.SetSizer(sw_sizer) window.Fit()
def test_defaults(self): history = UndoHistory() self.assertEqual(history.now, 0) self.assertFalse(history.can_undo) self.assertFalse(history.can_redo)
def test_undo(self): editor = create_editor() editor.prepare(None) editor.ui.history = UndoHistory() self.assertEqual(editor.old_value, "test") # Enter 'one' followed by 'two' with editor.updating_value(): self.gui.set_trait_later(editor.control, "control_value", "one") self.gui.set_trait_later(editor.control, "control_value", "two") # Perform an UNDO self.undo(editor) # Expect 2 items in history and pointer at first item self.assertEventuallyTrue(editor, "ui", functools.partial(self.check_history, expected_history_now=1, expected_history_length=2), timeout=5.0) # Perform a REDO self.redo(editor) # Expect 2 items in history and pointer at second item self.assertEventuallyTrue(editor, "ui", functools.partial(self.check_history, expected_history_now=2, expected_history_length=2), timeout=5.0) # Enter 'three' with editor.updating_value(): self.gui.set_trait_later(editor.control, "control_value", "three") # Perform an UNDO self.undo(editor) # Expect 3 items in history and pointer at second item self.assertEventuallyTrue(editor, "ui", functools.partial(self.check_history, expected_history_now=2, expected_history_length=3), timeout=5.0) # Enter 'four' with editor.updating_value(): self.gui.set_trait_later(editor.control, "control_value", "four") self.event_loop_helper.event_loop_with_timeout() # Expect 3 items in history and pointer at second item # Note: Modifying the history after an UNDO, clears the future, # hence, we expect 3 items in the history, not 4 self.assertEventuallyTrue(editor, "ui", functools.partial(self.check_history, expected_history_now=3, expected_history_length=3), timeout=5.0) # The following sequence after modifying the history had caused # the application to hang, verify it. # Perform an UNDO self.undo(editor) self.undo(editor) self.redo(editor) # Expect 3 items in history and pointer at second item self.assertEventuallyTrue(editor, "ui", functools.partial(self.check_history, expected_history_now=2, expected_history_length=3), timeout=5.0)
def init(self, ui, parent, style): """Initialise the object. FIXME: Note that we treat MODAL and POPUP as equivalent until we have an example that demonstrates how POPUP is supposed to work. """ self.ui = ui self.control = ui.control view = ui.view history = ui.history if self.control is not None: if history is not None: history.on_trait_change( self._on_undoable, "undoable", remove=True ) history.on_trait_change( self._on_redoable, "redoable", remove=True ) history.on_trait_change( self._on_revertable, "undoable", remove=True ) ui.reset() else: self.create_dialog(parent, style) self.set_icon(view.icon) # Convert the buttons to actions. buttons = [self.coerce_button(button) for button in view.buttons] nr_buttons = len(buttons) no_buttons = (nr_buttons == 1) and self.is_button(buttons[0], "") has_buttons = (not no_buttons) and ( (nr_buttons > 0) or view.undo or view.revert or view.ok or view.cancel ) if has_buttons or (view.menubar is not None): if history is None: history = UndoHistory() else: history = None ui.history = history if (not no_buttons) and (has_buttons or view.help): bbox = QtGui.QDialogButtonBox() # Create the necessary special function buttons. if nr_buttons == 0: if view.undo: self.check_button(buttons, UndoButton) if view.revert: self.check_button(buttons, RevertButton) if view.ok: self.check_button(buttons, OKButton) if view.cancel: self.check_button(buttons, CancelButton) if view.help: self.check_button(buttons, HelpButton) for raw_button, button in zip(view.buttons, buttons): default = raw_button == view.default_button if self.is_button(button, "Undo"): self.undo = self.add_button( button, bbox, QtGui.QDialogButtonBox.ActionRole, self._on_undo, False, default=default, ) history.on_trait_change( self._on_undoable, "undoable", dispatch="ui" ) if history.can_undo: self._on_undoable(True) self.redo = self.add_button( button, bbox, QtGui.QDialogButtonBox.ActionRole, self._on_redo, False, "Redo", ) history.on_trait_change( self._on_redoable, "redoable", dispatch="ui" ) if history.can_redo: self._on_redoable(True) elif self.is_button(button, "Revert"): self.revert = self.add_button( button, bbox, QtGui.QDialogButtonBox.ResetRole, self._on_revert, False, default=default, ) history.on_trait_change( self._on_revertable, "undoable", dispatch="ui" ) if history.can_undo: self._on_revertable(True) elif self.is_button(button, "OK"): self.ok = self.add_button( button, bbox, QtGui.QDialogButtonBox.AcceptRole, self.control.accept, default=default, ) ui.on_trait_change(self._on_error, "errors", dispatch="ui") elif self.is_button(button, "Cancel"): self.add_button( button, bbox, QtGui.QDialogButtonBox.RejectRole, self.control.reject, default=default, ) elif self.is_button(button, "Help"): self.add_button( button, bbox, QtGui.QDialogButtonBox.HelpRole, self._on_help, default=default, ) elif not self.is_button(button, ""): self.add_button( button, bbox, QtGui.QDialogButtonBox.ActionRole, default=default, ) else: bbox = None self.add_contents(panel(ui), bbox)