def __init__(self, main_controller): """Constructor. @type main_controller: virtaal.controllers.MainController""" GObjectWrapper.__init__(self) self.main_controller = main_controller self.main_controller.undo_controller = self self.unit_controller = self.main_controller.store_controller.unit_controller self.enabled = True self.model = UndoModel(self) self._setup_key_bindings() self._connect_undo_signals()
class UndoController(BaseController): """Contains "undo" logic.""" __gtype_name__ = 'UndoController' # INITIALIZERS # def __init__(self, main_controller): """Constructor. @type main_controller: virtaal.controllers.MainController""" GObjectWrapper.__init__(self) self.main_controller = main_controller self.main_controller.undo_controller = self self.unit_controller = self.main_controller.store_controller.unit_controller self.enabled = True self.model = UndoModel(self) self._setup_key_bindings() self._connect_undo_signals() def _connect_undo_signals(self): # First connect to the unit controller self.unit_controller.connect('unit-delete-text', self._on_unit_delete_text) self.unit_controller.connect('unit-insert-text', self._on_unit_insert_text) self.main_controller.store_controller.connect('store-closed', self._on_store_loaded_closed) self.main_controller.store_controller.connect('store-loaded', self._on_store_loaded_closed) mainview = self.main_controller.view mainview.gui.get_widget('menu_edit').set_accel_group(self.accel_group) self.mnu_undo = mainview.gui.get_widget('mnu_undo') self.mnu_undo.set_accel_path('<Virtaal>/Edit/Undo') self.mnu_undo.connect('activate', self._on_undo_activated) def _setup_key_bindings(self): """Setup Gtk+ key bindings (accelerators). This method *may* need to be moved into a view object, but if it is, it will be the only functionality in such a class. Therefore, it is done here. At least for now.""" gtk.accel_map_add_entry("<Virtaal>/Edit/Undo", gtk.keysyms.z, gdk.CONTROL_MASK) self.accel_group = gtk.AccelGroup() # The following line was commented out, because it caused a double undo when pressing # Ctrl+Z, but only one if done through the menu item. This way it all works as expected. #self.accel_group.connect_by_path("<Virtaal>/Edit/Undo", self._on_undo_activated) mainview = self.main_controller.view # FIXME: Is this acceptable? mainview.add_accel_group(self.accel_group) # DECORATORS # def if_enabled(method): def enabled_method(self, *args, **kwargs): if self.enabled: return method(self, *args, **kwargs) return enabled_method # METHODS # def disable(self): self.enabled = False def enable(self): self.enabled = True def push_current_text(self, textbox): """Save the current text in the given (target) text box on the undo stack.""" current_text = textbox.elem.copy() unitview = self.unit_controller.view curpos = textbox.get_cursor_position() targetn = unitview.targets.index(textbox) def undo_set_text(unit): textbox.elem.sub = current_text.sub self.model.push({ 'action': undo_set_text, 'cursorpos': curpos, 'desc': 'Set target %d text to %s' % (targetn, repr(current_text)), 'targetn': targetn, 'unit': unitview.unit }) def remove_blank_undo(self): """Removes items from the top of the undo stack with no C{value} or C{action} values. The "top of the stack" is one of the top 2 items. This is a convenience method that can be used by any code that directly sets unit values.""" if not self.model.undo_stack: return head = self.model.head() if 'action' in head and not head['action'] or True: self.model.pop(permanent=True) return item = self.model.peek(offset=-1) if 'action' in item and not item['action'] or True: self.model.index -= 1 self.model.undo_stack.remove(item) def record_stop(self): self.model.record_stop() def record_start(self): self.model.record_start() def _disable_unit_signals(self): """Disable all signals emitted by the unit view. This should always be followed, as soon as possible, by C{self._enable_unit_signals()}.""" self.unit_controller.view.disable_signals() def _enable_unit_signals(self): """Enable all signals emitted by the unit view. This should always follow, as soon as possible, after a call to C{self._disable_unit_signals()}.""" self.unit_controller.view.enable_signals() def _perform_undo(self, undo_info): self._select_unit(undo_info['unit']) #if 'desc' in undo_info: # logging.debug('Description: %s' % (undo_info['desc'])) self._disable_unit_signals() undo_info['action'](undo_info['unit']) self._enable_unit_signals() textbox = self.unit_controller.view.targets[undo_info['targetn']] def refresh(): textbox.refresh_cursor_pos = undo_info['cursorpos'] textbox.refresh() gobject.idle_add(refresh) def _select_unit(self, unit): """Select the given unit in the store view. This is to select the unit where the undo-action took place. @type unit: translate.storage.base.TranslationUnit @param unit: The unit to select in the store view.""" self.main_controller.select_unit(unit, force=True) # EVENT HANDLERS # def _on_store_loaded_closed(self, storecontroller): if storecontroller.store is not None: self.mnu_undo.set_sensitive(True) else: self.mnu_undo.set_sensitive(False) self.model.clear() @if_enabled def _on_undo_activated(self, *args): undo_info = self.model.pop() if not undo_info: return if isinstance(undo_info, list): for ui in reversed(undo_info): self._perform_undo(ui) else: self._perform_undo(undo_info) @if_enabled def _on_unit_delete_text(self, unit_controller, unit, deleted, parent, offset, cursor_pos, elem, target_num): def undo_action(unit): #logging.debug('(undo) %s.insert(%d, "%s")' % (repr(elem), start_offset, deleted)) if parent is None: elem.sub = deleted.sub return if isinstance(deleted, StringElem): parent.insert(offset, deleted) elem.prune() if hasattr(deleted, 'gui_info'): del_length = deleted.gui_info.length() else: del_length = len(deleted) desc = 'offset=%d, deleted="%s", parent=%s, cursor_pos=%d, elem=%s' % (offset, repr(deleted), repr(parent), cursor_pos, repr(elem)) self.model.push({ 'action': undo_action, 'cursorpos': cursor_pos, 'desc': desc, 'targetn': target_num, 'unit': unit, }) @if_enabled def _on_unit_insert_text(self, unit_controller, unit, ins_text, offset, elem, target_num): #logging.debug('_on_unit_insert_text(ins_text="%s", offset=%d, elem=%s, target_n=%d)' % (ins_text, offset, repr(elem), target_num)) def undo_action(unit): if isinstance(ins_text, StringElem) and hasattr(ins_text, 'gui_info') and ins_text.gui_info.widgets: # Only for elements with representation widgets elem.delete_elem(ins_text) else: tree_offset = elem.gui_info.gui_to_tree_index(offset) #logging.debug('(undo) %s.delete_range(%d, %d)' % (repr(elem), tree_offset, tree_offset+len(ins_text))) elem.delete_range(tree_offset, tree_offset+len(ins_text)) elem.prune() desc = 'ins_text="%s", offset=%d, elem=%s' % (ins_text, offset, repr(elem)) self.model.push({ 'action': undo_action, 'desc': desc, 'unit': unit, 'targetn': target_num, 'cursorpos': offset })