summaryrefslogtreecommitdiff
path: root/cad/src/widgets/menu_helpers.py
blob: ff58598b72e79a7f3a71d23e3e719752665bd14c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# Copyright 2004-2008 Nanorex, Inc.  See LICENSE file for details.
"""
menu_helpers.py - helpers for creating or modifying Qt menus

@author: Josh, Bruce
@version: $Id$
@copyright: 2004-2008 Nanorex, Inc.  See LICENSE file for details.

History:

Originally by Josh, as part of GLPane.py.

Bruce 050112 moved this code into widgets.py,
later added features including checkmark & Undo support,
split it into more than one function,
and on 080203 moved it into its own file.

At some point Will ported it to Qt4 (while it was in widgets.py).

Module classification: [bruce 080203]

Might have been in utilities except for depending on undo_manager (foundation).
Since its only purpose is to help make or modify menus for use in Qt widgets,
it seems more useful to file it in widgets than in foundation.
(Reusable widgets are in a sense just a certain kind of "ui utility".)
"""

from PyQt4.Qt import QMenu
from PyQt4.Qt import QAction
from PyQt4.Qt import SIGNAL
from PyQt4.Qt import QPixmap
from PyQt4.Qt import QIcon

from utilities import debug_flags

from foundation.undo_manager import wrap_callable_for_undo

# ==

# helper for making popup menus from our own "menu specs" description format,
# consisting of nested lists of text, callables or submenus, options.

def makemenu_helper(widget, menu_spec, menu = None):
    """
    Make and return a reusable or one-time-use (at caller's option)
    popup menu whose structure is specified by menu_spec,
    which is a list of menu item specifiers, each of which is either None
    (for a separator) or a tuple of the form (menu text, callable or submenu,
    option1, option2, ...) with 0 or more options (described below).
       A submenu can be either another menu_spec list, or a QMenu object
    (but in the latter case the menu text is ignored -- maybe it comes
    from that QMenu object somehow -- not sure if this was different in Qt3).
    In either case it is the 2nd menu-item-tuple element, in place of the callable.
       Otherwise the callable must satisfy the python 'callable' predicate,
    and is executed if the menu item is chosen, wrapped inside another function
    which handles Undo checkpointing and Undo-command-name setting.
       The options in a menu item tuple can be zero or more (in any order,
    duplicates allowed) of the following:
    'disabled' -- the menu item should be disabled;
    'checked' -- the menu item will be checked;
    None -- this option is legal but ignored (but the callable must still satisfy
    the python predicate "callable"; constants.noop might be useful for that case).
       The Qt3 version also supported tuple-options consisting of one of the words
    'iconset' and 'whatsThis' followed by an appropriate argument, but those have
    not yet been ported to Qt4 (except possibly for disabled menu items -- UNTESTED).
       Unrecognized options may or may not generate warnings, and are otherwise ignored.
    [###FIX that -- they always ought to print a warning to developers. Note that right
    now they do iff 'disabled' is one of the options and ATOM_DEBUG is set.]
       The 'widget' argument should be the Qt widget
    which is using this function to put up a menu.
       If the menu argument is provided, it should be a QMenu
    to which we'll add items; otherwise we create our own QMenu
    and add items to it.
    """
    from utilities.debug import print_compact_traceback
    import types
    if menu is None:
        menu = QMenu(widget)
        ## menu.show()
        #bruce 070514 removed menu.show() to fix a cosmetic and performance bug
        # (on Mac, possibly on other platforms too; probably unreported)
        # in which the debug menu first appears in screen center, slowly grows
        # to full size while remaining blank, then moves to its final position
        # and looks normal (causing a visual glitch, and a 2-3 second delay
        # in being able to use it). May fix similar issues with other menus.
        # If this causes harm for some menus or platforms, we can adapt it.
    # bruce 040909-16 moved this method from basicMode to GLPane,
    # leaving a delegator for it in basicMode.
    # (bruce was not the original author, but modified it)
    #menu = QMenu( widget)
    for m in menu_spec:
        try: #bruce 050416 added try/except as debug code and for safety
            menutext = m and widget.trUtf8(m[0])
            if m and isinstance(m[1], QMenu): #bruce 041010 added this case
                submenu = m[1]
                #menu.insertItem( menutext, submenu )
                menu.addMenu(submenu)   # how do I get menutext in there?
                    # (similar code might work for QAction case too, not sure)
            elif m and isinstance(m[1], types.ListType): #bruce 041103 added this case
                submenu = QMenu(menutext, menu)
                submenu = makemenu_helper(widget, m[1], submenu) # [this used to call widget.makemenu]
                menu.addMenu(submenu)
            elif m:
                assert callable(m[1]), \
                    "%r[1] needs to be a callable" % (m,) #bruce 041103
                # transform m[1] into a new callable that makes undo checkpoints and provides an undo command-name
                # [bruce 060324 for possible bugs in undo noticing cmenu items, and for the cmdnames]
                func = wrap_callable_for_undo(m[1], cmdname = m[0])
                    # guess about cmdname, but it might be reasonable for A7 as long as we ensure weird characters won't confuse it
                import foundation.changes as changes
                changes.keep_forever(func) # THIS IS BAD (memory leak), but it's not a severe one, so ok for A7 [bruce 060324]
                    # (note: the hard part about removing these when we no longer need them is knowing when to do that
                    #  if the user ends up not selecting anything from the menu. Also, some callers make these
                    #  menus for reuse multiple times, and for them we never want to deallocate func even when some
                    #  menu command gets used. We could solve both of these by making the caller pass a place to keep these
                    #  which it would deallocate someday or which would ensure only one per distinct kind of menu is kept. #e)
                if 'disabled' not in m[2:]:
                    act = QAction(widget)
                    act.setText( menutext)
                    if 'checked' in m[2:]:
                        act.setCheckable(True)
                        act.setChecked(True)
                    menu.addAction(act)
                    widget.connect(act, SIGNAL("activated()"), func)
                else:
                    # disabled case
                    # [why is this case done differently, in this Qt4 port?? -- bruce 070522 question]
                    insert_command_into_menu(menu, menutext, func, options = m[2:], raw_command = True)
            else:
                menu.addSeparator() #bruce 070522 bugfix -- before this, separators were placed lower down or dropped
                    # so as not to come before disabled items, for unknown reasons.
                    # (Speculation: maybe because insertSeparator was used, since addSeparator didn't work or wasn't noticed,
                    #  and since disabled item were added by an older function (also for unknown reasons)?)
                pass
        except Exception, e:
            if isinstance(e, SystemExit):
                raise
            print_compact_traceback("exception in makemenu_helper ignored, for %r:\n" % (m,) )
                #bruce 070522 restored this (was skipped in Qt4 port)
            pass #e could add a fake menu item here as an error message
    return menu # from makemenu_helper

