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