# Natural Language Toolkit: Table widget
#
# Copyright (C) 2001-2010 NLTK Project
# Author: Edward Loper <edloper@gradient.cis.upenn.edu>
# URL: <http://www.nltk.org/>
# For license information, see LICENSE.TXT
#
# $Id:$

"""
Tkinter widgets for displaying multi-column listboxes and tables.
"""

from util import *
import operator

######################################################################
# Multi-Column Listbox
######################################################################

class MultiListbox(Frame):
    """
    A multi-column listbox, where the current selection applies to an
    entire row.  Based on the 'U{MultiListbox Tkinter widget
    <http://code.activestate.com/recipes/52266/>}' recipe from the
    Python Cookbook.

    For the most part, C{MultiListbox}'s methods just delegate to its
    contained listboxes.  For any methods that do not have docstrings,
    see C{Tkinter.Listbox} for a description of what that method does.
    """
    #/////////////////////////////////////////////////////////////////
    # Configuration
    #/////////////////////////////////////////////////////////////////

    #: Default configuration values for the frame.
    FRAME_CONFIG = dict(background='#888',
                        takefocus=True,
                        highlightthickness=1)

    #: Default configurations for the column labels.
    LABEL_CONFIG = dict(borderwidth=1, relief='raised',
                        font='helvetica -16 bold',
                      background='#444', foreground='white')

    #: Default configuration for the column listboxes.
    LISTBOX_CONFIG = dict(borderwidth=1,
                          selectborderwidth=0,
                          highlightthickness=0,
                          exportselection=False,
                          selectbackground='#888',
                          activestyle='none',
                          takefocus=False)

    #/////////////////////////////////////////////////////////////////
    # Constructor
    #/////////////////////////////////////////////////////////////////

    def __init__(self, master, columns, column_weights=None, cnf={}, **kw):
        """
        Construct a new multi-column listbox widget.

        @param master: The widget that should contain the new
            multi-column listbox.
            
        @param columns: Specifies what columns should be included in
            the new multi-column listbox.  If C{columns} is an integer,
            the it is the number of columns to include.  If it is
            a list, then its length indicates the number of columns
            to include; and each element of the list will be used as
            a label for the corresponding column.

        @param cnf, kw: Configuration parameters for this widget.
            Use C{label_*} to configure all labels; and C{listbox_*}
            to configure all listboxes.  E.g.:

                >>> mlb = MultiListbox(master, 5, label_foreground='red')
        """
        # If columns was specified as an int, convert it to a list.
        if isinstance(columns, int):
            columns = range(columns)
            include_labels = False
        else:
            include_labels = True
        
        if len(columns) == 0:
            raise ValuError("Expected at least one column")

        # Instance variables
        self._column_names = tuple(columns)
        self._listboxes = []
        self._labels = []

        # Pick a default value for column_weights, if none was specified.
        if column_weights is None:
            column_weights = [1] * len(columns)
        elif len(column_weights) != len(columns):
            raise ValueError('Expected one column_weight for each column')
        self._column_weights = column_weights

        # Configure our widgets.
        Frame.__init__(self, master, **self.FRAME_CONFIG)
        self.grid_rowconfigure(1, weight=1)
        for i, label in enumerate(self._column_names):
            self.grid_columnconfigure(i, weight=column_weights[i])

            # Create a label for the column
            if include_labels:
                l = Label(self, text=label, **self.LABEL_CONFIG)
                self._labels.append(l)
                l.grid(column=i, row=0, sticky='news', padx=0, pady=0)
                l.column_index = i
            
            # Create a listbox for the column
            lb = Listbox(self, **self.LISTBOX_CONFIG)
            self._listboxes.append(lb)
            lb.grid(column=i, row=1, sticky='news', padx=0, pady=0)
            lb.column_index = i

            # Clicking or dragging selects:
            lb.bind('<Button-1>', self._select)
            lb.bind('<B1-Motion>', self._select)
            # Scroll whell scrolls:
            lb.bind('<Button-4>', lambda e: self._scroll(-1))
            lb.bind('<Button-5>', lambda e: self._scroll(+1))
            lb.bind('<MouseWheel>', lambda e: self._scroll(e.delta))
            # Button 2 can be used to scan:
            lb.bind('<Button-2>', lambda e: self.scan_mark(e.x, e.y))
            lb.bind('<B2-Motion>', lambda e: self.scan_dragto(e.x, e.y))
            # Dragging outside the window has no effect (diable
            # the default listbox behavior, which scrolls):
            lb.bind('<B1-Leave>', lambda e: 'break')
            # Columns can be resized by dragging them:
            l.bind('<Button-1>', self._resize_column)

        # Columns can be resized by dragging them.  (This binding is
        # used if they click on the grid between columns:)
        self.bind('<Button-1>', self._resize_column)
            
        # Set up key bindings for the widget:
        self.bind('<Up>', lambda e: self.select(delta=-1))
        self.bind('<Down>', lambda e: self.select(delta=1))
        self.bind('<Prior>', lambda e: self.select(delta=-self._pagesize()))
        self.bind('<Next>', lambda e: self.select(delta=self._pagesize()))

        # Configuration customizations
        self.configure(cnf, **kw)

    #/////////////////////////////////////////////////////////////////
    # Column Resizing
    #/////////////////////////////////////////////////////////////////

    def _resize_column(self, event):
        """
        Callback used to resize a column of the table.  Return C{True}
        if the column is actually getting resized (if the user clicked
        on the far left or far right 5 pixels of a label); and
        C{False} otherwies.
        """
        # If we're already waiting for a button release, then ignore
        # the new button press.
        if event.widget.bind('<ButtonRelease>'):
            return False

        # Decide which column (if any) to resize.
        self._resize_column_index = None
        if event.widget is self:
            for i, lb in enumerate(self._listboxes):
                if abs(event.x-(lb.winfo_x()+lb.winfo_width())) < 10:
                    self._resize_column_index = i
        elif event.x > (event.widget.winfo_width()-5):
            self._resize_column_index = event.widget.column_index
        elif event.x < 5 and event.widget.column_index != 0:
            self._resize_column_index = event.widget.column_index-1

        # Bind callbacks that are used to resize it.
        if self._resize_column_index is not None:
            event.widget.bind('<Motion>', self._resize_column_motion_cb)
            event.widget.bind('<ButtonRelease-%d>' % event.num,
                              self._resize_column_buttonrelease_cb)
            return True
        else:
            return False
            
    def _resize_column_motion_cb(self, event):
        lb = self._listboxes[self._resize_column_index]
        charwidth = lb.winfo_width() / float(lb['width'])
        
        x1 = event.x + event.widget.winfo_x()
        x2 = lb.winfo_x() + lb.winfo_width()

        lb['width'] = max(3, lb['width'] + int((x1-x2)/charwidth))

    def _resize_column_buttonrelease_cb(self, event):
        event.widget.unbind('<ButtonRelease-%d>' % event.num)
        event.widget.unbind('<Motion>')

    #/////////////////////////////////////////////////////////////////
    # Properties
    #/////////////////////////////////////////////////////////////////

    column_names = property(lambda self: self._column_names, doc="""
        A tuple containing the names of the columns used by this
        multi-column listbox.""")
    
    column_labels = property(lambda self: tuple(self._labels), doc="""
        A tuple containing the C{Tkinter.Label} widgets used to
        display the label of each column.  If this multi-column
        listbox was created without labels, then this will be an empty
        tuple.  These widgets will all be augmented with a
        C{column_index} attribute, which can be used to determine
        which column they correspond to.  This can be convenient,
        e.g., when defining callbacks for bound events.""")
    
    listboxes = property(lambda self: tuple(self._listboxes), doc="""
        A tuple containing the C{Tkinter.Listbox} widgets used to
        display individual columns.  These widgets will all be
        augmented with a C{column_index} attribute, which can be used
        to determine which column they correspond to.  This can be
        convenient, e.g., when defining callbacks for bound events.""")

    #/////////////////////////////////////////////////////////////////
    # Mouse & Keyboard Callback Functions
    #/////////////////////////////////////////////////////////////////

    def _select(self, e):
        i = e.widget.nearest(e.y)
        self.selection_clear(0, 'end')
        self.selection_set(i)
        self.activate(i)
        self.focus()

    def _scroll(self, delta):
        for lb in self._listboxes:
            lb.yview_scroll(delta, 'unit')
        return 'break'
        
    def _pagesize(self):
        """@return: The number of rows that makes up one page"""
        return int(self.index('@0,1000000')) - int(self.index('@0,0'))

    #/////////////////////////////////////////////////////////////////
    # Row selection
    #/////////////////////////////////////////////////////////////////

    def select(self, index=None, delta=None, see=True):
        """
        Set the selected row.  If C{index} is specified, then select
        row C{index}.  Otherwise, if C{delta} is specified, then move
        the current selection by C{delta} (negative numbers for up,
        positive numbers for down).  This will not move the selection
        past the top or the bottom of the list.

        @param see: If true, then call C{self.see()} with the newly
            selected index, to ensure that it is visible.
        """
        if (index is not None) and (delta is not None):
            raise ValueError('specify index or delta, but not both')

        # If delta was given, then calculate index.
        if delta is not None:
            if len(self.curselection()) == 0:
                index = -1 + delta
            else:
                index = int(self.curselection()[0]) + delta

        # Clear all selected rows.
        self.selection_clear(0, 'end')

        # Select the specified index
        if index is not None:
            index = min(max(index, 0), self.size()-1)
            #self.activate(index)
            self.selection_set(index)
            if see: self.see(index)

    #/////////////////////////////////////////////////////////////////
    # Configuration
    #/////////////////////////////////////////////////////////////////

    def configure(self, cnf={}, **kw):
        """
        Configure this widget.  Use C{label_*} to configure all
        labels; and C{listbox_*} to configure all listboxes.  E.g.:

                >>> mlb = MultiListbox(master, 5)
                >>> mlb.configure(label_foreground='red')
                >>> mlb.configure(listbox_foreground='red')
        """
        cnf = dict(cnf.items() + kw.items())
        for (key, val) in cnf.items():
            if key.startswith('label_') or key.startswith('label-'):
                for label in self._labels:
                    label.configure({key[6:]: val})
            elif key.startswith('listbox_') or key.startswith('listbox-'):
                for listbox in self._listboxes:
                    listbox.configure({key[8:]: val})
            else:
                Frame.configure(self, {key:val})
                       
    def __setitem__(self, key, val):
        """
        Configure this widget.  This is equivalent to
        C{self.configure({key,val})}.  See L{configure()}.
        """
        self.configure({key:val})

    def rowconfigure(self, row_index, cnf={}, **kw):
        """
        Configure all table cells in the given row.  Valid keyword
        arguments are: C{background}, C{bg}, C{foreground}, C{fg},
        C{selectbackground}, C{selectforeground}.
        """
        for lb in self._listboxes: lb.itemconfigure(row_index, cnf, **kw)
        
    def columnconfigure(self, col_index, cnf={}, **kw):
        """
        Configure all table cells in the given column.  Valid keyword
        arguments are: C{background}, C{bg}, C{foreground}, C{fg},
        C{selectbackground}, C{selectforeground}.
        """
        lb = self._listboxes[col_index]

        cnf = dict(cnf.items() + kw.items())
        for (key, val) in cnf.items():
            if key in ('background', 'bg', 'foreground', 'fg',
                       'selectbackground', 'selectforeground'):
                for i in range(lb.size()): lb.itemconfigure(i, {key:val})
            else:
                lb.configure({key:val})

    def itemconfigure(self, row_index, col_index, cnf=None, **kw):
        """
        Configure the table cell at the given row and column.  Valid
        keyword arguments are: C{background}, C{bg}, C{foreground},
        C{fg}, C{selectbackground}, C{selectforeground}.
        """
        lb = self._listboxes[col_index]
        return lb.itemconfigure(row_index, cnf, **kw)

    #/////////////////////////////////////////////////////////////////
    # Value Access
    #/////////////////////////////////////////////////////////////////

    def insert(self, index, *rows):
        """
        Insert the given row or rows into the table, at the given
        index.  Each row value should be a tuple of cell values, one
        for each column in the row.  Index may be an integer or any of
        the special strings (such as C{'end'}) accepted by
        C{Tkinter.Listbox}.
        """
        for elt in rows:
            if len(elt) != len(self._column_names):
                raise ValueError('rows should be tuples whose length '
                                 'is equal to the number of columns')
        for (lb,elts) in zip(self._listboxes, zip(*rows)):
            lb.insert(index, *elts)
        
    def get(self, first, last=None):
        """
        Return the value(s) of the specified row(s).  If C{last} is
        not specified, then return a single row value; otherwise,
        return a list of row values.  Each row value is a tuple of
        cell values, one for each column in the row.
        """
        values = [lb.get(first, last) for lb in self._listboxes]
        if last:
            return [tuple(row) for row in zip(*values)]
        else:
            return tuple(values)

    def bbox(self, row, col):
        """
        Return the bounding box for the given table cell, relative to
        this widget's top-left corner.  The bounding box is a tuple
        of integers C{(left, top, width, height)}.
        """
        dx, dy, _, _ = self.grid_bbox(row=0, column=col)
        x, y, w, h = self._listboxes[col].bbox(row)
        return int(x)+int(dx), int(y)+int(dy), int(w), int(h)
    
    #/////////////////////////////////////////////////////////////////
    # Hide/Show Columns
    #/////////////////////////////////////////////////////////////////

    def hide_column(self, col_index):
        """
        Hide the given column.  The column's state is still
        maintained: its values will still be returned by L{get()}, and
        you must supply its values when calling L{insert()}.  It is
        safe to call this on a column that is already hidden.

        @see: L{show_column()}
        """
        if self._labels:
            self._labels[col_index].grid_forget()
        self.listboxes[col_index].grid_forget()
        self.grid_columnconfigure(col_index, weight=0)

    def show_column(self, col_index):
        """
        Display a column that has been hidden using L{hide_column()}.
        It is safe to call this on a column that is not hidden.
        """
        weight = self._column_weights[col_index]
        if self._labels:
            self._labels[col_index].grid(column=col_index, row=0,
                                         sticky='news', padx=0, pady=0)
        self._listboxes[col_index].grid(column=col_index, row=1,
                                        sticky='news', padx=0, pady=0)
        self.grid_columnconfigure(col_index, weight=weight)

    #/////////////////////////////////////////////////////////////////
    # Binding Methods
    #/////////////////////////////////////////////////////////////////

    def bind_to_labels(self, sequence=None, func=None, add=None):
        """
        Add a binding to each C{Tkinter.Label} widget in this
        mult-column listbox that will call C{func} in response to the
        event C{sequence}.

        @return: A list of the identifiers of replaced binding
            functions (if any), allowing for their deletion (to
            prevent a memory leak).
        """
        return [label.bind(sequence, func, add)
                for label in self.column_labels]
    
    def bind_to_listboxes(self, sequence=None, func=None, add=None):
        """
        Add a binding to each C{Tkinter.Listbox} widget in this
        mult-column listbox that will call C{func} in response to the
        event C{sequence}.

        @return: A list of the identifiers of replaced binding
            functions (if any), allowing for their deletion (to
            prevent a memory leak).
        """
        for listbox in self.listboxes:
            listbox.bind(sequence, func, add)

    def bind_to_columns(self, sequence=None, func=None, add=None):
        """
        Add a binding to each C{Tkinter.Label} and C{Tkinter.Listbox}
        widget in this mult-column listbox that will call C{func} in
        response to the event C{sequence}.

        @return: A list of the identifiers of replaced binding
            functions (if any), allowing for their deletion (to
            prevent a memory leak).
        """
        return (self.bind_to_labels(sequence, func, add) + 
                self.bind_to_listboxes(sequence, func, add))
    
    #/////////////////////////////////////////////////////////////////
    # Simple Delegation
    #/////////////////////////////////////////////////////////////////
    
    # These methods delegate to the first listbox:
    def curselection(self, *args, **kwargs):
        return self._listboxes[0].curselection(*args, **kwargs)
    def selection_includes(self, *args, **kwargs):
        return self._listboxes[0].selection_includes(*args, **kwargs)
    def itemcget(self, *args, **kwargs):
        return self._listboxes[0].itemcget(*args, **kwargs)
    def size(self, *args, **kwargs):
        return self._listboxes[0].size(*args, **kwargs)
    def index(self, *args, **kwargs):
        return self._listboxes[0].index(*args, **kwargs)
    def nearest(self, *args, **kwargs):
        return self._listboxes[0].nearest(*args, **kwargs)
    
    # These methods delegate to each listbox (and return None):
    def activate(self, *args, **kwargs):
        for lb in self._listboxes: lb.activate(*args, **kwargs)
    def delete(self, *args, **kwargs):
        for lb in self._listboxes: lb.delete(*args, **kwargs)
    def scan_mark(self, *args, **kwargs):
        for lb in self._listboxes: lb.scan_mark(*args, **kwargs)
    def scan_dragto(self, *args, **kwargs):
        for lb in self._listboxes: lb.scan_dragto(*args, **kwargs)
    def see(self, *args, **kwargs):
        for lb in self._listboxes: lb.see(*args, **kwargs)
    def selection_anchor(self, *args, **kwargs):
        for lb in self._listboxes: lb.selection_anchor(*args, **kwargs)
    def selection_clear(self, *args, **kwargs):
        for lb in self._listboxes: lb.selection_clear(*args, **kwargs)
    def selection_set(self, *args, **kwargs):
        for lb in self._listboxes: lb.selection_set(*args, **kwargs)
    def yview(self, *args, **kwargs):
        for lb in self._listboxes: v = lb.yview(*args, **kwargs)
        return v # if called with no arguments
    def yview_moveto(self, *args, **kwargs):
        for lb in self._listboxes: lb.yview_moveto(*args, **kwargs)
    def yview_scroll(self, *args, **kwargs):
        for lb in self._listboxes: lb.yview_scroll(*args, **kwargs)

    #/////////////////////////////////////////////////////////////////
    # Aliases
    #/////////////////////////////////////////////////////////////////
    
    itemconfig = itemconfigure
    rowconfig = rowconfigure
    columnconfig = columnconfigure
    select_anchor = selection_anchor
    select_clear = selection_clear
    select_includes = selection_includes
    select_set = selection_set

    #/////////////////////////////////////////////////////////////////
    # These listbox methods are not defined for multi-listbox
    #/////////////////////////////////////////////////////////////////
    # def xview(self, *what): pass
    # def xview_moveto(self, fraction): pass
    # def xview_scroll(self, number, what): pass

