def setup_graphics_menu_specs( self ): # review: rename/revise for new command api? not urgent. [080806] ### TODO: make this more easily customized, esp the web help part; ### TODO if possible: fix the API (also of makeMenus) to not depend # on setting attrs as side effect """ Call self.makeMenus(), then postprocess the menu_spec attrs it sets on self, namely some or all of Menu_spec, Menu_spec_shift, Menu_spec_control, debug_Menu_spec, and make sure the first three of those are set on self in their final (modified) forms, ready for the caller to (presumably) turn into actual menus. (TODO: optim: if we know we're being called again for each event, optim by producing only whichever menu_specs are needed. This is not always just one, since we sometimes copy one into a submenu of another.) """ # Note: this was split between Command.setup_graphics_menu_specs and # GraphicsMode._setup_menus, bruce 071009 # lists of attributes of self we examine and perhaps remake: mod_attrs = ['Menu_spec_shift', 'Menu_spec_control'] all_attrs = ['Menu_spec'] + mod_attrs + ['debug_Menu_spec'] # delete any Menu_spec attrs previously set on self # (needed when self.call_makeMenus_for_each_event is true) for attr in all_attrs: if hasattr(self, attr): del self.__dict__[attr] #bruce 050416: give it a default menu; for modes we have now, # this won't ever be seen unless there are bugs #bruce 060407 update: improve the text, re bug 1739 comment #3, # since it's now visible for zoom/pan/rotate tools self.Menu_spec = [("%s" % self.get_featurename(), noop, 'disabled')] self.makeMenus() # bruce 040923 moved this here, from the subclasses; # for most modes, it replaces self.Menu_spec # bruce 041103 changed details of what self.makeMenus() should do # self.makeMenus should have set self.Menu_spec, and maybe some sister attrs assert hasattr(self, 'Menu_spec'), "%r.makeMenus() failed to set up" \ " self.Menu_spec (to be a menu spec list)" % self # should never happen after 050416 orig_Menu_spec = list(self.Menu_spec) # save a copy for comparisons, before we modify it # define the ones not defined by self.makeMenus; # make them all unique lists by copying them, # to avoid trouble when we modify them later. for attr in mod_attrs: if not hasattr(self, attr): setattr(self, attr, list(self.Menu_spec)) # note: spec should be a list (which is copyable) for attr in ['debug_Menu_spec']: if not hasattr(self, attr): setattr(self, attr, []) for attr in ['Menu_spec']: setattr(self, attr, list(getattr(self, attr))) if debug_flags.atom_debug and self.debug_Menu_spec: # put the debug items into the main menu self.Menu_spec.extend([None] + self.debug_Menu_spec) # note: [bruce 050914, re bug 971; edited 071009, 'mode' -> 'command'] # for commands that don't remake their menus on each use, # the user who turns on ATOM-DEBUG won't see the menu items # defined by debug_Menu_spec until they remake all command objects # (lazily?) by opening a new file. This might change if we remake # command objects more often (like whenever the command is entered), # but the best fix would be to remake all menus on each use. # But this requires review of the menu-spec making code for # each command (for correctness when run often), so for now, # it has to be enabled per-command by setting # command.call_makeMenus_for_each_event for that command. # It's worth doing this in the commands that define # command.debug_Menu_spec.] # new feature, bruce 041103: # add submenus to Menu_spec for each modifier-key menu which is # nonempty and different than Menu_spec # (was prototyped in extrudeMode.py, bruce 041010] doit = [] for attr, modkeyname in [('Menu_spec_shift', shift_name()), ('Menu_spec_control', control_name())]: submenu_spec = getattr(self, attr) if orig_Menu_spec != submenu_spec and submenu_spec: doit.append((modkeyname, submenu_spec)) if doit: self.Menu_spec.append(None) for modkeyname, submenu_spec in doit: itemtext = '%s-%s Menu' % (context_menu_prefix(), modkeyname) self.Menu_spec.append((itemtext, submenu_spec)) # note: use PlatformDependent functions so names work on Mac or non-Mac, # e.g. "Control-Shift Menu" vs. "Right-Shift Menu", # or "Control-Command Menu" vs. "Right-Control Menu". # [bruce 041014] if isinstance(self.o.selobj, Jig): # NFR 1740. mark 060322 # TODO: find out whether this works at all (I would be surprised if it does, # since I'd think that we'd never call this if selobj is not None); # if it does, put it on the Jig's cmenu maker, not here, if possible; # if it doesn't, also put it there if NFR 1740 remains undone and desired. # [bruce comment 071009] ##print "fyi: basicCommand.setup_graphics_menu_specs sees isinstance(selobj, Jig)" ## # see if this can ever happen ## # yes, this happened when I grabbed an RMotor's GLPane cmenu. [bruce 071025] from foundation.wiki_help import wiki_help_menuspec_for_object ms = wiki_help_menuspec_for_object(self.o.selobj) if ms: self.Menu_spec.append(None) self.Menu_spec.extend(ms) else: featurename = self.get_featurename() if featurename: from foundation.wiki_help import wiki_help_menuspec_for_featurename ms = wiki_help_menuspec_for_featurename(featurename) if ms: self.Menu_spec.append(None) # there's a bug in this separator, for BuildCrystal_Command... # [did I fix that? I vaguely recall fixing a separator # logic bug in the menu_spec processor... bruce 071009] # might this look better before the above submenus, with no separator? ## self.Menu_spec.append( ("web help: " + self.get_featurename(), ## self.menucmd_open_wiki_help_page ) ) self.Menu_spec.extend(ms) return # from setup_graphics_menu_specs
def setup_graphics_menu_specs(self): # review: rename/revise for new command api? not urgent. [080806] ### TODO: make this more easily customized, esp the web help part; ### TODO if possible: fix the API (also of makeMenus) to not depend # on setting attrs as side effect """ Call self.makeMenus(), then postprocess the menu_spec attrs it sets on self, namely some or all of Menu_spec, Menu_spec_shift, Menu_spec_control, debug_Menu_spec, and make sure the first three of those are set on self in their final (modified) forms, ready for the caller to (presumably) turn into actual menus. (TODO: optim: if we know we're being called again for each event, optim by producing only whichever menu_specs are needed. This is not always just one, since we sometimes copy one into a submenu of another.) """ # Note: this was split between Command.setup_graphics_menu_specs and # GraphicsMode._setup_menus, bruce 071009 # lists of attributes of self we examine and perhaps remake: mod_attrs = ['Menu_spec_shift', 'Menu_spec_control'] all_attrs = ['Menu_spec'] + mod_attrs + ['debug_Menu_spec'] # delete any Menu_spec attrs previously set on self # (needed when self.call_makeMenus_for_each_event is true) for attr in all_attrs: if hasattr(self, attr): del self.__dict__[attr] #bruce 050416: give it a default menu; for modes we have now, # this won't ever be seen unless there are bugs #bruce 060407 update: improve the text, re bug 1739 comment #3, # since it's now visible for zoom/pan/rotate tools self.Menu_spec = [("%s" % self.get_featurename(), noop, 'disabled')] self.makeMenus() # bruce 040923 moved this here, from the subclasses; # for most modes, it replaces self.Menu_spec # bruce 041103 changed details of what self.makeMenus() should do # self.makeMenus should have set self.Menu_spec, and maybe some sister attrs assert hasattr(self, 'Menu_spec'), "%r.makeMenus() failed to set up" \ " self.Menu_spec (to be a menu spec list)" % self # should never happen after 050416 orig_Menu_spec = list(self.Menu_spec) # save a copy for comparisons, before we modify it # define the ones not defined by self.makeMenus; # make them all unique lists by copying them, # to avoid trouble when we modify them later. for attr in mod_attrs: if not hasattr(self, attr): setattr(self, attr, list(self.Menu_spec)) # note: spec should be a list (which is copyable) for attr in ['debug_Menu_spec']: if not hasattr(self, attr): setattr(self, attr, []) for attr in ['Menu_spec']: setattr(self, attr, list(getattr(self, attr))) if debug_flags.atom_debug and self.debug_Menu_spec: # put the debug items into the main menu self.Menu_spec.extend( [None] + self.debug_Menu_spec ) # note: [bruce 050914, re bug 971; edited 071009, 'mode' -> 'command'] # for commands that don't remake their menus on each use, # the user who turns on ATOM-DEBUG won't see the menu items # defined by debug_Menu_spec until they remake all command objects # (lazily?) by opening a new file. This might change if we remake # command objects more often (like whenever the command is entered), # but the best fix would be to remake all menus on each use. # But this requires review of the menu-spec making code for # each command (for correctness when run often), so for now, # it has to be enabled per-command by setting # command.call_makeMenus_for_each_event for that command. # It's worth doing this in the commands that define # command.debug_Menu_spec.] # new feature, bruce 041103: # add submenus to Menu_spec for each modifier-key menu which is # nonempty and different than Menu_spec # (was prototyped in extrudeMode.py, bruce 041010] doit = [] for attr, modkeyname in [ ('Menu_spec_shift', shift_name()), ('Menu_spec_control', control_name()) ]: submenu_spec = getattr(self, attr) if orig_Menu_spec != submenu_spec and submenu_spec: doit.append( (modkeyname, submenu_spec) ) if doit: self.Menu_spec.append(None) for modkeyname, submenu_spec in doit: itemtext = '%s-%s Menu' % (context_menu_prefix(), modkeyname) self.Menu_spec.append( (itemtext, submenu_spec) ) # note: use PlatformDependent functions so names work on Mac or non-Mac, # e.g. "Control-Shift Menu" vs. "Right-Shift Menu", # or "Control-Command Menu" vs. "Right-Control Menu". # [bruce 041014] if isinstance( self.o.selobj, Jig): # NFR 1740. mark 060322 # TODO: find out whether this works at all (I would be surprised if it does, # since I'd think that we'd never call this if selobj is not None); # if it does, put it on the Jig's cmenu maker, not here, if possible; # if it doesn't, also put it there if NFR 1740 remains undone and desired. # [bruce comment 071009] ##print "fyi: basicCommand.setup_graphics_menu_specs sees isinstance(selobj, Jig)" ## # see if this can ever happen ## # yes, this happened when I grabbed an RMotor's GLPane cmenu. [bruce 071025] from foundation.wiki_help import wiki_help_menuspec_for_object ms = wiki_help_menuspec_for_object( self.o.selobj ) if ms: self.Menu_spec.append( None ) self.Menu_spec.extend( ms ) else: featurename = self.get_featurename() if featurename: from foundation.wiki_help import wiki_help_menuspec_for_featurename ms = wiki_help_menuspec_for_featurename( featurename ) if ms: self.Menu_spec.append( None ) # there's a bug in this separator, for BuildCrystal_Command... # [did I fix that? I vaguely recall fixing a separator # logic bug in the menu_spec processor... bruce 071009] # might this look better before the above submenus, with no separator? ## self.Menu_spec.append( ("web help: " + self.get_featurename(), ## self.menucmd_open_wiki_help_page ) ) self.Menu_spec.extend( ms ) return # from setup_graphics_menu_specs
def make_cmenuspec_for_set(self, nodeset, nodeset_whole, optflag): """ #doc... see superclass docstring, and the term "menu_spec" """ # Note: we use nodeset_whole (a subset of nodeset, or equal to it) # for operations which might be unsafe on partly-selected "whole nodes" # (i.e. unopenable nodes such as DnaStrand). # (Doing this on the Group operation is part of fixing bug 2948.) # These menu commands need corresponding changes in their cm methods, # namely, calling deselect_partly_picked_whole_nodes at the start. # All cm_duplicate methods (on individual nodes) also may need to do # this if they care about the selection. # [bruce 081218] #e some advice [bruce]: put "display changes" (eg Hide) before "structural changes" (such as Group/Ungroup)... #e a context-menu command "duplicate" which produces ##a copy of them, with related names and a "sibling" position. ##Whereas the menu command called "copy" produces a copy of the selected ##things in the system-wide "clipboard" shared by all apps.) # I think we might as well remake this every time, for most kinds of menus, # so it's easy for it to depend on current state. # I really doubt this will be too slow. [bruce 050113] if not nodeset: #e later we'll add useful menu commands for no nodes, # i.e. for a "context menu of the background". # In fact, we'll probably remove this special case # and instead let each menu command decide whether it applies # in this case. res = [('Model Tree (nothing selected)', noop, 'disabled')] #bruce 050505 adding some commands here (cm_delete_clipboard is a just-reported NFR from Mark) res.append(('Create new empty clipboard item', self.cm_new_clipboard_item)) lenshelf = len( self.assy.shelf.MT_kids()) #bruce 081217 use MT_kids if lenshelf: if lenshelf > 2: text = 'Delete all %d clipboard items' % lenshelf else: text = 'Delete all clipboard items' res.append((text, self.cm_delete_clipboard)) return res res = [] if len(nodeset_whole) < len(nodeset): # alert user to presence of partly-selected items [bruce 081218] # (which are not marked as selected on nodes seeable in MT) # (review: should we mark them in some other way?) # # (note about older text, # "deselect %d partly-selected item(s)": # the count is wrong if one partly-selected leaflike group # contains more than one selected node, and nothing explains # the situation well to the user) # # (about this text: it might be ambiguous whether they're too deep # because of groups being closed, or being leaflike; mitigated # by saying "shown" rather than "visible") text = "Deselect %d node(s) too deep to be shown" % \ (len(nodeset) - len(nodeset_whole)) text = fix_plurals(text) res.append((text, self.cm_deselect_partly_selected_items)) res.append(None) pass # old comment, not recently reviewed/updated as of 081217: # first put in a Hide item, checked or unchecked. But what if the hidden-state is mixed? # then there is a need for two menu commands! Or, use the command twice, fully hide then fully unhide -- not so good. # Hmm... let's put in Hide (with checkmark meaning "all hidden"), then iff that's not enough, Unhide. # So how do we know if a node is hidden -- this is only defined for leaf nodes now! # I guess we figure it out... I guess we might as well classify nodeset and its kids. # [update, bruce 080108/080306: does "and its kids" refer to members, or MT_kids? # It might be some of each -- we would want to include members present but not shown # in the MT (like the members of DnaGroup or DnaStrand), which are in members but not in # MT_kids, but we might also want to cover "shared members", like DnaStrandChunks, # which *might* be included in both strands and segments for this purpose (in the future; # shared members are NIM now).] allstats = all_attrs_act_as_counters() for node in nodeset: node_stats = all_attrs_act_as_counters() node.apply2all( lambda node1: mt_accumulate_stats(node1, node_stats)) allstats += node_stats # totals to allstats # Hide command (and sometimes Unhide) # now can we figure out how much is/could be hidden, etc #e (later, modularize this, make assertfails only affect certain menu commands, etc) nleafs = allstats.n - allstats.ngroups assert nleafs >= 0 nhidden = allstats.nhidden nunhidden = nleafs - nhidden # since only leafs can be hidden assert nunhidden >= 0 # We'll always define a Hide item. Checked means all is hidden (and the command will be unhide); # unchecked means not all is hidden (and the command will be hide). # First handle degenerate case where there are no leafs selected. if nleafs == 0: res.append( ('Hide', noop, 'disabled')) # nothing that can be hidden elif nunhidden == 0: # all is hidden -- show that, and offer to unhide it all ## res.append(( 'Hidden', self.cm_unhide, 'checked')) res.append(('Unhide', self.cm_unhide)) # will this be better? ##e do we want special cases saying "Unhide All", here and below, # when all hidden items would be unhidden, or vice versa? # (on PartGroup, or in other cases, so detect by comparing counts for sel and tree_node.) elif nhidden > 0: # some is not hidden, some is hidden -- make this clear & offer both extremes ## res.append(( 'Hide (' + fix_plurals('%d item(s)' % nunhidden) + ')', self.cm_hide )) #e fix_plurals bug, worked around res.append( (fix_plurals('Unhide %d item(s)' % nhidden), self.cm_unhide)) res.append( (fix_plurals('Hide %d item(s)' % nunhidden), self.cm_hide)) else: # all is unhidden -- just offer to hide it res.append(('Hide', self.cm_hide)) try: njigs = allstats.njigs if njigs == 1 and allstats.n == 1: # exactly one jig selected. Show its disabled state, with option to change this if permitted. # warning: depends on details of Jig.is_disabled() implem. Ideally we should ask Jig to contribute # this part of the menu-spec itself #e. [bruce 050421] jig = nodeset[0] if not isinstance( jig, RectGadget ): # remove this menu item for RectGadget [Huaicai 10/11/05] disabled_must = jig.disabled_by_atoms( ) # (by its atoms being in the wrong part) disabled_choice = jig.disabled_by_user_choice disabled_menu_item = disabled_must # menu item is disabled iff jig disabled state can't be changed, ie is "stuck on" checked = disabled_must or disabled_choice # menu item is checked if it's disabled for whatever reason (also affects text) if checked: command = self.cm_enable if disabled_must: text = "Disabled (atoms in other Part)" else: text = "Disabled" else: command = self.cm_disable text = "Disable" res.append( (text, command, checked and 'checked' or None, disabled_menu_item and 'disabled' or None)) except: print "bug in MT njigs == 1, ignored" ## raise # just during devel pass if nodeset_whole: res.append(None) # separator # (from here on, only add these at start of optional items # or sets of items) # Group command -- only offered for 2 or more subtrees of any Part, # or for exactly one clipboard item topnode itself if it's not already a Group. # [rules loosened by bruce 050419-050421] if optflag or len(nodeset_whole) >= 2: # note that these nodes are always in the same Part and can't include its topnode ok = True else: # exactly one node - ok iff it's a clipboard item and not a group node = nodeset_whole[0] ok = (node.dad is self.shelf_node and not node.is_group()) if not ok: res.append(('Group', noop, 'disabled')) else: res.append(('Group', self.cm_group)) # Ungroup command -- only when exactly one picked Group is what we have, of a suitable kind. # (As for Group, later this can become more general, tho in this case it might be general # enough already -- it's more "self-contained" than the Group command can be.) offered_ungroup = False # modified below; used by other menu items farther below if len(nodeset_whole) == 1 and nodeset_whole[0].permits_ungrouping( ): # (this implies it's a group, or enough like one) node = nodeset_whole[0] if not node.members: #bruce 080207 #REVIEW: use MT_kids? [same issue in many places in this file, as of 080306] #reply, bruce 081217: not yet; really we need a new Node or Group API method # "offer to remove as empty Group"; meanwhile, be conservative by using .members text = "Remove empty Group" elif node.dad == self.shelf_node and len(node.members) > 1: # todo: "Ungroup into %d separate clipboard item(s)" text = "Ungroup into separate clipboard items" #bruce 050419 new feature (distinct text in this case) else: # todo: "Ungroup %d item(s)" text = "Ungroup" res.append((text, self.cm_ungroup)) offered_ungroup = True else: # review: is this clear enough for nodes that are internally Groups # but for which permits_ungrouping is false, or would some other # text be better, or would leaving this item out be better? # An old suggestion of "Ungroup (unsupported)" seems bad now, # since it might sound like "a desired feature that's nim". # [bruce 081212 comment] res.append(("Ungroup", noop, 'disabled')) # Remove all %d empty Groups (which permit ungrouping) [bruce 080207] count_holder = [0] def func(group, count_holder=count_holder): if not group.members and group.permits_ungrouping(): count_holder[ 0] += 1 # UnboundLocalError when this was count += 1 for node in nodeset_whole: node.apply_to_groups( func ) # note: this descends into groups that don't permit ungrouping, e.g. DnaStrand count = count_holder[0] if count == 1 and len( nodeset_whole) == 1 and not nodeset_whole[0].members: # this is about the single top selected node, # so it's redundant with the Ungroup command above # (and if that was not offered, this should not be either) pass elif count: res.append(('Remove all %d empty Groups' % count, self.cm_remove_empty_groups)) # lack of fix_plurals seems best here; review when seen else: pass pass # Edit Properties command -- only provide this when there's exactly one thing to apply it to, # and it says it can handle it. ###e Command name should depend on what the thing is, e.g. "Part Properties", "Chunk Properties". # Need to add methods to return that "user-visible class name". res.append(None) # separator if debug_flags.atom_debug: if len(nodeset) == 1: res.append(("debug._node =", self.cm_set_node)) else: res.append(("debug._nodeset =", self.cm_set_node)) if len(nodeset) == 1 and nodeset[0].editProperties_enabled(): res.append(('Edit Properties...', self.cm_properties)) else: res.append(('Edit Properties...', noop, 'disabled')) # nim for multiple items #ninad 070320 - context menu option to edit color of multiple chunks if allstats.nchunks: res.append(("Edit Chunk Color...", self.cmEditChunkColor)) if allstats.canShowOverlayText: res.append(("Show Overlay Text", self.cmShowOverlayText)) if allstats.canHideOverlayText: res.append(("Hide Overlay Text", self.cmHideOverlayText)) #bruce 070531 - rename node -- temporary workaround for inability to do this in MT, or, maybe we'll like it to stay if len(nodeset) == 1: node = nodeset[0] if node.rename_enabled(): res.append( ("Rename node...", self.cmRenameNode) ) ##k should it be called node or item in this menu text? # subsection of menu (not a submenu unless they specify one) # for node-class-specific menu items, when exactly one node # (old way, based on methodnames that start with __CM; # and new better way, using Node method ModelTree_context_menu_section) if len(nodeset) == 1: node = nodeset[0] submenu = [] attrs = filter(lambda attr: "__CM_" in attr, dir( node.__class__)) #e should do in order of superclasses attrs.sort() # ok if empty list #bruce 050708 -- provide a way for these custom menu items to specify a list of menu_spec options (e.g. 'disabled') -- # they should define a method with the same name + "__options" and have it return a list of options, e.g. ['disabled'], # or [] if it doesn't want to provide any options. It will be called again every time the context menu is shown. # If it wants to remove the menu item entirely, it can return the special value (not a list) 'remove'. opts = {} for attr in attrs: # pass 1 - record menu options for certain commands if attr.endswith("__options"): boundmethod = getattr(node, attr) try: lis = boundmethod() assert type(lis) == type([]) or lis == 'remove' opts[attr] = lis # for use in pass 2 except: print_compact_traceback( "exception ignored in %r.%s(): " % (node, attr)) pass for attr in attrs: # pass 2 if attr.endswith("__options"): continue classname, menutext = attr.split("__CM_", 1) boundmethod = getattr(node, attr) if callable(boundmethod): lis = opts.get(attr + "__options") or [] if lis != 'remove': mitem = tuple( [menutext.replace('_', ' '), boundmethod] + lis) submenu.append(mitem) elif boundmethod is None: # kluge: None means remove any existing menu items (before the submenu) with this menutext! res = filter( lambda text_cmd: text_cmd and text_cmd[0] != menutext, res) # text_cmd might be None while res and res[0] == None: res = res[1:] #e should also remove adjacent Nones inside res else: assert 0, "not a callable or None: %r" % boundmethod if submenu: ## res.append(( 'other', submenu )) #e improve submenu name, ordering, location res.extend( submenu ) # changed append to extend -- Mark and Bruce at Retreat 050621 # new system, used in addition to __CM above (preferred in new code): # [bruce 080225] try: submenu = node.ModelTree_context_menu_section() assert submenu is not None # catch a likely possible error specifically assert type(submenu) is type( []) # it should be a menu_spec list except: print_compact_traceback("exception ignored in %r.%s() " \ "or in checking its result: " % \ (node, 'ModelTree_context_menu_section')) submenu = [] if submenu: res.extend(submenu) pass if nodeset_whole: # copy, cut, delete, maybe duplicate... # bruce 050704 revisions: # - these are probably ok for clipboard items; I'll enable them there and let them be tested there. # - I'll remove Copy when the selection only contains jigs that won't copy themselves # unless some of their atoms are copied (which for now is true of all jigs). # More generally (in principle -- the implem is not general), Copy should be removed # when the selection contains nothing which makes sense to copy on its own, # only things which make sense to copy only in conjunction with other things. # I think this is equivalent to whether all the selected things would fail to get copied, # when the copy command was run. # - I'll add Duplicate for single selected jigs which provide an appropriate method, # and show it dimmed for those that don't. res.append(None) # separator # figure out whether Copy would actually copy anything. part = nodeset_whole[ 0].part # the same for all nodes in nodeset_whole sel = selection_from_part( part, use_selatoms=False ) #k should this be the first code to use selection_from_MT() instead? doit = False for node in nodeset_whole: if node.will_copy_if_selected(sel, False): #wware 060329 added realCopy arg, False here (this is not a real copy, so do not issue a warning). #bruce 060329 points out about realCopy being False vs True that at this point in the code we don't # yet know whether the real copy will be made, and when we do, will_copy_if_selected # might like to be re-called with True, but that's presently nim. ###@@@ # # if this test is too slow, could inline it by knowing about Jigs here; but better to speed it up instead! doit = True break if doit: res.append(('Copy', self.cm_copy)) # For single items, add a Duplicate command and enable it if they support the method. [bruce 050704 new feature] # For now, hardly anything offers this command, so I'm changing the plan, and removing it (not disabling it) # when not available. This should be reconsidered if more things offer it. if len(nodeset_whole) == 1: node = nodeset_whole[0] try: method = node.cm_duplicate # Warning 1: different API than self.cm_xxx methods (arg differs) # or __CM_ methods (disabled rather than missing, if not defined). # Warning 2: if a class provides it, no way for a subclass to stop # providing it. This aspect of the API is bad, should be revised. # Warning 3: consider whether each implem of this needs to call # self.deselect_partly_picked_whole_nodes(). assert callable(method) except: dupok = False else: dupok = True if dupok: res.append(('Duplicate', method)) else: pass ## res.append(( 'Duplicate', noop, 'disabled' )) # Cut (unlike Copy), and Delete, should always be ok. res.append(('Cut', self.cm_cut)) res.append(('Delete', self.cm_delete)) #ninad060816 added option to select all atoms of the selected chunks. #I don't know how to handle a case when a whole group is selected. #So putting a condition allstats.nchunks == allstats.n. #Perhaps, I should unpick the groups while picking atoms? if allstats.nchunks == allstats.n and allstats.nchunks: res.append( (fix_plurals("Select all atoms of %d chunk(s)" % allstats.nchunks), self.cmSelectAllAtomsInChunk)) # add basic info on what's selected at the end # (later might turn into commands related to subclasses of nodes) if allstats.nchunks + allstats.njigs: # otherwise, nothing we can yet print stats on... (e.g. clipboard) res.append(None) # separator res.append(("selection:", noop, 'disabled')) if allstats.nchunks: res.append((fix_plurals("%d chunk(s)" % allstats.nchunks), noop, 'disabled')) if allstats.njigs: res.append((fix_plurals("%d jig(s)" % allstats.njigs), noop, 'disabled')) if allstats.nhidden: res.append(("(%d of these are hidden)" % allstats.nhidden, noop, 'disabled')) if allstats.njigs == allstats.n and allstats.njigs: # only jigs are selected -- offer to select their atoms [bruce 050504] # (text searches for this code might like to find "Select this jig's" or "Select these jigs'") want_select_item = True #bruce 051208 if allstats.njigs == 1: jig = nodeset[0] if isinstance( jig, RectGadget ): # remove menu item for RectGadget [Huaicai 10/11/05] ## return res -- this 'return' was causing bug 1189 by skipping the rest of the menu, not just this item. # Try to do something less drastic. [bruce 051208] want_select_item = False else: natoms = len(nodeset[0].atoms) myatoms = fix_plurals("this jig's %d atom(s)" % natoms) else: myatoms = "these jigs' atoms" if want_select_item: res.append( ('Select ' + myatoms, self.cm_select_jigs_atoms)) ## ##e following msg is not true, since nodeset doesn't include selection under selected groups! ## # need to replace it with a better breakdown of what's selected, ## # incl how much under selected groups is selected. Maybe we'll add a list of major types ## # of selected things, as submenus, lower down (with commands like "select only these", "deselect these"). ## ## res.append(( fix_plurals("(%d selected item(s))" % len(nodeset)), noop, 'disabled' )) # for single items that have a featurename, add wiki-help command [bruce 051201] if len(nodeset) == 1: node = nodeset[0] ms = wiki_help_menuspec_for_object( node ) # will be [] if this node should have no wiki help menu items #review: will this func ever need to know which widget is asking? if ms: res.append(None) # separator res.extend(ms) return res # from make_cmenuspec_for_set
def make_cmenuspec_for_set(self, nodeset, nodeset_whole, optflag): """ #doc... see superclass docstring, and the term "menu_spec" """ # Note: we use nodeset_whole (a subset of nodeset, or equal to it) # for operations which might be unsafe on partly-selected "whole nodes" # (i.e. unopenable nodes such as DnaStrand). # (Doing this on the Group operation is part of fixing bug 2948.) # These menu commands need corresponding changes in their cm methods, # namely, calling deselect_partly_picked_whole_nodes at the start. # All cm_duplicate methods (on individual nodes) also may need to do # this if they care about the selection. # [bruce 081218] #e some advice [bruce]: put "display changes" (eg Hide) before "structural changes" (such as Group/Ungroup)... #e a context-menu command "duplicate" which produces ##a copy of them, with related names and a "sibling" position. ##Whereas the menu command called "copy" produces a copy of the selected ##things in the system-wide "clipboard" shared by all apps.) # I think we might as well remake this every time, for most kinds of menus, # so it's easy for it to depend on current state. # I really doubt this will be too slow. [bruce 050113] if not nodeset: #e later we'll add useful menu commands for no nodes, # i.e. for a "context menu of the background". # In fact, we'll probably remove this special case # and instead let each menu command decide whether it applies # in this case. res = [('Model Tree (nothing selected)',noop,'disabled')] #bruce 050505 adding some commands here (cm_delete_clipboard is a just-reported NFR from Mark) res.append(( 'Create new empty clipboard item', self.cm_new_clipboard_item )) lenshelf = len(self.assy.shelf.MT_kids()) #bruce 081217 use MT_kids if lenshelf: if lenshelf > 2: text = 'Delete all %d clipboard items' % lenshelf else: text = 'Delete all clipboard items' res.append(( text, self.cm_delete_clipboard )) return res res = [] if len(nodeset_whole) < len(nodeset): # alert user to presence of partly-selected items [bruce 081218] # (which are not marked as selected on nodes seeable in MT) # (review: should we mark them in some other way?) # # (note about older text, # "deselect %d partly-selected item(s)": # the count is wrong if one partly-selected leaflike group # contains more than one selected node, and nothing explains # the situation well to the user) # # (about this text: it might be ambiguous whether they're too deep # because of groups being closed, or being leaflike; mitigated # by saying "shown" rather than "visible") text = "Deselect %d node(s) too deep to be shown" % \ (len(nodeset) - len(nodeset_whole)) text = fix_plurals(text) res.append(( text, self.cm_deselect_partly_selected_items )) res.append(None) pass # old comment, not recently reviewed/updated as of 081217: # first put in a Hide item, checked or unchecked. But what if the hidden-state is mixed? # then there is a need for two menu commands! Or, use the command twice, fully hide then fully unhide -- not so good. # Hmm... let's put in Hide (with checkmark meaning "all hidden"), then iff that's not enough, Unhide. # So how do we know if a node is hidden -- this is only defined for leaf nodes now! # I guess we figure it out... I guess we might as well classify nodeset and its kids. # [update, bruce 080108/080306: does "and its kids" refer to members, or MT_kids? # It might be some of each -- we would want to include members present but not shown # in the MT (like the members of DnaGroup or DnaStrand), which are in members but not in # MT_kids, but we might also want to cover "shared members", like DnaStrandChunks, # which *might* be included in both strands and segments for this purpose (in the future; # shared members are NIM now).] allstats = all_attrs_act_as_counters() for node in nodeset: node_stats = all_attrs_act_as_counters() node.apply2all( lambda node1: mt_accumulate_stats( node1, node_stats) ) allstats += node_stats # totals to allstats # Hide command (and sometimes Unhide) # now can we figure out how much is/could be hidden, etc #e (later, modularize this, make assertfails only affect certain menu commands, etc) nleafs = allstats.n - allstats.ngroups assert nleafs >= 0 nhidden = allstats.nhidden nunhidden = nleafs - nhidden # since only leafs can be hidden assert nunhidden >= 0 # We'll always define a Hide item. Checked means all is hidden (and the command will be unhide); # unchecked means not all is hidden (and the command will be hide). # First handle degenerate case where there are no leafs selected. if nleafs == 0: res.append(( 'Hide', noop, 'disabled')) # nothing that can be hidden elif nunhidden == 0: # all is hidden -- show that, and offer to unhide it all ## res.append(( 'Hidden', self.cm_unhide, 'checked')) res.append(( 'Unhide', self.cm_unhide)) # will this be better? ##e do we want special cases saying "Unhide All", here and below, # when all hidden items would be unhidden, or vice versa? # (on PartGroup, or in other cases, so detect by comparing counts for sel and tree_node.) elif nhidden > 0: # some is not hidden, some is hidden -- make this clear & offer both extremes ## res.append(( 'Hide (' + fix_plurals('%d item(s)' % nunhidden) + ')', self.cm_hide )) #e fix_plurals bug, worked around res.append(( fix_plurals('Unhide %d item(s)' % nhidden), self.cm_unhide )) res.append(( fix_plurals('Hide %d item(s)' % nunhidden), self.cm_hide )) else: # all is unhidden -- just offer to hide it res.append(( 'Hide', self.cm_hide )) try: njigs = allstats.njigs if njigs == 1 and allstats.n == 1: # exactly one jig selected. Show its disabled state, with option to change this if permitted. # warning: depends on details of Jig.is_disabled() implem. Ideally we should ask Jig to contribute # this part of the menu-spec itself #e. [bruce 050421] jig = nodeset[0] if not isinstance(jig, RectGadget): # remove this menu item for RectGadget [Huaicai 10/11/05] disabled_must = jig.disabled_by_atoms() # (by its atoms being in the wrong part) disabled_choice = jig.disabled_by_user_choice disabled_menu_item = disabled_must # menu item is disabled iff jig disabled state can't be changed, ie is "stuck on" checked = disabled_must or disabled_choice # menu item is checked if it's disabled for whatever reason (also affects text) if checked: command = self.cm_enable if disabled_must: text = "Disabled (atoms in other Part)" else: text = "Disabled" else: command = self.cm_disable text = "Disable" res.append(( text, command, checked and 'checked' or None, disabled_menu_item and 'disabled' or None )) except: print "bug in MT njigs == 1, ignored" ## raise # just during devel pass if nodeset_whole: res.append(None) # separator # (from here on, only add these at start of optional items # or sets of items) # Group command -- only offered for 2 or more subtrees of any Part, # or for exactly one clipboard item topnode itself if it's not already a Group. # [rules loosened by bruce 050419-050421] if optflag or len(nodeset_whole) >= 2: # note that these nodes are always in the same Part and can't include its topnode ok = True else: # exactly one node - ok iff it's a clipboard item and not a group node = nodeset_whole[0] ok = (node.dad is self.shelf_node and not node.is_group()) if not ok: res.append(( 'Group', noop, 'disabled' )) else: res.append(( 'Group', self.cm_group )) # Ungroup command -- only when exactly one picked Group is what we have, of a suitable kind. # (As for Group, later this can become more general, tho in this case it might be general # enough already -- it's more "self-contained" than the Group command can be.) offered_ungroup = False # modified below; used by other menu items farther below if len(nodeset_whole) == 1 and nodeset_whole[0].permits_ungrouping(): # (this implies it's a group, or enough like one) node = nodeset_whole[0] if not node.members: #bruce 080207 #REVIEW: use MT_kids? [same issue in many places in this file, as of 080306] #reply, bruce 081217: not yet; really we need a new Node or Group API method # "offer to remove as empty Group"; meanwhile, be conservative by using .members text = "Remove empty Group" elif node.dad == self.shelf_node and len(node.members) > 1: # todo: "Ungroup into %d separate clipboard item(s)" text = "Ungroup into separate clipboard items" #bruce 050419 new feature (distinct text in this case) else: # todo: "Ungroup %d item(s)" text = "Ungroup" res.append(( text, self.cm_ungroup )) offered_ungroup = True else: # review: is this clear enough for nodes that are internally Groups # but for which permits_ungrouping is false, or would some other # text be better, or would leaving this item out be better? # An old suggestion of "Ungroup (unsupported)" seems bad now, # since it might sound like "a desired feature that's nim". # [bruce 081212 comment] res.append(( "Ungroup", noop, 'disabled' )) # Remove all %d empty Groups (which permit ungrouping) [bruce 080207] count_holder = [0] def func(group, count_holder = count_holder): if not group.members and group.permits_ungrouping(): count_holder[0] += 1 # UnboundLocalError when this was count += 1 for node in nodeset_whole: node.apply_to_groups(func) # note: this descends into groups that don't permit ungrouping, e.g. DnaStrand count = count_holder[0] if count == 1 and len(nodeset_whole) == 1 and not nodeset_whole[0].members: # this is about the single top selected node, # so it's redundant with the Ungroup command above # (and if that was not offered, this should not be either) pass elif count: res.append(( 'Remove all %d empty Groups' % count, self.cm_remove_empty_groups )) # lack of fix_plurals seems best here; review when seen else: pass pass # Edit Properties command -- only provide this when there's exactly one thing to apply it to, # and it says it can handle it. ###e Command name should depend on what the thing is, e.g. "Part Properties", "Chunk Properties". # Need to add methods to return that "user-visible class name". res.append(None) # separator if debug_flags.atom_debug: if len(nodeset) == 1: res.append(( "debug._node =", self.cm_set_node )) else: res.append(( "debug._nodeset =", self.cm_set_node )) if len(nodeset) == 1 and nodeset[0].editProperties_enabled(): res.append(( 'Edit Properties...', self.cm_properties )) else: res.append(( 'Edit Properties...', noop, 'disabled' )) # nim for multiple items #ninad 070320 - context menu option to edit color of multiple chunks if allstats.nchunks: res.append(("Edit Chunk Color...", self.cmEditChunkColor)) if allstats.canShowOverlayText: res.append(("Show Overlay Text", self.cmShowOverlayText)) if allstats.canHideOverlayText: res.append(("Hide Overlay Text", self.cmHideOverlayText)) #bruce 070531 - rename node -- temporary workaround for inability to do this in MT, or, maybe we'll like it to stay if len(nodeset) == 1: node = nodeset[0] if node.rename_enabled(): res.append(("Rename node...", self.cmRenameNode)) ##k should it be called node or item in this menu text? # subsection of menu (not a submenu unless they specify one) # for node-class-specific menu items, when exactly one node # (old way, based on methodnames that start with __CM; # and new better way, using Node method ModelTree_context_menu_section) if len(nodeset) == 1: node = nodeset[0] submenu = [] attrs = filter( lambda attr: "__CM_" in attr, dir( node.__class__ )) #e should do in order of superclasses attrs.sort() # ok if empty list #bruce 050708 -- provide a way for these custom menu items to specify a list of menu_spec options (e.g. 'disabled') -- # they should define a method with the same name + "__options" and have it return a list of options, e.g. ['disabled'], # or [] if it doesn't want to provide any options. It will be called again every time the context menu is shown. # If it wants to remove the menu item entirely, it can return the special value (not a list) 'remove'. opts = {} for attr in attrs: # pass 1 - record menu options for certain commands if attr.endswith("__options"): boundmethod = getattr( node, attr) try: lis = boundmethod() assert type(lis) == type([]) or lis == 'remove' opts[attr] = lis # for use in pass 2 except: print_compact_traceback("exception ignored in %r.%s(): " % (node, attr)) pass for attr in attrs: # pass 2 if attr.endswith("__options"): continue classname, menutext = attr.split("__CM_",1) boundmethod = getattr( node, attr) if callable(boundmethod): lis = opts.get(attr + "__options") or [] if lis != 'remove': mitem = tuple([menutext.replace('_',' '), boundmethod] + lis) submenu.append(mitem) elif boundmethod is None: # kluge: None means remove any existing menu items (before the submenu) with this menutext! res = filter( lambda text_cmd: text_cmd and text_cmd[0] != menutext, res ) # text_cmd might be None while res and res[0] == None: res = res[1:] #e should also remove adjacent Nones inside res else: assert 0, "not a callable or None: %r" % boundmethod if submenu: ## res.append(( 'other', submenu )) #e improve submenu name, ordering, location res.extend(submenu) # changed append to extend -- Mark and Bruce at Retreat 050621 # new system, used in addition to __CM above (preferred in new code): # [bruce 080225] try: submenu = node.ModelTree_context_menu_section() assert submenu is not None # catch a likely possible error specifically assert type(submenu) is type([]) # it should be a menu_spec list except: print_compact_traceback("exception ignored in %r.%s() " \ "or in checking its result: " % \ (node, 'ModelTree_context_menu_section')) submenu = [] if submenu: res.extend(submenu) pass if nodeset_whole: # copy, cut, delete, maybe duplicate... # bruce 050704 revisions: # - these are probably ok for clipboard items; I'll enable them there and let them be tested there. # - I'll remove Copy when the selection only contains jigs that won't copy themselves # unless some of their atoms are copied (which for now is true of all jigs). # More generally (in principle -- the implem is not general), Copy should be removed # when the selection contains nothing which makes sense to copy on its own, # only things which make sense to copy only in conjunction with other things. # I think this is equivalent to whether all the selected things would fail to get copied, # when the copy command was run. # - I'll add Duplicate for single selected jigs which provide an appropriate method, # and show it dimmed for those that don't. res.append(None) # separator # figure out whether Copy would actually copy anything. part = nodeset_whole[0].part # the same for all nodes in nodeset_whole sel = selection_from_part(part, use_selatoms = False) #k should this be the first code to use selection_from_MT() instead? doit = False for node in nodeset_whole: if node.will_copy_if_selected(sel, False): #wware 060329 added realCopy arg, False here (this is not a real copy, so do not issue a warning). #bruce 060329 points out about realCopy being False vs True that at this point in the code we don't # yet know whether the real copy will be made, and when we do, will_copy_if_selected # might like to be re-called with True, but that's presently nim. ###@@@ # # if this test is too slow, could inline it by knowing about Jigs here; but better to speed it up instead! doit = True break if doit: res.append(( 'Copy', self.cm_copy )) # For single items, add a Duplicate command and enable it if they support the method. [bruce 050704 new feature] # For now, hardly anything offers this command, so I'm changing the plan, and removing it (not disabling it) # when not available. This should be reconsidered if more things offer it. if len(nodeset_whole) == 1: node = nodeset_whole[0] try: method = node.cm_duplicate # Warning 1: different API than self.cm_xxx methods (arg differs) # or __CM_ methods (disabled rather than missing, if not defined). # Warning 2: if a class provides it, no way for a subclass to stop # providing it. This aspect of the API is bad, should be revised. # Warning 3: consider whether each implem of this needs to call # self.deselect_partly_picked_whole_nodes(). assert callable(method) except: dupok = False else: dupok = True if dupok: res.append(( 'Duplicate', method )) else: pass ## res.append(( 'Duplicate', noop, 'disabled' )) # Cut (unlike Copy), and Delete, should always be ok. res.append(( 'Cut', self.cm_cut )) res.append(( 'Delete', self.cm_delete )) #ninad060816 added option to select all atoms of the selected chunks. #I don't know how to handle a case when a whole group is selected. #So putting a condition allstats.nchunks == allstats.n. #Perhaps, I should unpick the groups while picking atoms? if allstats.nchunks == allstats.n and allstats.nchunks : res.append((fix_plurals("Select all atoms of %d chunk(s)" % allstats.nchunks), self.cmSelectAllAtomsInChunk)) # add basic info on what's selected at the end # (later might turn into commands related to subclasses of nodes) if allstats.nchunks + allstats.njigs: # otherwise, nothing we can yet print stats on... (e.g. clipboard) res.append(None) # separator res.append(( "selection:", noop, 'disabled' )) if allstats.nchunks: res.append(( fix_plurals("%d chunk(s)" % allstats.nchunks), noop, 'disabled' )) if allstats.njigs: res.append(( fix_plurals("%d jig(s)" % allstats.njigs), noop, 'disabled' )) if allstats.nhidden: res.append(( "(%d of these are hidden)" % allstats.nhidden, noop, 'disabled' )) if allstats.njigs == allstats.n and allstats.njigs: # only jigs are selected -- offer to select their atoms [bruce 050504] # (text searches for this code might like to find "Select this jig's" or "Select these jigs'") want_select_item = True #bruce 051208 if allstats.njigs == 1: jig = nodeset[0] if isinstance(jig, RectGadget): # remove menu item for RectGadget [Huaicai 10/11/05] ## return res -- this 'return' was causing bug 1189 by skipping the rest of the menu, not just this item. # Try to do something less drastic. [bruce 051208] want_select_item = False else: natoms = len(nodeset[0].atoms) myatoms = fix_plurals( "this jig's %d atom(s)" % natoms ) else: myatoms = "these jigs' atoms" if want_select_item: res.append(('Select ' + myatoms, self.cm_select_jigs_atoms )) ## ##e following msg is not true, since nodeset doesn't include selection under selected groups! ## # need to replace it with a better breakdown of what's selected, ## # incl how much under selected groups is selected. Maybe we'll add a list of major types ## # of selected things, as submenus, lower down (with commands like "select only these", "deselect these"). ## ## res.append(( fix_plurals("(%d selected item(s))" % len(nodeset)), noop, 'disabled' )) # for single items that have a featurename, add wiki-help command [bruce 051201] if len(nodeset) == 1: node = nodeset[0] ms = wiki_help_menuspec_for_object(node) # will be [] if this node should have no wiki help menu items #review: will this func ever need to know which widget is asking? if ms: res.append(None) # separator res.extend(ms) return res # from make_cmenuspec_for_set