# 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', 150, INITOPT), ('selectioncommand', None, None), ('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='nsew') interior.grid_columnconfigure(2, weight = 1) interior.grid_rowconfigure(2, weight = 1) self._entryWidget = self._entryfield.component('entry') if self['dropdown']: self._isPosted = 0 # Create the arrow button. self._arrowBtn = self.createcomponent('arrowbutton', (), None, Tkinter.Canvas, (interior,), borderwidth = 2, relief = 'raised', width = 16, height = 16) self._arrowBtn.grid(column=3, row=2) 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('<Configure>', self._drawArrow) self._arrowBtn.bind('<3>', self._next) self._arrowBtn.bind('<Shift-3>', self._previous) self._arrowBtn.bind('<Down>', self._next) self._arrowBtn.bind('<Up>', self._previous) self._arrowBtn.bind('<Control-n>', self._next) self._arrowBtn.bind('<Control-p>', self._previous) self._arrowBtn.bind('<Shift-Down>', self._postList) self._arrowBtn.bind('<Shift-Up>', self._postList) self._arrowBtn.bind('<F34>', self._postList) self._arrowBtn.bind('<F28>', self._postList) self._arrowBtn.bind('<space>', self._postList) # Bind events to the dropdown window. self._popup.bind('<Escape>', self._unpostList) self._popup.bind('<space>', self._selectUnpost) self._popup.bind('<Return>', self._selectUnpost) self._popup.bind('<ButtonRelease-1>', self._dropdownBtnRelease) self._popup.bind('<ButtonPress-1>', self._unpostOnNextRelease) # Bind events to the Tk listbox. self.__listbox.bind('<Enter>', self._unpostOnNextRelease) # Bind events to the Tk entry widget. self._entryWidget.bind('<Configure>', self._resizeArrow) self._entryWidget.bind('<Shift-Down>', self._postList) self._entryWidget.bind('<Shift-Up>', self._postList) self._entryWidget.bind('<F34>', self._postList) self._entryWidget.bind('<F28>', 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('<Unmap>', self._unpostList) else: # Create the scrolled listbox below the entry field. self._list = self.createcomponent('scrolledlist', (('listbox', 'scrolledlist_listbox'),), None, Pmw.ScrolledListBox, (interior,)) 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) # Bind events to the Tk listbox. self.__listbox.bind('<ButtonRelease-1>', self._simpleBtnRelease) self.__listbox.bind('<space>', self._selectCmd) self.__listbox.bind('<Return>', self._selectCmd) self._entryWidget.bind('<Down>', self._next) self._entryWidget.bind('<Up>', self._previous) self._entryWidget.bind('<Control-n>', self._next) self._entryWidget.bind('<Control-p>', self._previous) self.__listbox.bind('<Control-n>', self._next) self.__listbox.bind('<Control-p>', self._previous) if self['history']: self._entryfield.configure(command=self._addHistory) # Check keywords and initialise options. self.initialiseoptions(ComboBox) 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() #====================================================================== # 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 method for simple combobox. def _simpleBtnRelease(self, event): # Only execute the command if the mouse was released over the # listbox. if (event.x >= 0 and event.x < self.__listbox.winfo_width() and event.y >= 0 and event.y < self.__listbox.winfo_height()): self._selectCmd() #====================================================================== # Private methods for dropdown combobox. def _drawArrow(self, event=None, sunken=0): if sunken: self._arrowRelief = self._arrowBtn.cget('relief') self._arrowBtn.configure(relief = 'sunken') else: self._arrowBtn.configure(relief = self._arrowRelief) fg = self['entry_foreground'] self._arrowBtn.delete('arrow') bw = (string.atoi(self._arrowBtn['borderwidth']) + string.atoi(self._arrowBtn['highlightthickness'])) / 2 h = string.atoi(self._arrowBtn['height']) + 2 * bw w = string.atoi(self._arrowBtn['width']) + 2 * bw if self._isPosted and self['fliparrow']: x1, x2, x3 = 0.25, 0.75, 0.50 y1, y2, y3 = 0.75, 0.75, 0.25 fudge = 1 else: x1, x2, x3 = 0.25, 0.75, 0.50 y1, y2, y3 = 0.25, 0.25, 0.75 fudge = 0 self._arrowBtn.create_polygon( x1 * w + bw, y1 * h + bw, x2 * w + bw, y2 * h + bw, x3 * w + bw, y3 * h + bw - fudge, fill=fg, tag='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) # To avoid flashes on X and to position the window # correctly on Win95 (caused by Tk bugs): if os.name != "nt": self._popup.geometry('%+d%+d' % (x, y)) self._popup.deiconify() # 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._popup.tkraise() if os.name == "nt": self._popup.geometry('%+d%+d' % (x, y)) 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 <space> 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')