# 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')