# Based on iwidgets2.2.0/combobox.itk code. import os import string import types import Tkinter import Pmw class ComboBox(Pmw.MegaWidget): def __init__(self, parent = None, **kw): # Define the megawidget options. INITOPT = Pmw.INITOPT optiondefs = ( ('autoclear', 0, INITOPT), ('buttonaspect', 1.0, INITOPT), ('dropdown', 1, INITOPT), ('fliparrow', 0, INITOPT), ('history', 1, INITOPT), ('labelmargin', 0, INITOPT), ('labelpos', None, INITOPT), ('listheight', 200, INITOPT), ('selectioncommand', None, None), ('sticky', 'ew', INITOPT), ('unique', 1, INITOPT), ) self.defineoptions(kw, optiondefs) # Initialise the base class (after defining the options). Pmw.MegaWidget.__init__(self, parent) # Create the components. interior = self.interior() self._entryfield = self.createcomponent('entryfield', (('entry', 'entryfield_entry'),), None, Pmw.EntryField, (interior,)) self._entryfield.grid(column=2, row=2, sticky=self['sticky']) interior.grid_columnconfigure(2, weight = 1) self._entryWidget = self._entryfield.component('entry') if self['dropdown']: self._isPosted = 0 interior.grid_rowconfigure(2, weight = 1) # Create the arrow button. self._arrowBtn = self.createcomponent('arrowbutton', (), None, Tkinter.Canvas, (interior,), borderwidth = 2, relief = 'raised', width = 16, height = 16) if 'n' in self['sticky']: sticky = 'n' else: sticky = '' if 's' in self['sticky']: sticky = sticky + 's' self._arrowBtn.grid(column=3, row=2, sticky = sticky) self._arrowRelief = self._arrowBtn.cget('relief') # Create the label. self.createlabel(interior, childCols=2) # Create the dropdown window. self._popup = self.createcomponent('popup', (), None, Tkinter.Toplevel, (interior,)) self._popup.withdraw() self._popup.overrideredirect(1) # Create the scrolled listbox inside the dropdown window. self._list = self.createcomponent('scrolledlist', (('listbox', 'scrolledlist_listbox'),), None, Pmw.ScrolledListBox, (self._popup,), hull_borderwidth = 2, hull_relief = 'raised', hull_height = self['listheight'], usehullsize = 1, listbox_exportselection = 0) self._list.pack(expand=1, fill='both') self.__listbox = self._list.component('listbox') # Bind events to the arrow button. self._arrowBtn.bind('<1>', self._postList) self._arrowBtn.bind('', self._drawArrow) self._arrowBtn.bind('<3>', self._next) self._arrowBtn.bind('', self._previous) self._arrowBtn.bind('', self._next) self._arrowBtn.bind('', self._previous) self._arrowBtn.bind('', self._next) self._arrowBtn.bind('', self._previous) self._arrowBtn.bind('', self._postList) self._arrowBtn.bind('', self._postList) self._arrowBtn.bind('', self._postList) self._arrowBtn.bind('', self._postList) self._arrowBtn.bind('', self._postList) # Bind events to the dropdown window. self._popup.bind('', self._unpostList) self._popup.bind('', self._selectUnpost) self._popup.bind('', self._selectUnpost) self._popup.bind('', self._dropdownBtnRelease) self._popup.bind('', self._unpostOnNextRelease) # Bind events to the Tk listbox. self.__listbox.bind('', self._unpostOnNextRelease) # Bind events to the Tk entry widget. self._entryWidget.bind('', self._resizeArrow) self._entryWidget.bind('', self._postList) self._entryWidget.bind('', self._postList) self._entryWidget.bind('', self._postList) self._entryWidget.bind('', self._postList) # Need to unpost the popup if the entryfield is unmapped (eg: # its toplevel window is withdrawn) while the popup list is # displayed. self._entryWidget.bind('', self._unpostList) else: # Create the scrolled listbox below the entry field. self._list = self.createcomponent('scrolledlist', (('listbox', 'scrolledlist_listbox'),), None, Pmw.ScrolledListBox, (interior,), selectioncommand = self._selectCmd) self._list.grid(column=2, row=3, sticky='nsew') self.__listbox = self._list.component('listbox') # The scrolled listbox should expand vertically. interior.grid_rowconfigure(3, weight = 1) # Create the label. self.createlabel(interior, childRows=2) self._entryWidget.bind('', self._next) self._entryWidget.bind('', self._previous) self._entryWidget.bind('', self._next) self._entryWidget.bind('', self._previous) self.__listbox.bind('', self._next) self.__listbox.bind('', self._previous) if self['history']: self._entryfield.configure(command=self._addHistory) # Check keywords and initialise options. self.initialiseoptions() def destroy(self): if self['dropdown'] and self._isPosted: Pmw.popgrab(self._popup) Pmw.MegaWidget.destroy(self) #====================================================================== # Public methods def get(self, first = None, last=None): if first is None: return self._entryWidget.get() else: return self._list.get(first, last) def invoke(self): if self['dropdown']: self._postList() else: return self._selectCmd() def selectitem(self, index, setentry=1): if type(index) == types.StringType: text = index items = self._list.get(0, 'end') if text in items: index = list(items).index(text) else: raise IndexError, 'index "%s" not found' % text elif setentry: text = self._list.get(0, 'end')[index] self._list.select_clear(0, 'end') self._list.select_set(index, index) self._list.activate(index) self.see(index) if setentry: self._entryfield.setentry(text) # Need to explicitly forward this to override the stupid # (grid_)size method inherited from Tkinter.Frame.Grid. def size(self): return self._list.size() # Need to explicitly forward this to override the stupid # (grid_)bbox method inherited from Tkinter.Frame.Grid. def bbox(self, index): return self._list.bbox(index) def clear(self): self._entryfield.clear() self._list.clear() #====================================================================== # Private methods for both dropdown and simple comboboxes. def _addHistory(self): input = self._entryWidget.get() if input != '': index = None if self['unique']: # If item is already in list, select it and return. items = self._list.get(0, 'end') if input in items: index = list(items).index(input) if index is None: index = self._list.index('end') self._list.insert('end', input) self.selectitem(index) if self['autoclear']: self._entryWidget.delete(0, 'end') # Execute the selectioncommand on the new entry. self._selectCmd() def _next(self, event): size = self.size() if size <= 1: return cursels = self.curselection() if len(cursels) == 0: index = 0 else: index = string.atoi(cursels[0]) if index == size - 1: index = 0 else: index = index + 1 self.selectitem(index) def _previous(self, event): size = self.size() if size <= 1: return cursels = self.curselection() if len(cursels) == 0: index = size - 1 else: index = string.atoi(cursels[0]) if index == 0: index = size - 1 else: index = index - 1 self.selectitem(index) def _selectCmd(self, event=None): sels = self.getcurselection() if len(sels) == 0: item = None else: item = sels[0] self._entryfield.setentry(item) cmd = self['selectioncommand'] if callable(cmd): if event is None: # Return result of selectioncommand for invoke() method. return cmd(item) else: cmd(item) #====================================================================== # Private methods for dropdown combobox. def _drawArrow(self, event=None, sunken=0): arrow = self._arrowBtn if sunken: self._arrowRelief = arrow.cget('relief') arrow.configure(relief = 'sunken') else: arrow.configure(relief = self._arrowRelief) if self._isPosted and self['fliparrow']: direction = 'up' else: direction = 'down' Pmw.drawarrow(arrow, self['entry_foreground'], direction, 'arrow') def _postList(self, event = None): self._isPosted = 1 self._drawArrow(sunken=1) # Make sure that the arrow is displayed sunken. self.update_idletasks() x = self._entryfield.winfo_rootx() y = self._entryfield.winfo_rooty() + \ self._entryfield.winfo_height() w = self._entryfield.winfo_width() + self._arrowBtn.winfo_width() h = self.__listbox.winfo_height() sh = self.winfo_screenheight() if y + h > sh and y > sh / 2: y = self._entryfield.winfo_rooty() - h self._list.configure(hull_width=w) Pmw.setgeometryanddeiconify(self._popup, '+%d+%d' % (x, y)) # Grab the popup, so that all events are delivered to it, and # set focus to the listbox, to make keyboard navigation # easier. Pmw.pushgrab(self._popup, 1, self._unpostList) self.__listbox.focus_set() self._drawArrow() # Ignore the first release of the mouse button after posting the # dropdown list, unless the mouse enters the dropdown list. self._ignoreRelease = 1 def _dropdownBtnRelease(self, event): if (event.widget == self._list.component('vertscrollbar') or event.widget == self._list.component('horizscrollbar')): return if self._ignoreRelease: self._unpostOnNextRelease() return self._unpostList() if (event.x >= 0 and event.x < self.__listbox.winfo_width() and event.y >= 0 and event.y < self.__listbox.winfo_height()): self._selectCmd() def _unpostOnNextRelease(self, event = None): self._ignoreRelease = 0 def _resizeArrow(self, event): bw = (string.atoi(self._arrowBtn['borderwidth']) + string.atoi(self._arrowBtn['highlightthickness'])) newHeight = self._entryfield.winfo_reqheight() - 2 * bw newWidth = int(newHeight * self['buttonaspect']) self._arrowBtn.configure(width=newWidth, height=newHeight) self._drawArrow() def _unpostList(self, event=None): if not self._isPosted: # It is possible to get events on an unposted popup. For # example, by repeatedly pressing the space key to post # and unpost the popup. The event may be # delivered to the popup window even though # Pmw.popgrab() has set the focus away from the # popup window. (Bug in Tk?) return # Restore the focus before withdrawing the window, since # otherwise the window manager may take the focus away so we # can't redirect it. Also, return the grab to the next active # window in the stack, if any. Pmw.popgrab(self._popup) self._popup.withdraw() self._isPosted = 0 self._drawArrow() def _selectUnpost(self, event): self._unpostList() self._selectCmd() Pmw.forwardmethods(ComboBox, Pmw.ScrolledListBox, '_list') Pmw.forwardmethods(ComboBox, Pmw.EntryField, '_entryfield')