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