1 # Based on iwidgets2.2.0/combobox.itk code.
9 class ComboBox(Pmw.MegaWidget):
10 def __init__(self, parent = None, **kw):
12 # Define the megawidget options.
15 ('autoclear', 0, INITOPT),
16 ('buttonaspect', 1.0, INITOPT),
17 ('dropdown', 1, INITOPT),
18 ('fliparrow', 0, INITOPT),
19 ('history', 1, INITOPT),
20 ('labelmargin', 0, INITOPT),
21 ('labelpos', None, INITOPT),
22 ('listheight', 200, INITOPT),
23 ('selectioncommand', None, None),
24 ('sticky', 'ew', INITOPT),
25 ('unique', 1, INITOPT),
27 self.defineoptions(kw, optiondefs)
29 # Initialise the base class (after defining the options).
30 Pmw.MegaWidget.__init__(self, parent)
32 # Create the components.
33 interior = self.interior()
35 self._entryfield = self.createcomponent('entryfield',
36 (('entry', 'entryfield_entry'),), None,
37 Pmw.EntryField, (interior,))
38 self._entryfield.grid(column=2, row=2, sticky=self['sticky'])
39 interior.grid_columnconfigure(2, weight = 1)
40 self._entryWidget = self._entryfield.component('entry')
44 interior.grid_rowconfigure(2, weight = 1)
46 # Create the arrow button.
47 self._arrowBtn = self.createcomponent('arrowbutton',
49 Tkinter.Canvas, (interior,), borderwidth = 2,
51 width = 16, height = 16)
52 if 'n' in self['sticky']:
56 if 's' in self['sticky']:
58 self._arrowBtn.grid(column=3, row=2, sticky = sticky)
59 self._arrowRelief = self._arrowBtn.cget('relief')
62 self.createlabel(interior, childCols=2)
64 # Create the dropdown window.
65 self._popup = self.createcomponent('popup',
67 Tkinter.Toplevel, (interior,))
68 self._popup.withdraw()
69 self._popup.overrideredirect(1)
71 # Create the scrolled listbox inside the dropdown window.
72 self._list = self.createcomponent('scrolledlist',
73 (('listbox', 'scrolledlist_listbox'),), None,
74 Pmw.ScrolledListBox, (self._popup,),
76 hull_relief = 'raised',
77 hull_height = self['listheight'],
79 listbox_exportselection = 0)
80 self._list.pack(expand=1, fill='both')
81 self.__listbox = self._list.component('listbox')
83 # Bind events to the arrow button.
84 self._arrowBtn.bind('<1>', self._postList)
85 self._arrowBtn.bind('<Configure>', self._drawArrow)
86 self._arrowBtn.bind('<3>', self._next)
87 self._arrowBtn.bind('<Shift-3>', self._previous)
88 self._arrowBtn.bind('<Down>', self._next)
89 self._arrowBtn.bind('<Up>', self._previous)
90 self._arrowBtn.bind('<Control-n>', self._next)
91 self._arrowBtn.bind('<Control-p>', self._previous)
92 self._arrowBtn.bind('<Shift-Down>', self._postList)
93 self._arrowBtn.bind('<Shift-Up>', self._postList)
94 self._arrowBtn.bind('<F34>', self._postList)
95 self._arrowBtn.bind('<F28>', self._postList)
96 self._arrowBtn.bind('<space>', self._postList)
98 # Bind events to the dropdown window.
99 self._popup.bind('<Escape>', self._unpostList)
100 self._popup.bind('<space>', self._selectUnpost)
101 self._popup.bind('<Return>', self._selectUnpost)
102 self._popup.bind('<ButtonRelease-1>', self._dropdownBtnRelease)
103 self._popup.bind('<ButtonPress-1>', self._unpostOnNextRelease)
105 # Bind events to the Tk listbox.
106 self.__listbox.bind('<Enter>', self._unpostOnNextRelease)
108 # Bind events to the Tk entry widget.
109 self._entryWidget.bind('<Configure>', self._resizeArrow)
110 self._entryWidget.bind('<Shift-Down>', self._postList)
111 self._entryWidget.bind('<Shift-Up>', self._postList)
112 self._entryWidget.bind('<F34>', self._postList)
113 self._entryWidget.bind('<F28>', self._postList)
115 # Need to unpost the popup if the entryfield is unmapped (eg:
116 # its toplevel window is withdrawn) while the popup list is
118 self._entryWidget.bind('<Unmap>', self._unpostList)
121 # Create the scrolled listbox below the entry field.
122 self._list = self.createcomponent('scrolledlist',
123 (('listbox', 'scrolledlist_listbox'),), None,
124 Pmw.ScrolledListBox, (interior,),
125 selectioncommand = self._selectCmd)
126 self._list.grid(column=2, row=3, sticky='nsew')
127 self.__listbox = self._list.component('listbox')
129 # The scrolled listbox should expand vertically.
130 interior.grid_rowconfigure(3, weight = 1)
133 self.createlabel(interior, childRows=2)
135 self._entryWidget.bind('<Down>', self._next)
136 self._entryWidget.bind('<Up>', self._previous)
137 self._entryWidget.bind('<Control-n>', self._next)
138 self._entryWidget.bind('<Control-p>', self._previous)
139 self.__listbox.bind('<Control-n>', self._next)
140 self.__listbox.bind('<Control-p>', self._previous)
143 self._entryfield.configure(command=self._addHistory)
145 # Check keywords and initialise options.
146 self.initialiseoptions()
149 if self['dropdown'] and self._isPosted:
150 Pmw.popgrab(self._popup)
151 Pmw.MegaWidget.destroy(self)
153 #======================================================================
157 def get(self, first = None, last=None):
159 return self._entryWidget.get()
161 return self._list.get(first, last)
167 return self._selectCmd()
169 def selectitem(self, index, setentry=1):
170 if type(index) == types.StringType:
172 items = self._list.get(0, 'end')
174 index = list(items).index(text)
176 raise IndexError, 'index "%s" not found' % text
178 text = self._list.get(0, 'end')[index]
180 self._list.select_clear(0, 'end')
181 self._list.select_set(index, index)
182 self._list.activate(index)
185 self._entryfield.setentry(text)
187 # Need to explicitly forward this to override the stupid
188 # (grid_)size method inherited from Tkinter.Frame.Grid.
190 return self._list.size()
192 # Need to explicitly forward this to override the stupid
193 # (grid_)bbox method inherited from Tkinter.Frame.Grid.
194 def bbox(self, index):
195 return self._list.bbox(index)
198 self._entryfield.clear()
201 #======================================================================
203 # Private methods for both dropdown and simple comboboxes.
205 def _addHistory(self):
206 input = self._entryWidget.get()
211 # If item is already in list, select it and return.
212 items = self._list.get(0, 'end')
214 index = list(items).index(input)
217 index = self._list.index('end')
218 self._list.insert('end', input)
220 self.selectitem(index)
221 if self['autoclear']:
222 self._entryWidget.delete(0, 'end')
224 # Execute the selectioncommand on the new entry.
227 def _next(self, event):
232 cursels = self.curselection()
234 if len(cursels) == 0:
237 index = string.atoi(cursels[0])
238 if index == size - 1:
243 self.selectitem(index)
245 def _previous(self, event):
250 cursels = self.curselection()
252 if len(cursels) == 0:
255 index = string.atoi(cursels[0])
261 self.selectitem(index)
263 def _selectCmd(self, event=None):
265 sels = self.getcurselection()
270 self._entryfield.setentry(item)
272 cmd = self['selectioncommand']
275 # Return result of selectioncommand for invoke() method.
280 #======================================================================
282 # Private methods for dropdown combobox.
284 def _drawArrow(self, event=None, sunken=0):
285 arrow = self._arrowBtn
287 self._arrowRelief = arrow.cget('relief')
288 arrow.configure(relief = 'sunken')
290 arrow.configure(relief = self._arrowRelief)
292 if self._isPosted and self['fliparrow']:
296 Pmw.drawarrow(arrow, self['entry_foreground'], direction, 'arrow')
298 def _postList(self, event = None):
300 self._drawArrow(sunken=1)
302 # Make sure that the arrow is displayed sunken.
303 self.update_idletasks()
305 x = self._entryfield.winfo_rootx()
306 y = self._entryfield.winfo_rooty() + \
307 self._entryfield.winfo_height()
308 w = self._entryfield.winfo_width() + self._arrowBtn.winfo_width()
309 h = self.__listbox.winfo_height()
310 sh = self.winfo_screenheight()
312 if y + h > sh and y > sh / 2:
313 y = self._entryfield.winfo_rooty() - h
315 self._list.configure(hull_width=w)
317 Pmw.setgeometryanddeiconify(self._popup, '+%d+%d' % (x, y))
319 # Grab the popup, so that all events are delivered to it, and
320 # set focus to the listbox, to make keyboard navigation
322 Pmw.pushgrab(self._popup, 1, self._unpostList)
323 self.__listbox.focus_set()
327 # Ignore the first release of the mouse button after posting the
328 # dropdown list, unless the mouse enters the dropdown list.
329 self._ignoreRelease = 1
331 def _dropdownBtnRelease(self, event):
332 if (event.widget == self._list.component('vertscrollbar') or
333 event.widget == self._list.component('horizscrollbar')):
336 if self._ignoreRelease:
337 self._unpostOnNextRelease()
342 if (event.x >= 0 and event.x < self.__listbox.winfo_width() and
343 event.y >= 0 and event.y < self.__listbox.winfo_height()):
346 def _unpostOnNextRelease(self, event = None):
347 self._ignoreRelease = 0
349 def _resizeArrow(self, event):
350 bw = (string.atoi(self._arrowBtn['borderwidth']) +
351 string.atoi(self._arrowBtn['highlightthickness']))
352 newHeight = self._entryfield.winfo_reqheight() - 2 * bw
353 newWidth = int(newHeight * self['buttonaspect'])
354 self._arrowBtn.configure(width=newWidth, height=newHeight)
357 def _unpostList(self, event=None):
358 if not self._isPosted:
359 # It is possible to get events on an unposted popup. For
360 # example, by repeatedly pressing the space key to post
361 # and unpost the popup. The <space> event may be
362 # delivered to the popup window even though
363 # Pmw.popgrab() has set the focus away from the
364 # popup window. (Bug in Tk?)
367 # Restore the focus before withdrawing the window, since
368 # otherwise the window manager may take the focus away so we
369 # can't redirect it. Also, return the grab to the next active
370 # window in the stack, if any.
371 Pmw.popgrab(self._popup)
372 self._popup.withdraw()
377 def _selectUnpost(self, event):
381 Pmw.forwardmethods(ComboBox, Pmw.ScrolledListBox, '_list')
382 Pmw.forwardmethods(ComboBox, Pmw.EntryField, '_entryfield')