# ==

def insert_command_into_menu(menu, menutext, command, options = (), position = -1, raw_command = False, undo_cmdname = None):
    """
    [This was part of makemenu_helper in the Qt3 version; in Qt4 it's only
     used for the disabled case, presumably for some good reason but not one
     which has been documented. It's also used independently of makemenu_helper.]

    Insert a new item into menu (a QMenu), whose menutext, command, and options are as given,
    with undo_cmdname defaulting to menutext (used only if raw_command is false),
    where options is a list or tuple in the same form as used in "menu_spec" lists
    such as are passed to makemenu_helper (which this function helps implement).
       The caller should have already translated/localized menutext if desired.
       If position is given, insert the new item at that position, otherwise at the end.
    Return the Qt menu item id of the new menu item.
    (I am not sure whether this remains valid if other items are inserted before it. ###k)
       If raw_command is False (default), this function will wrap command with standard logic for nE-1 menu commands
    (presently, wrap_callable_for_undo), and ensure that a python reference to the resulting callable is kept forever.
       If raw_command is True, this function will pass command unchanged into the menu,
    and the caller is responsible for retaining a Python reference to command.
       ###e This might need an argument for the path or function to be used to resolve icon filenames.
    """
    #bruce 060613 split this out of makemenu_helper.
    # Only called for len(options) > 0, though it presumably works
    # just as well for len 0 (try it sometime).
    import types
    from foundation.whatsthis_utilities import turn_featurenames_into_links, ENABLE_WHATSTHIS_LINKS
    if not raw_command:
        command = wrap_callable_for_undo(command, cmdname = undo_cmdname or menutext)
        import foundation.changes as changes
        changes.keep_forever(command)
            # see comments on similar code above about why this is bad in theory, but necessary and ok for now
    iconset = None
    for option in options:
        # some options have to be processed first
        # since they are only usable when the menu item is inserted. [bruce 050614]
        if type(option) is types.TupleType:
            if option[0] == 'iconset':
                # support iconset, pixmap, or pixmap filename [bruce 050614 new feature]
                iconset = option[1]
                if type(iconset) is types.StringType:
                    filename = iconset
                    from utilities.icon_utilities import imagename_to_pixmap
                    iconset = imagename_to_pixmap(filename)
                if isinstance(iconset, QPixmap):
                    # (this is true for imagename_to_pixmap retval)
                    iconset = QIcon(iconset)
    if iconset is not None:
        import foundation.changes as changes
        changes.keep_forever(iconset) #e memory leak; ought to make caller pass a place to keep it, or a unique id of what to keep
        #mitem_id = menu.insertItem( iconset, menutext, -1, position ) #bruce 050614, revised 060613 (added -1, position)
        mitem = menu.addAction( iconset, menutext ) #bruce 050614, revised 060613 (added -1, position)
            # Will this work with checkmark items? Yes, but it replaces the checkmark --
            # instead, the icon looks different for the checked item.
            # For the test case of gamess.png on Mac, the 'checked' item's icon
            # looked like a 3d-depressed button.
            # In the future we can tell the iconset what to display in each case
            # (see QIcon and/or QMenu docs, and helper funcs presently in debug_prefs.py.)
    else:
        # mitem_id = len(menu) -- mitem_id was previously an integer, indexing into the menu
        mitem = menu.addAction(menutext)
    for option in options:
        if option == 'checked':
            mitem.setChecked(True)
        elif option == 'unchecked': #bruce 050614 -- see what this does visually, if anything
            mitem.setChecked(False)
        elif option == 'disabled':
            mitem.setEnabled(False)
        elif type(option) is types.TupleType:
            if option[0] == 'whatsThis':
                text = option[1]
                if ENABLE_WHATSTHIS_LINKS:
                    text = turn_featurenames_into_links(text)
                mitem.setWhatsThis(text)
            elif option[0] == 'iconset':
                pass # this was processed above
            else:
                if debug_flags.atom_debug:
                    print "atom_debug: fyi: don't understand menu_spec option %r", (option,)
            pass
        elif option is None:
            pass # this is explicitly permitted and has no effect
        else:
            if debug_flags.atom_debug:
                print "atom_debug: fyi: don't understand menu_spec option %r", (option,)
        pass
        #e and something to use QMenuData.setShortcut
        #e and something to run whatever func you want on menu and mitem_id
    return mitem # from insert_command_into_menu

# end