# 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 = ( ('arrowrelief', 'raised', INITOPT), ('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), ('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']: # This is needed to protect against _postList being called # recursively (via the call to wait_visibility) if the user # clicks on the button quickly. self._isPosted = 0 # Create the arrow button. self._arrowBtn = self.createcomponent('arrowbutton', (), None, Tkinter.Canvas, (interior,), borderwidth = 2, relief = self['arrowrelief'], width = 16, height = 16) self._arrowBtn.grid(column=3, row=2) # 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'], listbox_width = 1, listbox_height = 1, listbox_exportselection = 0) self._list.pack(expand=1, fill='both') self._list.grid_propagate(0) 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('<Return>', 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) 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) #====================================================================== # 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: 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): 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: relief = 'sunken' else: relief = self['arrowrelief'] flip = 0 self._arrowBtn.configure(relief=relief) if not sunken and self._isPosted and self['fliparrow']: flip = 1 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 flip: self._arrowBtn.create_polygon(0.25 * w + bw, 0.75 * h + bw, 0.75 * w + bw, 0.75 * h + bw, 0.5 * w + bw, 0.25 * h + bw - 1, fill=fg, tag='arrow') else: self._arrowBtn.create_polygon(0.25 * w + bw, 0.25 * h + bw, 0.75 * w + bw, 0.25 * h + bw, 0.5 * w + bw, 0.75 * h + bw, fill=fg, tag='arrow') def _postList(self, event = None): if not self._isPosted: 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() self._popup.wait_visibility() self._popup.grab_set_global() self._popup.tkraise() self._popup.focus_set() 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 if (event.x >= 0 and event.x < self.__listbox.winfo_width() and event.y >= 0 and event.y < self.__listbox.winfo_height()): self._selectCmd() self._unpostList() 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 = newHeight * self['buttonaspect'] self._arrowBtn.configure(width=newWidth, height=newHeight) self._drawArrow() def _unpostList(self, event=None): self._popup.withdraw() self._popup.grab_release() self._isPosted = 0 self._drawArrow() def _selectUnpost(self, event): self._selectCmd() self._unpostList() Pmw.forwardmethods(ComboBox, Pmw.ScrolledListBox, '_list') Pmw.forwardmethods(ComboBox, Pmw.EntryField, '_entryfield')