Exemple #1
0
    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
Exemple #2
0
    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
Exemple #3
0
    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
Exemple #4
0
    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