1# Manager widget for menus.
2
3import string
4import types
5import tkinter
6import Pmw
7
8class MenuBar(Pmw.MegaWidget):
9
10    def __init__(self, parent = None, **kw):
11
12        # Define the megawidget options.
13        INITOPT = Pmw.INITOPT
14        optiondefs = (
15            ('balloon',      None,       None),
16            ('hotkeys',      1,          INITOPT),
17            ('padx',         0,          INITOPT),
18        )
19        self.defineoptions(kw, optiondefs, dynamicGroups = ('Menu', 'Button'))
20
21        # Initialise the base class (after defining the options).
22        Pmw.MegaWidget.__init__(self, parent)
23
24        self._menuInfo = {}
25        # Map from a menu name to a tuple of information about the menu.
26        # The first item in the tuple is the name of the parent menu (for
27        # toplevel menus this is None). The second item in the tuple is
28        # a list of status help messages for each item in the menu.
29        # The third item in the tuple is the id of the binding used
30        # to detect mouse motion to display status help.
31        # Information for the toplevel menubuttons is not stored here.
32
33        self._mydeletecommand = self.component('hull').tk.deletecommand
34        # Cache this method for use later.
35
36        # Check keywords and initialise options.
37        self.initialiseoptions()
38
39    def deletemenuitems(self, menuName, start, end = None):
40        self.component(menuName + '-menu').delete(start, end)
41        if end is None:
42            del self._menuInfo[menuName][1][start]
43        else:
44            self._menuInfo[menuName][1][start:end+1] = []
45
46    def deletemenu(self, menuName):
47        """Delete should be called for cascaded menus before main menus.
48        """
49
50        # Clean up binding for this menu.
51        parentName = self._menuInfo[menuName][0]
52        bindId = self._menuInfo[menuName][2]
53        _bindtag = 'PmwMenuBar' + str(self) + menuName
54        self.unbind_class(_bindtag, '<Motion>')
55        self._mydeletecommand(bindId) # unbind_class does not clean up
56        del self._menuInfo[menuName]
57
58        if parentName is None:
59            self.destroycomponent(menuName + '-button')
60        else:
61            parentMenu = self.component(parentName + '-menu')
62
63            menu = self.component(menuName + '-menu')
64            menuId = str(menu)
65            for item in range(parentMenu.index('end') + 1):
66                if parentMenu.type(item) == 'cascade':
67                    itemMenu = str(parentMenu.entrycget(item, 'menu'))
68                    if itemMenu == menuId:
69                        parentMenu.delete(item)
70                        del self._menuInfo[parentName][1][item]
71                        break
72
73        self.destroycomponent(menuName + '-menu')
74
75    def disableall(self):
76        for menuName in list(self._menuInfo.keys()):
77            if self._menuInfo[menuName][0] is None:
78                menubutton = self.component(menuName + '-button')
79                menubutton.configure(state = 'disabled')
80
81    def enableall(self):
82        for menuName in list(self._menuInfo.keys()):
83            if self._menuInfo[menuName][0] is None:
84                menubutton = self.component(menuName + '-button')
85                menubutton.configure(state = 'normal')
86
87    def addmenu(self, menuName, balloonHelp, statusHelp = None,
88            side = 'left', traverseSpec = None, **kw):
89
90        self._addmenu(None, menuName, balloonHelp, statusHelp,
91            traverseSpec, side, 'text', kw)
92
93    def addcascademenu(self, parentMenuName, menuName, statusHelp = '',
94            traverseSpec = None, **kw):
95
96        self._addmenu(parentMenuName, menuName, None, statusHelp,
97            traverseSpec, None, 'label', kw)
98
99    def _addmenu(self, parentMenuName, menuName, balloonHelp, statusHelp,
100            traverseSpec, side, textKey, kw):
101
102        if (menuName + '-menu') in self.components():
103            raise ValueError('menu "%s" already exists' % menuName)
104
105        menukw = {}
106        if 'tearoff' in kw:
107            menukw['tearoff'] = kw['tearoff']
108            del kw['tearoff']
109        else:
110            menukw['tearoff'] = 0
111
112        if textKey not in kw:
113            kw[textKey] = menuName
114
115        self._addHotkeyToOptions(parentMenuName, kw, textKey, traverseSpec)
116
117        if parentMenuName is None:
118            button = self.createcomponent(*(menuName + '-button',
119                    (), 'Button',
120                    tkinter.Menubutton, (self.interior(),)), **kw)
121            button.pack(side=side, padx = self['padx'])
122            balloon = self['balloon']
123            if balloon is not None:
124                balloon.bind(button, balloonHelp, statusHelp)
125            parentMenu = button
126        else:
127            parentMenu = self.component(parentMenuName + '-menu')
128            parentMenu.add_cascade(*(), **kw)
129            self._menuInfo[parentMenuName][1].append(statusHelp)
130
131        menu = self.createcomponent(*(menuName + '-menu',
132                (), 'Menu',
133                tkinter.Menu, (parentMenu,)), **menukw)
134        if parentMenuName is None:
135            button.configure(menu = menu)
136        else:
137            parentMenu.entryconfigure('end', menu = menu)
138
139        # Need to put this binding after the class bindings so that
140        # menu.index() does not lag behind.
141        _bindtag = 'PmwMenuBar' + str(self) + menuName
142        bindId = self.bind_class(_bindtag, '<Motion>',
143            lambda event=None, self=self, menuName=menuName:
144                    self._menuHelp(menuName))
145        menu.bindtags(menu.bindtags() + (_bindtag,))
146        menu.bind('<Leave>', self._resetHelpmessage)
147
148        self._menuInfo[menuName] = (parentMenuName, [], bindId)
149
150    def addmenuitem(self, menuName, itemType, statusHelp = '',
151            traverseSpec = None, **kw):
152
153        menu = self.component(menuName + '-menu')
154        if itemType != 'separator':
155            self._addHotkeyToOptions(menuName, kw, 'label', traverseSpec)
156
157        if itemType == 'command':
158            command = menu.add_command
159        elif itemType == 'separator':
160            command = menu.add_separator
161        elif itemType == 'checkbutton':
162            command = menu.add_checkbutton
163        elif itemType == 'radiobutton':
164            command = menu.add_radiobutton
165        elif itemType == 'cascade':
166            command = menu.add_cascade
167        else:
168            raise ValueError('unknown menuitem type "%s"' % itemType)
169
170        self._menuInfo[menuName][1].append(statusHelp)
171        command(*(), **kw)
172
173    def _addHotkeyToOptions(self, menuName, kw, textKey, traverseSpec):
174
175        if (not self['hotkeys'] or 'underline' in kw or
176                textKey not in kw):
177            return
178
179        if type(traverseSpec) == int:
180            kw['underline'] = traverseSpec
181            return
182
183        hotkeyList = []
184        if menuName is None:
185            for menuName in list(self._menuInfo.keys()):
186                if self._menuInfo[menuName][0] is None:
187                    menubutton = self.component(menuName + '-button')
188                    #Python 3 conversion
189                    #underline = string.atoi(str(menubutton.cget('underline')))
190                    underline = int(str(menubutton.cget('underline')))
191                    if underline != -1:
192                        label = str(menubutton.cget(textKey))
193                        if underline < len(label):
194                            hotkey = label[underline].lower()
195                            if hotkey not in hotkeyList:
196                                hotkeyList.append(hotkey)
197        else:
198            menu = self.component(menuName + '-menu')
199            end = menu.index('end')
200            if end is not None:
201                for item in range(end + 1):
202                    if menu.type(item) not in ('separator', 'tearoff'):
203                        #Python 3 conversion
204                        #underline = string.atoi(
205                        #    str(menu.entrycget(item, 'underline')))
206                        underline = int(
207                            str(menu.entrycget(item, 'underline')))
208                        if underline != -1:
209                            label = str(menu.entrycget(item, textKey))
210                            if underline < len(label):
211                                hotkey = label[underline].lower()
212                                if hotkey not in hotkeyList:
213                                    hotkeyList.append(hotkey)
214
215        name = kw[textKey]
216
217        if type(traverseSpec) is str:
218            lowerLetter = traverseSpec.lower()
219            if traverseSpec in name and lowerLetter not in hotkeyList:
220                kw['underline'] = name.index(traverseSpec)
221        else:
222            targets = string.digits + string.ascii_letters
223            lowerName = name.lower()
224            for letter_index in range(len(name)):
225                letter = lowerName[letter_index]
226                if letter in targets and letter not in hotkeyList:
227                    kw['underline'] = letter_index
228                    break
229
230    def _menuHelp(self, menuName):
231        menu = self.component(menuName + '-menu')
232        index = menu.index('active')
233
234        balloon = self['balloon']
235        if balloon is not None:
236            if index is None:
237                balloon.showstatus('')
238            else:
239                if str(menu.cget('tearoff')) == '1':
240                    index = index - 1
241                if index >= 0:
242                    help = self._menuInfo[menuName][1][index]
243                    balloon.showstatus(help)
244
245    def _resetHelpmessage(self, event=None):
246        balloon = self['balloon']
247        if balloon is not None:
248            balloon.clearstatus()
249