######################################################################
# Table
######################################################################

class Table(object):
    """
    A display widget for a table of values, based on a L{MultiListbox}
    widget.  For many purposes, C{Table} can be treated as a
    list-of-lists.  E.g., table[i] is a list of the values for row i;
    and table.append(row) adds a new row with the given lits of
    values.  Individual cells can be accessed using table[i,j], which
    refers to the j-th column of the i-th row.  This can be used to
    both read and write values from the table.  E.g.:

        >>> table[i,j] = 'hello'

    The column (j) can be given either as an index number, or as a
    column name.  E.g., the following prints the value in the 3rd row
    for the 'First Name' column:

        >>> print table[3, 'First Name']
        John

    You can configure the colors for individual rows, columns, or
    cells using L{rowconfig()}, L{columnconfig()}, and L{itemconfig()}.
    The color configuration for each row will be preserved if the
    table is modified; however, when new rows are added, any color
    configurations that have been made for I{columns} will not be
    applied to the new row.

    Note: Although C{Table} acts like a widget in some ways (e.g., it
    defines L{grid()}, L{pack()}, and L{bind()}), it is not itself a
    widget; it just contains one.  This is because widgets need to
    define C{__getitem__()}, C{__setitem__()}, and C{__nonzero__()} in
    a way that's incompatible with the fact that C{Table} behaves as a
    list-of-lists.

    @ivar _mlb: The multi-column listbox used to display this table's data.
    @ivar _rows: A list-of-lists used to hold the cell values of this
        table.  Each element of _rows is a row value, i.e., a list of
        cell values, one for each column in the row.
    """
    def __init__(self, master, column_names, rows=None,
                 column_weights=None,
                 scrollbar=True, click_to_sort=True,
                 reprfunc=None, cnf={}, **kw):
        """
        Construct a new Table widget.

        @type master: C{Tkinter.Widget}
        @param master: The widget that should contain the new table.

        @type column_names: C{list} of C{str}
        @param column_names: A list of names for the columns; these
            names will be used to create labels for each column;
            and can be used as an index when reading or writing
            cell values from the table.

        @type rows: C{list} of C{list}
        @param rows: A list of row values used to initialze the table.
            Each row value should be a tuple of cell values, one for
            each column in the row.

        @type scrollbar: C{bool}
        @param scrollbar: If true, then create a scrollbar for the
            new table widget.
            
        @type click_to_sort: C{bool}
        @param click_to_sort: If true, then create bindings that will
            sort the table's rows by a given column's values if the
            user clicks on that colum's label.

        @type reprfunc: C{function}
        @param reprfunc: If specified, then use this function to
            convert each table cell value to a string suitable for
            display.  C{reprfunc} has the following signature:

                >>> reprfunc(row_index, col_index, cell_value) -> str

            (Note that the column is specified by index, not by name.)

        @param cnf, kw: Configuration parameters for this widget's
            contained C{MultiListbox}.  See L{MultiListbox.__init__()}
            for details.
        """
        self._num_columns = len(column_names)
        self._reprfunc = reprfunc
        self._frame = Frame(master)

        self._column_name_to_index = dict((c,i) for (i,c) in
                                          enumerate(column_names))

        # Make a copy of the rows & check that it's valid.
        if rows is None: self._rows = []
        else: self._rows = [[v for v in row] for row in rows]
        for row in self._rows: self._checkrow(row)

        # Create our multi-list box.
        self._mlb = MultiListbox(self._frame, column_names,
                                 column_weights, cnf, **kw)
        self._mlb.pack(side='left', expand=True, fill='both')

        # Optional scrollbar
        if scrollbar:
            sb = Scrollbar(self._frame, orient='vertical',
                           command=self._mlb.yview)
            self._mlb.listboxes[0]['yscrollcommand'] = sb.set
            #for listbox in self._mlb.listboxes:
            #    listbox['yscrollcommand'] = sb.set
            sb.pack(side='right', fill='y')
            self._scrollbar = sb
            
        # Set up sorting
        self._sortkey = None
        if click_to_sort:
            for i, l in enumerate(self._mlb.column_labels):
                l.bind('<Button-1>', self._sort)

        # Fill in our multi-list box.
        self._fill_table()

    #/////////////////////////////////////////////////////////////////
    #{ Widget-like Methods
    #/////////////////////////////////////////////////////////////////
    # These all just delegate to either our frame or our MLB.

    def pack(self, *args, **kwargs):
        """Position this table's main frame widget in its parent
        widget.  See C{Tkinter.Frame.pack()} for more info."""
        self._frame.pack(*args, **kwargs)
        
    def grid(self, *args, **kwargs):
        """Position this table's main frame widget in its parent
        widget.  See C{Tkinter.Frame.grid()} for more info."""
        self._frame.grid(*args, **kwargs)
        
    def focus(self):
        """Direct (keyboard) input foxus to this widget."""
        self._mlb.focus()
        
    def bind(self, sequence=None, func=None, add=None):
        """Add a binding to this table's main frame that will call
        C{func} in response to the event C{sequence}."""
        self._mlb.bind(sequence, func, add)

    def rowconfigure(self, row_index, cnf={}, **kw):
        """@see: L{MultiListbox.rowconfigure()}"""
        self._mlb.rowconfigure(row_index, cnf, **kw)
        
    def columnconfigure(self, col_index, cnf={}, **kw):
        """@see: L{MultiListbox.columnconfigure()}"""
        col_index = self.column_index(col_index)
        self._mlb.columnconfigure(col_index, cnf, **kw)
        
    def itemconfigure(self, row_index, col_index, cnf=None, **kw):
        """@see: L{MultiListbox.itemconfigure()}"""
        col_index = self.column_index(col_index)
        return self._mlb.itemconfigure(row_index, col_index, cnf, **kw)
    
    def bind_to_labels(self, sequence=None, func=None, add=None):
        """@see: L{MultiListbox.bind_to_labels()}"""
        return self._mlb.bind_to_labels(sequence, func, add)

    def bind_to_listboxes(self, sequence=None, func=None, add=None):
        """@see: L{MultiListbox.bind_to_listboxes()}"""
        return self._mlb.bind_to_listboxes(sequence, func, add)

    def bind_to_columns(self, sequence=None, func=None, add=None):
        """@see: L{MultiListbox.bind_to_columns()}"""
        return self._mlb.bind_to_columns(sequence, func, add)

    rowconfig = rowconfigure
    columnconfig = columnconfigure
    itemconfig = itemconfigure

    #/////////////////////////////////////////////////////////////////
    #{ Table as list-of-lists
    #/////////////////////////////////////////////////////////////////

    def insert(self, row_index, rowvalue):
        """
        Insert a new row into the table, so that its row index will be
        C{row_index}.  If the table contains any rows whose row index
        is greater than or equal to C{row_index}, then they will be
        shifted down.

        @param rowvalue: A tuple of cell values, one for each column
            in the new row.
        """
        self._checkrow(rowvalue)
        self._rows.insert(row_index, rowvalue)
        if self._reprfunc is not None:
            rowvalue = [self._reprfunc(row_index,j,v)
                        for (j,v) in enumerate(rowvalue)]
        self._mlb.insert(row_index, rowvalue)
        if self._DEBUG: self._check_table_vs_mlb()

    def extend(self, rowvalues):
        """
        Add new rows at the end of the table.

        @param rowvalues: A list of row values used to initialze the
            table.  Each row value should be a tuple of cell values,
            one for each column in the row.
        """
        for rowvalue in rowvalues: self.append(rowvalue)
        if self._DEBUG: self._check_table_vs_mlb()

    def append(self, rowvalue):
        """
        Add a new row to the end of the table.

        @param rowvalue: A tuple of cell values, one for each column
            in the new row.
        """
        self.insert(len(self._rows), rowvalue)
        if self._DEBUG: self._check_table_vs_mlb()

    def clear(self):
        """
        Delete all rows in this table.
        """
        self._rows = []
        self._mlb.delete(0, 'end')
        if self._DEBUG: self._check_table_vs_mlb()

    def __getitem__(self, index):
        """
        Return the value of a row or a cell in this table.  If
        C{index} is an integer, then the row value for the C{index}th
        row.  This row value consists of a tuple of cell values, one
        for each column in the row.  If C{index} is a tuple of two
        integers, C{(i,j)}, then return the value of the cell in the
        C{i}th row and the C{j}th column.
        """
        if isinstance(index, slice):
            raise ValueError('Slicing not supported')
        elif isinstance(index, tuple) and len(index)==2:
            return self._rows[index[0]][self.column_index(index[1])]
        else:
            return tuple(self._rows[index])

    def __setitem__(self, index, val):
        """
        Replace the value of a row or a cell in this table with
        C{val}.

        If C{index} is an integer, then C{val} should be a row value
        (i.e., a tuple of cell values, one for each column).  In this
        case, the values of the C{index}th row of the table will be
        replaced with the values in C{val}.

        If C{index} is a tuple of integers, C{(i,j)}, then replace the
        value of the cell in the C{i}th row and C{j}th column with
        C{val}.
        """
        if isinstance(index, slice):
            raise ValueError('Slicing not supported')


        # table[i,j] = val
        elif isinstance(index, tuple) and len(index)==2:
            i, j = index[0], self.column_index(index[1])
            config_cookie = self._save_config_info([i])
            self._rows[i][j] = val
            if self._reprfunc is not None:
                val = self._reprfunc(i, j, val)
            self._mlb.listboxes[j].insert(i, val)
            self._mlb.listboxes[j].delete(i+1)
            self._restore_config_info(config_cookie)

        # table[i] = val
        else:
            config_cookie = self._save_config_info([index])
            self._checkrow(val)
            self._rows[index] = list(val)
            if self._reprfunc is not None:
                val = [self._reprfunc(index,j,v) for (j,v) in enumerate(val)]
            self._mlb.insert(index, val)
            self._mlb.delete(index+1)
            self._restore_config_info(config_cookie)

    def __delitem__(self, row_index):
        """
        Delete the C{row_index}th row from this table.
        """
        if isinstance(index, slice):
            raise ValueError('Slicing not supported')
        if isinstance(row_index, tuple) and len(row_index)==2:
            raise ValueError('Cannot delete a single cell!')
        del self._rows[row_index]
        self._mlb.delete(row_index)
        if self._DEBUG: self._check_table_vs_mlb()
        
    def __len__(self):
        """
        @return: the number of rows in this table.
        """
        return len(self._rows)

    def _checkrow(self, rowvalue):
        """
        Helper function: check that a given row value has the correct
        number of elements; and if not, raise an exception.
        """
        if len(rowvalue) != self._num_columns:
            raise ValueError('Row %r has %d columns; expected %d' %
                             (rowvalue, len(rowvalue), self._num_columns))

    #/////////////////////////////////////////////////////////////////
    # Columns
    #/////////////////////////////////////////////////////////////////

    column_names = property(lambda self: self._mlb.column_names, doc="""
        A list of the names of the columns in this table.""")

    def column_index(self, i):
        """
        If C{i} is a valid column index integer, then return it as is.
        Otherwise, check if C{i} is used as the name for any column;
        if so, return that column's index.  Otherwise, raise a
        C{KeyError} exception.
        """
        if isinstance(i, int) and 0 <= i < self._num_columns:
            return i
        else:
            # This raises a key error if the column is not found.
            return self._column_name_to_index[i]

    def hide_column(self, column_index):
        """@see: L{MultiListbox.hide_column()}"""
        self._mlb.hide_column(self.column_index(column_index))

    def show_column(self, column_index):
        """@see: L{MultiListbox.show_column()}"""
        self._mlb.show_column(self.column_index(column_index))

    #/////////////////////////////////////////////////////////////////
    # Selection
    #/////////////////////////////////////////////////////////////////

    def selected_row(self):
        """
        Return the index of the currently selected row, or C{None} if
        no row is selected.  To get the row value itself, use
        C{table[table.selected_row()]}.
        """
        sel = self._mlb.curselection()
        if sel: return int(sel[0])
        else: return None

    def select(self, index=None, delta=None, see=True):
        """@see: L{MultiListbox.select()}"""
        self._mlb.select(index, delta, see)

    #/////////////////////////////////////////////////////////////////
    # Sorting
    #/////////////////////////////////////////////////////////////////

    def sort_by(self, column_index, order='toggle'):
        """
        Sort the rows in this table, using the specified column's
        values as a sort key.

        @param column_index: Specifies which column to sort, using
            either a column index (C{int}) or a column's label name
            (C{str}).
            
        @param order: Specifies whether to sort the values in
            ascending or descending order:

              - C{'ascending'}: Sort from least to greatest.
              - C{'descending'}: Sort from greatest to least.
              - C{'toggle'}: If the most recent call to C{sort_by()}
                sorted the table by the same column C({column_index}),
                then reverse the rows; otherwise sort in ascending
                order.
        """
        if order not in ('ascending', 'descending', 'toggle'):
            raise ValueError('sort_by(): order should be "ascending", '
                             '"descending", or "toggle".')
        column_index = self.column_index(column_index)
        config_cookie = self._save_config_info(index_by_id=True)

        # Sort the rows.
        if order == 'toggle' and column_index == self._sortkey:
            self._rows.reverse()
        else:
            self._rows.sort(key=operator.itemgetter(column_index),
                            reverse=(order=='descending'))
            self._sortkey = column_index

        # Redraw the table.
        self._fill_table()
        self._restore_config_info(config_cookie, index_by_id=True, see=True)
        if self._DEBUG: self._check_table_vs_mlb()
    
    def _sort(self, event):
        """Event handler for clicking on a column label -- sort by
        that column."""
        column_index = event.widget.column_index
        
        # If they click on the far-left of far-right of a column's
        # label, then resize rather than sorting.
        if self._mlb._resize_column(event):
            return 'continue'

        # Otherwise, sort.
        else:
            self.sort_by(column_index)
            return 'continue'

    #/////////////////////////////////////////////////////////////////
    #{ Table Drawing Helpers
    #/////////////////////////////////////////////////////////////////

    def _fill_table(self, save_config=True):
        """
        Re-draw the table from scratch, by clearing out the table's
        multi-column listbox; and then filling it in with values from
        C{self._rows}.  Note that any cell-, row-, or column-specific
        color configuration that has been done will be lost.  The
        selection will also be lost -- i.e., no row will be selected
        after this call completes.
        """
        self._mlb.delete(0, 'end')
        for i, row in enumerate(self._rows):
            if self._reprfunc is not None:
                row = [self._reprfunc(i,j,v) for (j,v) in enumerate(row)]
            self._mlb.insert('end', row)

    def _get_itemconfig(self, r, c):
        return dict( (k, self._mlb.itemconfig(r, c, k)[-1])
                     for k in ('foreground', 'selectforeground',
                               'background', 'selectbackground') )

    def _save_config_info(self, row_indices=None, index_by_id=False):
        """
        Return a 'cookie' containing information about which row is
        selected, and what color configurations have been applied.
        this information can the be re-applied to the table (after
        making modifications) using L{_restore_config_info()}.  Color
        configuration information will be saved for any rows in
        C{row_indices}, or in the entire table, if
        C{row_indices=None}.  If C{index_by_id=True}, the the cookie
        will associate rows with their configuration information based
        on the rows' python id.  This is useful when performing
        operations that re-arrange the rows (e.g. C{sort}).  If
        C{index_by_id=False}, then it is assumed that all rows will be
        in the same order when C{_restore_config_info()} is called.
        """
        # Default value for row_indices is all rows.
        if row_indices is None:
            row_indices = range(len(self._rows))

        # Look up our current selection.
        selection = self.selected_row()
        if index_by_id and selection is not None:
            selection = id(self._rows[selection])

        # Look up the color configuration info for each row.
        if index_by_id:
            config = dict((id(self._rows[r]), [self._get_itemconfig(r, c)
                                        for c in range(self._num_columns)])
                          for r in row_indices)
        else:
            config = dict((r, [self._get_itemconfig(r, c)
                               for c in range(self._num_columns)])
                          for r in row_indices)
        

        return selection, config

    def _restore_config_info(self, cookie, index_by_id=False, see=False):
        """
        Restore selection & color configuration information that was
        saved using L{_save_config_info}.
        """
        selection, config = cookie

        # Clear the selection.
        if selection is None:
            self._mlb.selection_clear(0, 'end')

        # Restore selection & color config
        if index_by_id:
            for r, row in enumerate(self._rows):
                if id(row) in config:
                    for c in range(self._num_columns):
                        self._mlb.itemconfigure(r, c, config[id(row)][c])
                if id(row) == selection:
                    self._mlb.select(r, see=see)
        else:
            if selection is not None:
                self._mlb.select(selection, see=see)
            for r in config:
                for c in range(self._num_columns):
                    self._mlb.itemconfigure(r, c, config[r][c])
        
    #/////////////////////////////////////////////////////////////////
    # Debugging (Invariant Checker)
    #/////////////////////////////////////////////////////////////////

    _DEBUG = False
    """If true, then run L{_check_table_vs_mlb()} after any operation
       that modifies the table."""
    
    def _check_table_vs_mlb(self):
        """
        Verify that the contents of the table's L{_rows} variable match
        the contents of its multi-listbox (L{_mlb}).  This is just
        included for debugging purposes, to make sure that the
        list-modifying operations are working correctly.
        """
        for col in self._mlb.listboxes:
            assert len(self) == col.size()
        for row in self:
            assert len(row) == self._num_columns
        assert self._num_columns == len(self._mlb.column_names)
        #assert self._column_names == self._mlb.column_names
        for i, row in enumerate(self):
            for j, cell in enumerate(row):
                if self._reprfunc is not None:
                    cell = self._reprfunc(i, j, cell)
                assert self._mlb.get(i)[j] == cell

