def init1(self): #e might be merged into end of __init__ """ Do what we might do in __init__ except that it might be too early during assy.__init__ then (see also _initial_checkpoint) """ assy = self.assy self.archive = AssyUndoArchive(assy) ## assy._u_archive = self.archive ####@@@@ still safe in 060117 stub code?? [guess 060223: not needed anymore ###@@@] # [obs??] this is how model objects in assy find something to report changes to (typically in their __init__ methods); # we do it here (not in caller) since its name and value are private to our API for model objects to report changes ## self.archive.subscribe_to_checkpoints( self.remake_UI_menuitems ) ## self.remake_UI_menuitems() # so it runs for initial checkpoint and disables menu items, etc if is_macintosh(): win = assy.w from PyQt4.Qt import Qt win.editRedoAction.setShortcut( Qt.CTRL + Qt.SHIFT + Qt.Key_Z) # set up incorrectly (for Mac) as "Ctrl+Y" # note: long before 060414 this is probably no longer needed # (since now done in gui.WhatsThisText_for_MainWindow.py), # but it's safe and can be left in as a backup. # exercise the debug-only old pref (deprecated to use it): self.auto_checkpoint_pref( ) # exercise this, so it shows up in the debug-prefs submenu right away # (fixes bug in which the pref didn't show up until the first undoable change was made) [060125] # now look at the official pref for initial state of autocheckpointing [060314] ## done later -- set_initial_AutoCheckpointing_enabled( ... ) return
def init1(self): #e might be merged into end of __init__ """ Do what we might do in __init__ except that it might be too early during assy.__init__ then (see also _initial_checkpoint) """ assy = self.assy self.archive = AssyUndoArchive(assy) ## assy._u_archive = self.archive ####@@@@ still safe in 060117 stub code?? [guess 060223: not needed anymore ###@@@] # [obs??] this is how model objects in assy find something to report changes to (typically in their __init__ methods); # we do it here (not in caller) since its name and value are private to our API for model objects to report changes ## self.archive.subscribe_to_checkpoints( self.remake_UI_menuitems ) ## self.remake_UI_menuitems() # so it runs for initial checkpoint and disables menu items, etc if is_macintosh(): win = assy.w from PyQt4.Qt import Qt win.editRedoAction.setShortcut(Qt.CTRL+Qt.SHIFT+Qt.Key_Z) # set up incorrectly (for Mac) as "Ctrl+Y" # note: long before 060414 this is probably no longer needed # (since now done in gui.WhatsThisText_for_MainWindow.py), # but it's safe and can be left in as a backup. # exercise the debug-only old pref (deprecated to use it): self.auto_checkpoint_pref() # exercise this, so it shows up in the debug-prefs submenu right away # (fixes bug in which the pref didn't show up until the first undoable change was made) [060125] # now look at the official pref for initial state of autocheckpointing [060314] ## done later -- set_initial_AutoCheckpointing_enabled( ... ) return
class AssyUndoManager(UndoManager): """ An UndoManager specialized for handling the state held by an assy (an instance of class assembly). """ active = True #060223 changed this to True, since False mainly means it died, not that it's being initialized [060223] _undo_manager_initialized = False #060223 def __init__(self, assy, menus = ()): # called from assy.__init__ """ Do what can be done early in assy.__init__; caller must also (subsequently) call init1 and either _initial_checkpoint or (preferred) clear_undo_stack. @type assy: assembly.assembly @warning: callers concerned with performance should heed the warning in the docstring of clear_undo_stack about when to first call it. """ # assy owns the state whose changes we'll be managing... # [semiobs cmt:] should it have same undo-interface as eg chunks do?? self._current_main_menu_ops = {} self.assy = assy self.menus = menus return def init1(self): #e might be merged into end of __init__ """ Do what we might do in __init__ except that it might be too early during assy.__init__ then (see also _initial_checkpoint) """ assy = self.assy self.archive = AssyUndoArchive(assy) ## assy._u_archive = self.archive ####@@@@ still safe in 060117 stub code?? [guess 060223: not needed anymore ###@@@] # [obs??] this is how model objects in assy find something to report changes to (typically in their __init__ methods); # we do it here (not in caller) since its name and value are private to our API for model objects to report changes ## self.archive.subscribe_to_checkpoints( self.remake_UI_menuitems ) ## self.remake_UI_menuitems() # so it runs for initial checkpoint and disables menu items, etc if is_macintosh(): win = assy.w from PyQt4.Qt import Qt win.editRedoAction.setShortcut(Qt.CTRL+Qt.SHIFT+Qt.Key_Z) # set up incorrectly (for Mac) as "Ctrl+Y" # note: long before 060414 this is probably no longer needed # (since now done in gui.WhatsThisText_for_MainWindow.py), # but it's safe and can be left in as a backup. # exercise the debug-only old pref (deprecated to use it): self.auto_checkpoint_pref() # exercise this, so it shows up in the debug-prefs submenu right away # (fixes bug in which the pref didn't show up until the first undoable change was made) [060125] # now look at the official pref for initial state of autocheckpointing [060314] ## done later -- set_initial_AutoCheckpointing_enabled( ... ) return def _initial_checkpoint(self): #bruce 060223; not much happens until this is called (order is __init__, init1, _initial_checkpoint) """ Only called from self.clear_undo_stack(). """ set_initial_AutoCheckpointing_enabled( True ) # might have to be True for initial_checkpoint; do no UI effects or history msg; kluge that the flag is a global [060314] self.archive.initial_checkpoint() ## self.connect_or_disconnect_menu_signals(True) self.remake_UI_menuitems() # try to fix bug 1387 [060126] self.active = True # redundant env.command_segment_subscribers.append( self._in_event_loop_changed ) self._undo_manager_initialized = True ## redundant call (bug); i hope this is the right one to remove: self.archive.initial_checkpoint() # make sure the UI reflects the current pref for auto-checkpointing [060314] # (in practice this happens at startup and after File->Open); # only emit history message if it's different than it was last time this session, # or different than True the first time global _last_autocp autocp = env.prefs[undoAutomaticCheckpoints_prefs_key] update_UI = True print_to_history = (_last_autocp != autocp) _last_autocp = -1 # if there's an exception, then *always* print it next time around set_initial_AutoCheckpointing_enabled( autocp, update_UI = update_UI, print_to_history = print_to_history) _last_autocp = autocp # only print it if different, next time return def deinit(self): self.active = False ## self.connect_or_disconnect_menu_signals(False) # and effectively destroy self... [060126 precaution; not thought through] self.archive.destroy() self._current_main_menu_ops = {} self.assy = self.menus = None #e more?? return # this is useless, since we have to keep them always up to date for sake of accel keys and toolbuttons [060126] ## def connect_or_disconnect_menu_signals(self, connectQ): # this is a noop as of 060126 ## win = self.assy.w ## if connectQ: ## method = win.connect ## else: ## method = win.disconnect ## for menu in self.menus: ## method( menu, SIGNAL("aboutToShow()"), self.remake_UI_menuitems ) ####k ## pass ## return def clear_undo_stack(self, *args, **kws): #bruce 080229 revised docstring """ Intialize self if necessary, and make an initial checkpoint, discarding whatever undo archive data is recorded before that (if any). This can be used by our client to complete our initialization and define the earliest state which an Undo can get back to. (It is the preferred way for external code to do that.) And, it can be used later to redefine that point, making all earlier states inaccessible (as a user op for reducing RAM consumption). @note: calling this several times in the same user op is allowed, and leaves the state the same as if this had only been called the last of those times. @warning: the first time this is called, it scans and copies all currently reachable undoable state *twice*. All subsequent times, it does this only once. This means it should be called as soon as the client assy is fully initialized (when it is almost empty of undoable state), even if it will always be called again soon thereafter, after some initial (potentially large) data has been added to the assy. Otherwise, that second call will be the one which scans its state twice, and will take twice as long as necessary. """ # note: this is now callable from a debug menu / other command, # as of 060301 (experimental) if not self._undo_manager_initialized: self._initial_checkpoint() # have to do this here, not in archive.clear_undo_stack return self.archive.clear_undo_stack(*args, **kws) def menu_cmd_checkpoint(self): # no longer callable from UI as of 060301, and not recently reviewed for safety [060301 comment] self.checkpoint( cptype = 'user_explicit' ) def make_manual_checkpoint(self): #060312 """ #doc; called from editMakeCheckpoint, presumably only when autocheckpointing is disabled """ self.checkpoint( cptype = 'manual', merge_with_future = False ) # temporary comment 060312: this might be enough, once it sets up for remake_UI_menuitems return __begin_retval = None ###k this will be used when we're created by a cmd like file open... i guess grabbing pref then is best... def _in_event_loop_changed(self, beginflag, infodict, tracker): # 060127; 060321 added infodict to API "[this bound method will be added to env.command_segment_subscribers so as to be told when ..." # infodict is info about the nature of the stack change, passed from the tracker [bruce 060321 for bug 1440 et al] # this makes "report all checkpoints" useless -- too many null ones. # maybe i should make it only report if state changes or cmdname passed... if not self.active: self.__begin_retval = False #k probably doesn't matter return True # unsubscribe # print beginflag, len(tracker.stack) # typical: True 1; False 0 if 1: #bruce 060321 for bug 1440: we need to not do checkpoints in some cases. Not sure if this is correct re __begin_retval; # if not, either clean it up for that or pass the flag into the checkpoint routine to have it not really do the checkpoint # (which might turn out better for other reasons anyway, like tracking proper cmdnames for changes). ##e pushed = infodict.get('pushed') popped = infodict.get('popped') # zero or one of these exists, and is the op_run just pushed or popped from the stack if pushed is not None: typeflag = pushed.typeflag # entering this guy elif popped is not None: typeflag = popped.typeflag # leaving this guy (entering vs leaving doesn't matter for now) else: typeflag = '' # does this ever happen? (probably not) want_cp = (typeflag != 'beginrec') if not want_cp: if 0 and env.debug(): print "debug: skipping cp as we enter or leave recursive event processing" return # this might be problematic, see above comment [tho it seems to work for now, for Minimize All anyway]; # if it ever is, then instead of returning here, we'll pass want_cp to checkpoint routines below if beginflag: self.__begin_retval = self.undo_checkpoint_before_command() ###e grab cmdname guess from top op_run i.e. from begin_op? yes for debugging; doesn't matter in the end though. else: if self.__begin_retval is None: # print "self.__begin_retval is None" # not a bug, will be normal ... happens with file open (as expected) self.__begin_retval = self.auto_checkpoint_pref() self.undo_checkpoint_after_command( self.__begin_retval ) self.__begin_retval = False # should not matter return def checkpoint(self, *args, **kws): # Note, as of 060127 this is called *much* more frequently than before (for every signal->slot to a python slot); # we will need to optimize it when state hasn't changed. ###@@@ global _AutoCheckpointing_enabled, _disable_checkpoints res = None if not _disable_checkpoints: ###e are there any exceptions to this, like for initial cps?? (for open file in extrude) opts = dict(merge_with_future = not _AutoCheckpointing_enabled) # i.e., when not auto-checkpointing and when caller doesn't override, # we'll ask archive.checkpoint to (efficiently) merge changes so far with upcoming changes # (but to still cause real changes to trash redo stack, and to still record enough info # to allow us to properly remake_UI_menuitems) opts.update(kws) # we'll pass it differently from the manual checkpoint maker... ##e res = self.archive.checkpoint( *args, **opts ) self.remake_UI_menuitems() # needed here for toolbuttons and accel keys; not called for initial cp during self.archive init # (though for menu items themselves, the aboutToShow signal would be sufficient) return res # maybe no retval, this is just a precaution def auto_checkpoint_pref(self): ##e should remove calls to this, inline them as True return True # this is obsolete -- it's not the same as the checkmark item now in the edit menu! [bruce 060309] ## return debug_pref('undo: auto-checkpointing? (slow)', Choice_boolean_True, #bruce 060302 changed default to True, added ':' ## prefs_key = 'A7/undo/auto-checkpointing', ## non_debug = True) def undo_checkpoint_before_command(self, cmdname = ""): """ ###doc [returns a value which should be passed to undo_checkpoint_after_command; we make no guarantees at all about what type of value that is, whether it's boolean true, etc] """ #e should this be renamed begin_cmd_checkpoint() or begin_command_checkpoint() like I sometimes think it's called? # recheck the pref every time auto_checkpointing = self.auto_checkpoint_pref() # (this is obs, only True is supported, as of long before 060323) if not auto_checkpointing: return False # (everything before this point must be kept fast) cmdname2 = cmdname or "command" if undo_archive.debug_undo2: env.history.message("debug_undo2: begin_cmd_checkpoint for %r" % (cmdname2,)) # this will get fancier, use cmdname, worry about being fast when no diffs, merging ops, redundant calls in one cmd, etc: self.checkpoint( cptype = 'begin_cmd', cmdname_for_debug = cmdname ) if cmdname: self.archive.current_command_info(cmdname = cmdname) #060126 return True # this code should be passed to the matching undo_checkpoint_after_command (#e could make it fancier) def undo_checkpoint_after_command(self, begin_retval): assert begin_retval in [False, True], "begin_retval should not be %r" % (begin_retval,) if begin_retval: # this means [as of 060123] that debug pref for undo checkpointing is enabled if undo_archive.debug_undo2: env.history.message(" debug_undo2: end_cmd_checkpoint") # this will get fancier, use cmdname, worry about being fast when no diffs, merging ops, redundant calls in one cmd, etc: self.checkpoint( cptype = 'end_cmd' ) pass return # == def node_departing_assy(self, node, assy): #bruce 060315; # revised 060330 to make it almost a noop, since implem was obsolete and it caused bug 1797 #bruce 080219 making this a debug print only, since it happens with dna updater # (probably a bug) but exception may be causing further bugs; also adding a message. # Now I have a theory about the bug's cause: if this happens in a closed assy, # deinit has set self.assy to None. To repeat, open and close a dna file with dna updater # off, then turn dna updater on. Now this should cause the "bug (harmless?)" print below. #bruce 080314 update: that does happen, so that print is useless and verbose, # so disable it for now. Retain the other ones. if assy is None or node is None: print "\n*** BUG: node_departing_assy(%r, %r, %r) sees assy or node is None" % \ (self, node, assy) return if self.assy is None: # this will happen for now when the conditions that caused today's bug reoccur, # until we fix the dna updater to never run inside a closed assy (desirable) # [bruce 080219] if 0: #bruce 080314 print "\nbug (harmless?): node_departing_assy(%r, %r, %r), but " \ "self.assy is None (happens when self's file is closed)" % \ (self, node, assy) return if not (assy is self.assy): print "\n*** BUG: " \ "node_departing_assy(%r, %r, %r) sees wrong self.assy = %r" % \ (self, node, assy, self.assy) # assy is self.assy has to be true (given that neither is None), # since we were accessed as assy.undo_manager. return # == def current_command_info(self, *args, **kws): self.archive.current_command_info(*args, **kws) def undo_redo_ops(self): # copied code below [dup code is in undo_manager_older.py, not in cvs] # the following value for warn_when_change_indicators_seem_wrong is a kluge # (wrong in principle but probably safe, not entirely sure it's correct) [060309] # (note, same value was hardcoded inside that method before bruce 071025; # see comment there about when I see the warnings; it's known that it gives # false warnings if we pass True when _AutoCheckpointing_enabled is false): ops = self.archive.find_undoredos( warn_when_change_indicators_seem_wrong = _AutoCheckpointing_enabled ) # state_version - now held inside UndoArchive.last_cp (might be wrong) ###@@@ # [what the heck does that comment mean? bruce 071025 Q] undos = [] redos = [] d1 = {'Undo':undos, 'Redo':redos} for op in ops: optype = op.optype() d1[optype].append(op) # sort ops by type ## done in the subr: redos = filter( lambda redo: not redo.destroyed, redos) #060309 since destroyed ones are not yet unstored # remove obsolete redo ops if redos: lis = [ (redo.cps[1].cp_counter, redo) for redo in redos ] lis.sort() only_redo = lis[-1][1] redos = [only_redo] for obs_redo in lis[:-1]: if undo_archive.debug_undo2 or env.debug(): #060309 adding 'or env.debug()' since this should never happen once clear_redo_stack() is implemented in archive print "obsolete redo:", obs_redo pass #e discard it permanently? ####@@@@ return undos, redos def undo_cmds_menuspec(self, widget): # WARNING: this is not being maintained, it's just a development draft. # So far it lacks merging and history message and perhaps win_update and update_select_mode. [060227 comment] """ Return a menu_spec for including undo-related commands in a popup menu (to be shown in the given widget, tho i don't know why the widget could matter) """ del widget archive = self.archive # copied code below [dup code is in undo_manager_older.py, not in cvs] res = [] #bruce 060301 removing this one, since it hasn't been reviewed in awhile so it might cause bugs, # and maybe it did cause one... ## res.append(( 'undo checkpoint (in RAM only)', self.menu_cmd_checkpoint )) #060301 try this one instead: res.append(( 'clear undo stack (experimental)', self.clear_undo_stack )) undos, redos = self.undo_redo_ops() ###e sort each list by some sort of time order (maybe of most recent use of the op in either direction??), and limit lengths # there are at most one per chunk per undoable attr... so for this test, show them all, don't bother with submenus if not undos: res.append(( "Nothing we can Undo", noop, 'disabled' )) ###e should figure out whether "Can't Undo XXX" or "Nothing to Undo" is more correct for op in undos + redos: # for now, we're not even including them unless as far as we know we can do them, so no role for "Can't Undo" unless none arch = archive # it's on purpose that op itself has no ref to model, so we have to pass it [obs cmt?] cmd = lambda _guard1_ = None, _guard2_ = None, arch = arch: arch.do_op(op) #k guards needed? (does qt pass args to menu cmds?) ## text = "%s %s" % (op.type, op.what()) text = op.menu_desc() res.append(( text , cmd )) if not redos: res.append(( "Nothing we can Redo", noop, 'disabled' )) return res def remake_UI_menuitems(self): #e this should also be called again if any undo-related preferences change ###@@@ #e see also: void QPopupMenu::aboutToShow () [signal], for how to know when to run this (when Edit menu is about to show); # to find the menu, no easy way (only way: monitor QAction::addedTo in a custom QAction subclass - not worth the trouble), # so just hardcode it as edit menu for now. We'll need to connect & disconnect this when created/finished, # and get passed the menu (or list of them) from the caller, which is I guess assy.__init__. if undo_archive.debug_undo2: print "debug_undo2: running remake_UI_menuitems (could be direct call or signal)" global _disable_UndoRedo disable_reasons = list(_disable_UndoRedo) # avoid bugs if it changes while this function runs (might never happen) if disable_reasons: undos, redos = [], [] # note: more code notices the same condition, below else: undos, redos = self.undo_redo_ops() win = self.assy.w undo_mitem = win.editUndoAction redo_mitem = win.editRedoAction for ops, action, optype in [(undos, undo_mitem, 'Undo'), (redos, redo_mitem, 'Redo')]: #e or could grab op.optype()? extra = "" if disable_reasons: try: why_not = str(disable_reasons[0][1]) # kluges: depends on list format, and its whymsgs being designed for this use except: why_not = "" extra += " (not permitted %s)" % why_not # why_not is e.g. "during drag" (nim) or "during Extrude" if undo_archive.debug_undo2: extra += " (%s)" % str(time.time()) # show when it's updated in the menu text (remove when works) ####@@@@ if ops: action.setEnabled(True) if not ( len(ops) == 1): #e there should always be just one for now #060212 changed to debug msg, since this assert failed (due to process_events?? undoing esp image delete) print_compact_stack("bug: more than one %s op found: " % optype) op = ops[0] op = self.wrap_op_with_merging_flags(op) #060127 text = op.menu_desc() + extra #060126 action.setText(text) fix_tooltip(action, text) # replace description, leave (accelkeys) alone (they contain unicode chars on Mac) self._current_main_menu_ops[optype] = op #e should store it into menu item if we can, I suppose op.you_have_been_offered() # make sure it doesn't change its mind about being a visible undoable op, even if it gets merged_with_future # (from lack of autocp) and turns out to contain no net changes # [bruce 060326 re bug 1733; probably only needed for Undo, not Redo] else: action.setEnabled(False) ## action.setText("Can't %s" % optype) # someday we might have to say "can't undo Cmdxxx" for certain cmds ## action.setText("Nothing to %s" % optype) text = "%s%s" % (optype, extra) action.setText(text) # for 061117 commit, look like it used to look, for the time being fix_tooltip(action, text) self._current_main_menu_ops[optype] = None pass #bruce 060319 for bug 1421 win.editUndoAction.setWhatsThis( win.editUndoText ) win.editRedoAction.setWhatsThis( win.editRedoText ) from foundation.whatsthis_utilities import refix_whatsthis_text_and_links ## if 0: ## # this works, but is overkill and is probably too slow, and prints huge numbers of console messages, like this: ## ## TypeError: invalid result type from MyWhatsThis.text() ## # (I bet I could fix the messages by modifying MyWhatsThis.text() to return "" (guess)) ## from foundation.whatsthis_utilities import fix_whatsthis_text_and_links ## fix_whatsthis_text_and_links( win) ## if 0: ## # this prints no console messages, but doesn't work! (for whatsthis on tool buttons or menu items) ## # guess [much later]: it fails to actually do anything to these actions! ## from foundation.whatsthis_utilities import fix_whatsthis_text_and_links ## fix_whatsthis_text_and_links( win.editUndoAction ) ## fix_whatsthis_text_and_links( win.editRedoAction ) ## # try menu objects? and toolbars? refix_whatsthis_text_and_links( ) ###@@@ predict: will fix toolbuttons but not menu items #060304 also disable/enable Clear Undo Stack action = win.editClearUndoStackAction text = "Clear Undo Stack" + '...' # workaround missing '...' (remove this when the .ui file is fixed) #e future: add an estimate of RAM to be cleared action.setText(text) fix_tooltip(action, text) enable_it = not not (undos or redos) action.setEnabled( enable_it ) return # # the kinds of things we can set on one of those actions include: # # self.setViewFitToWindowAction.setText(QtGui.QApplication.translate(self.__class__.__name__, "Fit to Window")) # self.setViewFitToWindowAction.setText(QtGui.QApplication.translate(self.__class__.__name__, "&Fit to Window")) # self.setViewFitToWindowAction.setToolTip(QtGui.QApplication.translate(self.__class__.__name__, "Fit to Window (Ctrl+F)")) # self.setViewFitToWindowAction.setShortcut(QtGui.QApplication.translate(self.__class__.__name__, "Ctrl+F")) # self.viewRightAction.setStatusTip(QtGui.QApplication.translate(self.__class__.__name__, "Right View")) # self.helpMouseControlsAction.setWhatsThis(QtGui.QApplication.translate(self.__class__.__name__, "Displays help for mouse controls")) def wrap_op_with_merging_flags(self, op, flags = None): #e will also accept merging-flag or -pref arguments """ Return a higher-level op based on the given op, but with the appropriate diff-merging flags wrapped around it. Applying this higher-level op will (in general) apply op, then apply more diffs which should be merged with it according to those merging flags (though in an optimized way, e.g. first collect and merge the LL diffs, then apply all at once). The higher-level op might also have a different menu_desc, etc. In principle, caller could pass flag args, and call us more than once with different flag args for the same op; in making the wrapped op we don't modify the passed op. """ #e first we supply our own defaults for flags return self.archive.wrap_op_with_merging_flags(op, flags = flags) # main menu items (their slots in MWsemantics forward to assy which forwards to here) def editUndo(self): ## env.history.message(orangemsg("Undo: (prototype)")) self.do_main_menu_op('Undo') def editRedo(self): ## env.history.message(orangemsg("Redo: (prototype)")) self.do_main_menu_op('Redo') def do_main_menu_op(self, optype): """ @note: optype should be Undo or Redo """ op_was_available = not not self._current_main_menu_ops.get(optype) global _disable_UndoRedo if _disable_UndoRedo: #060414 env.history.message(redmsg("%s is not permitted now (and this action was only offered due to a bug)" % optype)) return global _AutoCheckpointing_enabled disabled = not _AutoCheckpointing_enabled #060312 if disabled: _AutoCheckpointing_enabled = True # temporarily enable it, just during the Undo or Redo command self.checkpoint( cptype = "preUndo" ) # do a checkpoint with it enabled, so Undo or Redo can work normally. # Note: in theory this might change what commands are offered and maybe even cause the error message below to come out # (so we might want to revise it when disabled is true ##e), but I think that can only happen if there's a change_counter # bug, since the only way the enabled cp will see changes not seen by disabled one is if archive.update_before_checkpoint() # is first to set the change_counters (probably a bug); if this happens it might make Redo suddenly unavailable. ####e if optype is Redo, we could pass an option to above checkpoint to not destroy redo stack or make it inaccessible! # (such an option is nim) try: op = self._current_main_menu_ops.get(optype) if op: undo_xxx = op.menu_desc() # note: menu_desc includes history sernos env.history.message(u"%s" % undo_xxx) #e say Undoing rather than Undo in case more msgs?? ######@@@@@@ TEST u"%s" self.archive.do_op(op) self.assy.w.update_select_mode() #bruce 060227 try to fix bug 1576 self.assy.w.win_update() #bruce 060227 not positive this isn't called elsewhere, or how we got away without it if not else: if not disabled: print "no op to %r; not sure how this slot was called, since it should have been disabled" % optype env.history.message(redmsg("Nothing to %s (and it's a bug that its menu item or tool button was enabled)" % optype)) else: print "no op to %r; autocp disabled (so ops to offer were recomputed just now; before that, op_was_available = %r); "\ "see code comments for more info" % ( optype, op_was_available) if op_was_available: env.history.message(redmsg("Nothing to %s (possibly due to a bug)" % optype)) else: env.history.message(redmsg("Nothing to %s (and this action was only offered due to a bug)" % optype)) pass except: print_compact_traceback() env.history.message(redmsg("Bug in %s; see traceback in console" % optype)) if disabled: # better get the end-cp done now (since we might be relying on it for some reason -- I'm not sure) self.checkpoint( cptype = "postUndo" ) _AutoCheckpointing_enabled = False # re-disable return pass # end of class AssyUndoManager
class AssyUndoManager(UndoManager): """ An UndoManager specialized for handling the state held by an assy (an instance of class assembly). """ active = True #060223 changed this to True, since False mainly means it died, not that it's being initialized [060223] _undo_manager_initialized = False #060223 def __init__(self, assy, menus=()): # called from assy.__init__ """ Do what can be done early in assy.__init__; caller must also (subsequently) call init1 and either _initial_checkpoint or (preferred) clear_undo_stack. @type assy: assembly.assembly @warning: callers concerned with performance should heed the warning in the docstring of clear_undo_stack about when to first call it. """ # assy owns the state whose changes we'll be managing... # [semiobs cmt:] should it have same undo-interface as eg chunks do?? self._current_main_menu_ops = {} self.assy = assy self.menus = menus return def init1(self): #e might be merged into end of __init__ """ Do what we might do in __init__ except that it might be too early during assy.__init__ then (see also _initial_checkpoint) """ assy = self.assy self.archive = AssyUndoArchive(assy) ## assy._u_archive = self.archive ####@@@@ still safe in 060117 stub code?? [guess 060223: not needed anymore ###@@@] # [obs??] this is how model objects in assy find something to report changes to (typically in their __init__ methods); # we do it here (not in caller) since its name and value are private to our API for model objects to report changes ## self.archive.subscribe_to_checkpoints( self.remake_UI_menuitems ) ## self.remake_UI_menuitems() # so it runs for initial checkpoint and disables menu items, etc if is_macintosh(): win = assy.w from PyQt4.Qt import Qt win.editRedoAction.setShortcut( Qt.CTRL + Qt.SHIFT + Qt.Key_Z) # set up incorrectly (for Mac) as "Ctrl+Y" # note: long before 060414 this is probably no longer needed # (since now done in gui.WhatsThisText_for_MainWindow.py), # but it's safe and can be left in as a backup. # exercise the debug-only old pref (deprecated to use it): self.auto_checkpoint_pref( ) # exercise this, so it shows up in the debug-prefs submenu right away # (fixes bug in which the pref didn't show up until the first undoable change was made) [060125] # now look at the official pref for initial state of autocheckpointing [060314] ## done later -- set_initial_AutoCheckpointing_enabled( ... ) return def _initial_checkpoint( self ): #bruce 060223; not much happens until this is called (order is __init__, init1, _initial_checkpoint) """ Only called from self.clear_undo_stack(). """ set_initial_AutoCheckpointing_enabled(True) # might have to be True for initial_checkpoint; do no UI effects or history msg; kluge that the flag is a global [060314] self.archive.initial_checkpoint() ## self.connect_or_disconnect_menu_signals(True) self.remake_UI_menuitems() # try to fix bug 1387 [060126] self.active = True # redundant env.command_segment_subscribers.append(self._in_event_loop_changed) self._undo_manager_initialized = True ## redundant call (bug); i hope this is the right one to remove: self.archive.initial_checkpoint() # make sure the UI reflects the current pref for auto-checkpointing [060314] # (in practice this happens at startup and after File->Open); # only emit history message if it's different than it was last time this session, # or different than True the first time global _last_autocp autocp = env.prefs[undoAutomaticCheckpoints_prefs_key] update_UI = True print_to_history = (_last_autocp != autocp) _last_autocp = -1 # if there's an exception, then *always* print it next time around set_initial_AutoCheckpointing_enabled( autocp, update_UI=update_UI, print_to_history=print_to_history) _last_autocp = autocp # only print it if different, next time return def deinit(self): self.active = False ## self.connect_or_disconnect_menu_signals(False) # and effectively destroy self... [060126 precaution; not thought through] self.archive.destroy() self._current_main_menu_ops = {} self.assy = self.menus = None #e more?? return # this is useless, since we have to keep them always up to date for sake of accel keys and toolbuttons [060126] ## def connect_or_disconnect_menu_signals(self, connectQ): # this is a noop as of 060126 ## win = self.assy.w ## if connectQ: ## method = win.connect ## else: ## method = win.disconnect ## for menu in self.menus: ## method( menu, SIGNAL("aboutToShow()"), self.remake_UI_menuitems ) ####k ## pass ## return def clear_undo_stack(self, *args, **kws): #bruce 080229 revised docstring """ Intialize self if necessary, and make an initial checkpoint, discarding whatever undo archive data is recorded before that (if any). This can be used by our client to complete our initialization and define the earliest state which an Undo can get back to. (It is the preferred way for external code to do that.) And, it can be used later to redefine that point, making all earlier states inaccessible (as a user op for reducing RAM consumption). @note: calling this several times in the same user op is allowed, and leaves the state the same as if this had only been called the last of those times. @warning: the first time this is called, it scans and copies all currently reachable undoable state *twice*. All subsequent times, it does this only once. This means it should be called as soon as the client assy is fully initialized (when it is almost empty of undoable state), even if it will always be called again soon thereafter, after some initial (potentially large) data has been added to the assy. Otherwise, that second call will be the one which scans its state twice, and will take twice as long as necessary. """ # note: this is now callable from a debug menu / other command, # as of 060301 (experimental) if not self._undo_manager_initialized: self._initial_checkpoint( ) # have to do this here, not in archive.clear_undo_stack return self.archive.clear_undo_stack(*args, **kws) def menu_cmd_checkpoint( self ): # no longer callable from UI as of 060301, and not recently reviewed for safety [060301 comment] self.checkpoint(cptype='user_explicit') def make_manual_checkpoint(self): #060312 """ #doc; called from editMakeCheckpoint, presumably only when autocheckpointing is disabled """ self.checkpoint(cptype='manual', merge_with_future=False) # temporary comment 060312: this might be enough, once it sets up for remake_UI_menuitems return __begin_retval = None ###k this will be used when we're created by a cmd like file open... i guess grabbing pref then is best... def _in_event_loop_changed( self, beginflag, infodict, tracker): # 060127; 060321 added infodict to API "[this bound method will be added to env.command_segment_subscribers so as to be told when ..." # infodict is info about the nature of the stack change, passed from the tracker [bruce 060321 for bug 1440 et al] # this makes "report all checkpoints" useless -- too many null ones. # maybe i should make it only report if state changes or cmdname passed... if not self.active: self.__begin_retval = False #k probably doesn't matter return True # unsubscribe # print beginflag, len(tracker.stack) # typical: True 1; False 0 if 1: #bruce 060321 for bug 1440: we need to not do checkpoints in some cases. Not sure if this is correct re __begin_retval; # if not, either clean it up for that or pass the flag into the checkpoint routine to have it not really do the checkpoint # (which might turn out better for other reasons anyway, like tracking proper cmdnames for changes). ##e pushed = infodict.get('pushed') popped = infodict.get('popped') # zero or one of these exists, and is the op_run just pushed or popped from the stack if pushed is not None: typeflag = pushed.typeflag # entering this guy elif popped is not None: typeflag = popped.typeflag # leaving this guy (entering vs leaving doesn't matter for now) else: typeflag = '' # does this ever happen? (probably not) want_cp = (typeflag != 'beginrec') if not want_cp: if 0 and env.debug(): print "debug: skipping cp as we enter or leave recursive event processing" return # this might be problematic, see above comment [tho it seems to work for now, for Minimize All anyway]; # if it ever is, then instead of returning here, we'll pass want_cp to checkpoint routines below if beginflag: self.__begin_retval = self.undo_checkpoint_before_command() ###e grab cmdname guess from top op_run i.e. from begin_op? yes for debugging; doesn't matter in the end though. else: if self.__begin_retval is None: # print "self.__begin_retval is None" # not a bug, will be normal ... happens with file open (as expected) self.__begin_retval = self.auto_checkpoint_pref() self.undo_checkpoint_after_command(self.__begin_retval) self.__begin_retval = False # should not matter return def checkpoint(self, *args, **kws): # Note, as of 060127 this is called *much* more frequently than before (for every signal->slot to a python slot); # we will need to optimize it when state hasn't changed. ###@@@ global _AutoCheckpointing_enabled, _disable_checkpoints res = None if not _disable_checkpoints: ###e are there any exceptions to this, like for initial cps?? (for open file in extrude) opts = dict(merge_with_future=not _AutoCheckpointing_enabled) # i.e., when not auto-checkpointing and when caller doesn't override, # we'll ask archive.checkpoint to (efficiently) merge changes so far with upcoming changes # (but to still cause real changes to trash redo stack, and to still record enough info # to allow us to properly remake_UI_menuitems) opts.update( kws ) # we'll pass it differently from the manual checkpoint maker... ##e res = self.archive.checkpoint(*args, **opts) self.remake_UI_menuitems( ) # needed here for toolbuttons and accel keys; not called for initial cp during self.archive init # (though for menu items themselves, the aboutToShow signal would be sufficient) return res # maybe no retval, this is just a precaution def auto_checkpoint_pref( self): ##e should remove calls to this, inline them as True return True # this is obsolete -- it's not the same as the checkmark item now in the edit menu! [bruce 060309] ## return debug_pref('undo: auto-checkpointing? (slow)', Choice_boolean_True, #bruce 060302 changed default to True, added ':' ## prefs_key = 'A7/undo/auto-checkpointing', ## non_debug = True) def undo_checkpoint_before_command(self, cmdname=""): """ ###doc [returns a value which should be passed to undo_checkpoint_after_command; we make no guarantees at all about what type of value that is, whether it's boolean true, etc] """ #e should this be renamed begin_cmd_checkpoint() or begin_command_checkpoint() like I sometimes think it's called? # recheck the pref every time auto_checkpointing = self.auto_checkpoint_pref( ) # (this is obs, only True is supported, as of long before 060323) if not auto_checkpointing: return False # (everything before this point must be kept fast) cmdname2 = cmdname or "command" if undo_archive.debug_undo2: env.history.message("debug_undo2: begin_cmd_checkpoint for %r" % (cmdname2, )) # this will get fancier, use cmdname, worry about being fast when no diffs, merging ops, redundant calls in one cmd, etc: self.checkpoint(cptype='begin_cmd', cmdname_for_debug=cmdname) if cmdname: self.archive.current_command_info(cmdname=cmdname) #060126 return True # this code should be passed to the matching undo_checkpoint_after_command (#e could make it fancier) def undo_checkpoint_after_command(self, begin_retval): assert begin_retval in [ False, True ], "begin_retval should not be %r" % (begin_retval, ) if begin_retval: # this means [as of 060123] that debug pref for undo checkpointing is enabled if undo_archive.debug_undo2: env.history.message(" debug_undo2: end_cmd_checkpoint") # this will get fancier, use cmdname, worry about being fast when no diffs, merging ops, redundant calls in one cmd, etc: self.checkpoint(cptype='end_cmd') pass return # == def node_departing_assy(self, node, assy): #bruce 060315; # revised 060330 to make it almost a noop, since implem was obsolete and it caused bug 1797 #bruce 080219 making this a debug print only, since it happens with dna updater # (probably a bug) but exception may be causing further bugs; also adding a message. # Now I have a theory about the bug's cause: if this happens in a closed assy, # deinit has set self.assy to None. To repeat, open and close a dna file with dna updater # off, then turn dna updater on. Now this should cause the "bug (harmless?)" print below. #bruce 080314 update: that does happen, so that print is useless and verbose, # so disable it for now. Retain the other ones. if assy is None or node is None: print "\n*** BUG: node_departing_assy(%r, %r, %r) sees assy or node is None" % \ (self, node, assy) return if self.assy is None: # this will happen for now when the conditions that caused today's bug reoccur, # until we fix the dna updater to never run inside a closed assy (desirable) # [bruce 080219] if 0: #bruce 080314 print "\nbug (harmless?): node_departing_assy(%r, %r, %r), but " \ "self.assy is None (happens when self's file is closed)" % \ (self, node, assy) return if not (assy is self.assy): print "\n*** BUG: " \ "node_departing_assy(%r, %r, %r) sees wrong self.assy = %r" % \ (self, node, assy, self.assy) # assy is self.assy has to be true (given that neither is None), # since we were accessed as assy.undo_manager. return # == def current_command_info(self, *args, **kws): self.archive.current_command_info(*args, **kws) def undo_redo_ops(self): # copied code below [dup code is in undo_manager_older.py, not in cvs] # the following value for warn_when_change_counters_seem_wrong is a kluge # (wrong in principle but probably safe, not entirely sure it's correct) [060309] # (note, same value was hardcoded inside that method before bruce 071025; # see comment there about when I see the warnings; it's known that it gives # false warnings if we pass True when _AutoCheckpointing_enabled is false): ops = self.archive.find_undoredos( warn_when_change_counters_seem_wrong=_AutoCheckpointing_enabled) # state_version - now held inside UndoArchive.last_cp (might be wrong) ###@@@ # [what the heck does that comment mean? bruce 071025 Q] undos = [] redos = [] d1 = {'Undo': undos, 'Redo': redos} for op in ops: optype = op.optype() d1[optype].append(op) # sort ops by type ## done in the subr: redos = filter( lambda redo: not redo.destroyed, redos) #060309 since destroyed ones are not yet unstored # remove obsolete redo ops if redos: lis = [(redo.cps[1].cp_counter, redo) for redo in redos] lis.sort() only_redo = lis[-1][1] redos = [only_redo] for obs_redo in lis[:-1]: if undo_archive.debug_undo2 or env.debug(): #060309 adding 'or env.debug()' since this should never happen once clear_redo_stack() is implemented in archive print "obsolete redo:", obs_redo pass #e discard it permanently? ####@@@@ return undos, redos def undo_cmds_menuspec(self, widget): # WARNING: this is not being maintained, it's just a development draft. # So far it lacks merging and history message and perhaps win_update and update_select_mode. [060227 comment] """ Return a menu_spec for including undo-related commands in a popup menu (to be shown in the given widget, tho i don't know why the widget could matter) """ del widget archive = self.archive # copied code below [dup code is in undo_manager_older.py, not in cvs] res = [] #bruce 060301 removing this one, since it hasn't been reviewed in awhile so it might cause bugs, # and maybe it did cause one... ## res.append(( 'undo checkpoint (in RAM only)', self.menu_cmd_checkpoint )) #060301 try this one instead: res.append(('clear undo stack (experimental)', self.clear_undo_stack)) undos, redos = self.undo_redo_ops() ###e sort each list by some sort of time order (maybe of most recent use of the op in either direction??), and limit lengths # there are at most one per chunk per undoable attr... so for this test, show them all, don't bother with submenus if not undos: res.append(("Nothing we can Undo", noop, 'disabled')) ###e should figure out whether "Can't Undo XXX" or "Nothing to Undo" is more correct for op in undos + redos: # for now, we're not even including them unless as far as we know we can do them, so no role for "Can't Undo" unless none arch = archive # it's on purpose that op itself has no ref to model, so we have to pass it [obs cmt?] cmd = lambda _guard1_=None, _guard2_=None, arch=arch: arch.do_op( op) #k guards needed? (does qt pass args to menu cmds?) ## text = "%s %s" % (op.type, op.what()) text = op.menu_desc() res.append((text, cmd)) if not redos: res.append(("Nothing we can Redo", noop, 'disabled')) return res def remake_UI_menuitems( self ): #e this should also be called again if any undo-related preferences change ###@@@ #e see also: void QPopupMenu::aboutToShow () [signal], for how to know when to run this (when Edit menu is about to show); # to find the menu, no easy way (only way: monitor QAction::addedTo in a custom QAction subclass - not worth the trouble), # so just hardcode it as edit menu for now. We'll need to connect & disconnect this when created/finished, # and get passed the menu (or list of them) from the caller, which is I guess assy.__init__. if undo_archive.debug_undo2: print "debug_undo2: running remake_UI_menuitems (could be direct call or signal)" global _disable_UndoRedo disable_reasons = list( _disable_UndoRedo ) # avoid bugs if it changes while this function runs (might never happen) if disable_reasons: undos, redos = [], [ ] # note: more code notices the same condition, below else: undos, redos = self.undo_redo_ops() win = self.assy.w undo_mitem = win.editUndoAction redo_mitem = win.editRedoAction for ops, action, optype in [(undos, undo_mitem, 'Undo'), (redos, redo_mitem, 'Redo') ]: #e or could grab op.optype()? extra = "" if disable_reasons: try: why_not = str( disable_reasons[0][1] ) # kluges: depends on list format, and its whymsgs being designed for this use except: why_not = "" extra += " (not permitted %s)" % why_not # why_not is e.g. "during drag" (nim) or "during Extrude" if undo_archive.debug_undo2: extra += " (%s)" % str( time.time() ) # show when it's updated in the menu text (remove when works) ####@@@@ if ops: action.setEnabled(True) if not (len(ops) == 1): #e there should always be just one for now #060212 changed to debug msg, since this assert failed (due to process_events?? undoing esp image delete) print_compact_stack("bug: more than one %s op found: " % optype) op = ops[0] op = self.wrap_op_with_merging_flags(op) #060127 text = op.menu_desc() + extra #060126 action.setText(text) fix_tooltip( action, text ) # replace description, leave (accelkeys) alone (they contain unicode chars on Mac) self._current_main_menu_ops[ optype] = op #e should store it into menu item if we can, I suppose op.you_have_been_offered() # make sure it doesn't change its mind about being a visible undoable op, even if it gets merged_with_future # (from lack of autocp) and turns out to contain no net changes # [bruce 060326 re bug 1733; probably only needed for Undo, not Redo] else: action.setEnabled(False) ## action.setText("Can't %s" % optype) # someday we might have to say "can't undo Cmdxxx" for certain cmds ## action.setText("Nothing to %s" % optype) text = "%s%s" % (optype, extra) action.setText( text ) # for 061117 commit, look like it used to look, for the time being fix_tooltip(action, text) self._current_main_menu_ops[optype] = None pass #bruce 060319 for bug 1421 win.editUndoAction.setWhatsThis(win.editUndoText) win.editRedoAction.setWhatsThis(win.editRedoText) from foundation.whatsthis_utilities import refix_whatsthis_text_and_links ## if 0: ## # this works, but is overkill and is probably too slow, and prints huge numbers of console messages, like this: ## ## TypeError: invalid result type from MyWhatsThis.text() ## # (I bet I could fix the messages by modifying MyWhatsThis.text() to return "" (guess)) ## from foundation.whatsthis_utilities import fix_whatsthis_text_and_links ## fix_whatsthis_text_and_links( win) ## if 0: ## # this prints no console messages, but doesn't work! (for whatsthis on tool buttons or menu items) ## # guess [much later]: it fails to actually do anything to these actions! ## from foundation.whatsthis_utilities import fix_whatsthis_text_and_links ## fix_whatsthis_text_and_links( win.editUndoAction ) ## fix_whatsthis_text_and_links( win.editRedoAction ) ## # try menu objects? and toolbars? refix_whatsthis_text_and_links( ) ###@@@ predict: will fix toolbuttons but not menu items #060304 also disable/enable Clear Undo Stack action = win.editClearUndoStackAction text = "Clear Undo Stack" + '...' # workaround missing '...' (remove this when the .ui file is fixed) #e future: add an estimate of RAM to be cleared action.setText(text) fix_tooltip(action, text) enable_it = not not (undos or redos) action.setEnabled(enable_it) return # # the kinds of things we can set on one of those actions include: # # self.setViewFitToWindowAction.setText(QtGui.QApplication.translate(self.__class__.__name__, "Fit to Window")) # self.setViewFitToWindowAction.setText(QtGui.QApplication.translate(self.__class__.__name__, "&Fit to Window")) # self.setViewFitToWindowAction.setToolTip(QtGui.QApplication.translate(self.__class__.__name__, "Fit to Window (Ctrl+F)")) # self.setViewFitToWindowAction.setShortcut(QtGui.QApplication.translate(self.__class__.__name__, "Ctrl+F")) # self.viewRightAction.setStatusTip(QtGui.QApplication.translate(self.__class__.__name__, "Right View")) # self.helpMouseControlsAction.setWhatsThis(QtGui.QApplication.translate(self.__class__.__name__, "Displays help for mouse controls")) def wrap_op_with_merging_flags( self, op, flags=None): #e will also accept merging-flag or -pref arguments """ Return a higher-level op based on the given op, but with the appropriate diff-merging flags wrapped around it. Applying this higher-level op will (in general) apply op, then apply more diffs which should be merged with it according to those merging flags (though in an optimized way, e.g. first collect and merge the LL diffs, then apply all at once). The higher-level op might also have a different menu_desc, etc. In principle, caller could pass flag args, and call us more than once with different flag args for the same op; in making the wrapped op we don't modify the passed op. """ #e first we supply our own defaults for flags return self.archive.wrap_op_with_merging_flags(op, flags=flags) # main menu items (their slots in MWsemantics forward to assy which forwards to here) def editUndo(self): ## env.history.message(orangemsg("Undo: (prototype)")) self.do_main_menu_op('Undo') def editRedo(self): ## env.history.message(orangemsg("Redo: (prototype)")) self.do_main_menu_op('Redo') def do_main_menu_op(self, optype): "optype should be Undo or Redo" op_was_available = not not self._current_main_menu_ops.get(optype) global _disable_UndoRedo if _disable_UndoRedo: #060414 env.history.message( redmsg( "%s is not permitted now (and this action was only offered due to a bug)" % optype)) return global _AutoCheckpointing_enabled disabled = not _AutoCheckpointing_enabled #060312 if disabled: _AutoCheckpointing_enabled = True # temporarily enable it, just during the Undo or Redo command self.checkpoint( cptype="preUndo" ) # do a checkpoint with it enabled, so Undo or Redo can work normally. # Note: in theory this might change what commands are offered and maybe even cause the error message below to come out # (so we might want to revise it when disabled is true ##e), but I think that can only happen if there's a change_counter # bug, since the only way the enabled cp will see changes not seen by disabled one is if archive.update_before_checkpoint() # is first to set the change_counters (probably a bug); if this happens it might make Redo suddenly unavailable. ####e if optype is Redo, we could pass an option to above checkpoint to not destroy redo stack or make it inaccessible! # (such an option is nim) try: op = self._current_main_menu_ops.get(optype) if op: undo_xxx = op.menu_desc( ) # note: menu_desc includes history sernos env.history.message( u"%s" % undo_xxx ) #e say Undoing rather than Undo in case more msgs?? ######@@@@@@ TEST u"%s" self.archive.do_op(op) self.assy.w.mt.update_select_mode( ) #bruce 060227 try to fix bug 1576 self.assy.w.win_update( ) #bruce 060227 not positive this isn't called elsewhere, or how we got away without it if not else: if not disabled: print "no op to %r; not sure how this slot was called, since it should have been disabled" % optype env.history.message( redmsg( "Nothing to %s (and it's a bug that its menu item or tool button was enabled)" % optype)) else: print "no op to %r; autocp disabled (so ops to offer were recomputed just now; before that, op_was_available = %r); "\ "see code comments for more info" % ( optype, op_was_available) if op_was_available: env.history.message( redmsg("Nothing to %s (possibly due to a bug)" % optype)) else: env.history.message( redmsg( "Nothing to %s (and this action was only offered due to a bug)" % optype)) pass except: print_compact_traceback() env.history.message( redmsg("Bug in %s; see traceback in console" % optype)) if disabled: # better get the end-cp done now (since we might be relying on it for some reason -- I'm not sure) self.checkpoint(cptype="postUndo") _AutoCheckpointing_enabled = False # re-disable return pass # end of class AssyUndoManager