1# Based on iwidgets2.2.0/combobox.itk code.
2
3import os
4import string
5import types
6import tkinter
7import Pmw
8import collections
9
10class ComboBox(Pmw.MegaWidget):
11    def __init__(self, parent = None, **kw):
12
13        # Define the megawidget options.
14        INITOPT = Pmw.INITOPT
15        optiondefs = (
16            ('autoclear',          0,          INITOPT),
17            ('buttonaspect',       1.0,        INITOPT),
18            ('dropdown',           1,          INITOPT),
19            ('fliparrow',          0,          INITOPT),
20            ('history',            1,          INITOPT),
21            ('labelmargin',        0,          INITOPT),
22            ('labelpos',           None,       INITOPT),
23            ('listheight',         200,        INITOPT),
24            ('selectioncommand',   None,       None),
25            ('sticky',            'ew',        INITOPT),
26            ('unique',             1,          INITOPT),
27        )
28        self.defineoptions(kw, optiondefs)
29
30        # Initialise the base class (after defining the options).
31        Pmw.MegaWidget.__init__(self, parent)
32
33        # Create the components.
34        interior = self.interior()
35
36        self._entryfield = self.createcomponent('entryfield',
37                (('entry', 'entryfield_entry'),), None,
38                Pmw.EntryField, (interior,))
39        self._entryfield.grid(column=2, row=2, sticky=self['sticky'])
40        interior.grid_columnconfigure(2, weight = 1)
41        self._entryWidget = self._entryfield.component('entry')
42
43        if self['dropdown']:
44            self._isPosted = 0
45            interior.grid_rowconfigure(2, weight = 1)
46
47            # Create the arrow button.
48            self._arrowBtn = self.createcomponent('arrowbutton',
49                    (), None,
50                    tkinter.Canvas, (interior,), borderwidth = 2,
51                    relief = 'raised',
52                    width = 16, height = 16)
53            if 'n' in self['sticky']:
54                sticky = 'n'
55            else:
56                sticky = ''
57            if 's' in self['sticky']:
58                sticky = sticky + 's'
59            self._arrowBtn.grid(column=3, row=2, sticky = sticky)
60            self._arrowRelief = self._arrowBtn.cget('relief')
61
62            # Create the label.
63            self.createlabel(interior, childCols=2)
64
65            # Create the dropdown window.
66            self._popup = self.createcomponent('popup',
67                    (), None,
68                    tkinter.Toplevel, (interior,))
69            self._popup.withdraw()
70            self._popup.overrideredirect(1)
71
72            # Create the scrolled listbox inside the dropdown window.
73            self._list = self.createcomponent('scrolledlist',
74                    (('listbox', 'scrolledlist_listbox'),), None,
75                    Pmw.ScrolledListBox, (self._popup,),
76                    hull_borderwidth = 2,
77                    hull_relief = 'raised',
78                    hull_height = self['listheight'],
79                    usehullsize = 1,
80                    listbox_exportselection = 0)
81            self._list.pack(expand=1, fill='both')
82            self.__listbox = self._list.component('listbox')
83
84            # Bind events to the arrow button.
85            self._arrowBtn.bind('<1>', self._postList)
86            self._arrowBtn.bind('<Configure>', self._drawArrow)
87            self._arrowBtn.bind('<3>', self._next)
88            self._arrowBtn.bind('<Shift-3>', self._previous)
89            self._arrowBtn.bind('<Down>', self._next)
90            self._arrowBtn.bind('<Up>', self._previous)
91            self._arrowBtn.bind('<Control-n>', self._next)
92            self._arrowBtn.bind('<Control-p>', self._previous)
93            self._arrowBtn.bind('<Shift-Down>', self._postList)
94            self._arrowBtn.bind('<Shift-Up>', self._postList)
95            self._arrowBtn.bind('<F34>', self._postList)
96            self._arrowBtn.bind('<F28>', self._postList)
97            self._arrowBtn.bind('<space>', self._postList)
98
99            # Bind events to the dropdown window.
100            self._popup.bind('<Escape>', self._unpostList)
101            self._popup.bind('<space>', self._selectUnpost)
102            self._popup.bind('<Return>', self._selectUnpost)
103            self._popup.bind('<ButtonRelease-1>', self._dropdownBtnRelease)
104            self._popup.bind('<ButtonPress-1>', self._unpostOnNextRelease)
105
106            # Bind events to the Tk listbox.
107            self.__listbox.bind('<Enter>', self._unpostOnNextRelease)
108
109            # Bind events to the Tk entry widget.
110            self._entryWidget.bind('<Configure>', self._resizeArrow)
111            self._entryWidget.bind('<Shift-Down>', self._postList)
112            self._entryWidget.bind('<Shift-Up>', self._postList)
113            self._entryWidget.bind('<F34>', self._postList)
114            self._entryWidget.bind('<F28>', self._postList)
115
116            # Need to unpost the popup if the entryfield is unmapped (eg:
117            # its toplevel window is withdrawn) while the popup list is
118            # displayed.
119            self._entryWidget.bind('<Unmap>', self._unpostList)
120
121        else:
122            # Create the scrolled listbox below the entry field.
123            self._list = self.createcomponent('scrolledlist',
124                    (('listbox', 'scrolledlist_listbox'),), None,
125                    Pmw.ScrolledListBox, (interior,),
126                    selectioncommand = self._selectCmd)
127            self._list.grid(column=2, row=3, sticky='nsew')
128            self.__listbox = self._list.component('listbox')
129
130            # The scrolled listbox should expand vertically.
131            interior.grid_rowconfigure(3, weight = 1)
132
133            # Create the label.
134            self.createlabel(interior, childRows=2)
135
136        self._entryWidget.bind('<Down>', self._next)
137        self._entryWidget.bind('<Up>', self._previous)
138        self._entryWidget.bind('<Control-n>', self._next)
139        self._entryWidget.bind('<Control-p>', self._previous)
140        self.__listbox.bind('<Control-n>', self._next)
141        self.__listbox.bind('<Control-p>', self._previous)
142
143        if self['history']:
144            self._entryfield.configure(command=self._addHistory)
145
146        # Check keywords and initialise options.
147        self.initialiseoptions()
148
149    def destroy(self):
150        if self['dropdown'] and self._isPosted:
151            Pmw.popgrab(self._popup)
152        Pmw.MegaWidget.destroy(self)
153
154    #======================================================================
155
156    # Public methods
157
158    def get(self, first = None, last=None):
159        if first is None:
160            return self._entryWidget.get()
161        else:
162            return self._list.get(first, last)
163
164    def invoke(self):
165        if self['dropdown']:
166            self._postList()
167        else:
168            return self._selectCmd()
169
170    def selectitem(self, index, setentry=1):
171        if type(index) is str:
172            text = index
173            items = self._list.get(0, 'end')
174            if text in items:
175                index = list(items).index(text)
176            else:
177                raise IndexError('index "%s" not found' % text)
178        elif setentry:
179            text = self._list.get(0, 'end')[index]
180
181        self._list.select_clear(0, 'end')
182        self._list.select_set(index, index)
183        self._list.activate(index)
184        self.see(index)
185        if setentry:
186            self._entryfield.setentry(text)
187
188    # Need to explicitly forward this to override the stupid
189    # (grid_)size method inherited from Tkinter.Frame.Grid.
190    def size(self):
191        return self._list.size()
192
193    # Need to explicitly forward this to override the stupid
194    # (grid_)bbox method inherited from Tkinter.Frame.Grid.
195    def bbox(self, index):
196        return self._list.bbox(index)
197
198    def clear(self):
199        self._entryfield.clear()
200        self._list.clear()
201
202    #======================================================================
203
204    # Private methods for both dropdown and simple comboboxes.
205
206    def _addHistory(self):
207        input = self._entryWidget.get()
208
209        if input != '':
210            index = None
211            if self['unique']:
212                # If item is already in list, select it and return.
213                items = self._list.get(0, 'end')
214                if input in items:
215                    index = list(items).index(input)
216
217            if index is None:
218                index = self._list.index('end')
219                self._list.insert('end', input)
220
221            self.selectitem(index)
222            if self['autoclear']:
223                self._entryWidget.delete(0, 'end')
224
225            # Execute the selectioncommand on the new entry.
226            self._selectCmd()
227
228    def _next(self, event):
229        size = self.size()
230        if size <= 1:
231            return
232
233        cursels = self.curselection()
234
235        if len(cursels) == 0:
236            index = 0
237        else:
238            #Python 3 conversion
239            #index = string.atoi(cursels[0])
240            index = int(cursels[0])
241            if index == size - 1:
242                index = 0
243            else:
244                index = index + 1
245
246        self.selectitem(index)
247
248    def _previous(self, event):
249        size = self.size()
250        if size <= 1:
251            return
252
253        cursels = self.curselection()
254
255        if len(cursels) == 0:
256            index = size - 1
257        else:
258            #Python 3 conversion
259            #index = string.atoi(cursels[0])
260            index = int(cursels[0])
261            if index == 0:
262                index = size - 1
263            else:
264                index = index - 1
265
266        self.selectitem(index)
267
268    def _selectCmd(self, event=None):
269
270        sels = self.getcurselection()
271        if len(sels) == 0:
272            item = None
273        else:
274            item = sels[0]
275            self._entryfield.setentry(item)
276
277        cmd = self['selectioncommand']
278        if isinstance(cmd, collections.Callable):
279            if event is None:
280                # Return result of selectioncommand for invoke() method.
281                return cmd(item)
282            else:
283                cmd(item)
284
285    #======================================================================
286
287    # Private methods for dropdown combobox.
288
289    def _drawArrow(self, event=None, sunken=0):
290        arrow = self._arrowBtn
291        if sunken:
292            self._arrowRelief = arrow.cget('relief')
293            arrow.configure(relief = 'sunken')
294        else:
295            arrow.configure(relief = self._arrowRelief)
296
297        if self._isPosted and self['fliparrow']:
298            direction = 'up'
299        else:
300            direction = 'down'
301        Pmw.drawarrow(arrow, self['entry_foreground'], direction, 'arrow')
302
303    def _postList(self, event = None):
304        self._isPosted = 1
305        self._drawArrow(sunken=1)
306
307        # Make sure that the arrow is displayed sunken.
308        self.update_idletasks()
309
310        x = self._entryfield.winfo_rootx()
311        y = self._entryfield.winfo_rooty() + \
312            self._entryfield.winfo_height()
313        w = self._entryfield.winfo_width() + self._arrowBtn.winfo_width()
314        h =  self.__listbox.winfo_height()
315        sh = self.winfo_screenheight()
316
317        if y + h > sh and y > sh / 2:
318            y = self._entryfield.winfo_rooty() - h
319
320        self._list.configure(hull_width=w)
321
322        Pmw.setgeometryanddeiconify(self._popup, '+%d+%d' % (x, y))
323
324        # Grab the popup, so that all events are delivered to it, and
325        # set focus to the listbox, to make keyboard navigation
326        # easier.
327        Pmw.pushgrab(self._popup, 1, self._unpostList)
328        self.__listbox.focus_set()
329
330        self._drawArrow()
331
332        # Ignore the first release of the mouse button after posting the
333        # dropdown list, unless the mouse enters the dropdown list.
334        self._ignoreRelease = 1
335
336    def _dropdownBtnRelease(self, event):
337        if (event.widget == self._list.component('vertscrollbar') or
338                event.widget == self._list.component('horizscrollbar')):
339            return
340
341        if self._ignoreRelease:
342            self._unpostOnNextRelease()
343            return
344
345        self._unpostList()
346
347        if (event.x >= 0 and event.x < self.__listbox.winfo_width() and
348                event.y >= 0 and event.y < self.__listbox.winfo_height()):
349            self._selectCmd()
350
351    def _unpostOnNextRelease(self, event = None):
352        self._ignoreRelease = 0
353
354    def _resizeArrow(self, event):
355        #Python 3 conversion
356        #bw = (string.atoi(self._arrowBtn['borderwidth']) +
357        #        string.atoi(self._arrowBtn['highlightthickness']))
358        bw = (int(self._arrowBtn['borderwidth']) +
359                int(self._arrowBtn['highlightthickness']))
360        newHeight = self._entryfield.winfo_reqheight() - 2 * bw
361        newWidth = int(newHeight * self['buttonaspect'])
362        self._arrowBtn.configure(width=newWidth, height=newHeight)
363        self._drawArrow()
364
365    def _unpostList(self, event=None):
366        if not self._isPosted:
367            # It is possible to get events on an unposted popup.  For
368            # example, by repeatedly pressing the space key to post
369            # and unpost the popup.  The <space> event may be
370            # delivered to the popup window even though
371            # Pmw.popgrab() has set the focus away from the
372            # popup window.  (Bug in Tk?)
373            return
374
375        # Restore the focus before withdrawing the window, since
376        # otherwise the window manager may take the focus away so we
377        # can't redirect it.  Also, return the grab to the next active
378        # window in the stack, if any.
379        Pmw.popgrab(self._popup)
380        self._popup.withdraw()
381
382        self._isPosted = 0
383        self._drawArrow()
384
385    def _selectUnpost(self, event):
386        self._unpostList()
387        self._selectCmd()
388
389Pmw.forwardmethods(ComboBox, Pmw.ScrolledListBox, '_list')
390Pmw.forwardmethods(ComboBox, Pmw.EntryField, '_entryfield')
391