######################################################################
# Demo/Test Function
######################################################################

# update this to use new WordNet API
def demo():
    root = Tk()
    root.bind('<Control-q>', lambda e: root.destroy())

    table = Table(root, 'Word Synset Hypernym Hyponym'.split(),
                  column_weights=[0, 1, 1, 1], 
                  reprfunc=(lambda i,j,s: '  %s' % s))
    table.pack(expand=True, fill='both')

    from nltk.corpus import wordnet
    from nltk.corpus import brown
    for word, pos in sorted(set(brown.tagged_words()[:500])):
        if pos[0] != 'N': continue
        word = word.lower()
        for synset in wordnet.synsets(word):
            hyper = (synset.hypernyms()+[''])[0]
            hypo = (synset.hyponyms()+[''])[0]
            table.append([word,
                          getattr(synset, 'definition', '*none*'),
                          getattr(hyper, 'definition', '*none*'),
                          getattr(hypo, 'definition', '*none*')])

    table.columnconfig('Word', background='#afa')
    table.columnconfig('Synset', background='#efe')
    table.columnconfig('Hypernym', background='#fee')
    table.columnconfig('Hyponym', background='#ffe')
    for row in range(len(table)):
        for column in ('Hypernym', 'Hyponym'):
            if table[row, column] == '*none*':
                table.itemconfig(row, column, foreground='#666',
                                 selectforeground='#666')
    root.mainloop()
                
if __name__ == '__main__':
    demo()
