Spaces:
Sleeping
Sleeping
# Natural Language Toolkit: Table widget | |
# | |
# Copyright (C) 2001-2023 NLTK Project | |
# Author: Edward Loper <[email protected]> | |
# URL: <https://www.nltk.org/> | |
# For license information, see LICENSE.TXT | |
""" | |
Tkinter widgets for displaying multi-column listboxes and tables. | |
""" | |
import operator | |
from tkinter import Frame, Label, Listbox, Scrollbar, Tk | |
###################################################################### | |
# Multi-Column Listbox | |
###################################################################### | |
class MultiListbox(Frame): | |
""" | |
A multi-column listbox, where the current selection applies to an | |
entire row. Based on the MultiListbox Tkinter widget | |
recipe from the Python Cookbook (https://code.activestate.com/recipes/52266/) | |
For the most part, ``MultiListbox`` methods delegate to its | |
contained listboxes. For any methods that do not have docstrings, | |
see ``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 ``columns`` is an integer, | |
then 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 ``label_*`` to configure all labels; and ``listbox_*`` | |
to configure all listboxes. E.g.: | |
>>> root = Tk() # doctest: +SKIP | |
>>> MultiListbox(root, ["Subject", "Sender", "Date"], label_foreground='red').pack() # doctest: +SKIP | |
""" | |
# If columns was specified as an int, convert it to a list. | |
if isinstance(columns, int): | |
columns = list(range(columns)) | |
include_labels = False | |
else: | |
include_labels = True | |
if len(columns) == 0: | |
raise ValueError("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 wheel 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 (disable | |
# the default listbox behavior, which scrolls): | |
lb.bind("<B1-Leave>", lambda e: "break") | |
# Columns can be resized by dragging them: | |
lb.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 ``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 | |
``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() / lb["width"] | |
x1 = event.x + event.widget.winfo_x() | |
x2 = lb.winfo_x() + lb.winfo_width() | |
lb["width"] = max(3, lb["width"] + (x1 - x2) // charwidth) | |
def _resize_column_buttonrelease_cb(self, event): | |
event.widget.unbind("<ButtonRelease-%d>" % event.num) | |
event.widget.unbind("<Motion>") | |
# ///////////////////////////////////////////////////////////////// | |
# Properties | |
# ///////////////////////////////////////////////////////////////// | |
def column_names(self): | |
""" | |
A tuple containing the names of the columns used by this | |
multi-column listbox. | |
""" | |
return self._column_names | |
def column_labels(self): | |
""" | |
A tuple containing the ``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 | |
``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. | |
""" | |
return tuple(self._labels) | |
def listboxes(self): | |
""" | |
A tuple containing the ``Tkinter.Listbox`` widgets used to | |
display individual columns. These widgets will all be | |
augmented with a ``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. | |
""" | |
return tuple(self._listboxes) | |
# ///////////////////////////////////////////////////////////////// | |
# 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 ``index`` is specified, then select | |
row ``index``. Otherwise, if ``delta`` is specified, then move | |
the current selection by ``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 ``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 ``label_*`` to configure all | |
labels; and ``listbox_*`` to configure all listboxes. E.g.: | |
>>> master = Tk() # doctest: +SKIP | |
>>> mlb = MultiListbox(master, 5) # doctest: +SKIP | |
>>> mlb.configure(label_foreground='red') # doctest: +SKIP | |
>>> mlb.configure(listbox_foreground='red') # doctest: +SKIP | |
""" | |
cnf = dict(list(cnf.items()) + list(kw.items())) | |
for (key, val) in list(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 | |
``self.configure({key,val``)}. See ``configure()``. | |
""" | |
self.configure({key: val}) | |
def rowconfigure(self, row_index, cnf={}, **kw): | |
""" | |
Configure all table cells in the given row. Valid keyword | |
arguments are: ``background``, ``bg``, ``foreground``, ``fg``, | |
``selectbackground``, ``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: ``background``, ``bg``, ``foreground``, ``fg``, | |
``selectbackground``, ``selectforeground``. | |
""" | |
lb = self._listboxes[col_index] | |
cnf = dict(list(cnf.items()) + list(kw.items())) | |
for (key, val) in list(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: ``background``, ``bg``, ``foreground``, | |
``fg``, ``selectbackground``, ``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 ``'end'``) accepted by | |
``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, list(zip(*rows))): | |
lb.insert(index, *elts) | |
def get(self, first, last=None): | |
""" | |
Return the value(s) of the specified row(s). If ``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 ``(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 ``get()``, and | |
you must supply its values when calling ``insert()``. It is | |
safe to call this on a column that is already hidden. | |
:see: ``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 ``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 ``Tkinter.Label`` widget in this | |
mult-column listbox that will call ``func`` in response to the | |
event 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 ``Tkinter.Listbox`` widget in this | |
mult-column listbox that will call ``func`` in response to the | |
event 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 ``Tkinter.Label`` and ``Tkinter.Listbox`` | |
widget in this mult-column listbox that will call ``func`` in | |
response to the event 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: | |
""" | |
A display widget for a table of values, based on a ``MultiListbox`` | |
widget. For many purposes, ``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 list 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' # doctest: +SKIP | |
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']) # doctest: +SKIP | |
John | |
You can configure the colors for individual rows, columns, or | |
cells using ``rowconfig()``, ``columnconfig()``, and ``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 *columns* will not be | |
applied to the new row. | |
Note: Although ``Table`` acts like a widget in some ways (e.g., it | |
defines ``grid()``, ``pack()``, and ``bind()``), it is not itself a | |
widget; it just contains one. This is because widgets need to | |
define ``__getitem__()``, ``__setitem__()``, and ``__nonzero__()`` in | |
a way that's incompatible with the fact that ``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: Tkinter.Widget | |
:param master: The widget that should contain the new table. | |
:type column_names: list(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: list(list) | |
:param rows: A list of row values used to initialize the table. | |
Each row value should be a tuple of cell values, one for | |
each column in the row. | |
:type scrollbar: bool | |
:param scrollbar: If true, then create a scrollbar for the | |
new table widget. | |
:type click_to_sort: 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: function | |
:param reprfunc: If specified, then use this function to | |
convert each table cell value to a string suitable for | |
display. ``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 ``MultiListbox``. See ``MultiListbox.__init__()`` | |
for details. | |
""" | |
self._num_columns = len(column_names) | |
self._reprfunc = reprfunc | |
self._frame = Frame(master) | |
self._column_name_to_index = {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 ``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 ``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 | |
``func`` in response to the event sequence.""" | |
self._mlb.bind(sequence, func, add) | |
def rowconfigure(self, row_index, cnf={}, **kw): | |
""":see: ``MultiListbox.rowconfigure()``""" | |
self._mlb.rowconfigure(row_index, cnf, **kw) | |
def columnconfigure(self, col_index, cnf={}, **kw): | |
""":see: ``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: ``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: ``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: ``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: ``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 | |
``row_index``. If the table contains any rows whose row index | |
is greater than or equal to ``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 initialize 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 | |
``index`` is an integer, then the row value for the ``index``th | |
row. This row value consists of a tuple of cell values, one | |
for each column in the row. If ``index`` is a tuple of two | |
integers, ``(i,j)``, then return the value of the cell in the | |
``i``th row and the ``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 | |
``val``. | |
If ``index`` is an integer, then ``val`` should be a row value | |
(i.e., a tuple of cell values, one for each column). In this | |
case, the values of the ``index``th row of the table will be | |
replaced with the values in ``val``. | |
If ``index`` is a tuple of integers, ``(i,j)``, then replace the | |
value of the cell in the ``i``th row and ``j``th column with | |
``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 ``row_index``th row from this table. | |
""" | |
if isinstance(row_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 | |
# ///////////////////////////////////////////////////////////////// | |
def column_names(self): | |
"""A list of the names of the columns in this table.""" | |
return self._mlb.column_names | |
def column_index(self, i): | |
""" | |
If ``i`` is a valid column index integer, then return it as is. | |
Otherwise, check if ``i`` is used as the name for any column; | |
if so, return that column's index. Otherwise, raise a | |
``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: ``MultiListbox.hide_column()``""" | |
self._mlb.hide_column(self.column_index(column_index)) | |
def show_column(self, column_index): | |
""":see: ``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 None if | |
no row is selected. To get the row value itself, use | |
``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: ``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 (int) or a column's label name | |
(str). | |
:param order: Specifies whether to sort the values in | |
ascending or descending order: | |
- ``'ascending'``: Sort from least to greatest. | |
- ``'descending'``: Sort from greatest to least. | |
- ``'toggle'``: If the most recent call to ``sort_by()`` | |
sorted the table by the same column (``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 | |
``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 { | |
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 ``_restore_config_info()``. Color | |
configuration information will be saved for any rows in | |
``row_indices``, or in the entire table, if | |
``row_indices=None``. If ``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. ``sort``). If | |
``index_by_id=False``, then it is assumed that all rows will be | |
in the same order when ``_restore_config_info()`` is called. | |
""" | |
# Default value for row_indices is all rows. | |
if row_indices is None: | |
row_indices = list(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 = { | |
id(self._rows[r]): [ | |
self._get_itemconfig(r, c) for c in range(self._num_columns) | |
] | |
for r in row_indices | |
} | |
else: | |
config = { | |
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 ``_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 ``_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 ``_rows`` variable match | |
the contents of its multi-listbox (``_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 brown, wordnet | |
for word, pos in sorted(set(brown.tagged_words()[:500])): | |
if pos[0] != "N": | |
continue | |
word = word.lower() | |
for synset in wordnet.synsets(word): | |
try: | |
hyper_def = synset.hypernyms()[0].definition() | |
except: | |
hyper_def = "*none*" | |
try: | |
hypo_def = synset.hypernyms()[0].definition() | |
except: | |
hypo_def = "*none*" | |
table.append([word, synset.definition(), hyper_def, hypo_def]) | |
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() | |