# Based on iwidgets2.2.0/scrolledlistbox.itk code.

import types
import Tkinter
import Pmw

class ScrolledListBox(Pmw.MegaWidget):
    def __init__(self, parent = None, **kw):

	# Define the megawidget options.
	INITOPT = Pmw.INITOPT
	optiondefs = (
	    ('dblclickcommand',    None,            None),
	    ('hscrollmode',        'dynamic',       self._hscrollMode),
	    ('items',              (),              INITOPT),
	    ('labelmargin',        0,               INITOPT),
	    ('labelpos',           None,            INITOPT),
	    ('scrollmargin',       2,               INITOPT),
	    ('selectioncommand',   None,            None),
	    ('usehullsize',        0,               INITOPT),
	    ('vscrollmode',        'dynamic',       self._vscrollMode),
	)
	self.defineoptions(kw, optiondefs)

	# Initialise the base class (after defining the options).
	Pmw.MegaWidget.__init__(self, parent)

	# Create the components.
	interior = self.interior()

	if self['usehullsize']:
	    interior.grid_propagate(0)

	# Create the listbox widget.
	self._listbox = self.createcomponent('listbox',
		(), None,
		Tkinter.Listbox, (interior,))
	self._listbox.grid(row = 2, column = 2, sticky = 'news')
	interior.grid_rowconfigure(2, weight = 1, minsize = 0)
	interior.grid_columnconfigure(2, weight = 1, minsize = 0)

	# Create the horizontal scrollbar
	self._horizScrollbar = self.createcomponent('horizscrollbar',
		(), 'Scrollbar',
		Tkinter.Scrollbar, (interior,),
	        orient='horizontal',
		command=self._listbox.xview
	)

	# Create the vertical scrollbar
	self._vertScrollbar = self.createcomponent('vertscrollbar',
		(), 'Scrollbar',
		Tkinter.Scrollbar, (interior,),
		orient='vertical',
		command=self._listbox.yview
	)

	self.createlabel(interior, childCols = 3, childRows = 3)

	# Add the items specified by the initialisation option.
	items = self['items']
	if type(items) != types.TupleType:
	    items = tuple(items)
	if len(items) > 0:
	    apply(self._listbox.insert, ('end',) + items)

	tag = 'SLBSelect' + str(self)
	self.bind_class(tag, '<Control-Key-backslash>', self._makeSelection)
	self.bind_class(tag, '<Control-Key-slash>', self._makeSelection)
	self.bind_class(tag, '<Key-Escape>', self._makeSelection)
	self.bind_class(tag, '<Shift-Key-Select>', self._makeSelection)
	self.bind_class(tag, '<Control-Shift-Key-space>', self._makeSelection)
	self.bind_class(tag, '<Key-Select>', self._makeSelection)
	self.bind_class(tag, '<Key-space>', self._makeSelection)
	self.bind_class(tag, '<Control-Shift-Key-End>', self._makeSelection)
	self.bind_class(tag, '<Control-Key-End>', self._makeSelection)
	self.bind_class(tag, '<Control-Shift-Key-Home>', self._makeSelection)
	self.bind_class(tag, '<Control-Key-Home>', self._makeSelection)
	self.bind_class(tag, '<Shift-Key-Down>', self._makeSelection)
	self.bind_class(tag, '<Shift-Key-Up>', self._makeSelection)
	self.bind_class(tag, '<Control-Button-1>', self._makeSelection)
	self.bind_class(tag, '<Shift-Button-1>', self._makeSelection)
	self.bind_class(tag, '<ButtonRelease-1>', self._makeSelection)
	self.bind_class(tag, '<Double-1>', self._doubleClick)

	self._listbox.bindtags(self._listbox.bindtags() + (tag,))

	# Initialise instance variables.
	self._horizScrollbarOn = 0
	self._vertScrollbarOn = 0
	self.scrollTimer = None
        self._scrollRecurse = 0
	self._horizScrollbarNeeded = 0
	self._vertScrollbarNeeded = 0

	# Check keywords and initialise options.
	self.initialiseoptions(ScrolledListBox)

    def destroy(self):
	if self.scrollTimer is not None:
	    self.after_cancel(self.scrollTimer)
	    self.scrollTimer = None
	Pmw.MegaWidget.destroy(self)

    # ======================================================================

    # Public methods.

    def getcurselection(self):
	rtn = []
	for sel in self.curselection():
	    rtn.append(self._listbox.get(sel))
	return tuple(rtn)

    def setlist(self, items):
        self._listbox.delete(0, 'end')
	if len(items) > 0:
	    if type(items) != types.TupleType:
		items = tuple(items)
	    apply(self._listbox.insert, (0,) + items)

    # Override Tkinter.Listbox get method, so that if it is called with
    # no arguments, return all list elements (consistent with other widgets).
    def get(self, first=None, last=None):
	if first is None:
	    return self._listbox.get(0, 'end')
	else:
	    return self._listbox.get(first, last)

    # ======================================================================

    # Configuration methods.

    def _hscrollMode(self):
	# The horizontal scroll mode has been configured.

	mode = self['hscrollmode']

	if mode == 'static':
	    if not self._horizScrollbarOn:
		self._toggleHorizScrollbar()
	elif mode == 'dynamic':
	    if self._horizScrollbarNeeded != self._horizScrollbarOn:
		self._toggleHorizScrollbar()
	elif mode == 'none':
	    if self._horizScrollbarOn:
		self._toggleHorizScrollbar()
	else:
	    message = 'bad hscrollmode option "%s": should be static, dynamic, or none' % mode
	    raise ValueError, message

        self._configureScrollCommands()

    def _vscrollMode(self):
	# The vertical scroll mode has been configured.

	mode = self['vscrollmode']

	if mode == 'static':
	    if not self._vertScrollbarOn:
		self._toggleVertScrollbar()
	elif mode == 'dynamic':
	    if self._vertScrollbarNeeded != self._vertScrollbarOn:
		self._toggleVertScrollbar()
	elif mode == 'none':
	    if self._vertScrollbarOn:
		self._toggleVertScrollbar()
	else:
	    message = 'bad vscrollmode option "%s": should be static, dynamic, or none' % mode
	    raise ValueError, message

        self._configureScrollCommands()

    # ======================================================================

    # Private methods.

    def _configureScrollCommands(self):
        # If both scrollmodes are not dynamic we can save a lot of
        # time by not having to create an idle job to handle the
        # scroll commands.

	if self['hscrollmode'] == self['vscrollmode'] == 'dynamic':
            self._listbox.configure(
                    xscrollcommand=self._scrollBothLater,
                    yscrollcommand=self._scrollBothLater
            )
        else:
            self._listbox.configure(
                    xscrollcommand=self._scrollXNow,
                    yscrollcommand=self._scrollYNow
            )

    def _scrollXNow(self, first, last):
        self._horizScrollbar.set(first, last)
        self._horizScrollbarNeeded = ((first, last) != ('0', '1'))

	if self['hscrollmode'] == 'dynamic':
	    if self._horizScrollbarNeeded != self._horizScrollbarOn:
		self._toggleHorizScrollbar()

    def _scrollYNow(self, first, last):
        self._vertScrollbar.set(first, last)
        self._vertScrollbarNeeded = ((first, last) != ('0', '1'))

        if self['vscrollmode'] == 'dynamic':
            if self._vertScrollbarNeeded != self._vertScrollbarOn:
                self._toggleVertScrollbar()

    def _scrollBothLater(self, first, last):
	# Called by the listbox to set the horizontal or vertical
	# scrollbar when it has scrolled or changed size or contents.

	if self.scrollTimer is None:
	    self.scrollTimer = self.after_idle(self._scrollBothNow)

    def _scrollBothNow(self):
        # This performs the function of _scrollXNow and _scrollYNow.
        # If one is changed, the other should be updated to match.
	self.scrollTimer = None

        # Call update_idletasks to make sure that the containing frame
        # has been resized before we attempt to set the scrollbars. 
        # Otherwise the scrollbars may be mapped/unmapped continuously.
        self._scrollRecurse = self._scrollRecurse + 1
        self.update_idletasks()
        self._scrollRecurse = self._scrollRecurse - 1
        if self._scrollRecurse != 0:
            return

	xview = self._listbox.xview()
	yview = self._listbox.yview()
	self._horizScrollbar.set(xview[0], xview[1])
	self._vertScrollbar.set(yview[0], yview[1])

	self._horizScrollbarNeeded = (xview != (0.0, 1.0))
	self._vertScrollbarNeeded = (yview != (0.0, 1.0))

	# If both horizontal and vertical scrollmodes are dynamic and
	# currently only one scrollbar is mapped and both should be
	# toggled, then unmap the mapped scrollbar.  This prevents a
	# continuous mapping and unmapping of the scrollbars. 
	if (self['hscrollmode'] == self['vscrollmode'] == 'dynamic' and
		self._horizScrollbarNeeded != self._horizScrollbarOn and
		self._vertScrollbarNeeded != self._vertScrollbarOn and
		self._vertScrollbarOn != self._horizScrollbarOn):
	    if self._horizScrollbarOn:
		self._toggleHorizScrollbar()
	    else:
		self._toggleVertScrollbar()
	    return

	if self['hscrollmode'] == 'dynamic':
	    if self._horizScrollbarNeeded != self._horizScrollbarOn:
		self._toggleHorizScrollbar()

	if self['vscrollmode'] == 'dynamic':
	    if self._vertScrollbarNeeded != self._vertScrollbarOn:
		self._toggleVertScrollbar()

    def _toggleHorizScrollbar(self):

	self._horizScrollbarOn = not self._horizScrollbarOn

	interior = self.interior()
	if self._horizScrollbarOn:
	    self._horizScrollbar.grid(row = 4, column = 2, sticky = 'news')
	    interior.grid_rowconfigure(3, minsize = self['scrollmargin'])
	else:
	    self._horizScrollbar.grid_forget()
	    interior.grid_rowconfigure(3, minsize = 0)

    def _toggleVertScrollbar(self):

	self._vertScrollbarOn = not self._vertScrollbarOn

	interior = self.interior()
	if self._vertScrollbarOn:
	    self._vertScrollbar.grid(row = 2, column = 4, sticky = 'news')
	    interior.grid_columnconfigure(3, minsize = self['scrollmargin'])
	else:
	    self._vertScrollbar.grid_forget()
	    interior.grid_columnconfigure(3, minsize = 0)

    def _makeSelection(self, event):
	command = self['selectioncommand']
	if callable(command):
	    command()

    def _doubleClick(self, event):
	command = self['dblclickcommand']
	if callable(command):
	    command()

    # Need to explicitly forward this to override the stupid
    # (grid_)size method inherited from Tkinter.Frame.Grid.
    def size(self):
	return self._listbox.size()

    # Need to explicitly forward this to override the stupid
    # (grid_)bbox method inherited from Tkinter.Frame.Grid.
    def bbox(self, index):
	return self._listbox.bbox(index)

Pmw.forwardmethods(ScrolledListBox, Tkinter.Listbox, '_listbox')