Commit | Line | Data |
---|---|---|
920dae64 AT |
1 | # Based on iwidgets2.2.0/combobox.itk code. |
2 | ||
3 | import os | |
4 | import string | |
5 | import types | |
6 | import Tkinter | |
7 | import Pmw | |
8 | ||
9 | class ComboBox(Pmw.MegaWidget): | |
10 | def __init__(self, parent = None, **kw): | |
11 | ||
12 | # Define the megawidget options. | |
13 | INITOPT = Pmw.INITOPT | |
14 | optiondefs = ( | |
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), | |
26 | ) | |
27 | self.defineoptions(kw, optiondefs) | |
28 | ||
29 | # Initialise the base class (after defining the options). | |
30 | Pmw.MegaWidget.__init__(self, parent) | |
31 | ||
32 | # Create the components. | |
33 | interior = self.interior() | |
34 | ||
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') | |
41 | ||
42 | if self['dropdown']: | |
43 | self._isPosted = 0 | |
44 | interior.grid_rowconfigure(2, weight = 1) | |
45 | ||
46 | # Create the arrow button. | |
47 | self._arrowBtn = self.createcomponent('arrowbutton', | |
48 | (), None, | |
49 | Tkinter.Canvas, (interior,), borderwidth = 2, | |
50 | relief = 'raised', | |
51 | width = 16, height = 16) | |
52 | if 'n' in self['sticky']: | |
53 | sticky = 'n' | |
54 | else: | |
55 | sticky = '' | |
56 | if 's' in self['sticky']: | |
57 | sticky = sticky + 's' | |
58 | self._arrowBtn.grid(column=3, row=2, sticky = sticky) | |
59 | self._arrowRelief = self._arrowBtn.cget('relief') | |
60 | ||
61 | # Create the label. | |
62 | self.createlabel(interior, childCols=2) | |
63 | ||
64 | # Create the dropdown window. | |
65 | self._popup = self.createcomponent('popup', | |
66 | (), None, | |
67 | Tkinter.Toplevel, (interior,)) | |
68 | self._popup.withdraw() | |
69 | self._popup.overrideredirect(1) | |
70 | ||
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,), | |
75 | hull_borderwidth = 2, | |
76 | hull_relief = 'raised', | |
77 | hull_height = self['listheight'], | |
78 | usehullsize = 1, | |
79 | listbox_exportselection = 0) | |
80 | self._list.pack(expand=1, fill='both') | |
81 | self.__listbox = self._list.component('listbox') | |
82 | ||
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) | |
97 | ||
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) | |
104 | ||
105 | # Bind events to the Tk listbox. | |
106 | self.__listbox.bind('<Enter>', self._unpostOnNextRelease) | |
107 | ||
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) | |
114 | ||
115 | # Need to unpost the popup if the entryfield is unmapped (eg: | |
116 | # its toplevel window is withdrawn) while the popup list is | |
117 | # displayed. | |
118 | self._entryWidget.bind('<Unmap>', self._unpostList) | |
119 | ||
120 | else: | |
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') | |
128 | ||
129 | # The scrolled listbox should expand vertically. | |
130 | interior.grid_rowconfigure(3, weight = 1) | |
131 | ||
132 | # Create the label. | |
133 | self.createlabel(interior, childRows=2) | |
134 | ||
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) | |
141 | ||
142 | if self['history']: | |
143 | self._entryfield.configure(command=self._addHistory) | |
144 | ||
145 | # Check keywords and initialise options. | |
146 | self.initialiseoptions() | |
147 | ||
148 | def destroy(self): | |
149 | if self['dropdown'] and self._isPosted: | |
150 | Pmw.popgrab(self._popup) | |
151 | Pmw.MegaWidget.destroy(self) | |
152 | ||
153 | #====================================================================== | |
154 | ||
155 | # Public methods | |
156 | ||
157 | def get(self, first = None, last=None): | |
158 | if first is None: | |
159 | return self._entryWidget.get() | |
160 | else: | |
161 | return self._list.get(first, last) | |
162 | ||
163 | def invoke(self): | |
164 | if self['dropdown']: | |
165 | self._postList() | |
166 | else: | |
167 | return self._selectCmd() | |
168 | ||
169 | def selectitem(self, index, setentry=1): | |
170 | if type(index) == types.StringType: | |
171 | text = index | |
172 | items = self._list.get(0, 'end') | |
173 | if text in items: | |
174 | index = list(items).index(text) | |
175 | else: | |
176 | raise IndexError, 'index "%s" not found' % text | |
177 | elif setentry: | |
178 | text = self._list.get(0, 'end')[index] | |
179 | ||
180 | self._list.select_clear(0, 'end') | |
181 | self._list.select_set(index, index) | |
182 | self._list.activate(index) | |
183 | self.see(index) | |
184 | if setentry: | |
185 | self._entryfield.setentry(text) | |
186 | ||
187 | # Need to explicitly forward this to override the stupid | |
188 | # (grid_)size method inherited from Tkinter.Frame.Grid. | |
189 | def size(self): | |
190 | return self._list.size() | |
191 | ||
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) | |
196 | ||
197 | def clear(self): | |
198 | self._entryfield.clear() | |
199 | self._list.clear() | |
200 | ||
201 | #====================================================================== | |
202 | ||
203 | # Private methods for both dropdown and simple comboboxes. | |
204 | ||
205 | def _addHistory(self): | |
206 | input = self._entryWidget.get() | |
207 | ||
208 | if input != '': | |
209 | index = None | |
210 | if self['unique']: | |
211 | # If item is already in list, select it and return. | |
212 | items = self._list.get(0, 'end') | |
213 | if input in items: | |
214 | index = list(items).index(input) | |
215 | ||
216 | if index is None: | |
217 | index = self._list.index('end') | |
218 | self._list.insert('end', input) | |
219 | ||
220 | self.selectitem(index) | |
221 | if self['autoclear']: | |
222 | self._entryWidget.delete(0, 'end') | |
223 | ||
224 | # Execute the selectioncommand on the new entry. | |
225 | self._selectCmd() | |
226 | ||
227 | def _next(self, event): | |
228 | size = self.size() | |
229 | if size <= 1: | |
230 | return | |
231 | ||
232 | cursels = self.curselection() | |
233 | ||
234 | if len(cursels) == 0: | |
235 | index = 0 | |
236 | else: | |
237 | index = string.atoi(cursels[0]) | |
238 | if index == size - 1: | |
239 | index = 0 | |
240 | else: | |
241 | index = index + 1 | |
242 | ||
243 | self.selectitem(index) | |
244 | ||
245 | def _previous(self, event): | |
246 | size = self.size() | |
247 | if size <= 1: | |
248 | return | |
249 | ||
250 | cursels = self.curselection() | |
251 | ||
252 | if len(cursels) == 0: | |
253 | index = size - 1 | |
254 | else: | |
255 | index = string.atoi(cursels[0]) | |
256 | if index == 0: | |
257 | index = size - 1 | |
258 | else: | |
259 | index = index - 1 | |
260 | ||
261 | self.selectitem(index) | |
262 | ||
263 | def _selectCmd(self, event=None): | |
264 | ||
265 | sels = self.getcurselection() | |
266 | if len(sels) == 0: | |
267 | item = None | |
268 | else: | |
269 | item = sels[0] | |
270 | self._entryfield.setentry(item) | |
271 | ||
272 | cmd = self['selectioncommand'] | |
273 | if callable(cmd): | |
274 | if event is None: | |
275 | # Return result of selectioncommand for invoke() method. | |
276 | return cmd(item) | |
277 | else: | |
278 | cmd(item) | |
279 | ||
280 | #====================================================================== | |
281 | ||
282 | # Private methods for dropdown combobox. | |
283 | ||
284 | def _drawArrow(self, event=None, sunken=0): | |
285 | arrow = self._arrowBtn | |
286 | if sunken: | |
287 | self._arrowRelief = arrow.cget('relief') | |
288 | arrow.configure(relief = 'sunken') | |
289 | else: | |
290 | arrow.configure(relief = self._arrowRelief) | |
291 | ||
292 | if self._isPosted and self['fliparrow']: | |
293 | direction = 'up' | |
294 | else: | |
295 | direction = 'down' | |
296 | Pmw.drawarrow(arrow, self['entry_foreground'], direction, 'arrow') | |
297 | ||
298 | def _postList(self, event = None): | |
299 | self._isPosted = 1 | |
300 | self._drawArrow(sunken=1) | |
301 | ||
302 | # Make sure that the arrow is displayed sunken. | |
303 | self.update_idletasks() | |
304 | ||
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() | |
311 | ||
312 | if y + h > sh and y > sh / 2: | |
313 | y = self._entryfield.winfo_rooty() - h | |
314 | ||
315 | self._list.configure(hull_width=w) | |
316 | ||
317 | Pmw.setgeometryanddeiconify(self._popup, '+%d+%d' % (x, y)) | |
318 | ||
319 | # Grab the popup, so that all events are delivered to it, and | |
320 | # set focus to the listbox, to make keyboard navigation | |
321 | # easier. | |
322 | Pmw.pushgrab(self._popup, 1, self._unpostList) | |
323 | self.__listbox.focus_set() | |
324 | ||
325 | self._drawArrow() | |
326 | ||
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 | |
330 | ||
331 | def _dropdownBtnRelease(self, event): | |
332 | if (event.widget == self._list.component('vertscrollbar') or | |
333 | event.widget == self._list.component('horizscrollbar')): | |
334 | return | |
335 | ||
336 | if self._ignoreRelease: | |
337 | self._unpostOnNextRelease() | |
338 | return | |
339 | ||
340 | self._unpostList() | |
341 | ||
342 | if (event.x >= 0 and event.x < self.__listbox.winfo_width() and | |
343 | event.y >= 0 and event.y < self.__listbox.winfo_height()): | |
344 | self._selectCmd() | |
345 | ||
346 | def _unpostOnNextRelease(self, event = None): | |
347 | self._ignoreRelease = 0 | |
348 | ||
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) | |
355 | self._drawArrow() | |
356 | ||
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?) | |
365 | return | |
366 | ||
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() | |
373 | ||
374 | self._isPosted = 0 | |
375 | self._drawArrow() | |
376 | ||
377 | def _selectUnpost(self, event): | |
378 | self._unpostList() | |
379 | self._selectCmd() | |
380 | ||
381 | Pmw.forwardmethods(ComboBox, Pmw.ScrolledListBox, '_list') | |
382 | Pmw.forwardmethods(ComboBox, Pmw.EntryField, '_entryfield') |