2658 lines
90 KiB
Python
2658 lines
90 KiB
Python
|
import tkinter as tk
|
|||
|
import ttkbootstrap as ttk
|
|||
|
from ttkbootstrap.constants import *
|
|||
|
from math import ceil
|
|||
|
from datetime import datetime
|
|||
|
from tkinter import font
|
|||
|
from ttkbootstrap import utility
|
|||
|
from typing import Any, Dict, List, Union
|
|||
|
from ttkbootstrap.localization import MessageCatalog
|
|||
|
|
|||
|
UPARROW = "⬆"
|
|||
|
DOWNARROW = "⬇"
|
|||
|
ASCENDING = 0
|
|||
|
DESCENDING = 1
|
|||
|
|
|||
|
|
|||
|
class TableColumn:
|
|||
|
"""Represents a column in a Tableview object"""
|
|||
|
|
|||
|
def __init__(
|
|||
|
self,
|
|||
|
tableview,
|
|||
|
cid,
|
|||
|
text,
|
|||
|
image="",
|
|||
|
command="",
|
|||
|
anchor=W,
|
|||
|
width=200,
|
|||
|
minwidth=20,
|
|||
|
stretch=False,
|
|||
|
):
|
|||
|
"""
|
|||
|
Parameters:
|
|||
|
|
|||
|
tableview (Tableview):
|
|||
|
The parent tableview object.
|
|||
|
|
|||
|
cid (str):
|
|||
|
The column id.
|
|||
|
|
|||
|
text (str):
|
|||
|
The header text.
|
|||
|
|
|||
|
image (PhotoImage):
|
|||
|
An image that is displayed to the left of the header text.
|
|||
|
|
|||
|
command (Callable):
|
|||
|
A function called whenever the header button is clicked.
|
|||
|
|
|||
|
anchor (str):
|
|||
|
The position of the header text within the header. One
|
|||
|
of "e", "w", "center".
|
|||
|
|
|||
|
width (int):
|
|||
|
Specifies the width of the column in pixels.
|
|||
|
|
|||
|
minwidth (int):
|
|||
|
Specifies the minimum width of the column in pixels.
|
|||
|
|
|||
|
stretch (bool):
|
|||
|
Specifies whether or not the column width should be
|
|||
|
adjusted whenever the widget is resized or the user
|
|||
|
drags the column separator.
|
|||
|
"""
|
|||
|
self._table = tableview
|
|||
|
self._cid = cid
|
|||
|
self._headertext = text
|
|||
|
self._sort = ASCENDING
|
|||
|
self._settings_column = {}
|
|||
|
self._settings_heading = {}
|
|||
|
|
|||
|
self.view: ttk.Treeview = tableview.view
|
|||
|
self.view.column(
|
|||
|
self._cid,
|
|||
|
width=width,
|
|||
|
minwidth=minwidth,
|
|||
|
stretch=stretch,
|
|||
|
anchor=anchor,
|
|||
|
)
|
|||
|
self.view.heading(
|
|||
|
self._cid,
|
|||
|
text=text,
|
|||
|
anchor=anchor,
|
|||
|
image=image,
|
|||
|
command=command,
|
|||
|
)
|
|||
|
self._capture_settings()
|
|||
|
self._table._cidmap[self._cid] = self
|
|||
|
|
|||
|
@property
|
|||
|
def headertext(self):
|
|||
|
"""The text on the header label"""
|
|||
|
return self._headertext
|
|||
|
|
|||
|
@property
|
|||
|
def columnsort(self):
|
|||
|
"""Indicates how the column is to be sorted when the sorting
|
|||
|
method is invoked."""
|
|||
|
return self._sort
|
|||
|
|
|||
|
@columnsort.setter
|
|||
|
def columnsort(self, value):
|
|||
|
self._sort = value
|
|||
|
|
|||
|
@property
|
|||
|
def cid(self):
|
|||
|
"""A unique column identifier"""
|
|||
|
return str(self._cid)
|
|||
|
|
|||
|
@property
|
|||
|
def tableindex(self):
|
|||
|
"""The index of the column as it is in the table configuration."""
|
|||
|
cols = self.view.cget("columns")
|
|||
|
if cols is None:
|
|||
|
return
|
|||
|
try:
|
|||
|
return cols.index(self.cid)
|
|||
|
except IndexError:
|
|||
|
return
|
|||
|
|
|||
|
@property
|
|||
|
def displayindex(self):
|
|||
|
"""The index of the column as it is displayed"""
|
|||
|
cols = self.view.cget("displaycolumns")
|
|||
|
if "#all" in cols:
|
|||
|
return self.tableindex
|
|||
|
else:
|
|||
|
return cols.index(self.cid)
|
|||
|
|
|||
|
def configure(self, opt=None, **kwargs):
|
|||
|
"""Configure the column. If opt is provided, the
|
|||
|
current value is returned, otherwise, sets the widget
|
|||
|
options specified in kwargs. See the documentation for
|
|||
|
`Tableview.insert_column` for configurable options.
|
|||
|
|
|||
|
Parameters:
|
|||
|
|
|||
|
opt (str):
|
|||
|
A configuration option to query.
|
|||
|
|
|||
|
**kwargs (Dict):
|
|||
|
Optional keyword arguments used to configure the
|
|||
|
column and headers.
|
|||
|
"""
|
|||
|
# return queried options
|
|||
|
if opt is not None:
|
|||
|
if opt in ("anchor", "width", "minwidth", "stretch"):
|
|||
|
return self.view.column(self.cid, opt)
|
|||
|
elif opt in ("command", "text", "image"):
|
|||
|
return self.view.heading(self.cid, opt)
|
|||
|
else:
|
|||
|
return
|
|||
|
|
|||
|
# configure column and heading
|
|||
|
for k, v in kwargs.items():
|
|||
|
if k in ("anchor", "width", "minwidth", "stretch"):
|
|||
|
self._settings_column[k] = v
|
|||
|
elif k in ("command", "text", "image"):
|
|||
|
self._settings_heading[k] = v
|
|||
|
self.view.column(self._cid, **self._settings_column)
|
|||
|
self.view.heading(self._cid, **self._settings_heading)
|
|||
|
if "text" in kwargs:
|
|||
|
self._headertext = kwargs["text"]
|
|||
|
|
|||
|
def show(self):
|
|||
|
"""Make the column visible in the tableview"""
|
|||
|
displaycols = list(self.view.cget("displaycolumns"))
|
|||
|
if "#all" in displaycols:
|
|||
|
return
|
|||
|
if self.cid in displaycols:
|
|||
|
return
|
|||
|
columns = list(self.view.cget("columns"))
|
|||
|
index = columns.index(self.cid)
|
|||
|
displaycols.insert(index, self.cid)
|
|||
|
self.view.configure(displaycolumns=displaycols)
|
|||
|
|
|||
|
def hide(self):
|
|||
|
"""Hide the column in the tableview"""
|
|||
|
displaycols = list(self.view.cget("displaycolumns"))
|
|||
|
cols = list(self.view.cget("columns"))
|
|||
|
if "#all" in displaycols:
|
|||
|
displaycols = cols
|
|||
|
displaycols.remove(self.cid)
|
|||
|
self.view.configure(displaycolumns=displaycols)
|
|||
|
|
|||
|
def delete(self):
|
|||
|
"""Remove the column from the tableview permanently."""
|
|||
|
# update the tablerow columns
|
|||
|
index = self.tableindex
|
|||
|
if index is None:
|
|||
|
return
|
|||
|
|
|||
|
for row in self._table.tablerows:
|
|||
|
row.values.pop(index)
|
|||
|
row.refresh()
|
|||
|
|
|||
|
# actual columns
|
|||
|
cols = list(self.view.cget("columns"))
|
|||
|
cols.remove(self.cid)
|
|||
|
self._table.tablecolumns.remove(self)
|
|||
|
|
|||
|
# visible columns
|
|||
|
dcols = list(self.view.cget("displaycolumns"))
|
|||
|
if "#all" in dcols:
|
|||
|
dcols = cols
|
|||
|
else:
|
|||
|
dcols.remove(self.cid)
|
|||
|
|
|||
|
# remove cid mapping
|
|||
|
self._table.cidmap.pop(self._cid)
|
|||
|
|
|||
|
# reconfigure the tableview column and displaycolumns
|
|||
|
self.view.configure(columns=cols, displaycolumns=dcols)
|
|||
|
|
|||
|
# remove the internal object references
|
|||
|
for i, column in enumerate(self._table.tablecolumns):
|
|||
|
if column.cid == self.cid:
|
|||
|
self._table.tablecolumns.pop(i)
|
|||
|
else:
|
|||
|
column.restore_settings()
|
|||
|
|
|||
|
def restore_settings(self):
|
|||
|
"""Update the configuration based on stored settings"""
|
|||
|
self.view.column(self.cid, **self._settings_column)
|
|||
|
self.view.heading(self.cid, **self._settings_heading)
|
|||
|
|
|||
|
def _capture_settings(self):
|
|||
|
"""Update the stored settings for the column and heading.
|
|||
|
This is required because the settings are erased whenever
|
|||
|
the `columns` parameter is configured in the underlying
|
|||
|
Treeview widget."""
|
|||
|
self._settings_heading = self.view.heading(self.cid)
|
|||
|
self._settings_heading.pop("state")
|
|||
|
self._settings_column = self.view.column(self.cid)
|
|||
|
self._settings_column.pop("id")
|
|||
|
|
|||
|
|
|||
|
class TableRow:
|
|||
|
"""Represents a row in a Tableview object"""
|
|||
|
|
|||
|
_cnt = 0
|
|||
|
|
|||
|
def __init__(self, tableview, values):
|
|||
|
"""
|
|||
|
Parameters:
|
|||
|
|
|||
|
tableview (Tableview):
|
|||
|
The Tableview widget that contains this row
|
|||
|
|
|||
|
values (List[Any, ...]):
|
|||
|
A list of values to display in the row
|
|||
|
"""
|
|||
|
self.view: ttk.Treeview = tableview.view
|
|||
|
self._values = list(values)
|
|||
|
self._iid = None
|
|||
|
self._sort = TableRow._cnt + 1
|
|||
|
self._table = tableview
|
|||
|
|
|||
|
# increment cnt
|
|||
|
TableRow._cnt += 1
|
|||
|
|
|||
|
@property
|
|||
|
def values(self):
|
|||
|
"""The table row values"""
|
|||
|
return self._values
|
|||
|
|
|||
|
@values.setter
|
|||
|
def values(self, values):
|
|||
|
self._values = values
|
|||
|
self.refresh()
|
|||
|
|
|||
|
@property
|
|||
|
def iid(self):
|
|||
|
"""A unique record identifier"""
|
|||
|
return str(self._iid)
|
|||
|
|
|||
|
def configure(self, opt=None, **kwargs):
|
|||
|
"""Configure the row. If opt is provided, the
|
|||
|
current value is returned, otherwise, sets the widget
|
|||
|
options specified in kwargs. See the documentation for
|
|||
|
`Tableview.insert_row` for configurable options.
|
|||
|
|
|||
|
Parameters:
|
|||
|
|
|||
|
opt (str):
|
|||
|
A configuration option to query.
|
|||
|
|
|||
|
**kwargs { values, tags }:
|
|||
|
Optional keyword arguments used to configure the
|
|||
|
row.
|
|||
|
"""
|
|||
|
if self._iid is None:
|
|||
|
self.build()
|
|||
|
|
|||
|
if opt is not None:
|
|||
|
return self.view.item(self.iid, opt)
|
|||
|
elif 'values' in kwargs:
|
|||
|
values = kwargs.pop('values')
|
|||
|
self.values = values
|
|||
|
else:
|
|||
|
self.view.item(self.iid, **kwargs)
|
|||
|
|
|||
|
def show(self, striped=False):
|
|||
|
"""Show the row in the data table view"""
|
|||
|
if self._iid is None:
|
|||
|
self.build()
|
|||
|
self.view.reattach(self.iid, "", END)
|
|||
|
|
|||
|
# remove existing stripes
|
|||
|
tags = list(self.view.item(self.iid, "tags"))
|
|||
|
try:
|
|||
|
tags.remove("striped")
|
|||
|
except ValueError:
|
|||
|
pass
|
|||
|
|
|||
|
# add stripes (if needed)
|
|||
|
if striped:
|
|||
|
tags.append("striped")
|
|||
|
self.view.item(self.iid, tags=tags)
|
|||
|
|
|||
|
def delete(self):
|
|||
|
"""Delete the row from the dataset"""
|
|||
|
if self.iid:
|
|||
|
self._table.iidmap.pop(self.iid)
|
|||
|
self._table.tablerows_visible.remove(self)
|
|||
|
self._table._tablerows.remove(self)
|
|||
|
self._table.load_table_data()
|
|||
|
self.view.delete(self.iid)
|
|||
|
|
|||
|
def hide(self):
|
|||
|
"""Remove the row from the data table view"""
|
|||
|
self.view.detach(self.iid)
|
|||
|
|
|||
|
def refresh(self):
|
|||
|
"""Syncs the tableview values with the object values"""
|
|||
|
if self._iid:
|
|||
|
self.view.item(self.iid, values=self.values)
|
|||
|
|
|||
|
def build(self):
|
|||
|
"""Create the row object in the `Treeview` and capture
|
|||
|
the resulting item id (iid).
|
|||
|
"""
|
|||
|
if self._iid is None:
|
|||
|
self._iid = self.view.insert("", END, values=self.values)
|
|||
|
self._table.iidmap[self.iid] = self
|
|||
|
|
|||
|
|
|||
|
class TableEvent:
|
|||
|
"""A container class for holding table event objects"""
|
|||
|
|
|||
|
def __init__(self, column: TableColumn, row: TableRow):
|
|||
|
self.column = column
|
|||
|
self.row = row
|
|||
|
|
|||
|
|
|||
|
class Tableview(ttk.Frame):
|
|||
|
"""A class built on the `ttk.Treeview` widget for arranging data in
|
|||
|
rows and columns. The underlying Treeview object and its methods are
|
|||
|
exposed in the `Tableview.view` property.
|
|||
|
|
|||
|
A Tableview object contains various features such has striped rows,
|
|||
|
pagination, and autosized and autoaligned columns.
|
|||
|
|
|||
|
The pagination option is recommended when loading a lot of data as
|
|||
|
the table records are inserted on-demand. Table records are only
|
|||
|
created when requested to be in a page view. This allows the table
|
|||
|
to be loaded very quickly even with hundreds of thousands of
|
|||
|
records.
|
|||
|
|
|||
|
All table columns are sortable. Clicking a column header will toggle
|
|||
|
between sorting "ascending" and "descending".
|
|||
|
|
|||
|
Columns are configurable by passing a simple list of header names or
|
|||
|
by passing in a dictionary of column names with settings. You can
|
|||
|
use both as well, as in the example below, where a column header
|
|||
|
name is use for one column, and a dictionary of settings is used
|
|||
|
for another.
|
|||
|
|
|||
|
The object has a right-click menu on the header and the cells that
|
|||
|
allow you to configure various settings.
|
|||
|
|
|||
|
![](../../assets/widgets/tableview-1.png)
|
|||
|
![](../../assets/widgets/tableview-2.png)
|
|||
|
|
|||
|
Examples:
|
|||
|
|
|||
|
Adding data with the constructor
|
|||
|
```python
|
|||
|
import ttkbootstrap as ttk
|
|||
|
from ttkbootstrap.tableview import Tableview
|
|||
|
from ttkbootstrap.constants import *
|
|||
|
|
|||
|
app = ttk.Window()
|
|||
|
colors = app.style.colors
|
|||
|
|
|||
|
coldata = [
|
|||
|
{"text": "LicenseNumber", "stretch": False},
|
|||
|
"CompanyName",
|
|||
|
{"text": "UserCount", "stretch": False},
|
|||
|
]
|
|||
|
|
|||
|
rowdata = [
|
|||
|
('A123', 'IzzyCo', 12),
|
|||
|
('A136', 'Kimdee Inc.', 45),
|
|||
|
('A158', 'Farmadding Co.', 36)
|
|||
|
]
|
|||
|
|
|||
|
dt = Tableview(
|
|||
|
master=app,
|
|||
|
coldata=coldata,
|
|||
|
rowdata=rowdata,
|
|||
|
paginated=True,
|
|||
|
searchable=True,
|
|||
|
bootstyle=PRIMARY,
|
|||
|
stripecolor=(colors.light, None),
|
|||
|
)
|
|||
|
dt.pack(fill=BOTH, expand=YES, padx=10, pady=10)
|
|||
|
|
|||
|
app.mainloop()
|
|||
|
```
|
|||
|
|
|||
|
Add data with methods
|
|||
|
```python
|
|||
|
dt.insert_row('end', ['Marzale LLC', 26])
|
|||
|
```
|
|||
|
"""
|
|||
|
|
|||
|
def __init__(
|
|||
|
self,
|
|||
|
master=None,
|
|||
|
bootstyle=DEFAULT,
|
|||
|
coldata=[],
|
|||
|
rowdata=[],
|
|||
|
paginated=False,
|
|||
|
searchable=False,
|
|||
|
autofit=False,
|
|||
|
autoalign=True,
|
|||
|
stripecolor=None,
|
|||
|
pagesize=10,
|
|||
|
height=10,
|
|||
|
delimiter=",",
|
|||
|
):
|
|||
|
"""
|
|||
|
Parameters:
|
|||
|
|
|||
|
master (Widget):
|
|||
|
The parent widget.
|
|||
|
|
|||
|
bootstyle (str):
|
|||
|
A style keyword used to set the focus color of the entry
|
|||
|
and the background color of the date button. Available
|
|||
|
options include -> primary, secondary, success, info,
|
|||
|
warning, danger, dark, light.
|
|||
|
|
|||
|
coldata (List[str | Dict]):
|
|||
|
An iterable containing either the heading name or a
|
|||
|
dictionary of column settings. Configurable settings
|
|||
|
include >> text, image, command, anchor, width, minwidth,
|
|||
|
maxwidth, stretch. Also see `Tableview.insert_column`.
|
|||
|
|
|||
|
rowdata (List):
|
|||
|
An iterable of row data. The lenth of each row of data
|
|||
|
must match the number of columns. Also see
|
|||
|
`Tableview.insert_row`.
|
|||
|
|
|||
|
paginated (bool):
|
|||
|
Specifies that the data is to be paginated. A pagination
|
|||
|
frame will be created below the table with controls that
|
|||
|
enable the user to page forward and backwards in the
|
|||
|
data set.
|
|||
|
|
|||
|
pagesize (int):
|
|||
|
When `paginated=True`, this specifies the number of rows
|
|||
|
to show per page.
|
|||
|
|
|||
|
searchable (bool):
|
|||
|
If `True`, a searchbar will be created above the table.
|
|||
|
Press the <Return> key to initiate a search. Searching
|
|||
|
with an empty string will reset the search criteria, or
|
|||
|
pressing the reset button to the right of the search
|
|||
|
bar. Currently, the search method looks for any row
|
|||
|
that contains the search text. The filtered results
|
|||
|
are displayed in the table view.
|
|||
|
|
|||
|
autofit (bool):
|
|||
|
If `True`, the table columns will be automatically sized
|
|||
|
when loaded based on the records in the current view.
|
|||
|
Also see `Tableview.autofit_columns`.
|
|||
|
|
|||
|
autoalign (bool):
|
|||
|
If `True`, the column headers and data are automatically
|
|||
|
aligned. Numbers and number headers are right-aligned
|
|||
|
and all other data types are left-aligned. The auto
|
|||
|
align method evaluates the first record in each column
|
|||
|
to determine the data type for alignment. Also see
|
|||
|
`Tableview.autoalign_columns`.
|
|||
|
|
|||
|
stripecolor (Tuple[str, str]):
|
|||
|
If provided, even numbered rows will be color using the
|
|||
|
(background, foreground) specified. You may specify one
|
|||
|
or the other by passing in **None**. For example,
|
|||
|
`stripecolor=('green', None)` will set the stripe
|
|||
|
background as green, but the foreground will remain as
|
|||
|
default. You may use standand color names, hexadecimal
|
|||
|
color codes, or bootstyle color keywords. For example,
|
|||
|
('light', '#222') will set the background to the "light"
|
|||
|
themed ttkbootstrap color and the foreground to the
|
|||
|
specified hexadecimal color. Also see
|
|||
|
`Tableview.apply_table_stripes`.
|
|||
|
|
|||
|
height (int):
|
|||
|
Specifies how many rows will appear in the table's viewport.
|
|||
|
If the number of records extends beyond the table height,
|
|||
|
the user may use the mousewheel or scrollbar to navigate
|
|||
|
the data.
|
|||
|
|
|||
|
delimiter (str):
|
|||
|
The character to use as a delimiter when exporting data
|
|||
|
to CSV.
|
|||
|
"""
|
|||
|
super().__init__(master)
|
|||
|
self._tablecols = []
|
|||
|
self._tablerows = []
|
|||
|
self._tablerows_filtered = []
|
|||
|
self._viewdata = []
|
|||
|
self._rowindex = tk.IntVar(value=0)
|
|||
|
self._pageindex = tk.IntVar(value=1)
|
|||
|
self._pagelimit = tk.IntVar(value=0)
|
|||
|
self._height = height
|
|||
|
self._pagesize = tk.IntVar(value=pagesize)
|
|||
|
self._paginated = paginated
|
|||
|
self._searchable = searchable
|
|||
|
self._stripecolor = stripecolor
|
|||
|
self._autofit = autofit
|
|||
|
self._autoalign = autoalign
|
|||
|
self._filtered = False
|
|||
|
self._sorted = False
|
|||
|
self._searchcriteria = tk.StringVar()
|
|||
|
self._rightclickmenu_cell = None
|
|||
|
self._delimiter = delimiter
|
|||
|
self._iidmap = {} # maps iid to row object
|
|||
|
self._cidmap = {} # maps cid to col object
|
|||
|
|
|||
|
self.view: ttk.Treeview = None
|
|||
|
self._build_tableview_widget(coldata, rowdata, bootstyle)
|
|||
|
|
|||
|
@property
|
|||
|
def tablerows(self):
|
|||
|
"""A list of all tablerow objects"""
|
|||
|
return self._tablerows
|
|||
|
|
|||
|
@property
|
|||
|
def tablerows_filtered(self):
|
|||
|
"""A list of filtered tablerow objects"""
|
|||
|
return self._tablerows_filtered
|
|||
|
|
|||
|
@property
|
|||
|
def tablerows_visible(self):
|
|||
|
"""A list of visible tablerow objects"""
|
|||
|
return self._viewdata
|
|||
|
|
|||
|
@property
|
|||
|
def tablecolumns(self):
|
|||
|
"""A list of table column objects"""
|
|||
|
return self._tablecols
|
|||
|
|
|||
|
@property
|
|||
|
def tablecolumns_visible(self):
|
|||
|
"""A list of visible table column objects"""
|
|||
|
cids = list(self.view.cget("displaycolumns"))
|
|||
|
if "#all" in cids:
|
|||
|
return self._tablecols
|
|||
|
columns = []
|
|||
|
for cid in cids:
|
|||
|
# the cidmap expects an integer
|
|||
|
columns.append(self.cidmap.get(int(cid)))
|
|||
|
return columns
|
|||
|
|
|||
|
@property
|
|||
|
def is_filtered(self):
|
|||
|
"""Indicates whether the table is currently filtered"""
|
|||
|
return self._filtered
|
|||
|
|
|||
|
@property
|
|||
|
def searchcriteria(self):
|
|||
|
"""The criteria used to filter the records when the search
|
|||
|
method is invoked"""
|
|||
|
return self._searchcriteria.get()
|
|||
|
|
|||
|
@searchcriteria.setter
|
|||
|
def searchcriteria(self, value):
|
|||
|
self._searchcriteria.set(value)
|
|||
|
|
|||
|
@property
|
|||
|
def pagesize(self):
|
|||
|
"""The number of records visible on a single page"""
|
|||
|
return self._pagesize.get()
|
|||
|
|
|||
|
@pagesize.setter
|
|||
|
def pagesize(self, value):
|
|||
|
self._pagesize.set(value)
|
|||
|
|
|||
|
@property
|
|||
|
def iidmap(self) -> Dict[str, TableRow]:
|
|||
|
"""A map of iid to tablerow object"""
|
|||
|
return self._iidmap
|
|||
|
|
|||
|
@property
|
|||
|
def cidmap(self) -> Dict[str, TableColumn]:
|
|||
|
"""A map of cid to tablecolumn object"""
|
|||
|
return self._cidmap
|
|||
|
|
|||
|
def configure(self, cnf=None, **kwargs) -> Union[Any, None]:
|
|||
|
"""Configure the internal `Treeview` widget. If cnf is provided,
|
|||
|
value of the option is return. Otherwise the widget is
|
|||
|
configured via kwargs.
|
|||
|
|
|||
|
Parameters:
|
|||
|
|
|||
|
cnf (Any):
|
|||
|
An option to query.
|
|||
|
|
|||
|
**kwargs (Dict):
|
|||
|
Optional keyword arguments used to configure the internal
|
|||
|
Treeview widget.
|
|||
|
|
|||
|
Returns:
|
|||
|
|
|||
|
Union[Any, None]:
|
|||
|
The value of cnf or None.
|
|||
|
"""
|
|||
|
try:
|
|||
|
if "pagesize" in kwargs:
|
|||
|
pagesize: int = kwargs.pop("pagesize")
|
|||
|
self._pagesize.set(value=pagesize)
|
|||
|
|
|||
|
self.view.configure(cnf, **kwargs)
|
|||
|
except:
|
|||
|
super().configure(cnf, **kwargs)
|
|||
|
|
|||
|
# DATA HANDLING
|
|||
|
|
|||
|
def build_table_data(self, coldata, rowdata):
|
|||
|
"""Insert the specified column and row data.
|
|||
|
|
|||
|
The coldata can be either a string column name or a dictionary
|
|||
|
of column settings that are passed to the `insert_column`
|
|||
|
method. You may use a mixture of string and dictionary in
|
|||
|
the list of coldata.
|
|||
|
|
|||
|
!!!warning "Existing table data will be erased.
|
|||
|
This method will completely rebuild the underlying table
|
|||
|
with the new column and row data. Any existing data will
|
|||
|
be lost.
|
|||
|
|
|||
|
Parameters:
|
|||
|
|
|||
|
coldata (List[Union[str, Dict]]):
|
|||
|
An iterable of column names and/or settings.
|
|||
|
|
|||
|
rowdata (List):
|
|||
|
An iterable of row values.
|
|||
|
"""
|
|||
|
# destroy the existing data if existing
|
|||
|
self.purge_table_data()
|
|||
|
|
|||
|
# build the table columns
|
|||
|
for i, col in enumerate(coldata):
|
|||
|
if isinstance(col, str):
|
|||
|
# just a column name
|
|||
|
self.insert_column(i, col)
|
|||
|
else:
|
|||
|
# a dictionary of column settings
|
|||
|
self.insert_column(i, **col)
|
|||
|
|
|||
|
# build the table rows
|
|||
|
for values in rowdata:
|
|||
|
self.insert_row(values=values)
|
|||
|
|
|||
|
# load the table data
|
|||
|
self.load_table_data()
|
|||
|
|
|||
|
# apply table formatting
|
|||
|
if self._autofit:
|
|||
|
self.autofit_columns()
|
|||
|
|
|||
|
if self._autoalign:
|
|||
|
self.autoalign_columns()
|
|||
|
|
|||
|
if self._stripecolor is not None:
|
|||
|
self.apply_table_stripes(self._stripecolor)
|
|||
|
|
|||
|
self.goto_first_page()
|
|||
|
|
|||
|
def insert_row(self, index=END, values=[]) -> TableRow:
|
|||
|
"""Insert a row into the tableview at index.
|
|||
|
|
|||
|
You must call `Tableview.load_table_data()` to update the
|
|||
|
current view. If the data is filtered, you will need to call
|
|||
|
`Tableview.load_table_data(clear_filters=True)`.
|
|||
|
|
|||
|
Parameters:
|
|||
|
|
|||
|
index (Union[int, str]):
|
|||
|
A numerical index that specifieds where to insert
|
|||
|
the record in the dataset. You may also use the string
|
|||
|
'end' to append the record to the end of the data set.
|
|||
|
If the index exceeds the record count, it will be
|
|||
|
appended to the end of the dataset.
|
|||
|
|
|||
|
values (Iterable):
|
|||
|
An iterable of values to insert into the data set.
|
|||
|
The number of columns implied by the list of values
|
|||
|
must match the number of columns in the data set for
|
|||
|
the values to be visible.
|
|||
|
|
|||
|
Returns:
|
|||
|
|
|||
|
TableRow:
|
|||
|
A table row object.
|
|||
|
"""
|
|||
|
rowcount = len(self._tablerows)
|
|||
|
|
|||
|
# validate the index
|
|||
|
if len(values) == 0:
|
|||
|
return
|
|||
|
if index == END:
|
|||
|
index = -1
|
|||
|
elif index > rowcount - 1:
|
|||
|
index = -1
|
|||
|
|
|||
|
record = TableRow(self, values)
|
|||
|
if rowcount == 0 or index == -1:
|
|||
|
self._tablerows.append(record)
|
|||
|
else:
|
|||
|
self._tablerows.insert(index, record)
|
|||
|
|
|||
|
return record
|
|||
|
|
|||
|
def insert_rows(self, index, rowdata):
|
|||
|
"""Insert row after index for each row in *row. If index does
|
|||
|
not exist then the records are appended to the end of the table.
|
|||
|
You can also use the string 'end' to append records at the end
|
|||
|
of the table.
|
|||
|
|
|||
|
Parameters:
|
|||
|
|
|||
|
index (Union[int, str]):
|
|||
|
The location in the data set after where the records
|
|||
|
will be inserted. You may use a numerical index or
|
|||
|
the string 'end', which will append the records to the
|
|||
|
end of the data set.
|
|||
|
|
|||
|
rowdata (List[Any, List]):
|
|||
|
A list of row values to be inserted into the table.
|
|||
|
|
|||
|
Examples:
|
|||
|
|
|||
|
```python
|
|||
|
Tableview.insert_rows('end', ['one', 1], ['two', 2])
|
|||
|
```
|
|||
|
"""
|
|||
|
if len(rowdata) == 0:
|
|||
|
return
|
|||
|
for values in reversed(rowdata):
|
|||
|
self.insert_row(index, values)
|
|||
|
|
|||
|
def delete_column(self, index=None, cid=None, visible=True):
|
|||
|
"""Delete the specified column based on the column index or the
|
|||
|
unique cid.
|
|||
|
|
|||
|
Unless otherwise specified, the index refers to the column index
|
|||
|
as displayed in the tableview.
|
|||
|
|
|||
|
If cid is provided, the column associated with the cid is deleted
|
|||
|
regardless of whether it is in the visible data sets.
|
|||
|
|
|||
|
Parameters:
|
|||
|
|
|||
|
index (int):
|
|||
|
The numerical index of the column.
|
|||
|
|
|||
|
cid (str):
|
|||
|
A unique column indentifier.
|
|||
|
|
|||
|
visible (bool):
|
|||
|
Specifies that the index should refer to the visible
|
|||
|
columns. Otherwise, if False, the original column
|
|||
|
position is used.
|
|||
|
"""
|
|||
|
if cid is not None:
|
|||
|
column: TableColumn = self.cidmap(int(cid))
|
|||
|
column.delete()
|
|||
|
|
|||
|
elif index is not None and visible:
|
|||
|
self.tablecolumns_visible[int(index)].delete()
|
|||
|
|
|||
|
elif index is None and not visible:
|
|||
|
self.tablecolumns[int(index)].delete()
|
|||
|
|
|||
|
def delete_columns(self, indices=None, cids=None, visible=True):
|
|||
|
"""Delete columns specified by indices or cids.
|
|||
|
|
|||
|
Unless specified otherwise, the index refers to the position
|
|||
|
of the columns in the table from left to right starting with
|
|||
|
index 0.
|
|||
|
|
|||
|
!!!Warning "Use this method with caution!
|
|||
|
This method may or may not suffer performance issues.
|
|||
|
Internally, this method calls the `delete_column` method
|
|||
|
on each column specified in the list. The `delete_column`
|
|||
|
method deletes the related column from each record in
|
|||
|
the table data. So, if there are a lot of records this
|
|||
|
could be problematic. It may be more beneficial to use
|
|||
|
the `build_table_data` if you plan on changing the
|
|||
|
structure of the table dramatically.
|
|||
|
|
|||
|
Parameters:
|
|||
|
|
|||
|
indices (List[int]):
|
|||
|
A list of column indices to delete from the table.
|
|||
|
|
|||
|
cids (List[str]):
|
|||
|
A list of unique column identifiers to delete from the
|
|||
|
table.
|
|||
|
|
|||
|
visible (bool):
|
|||
|
If True, the index refers to the visible position of the
|
|||
|
column in the stable, from left to right starting at
|
|||
|
index 0.
|
|||
|
"""
|
|||
|
if cids is not None:
|
|||
|
for cid in cids:
|
|||
|
self.delete_column(cid=cid)
|
|||
|
elif indices is not None:
|
|||
|
for index in indices:
|
|||
|
self.delete_column(index=index, visible=visible)
|
|||
|
|
|||
|
def delete_row(self, index=None, iid=None, visible=True):
|
|||
|
"""Delete a record from the data set.
|
|||
|
|
|||
|
Unless specified otherwise, the index refers to the record
|
|||
|
position within the visible data set from top to bottom
|
|||
|
starting with index 0.
|
|||
|
|
|||
|
If iid is provided, the record associated with the cid is deleted
|
|||
|
regardless of whether it is in the visible data set.
|
|||
|
|
|||
|
Parameters:
|
|||
|
|
|||
|
index (int):
|
|||
|
The numerical index of the record within the data set.
|
|||
|
|
|||
|
iid (str):
|
|||
|
A unique record identifier.
|
|||
|
|
|||
|
visible (bool):
|
|||
|
Indicates that the record index is relative to the current
|
|||
|
records in view, otherwise, the original data set index is
|
|||
|
used if False.
|
|||
|
"""
|
|||
|
# delete from iid
|
|||
|
if iid is not None:
|
|||
|
record: TableRow = self.iidmap.get(iid)
|
|||
|
record.delete()
|
|||
|
elif index is not None:
|
|||
|
# visible index
|
|||
|
if visible:
|
|||
|
record = self.tablerows_visible[index]
|
|||
|
record.delete()
|
|||
|
# original index
|
|||
|
else:
|
|||
|
for record in self.tablerows:
|
|||
|
if record._sort == index:
|
|||
|
record.delete()
|
|||
|
|
|||
|
def delete_rows(self, indices=None, iids=None, visible=True):
|
|||
|
"""Delete rows specified by indices or iids.
|
|||
|
|
|||
|
If both indices and iids are None, then all records in the
|
|||
|
table will be deleted.
|
|||
|
"""
|
|||
|
# remove records by iid
|
|||
|
if iids is not None:
|
|||
|
for iid in iids:
|
|||
|
self.delete_row(iid=iid)
|
|||
|
# remove records by index
|
|||
|
elif indices is not None:
|
|||
|
for index in indices:
|
|||
|
self.delete_row(index=index, visible=visible)
|
|||
|
# remove ALL records
|
|||
|
else:
|
|||
|
self._tablerows.clear()
|
|||
|
self._tablerows_filtered.clear()
|
|||
|
self._viewdata.clear()
|
|||
|
self._iidmap.clear()
|
|||
|
records = self.view.get_children()
|
|||
|
self.view.delete(*records)
|
|||
|
# route to new page if no records visible
|
|||
|
if len(self._viewdata) == 0:
|
|||
|
self.goto_page()
|
|||
|
|
|||
|
def insert_column(
|
|||
|
self,
|
|||
|
index,
|
|||
|
text="",
|
|||
|
image="",
|
|||
|
command="",
|
|||
|
anchor=W,
|
|||
|
width=200,
|
|||
|
minwidth=20,
|
|||
|
stretch=False,
|
|||
|
) -> TableColumn:
|
|||
|
"""
|
|||
|
Parameters:
|
|||
|
|
|||
|
index (Union[int, str]):
|
|||
|
A numerical index that specifieds where to insert
|
|||
|
the column. You may also use the string 'end' to
|
|||
|
insert the column in the right-most position. If the
|
|||
|
index exceeds the column count, it will be inserted
|
|||
|
at the right-most position.
|
|||
|
|
|||
|
text (str):
|
|||
|
The header text.
|
|||
|
|
|||
|
image (PhotoImage):
|
|||
|
An image that is displayed to the left of the header text.
|
|||
|
|
|||
|
command (Callable):
|
|||
|
A function called whenever the header button is clicked.
|
|||
|
|
|||
|
anchor (str):
|
|||
|
The position of the header text within the header. One
|
|||
|
of "e", "w", "center".
|
|||
|
|
|||
|
width (int):
|
|||
|
Specifies the width of the column in pixels.
|
|||
|
|
|||
|
minwidth (int):
|
|||
|
Specifies the minimum width of the column in pixels.
|
|||
|
|
|||
|
stretch (bool):
|
|||
|
Specifies whether or not the column width should be
|
|||
|
adjusted whenever the widget is resized or the user
|
|||
|
drags the column separator.
|
|||
|
|
|||
|
Returns:
|
|||
|
|
|||
|
TableColumn:
|
|||
|
A table column object.
|
|||
|
"""
|
|||
|
self.reset_table()
|
|||
|
colcount = len(self.tablecolumns)
|
|||
|
cid = colcount
|
|||
|
if index == END:
|
|||
|
index = -1
|
|||
|
elif index > colcount - 1:
|
|||
|
index = -1
|
|||
|
|
|||
|
# actual columns
|
|||
|
cols = self.view.cget("columns")
|
|||
|
if len(cols) > 0:
|
|||
|
cols = [int(x) for x in cols]
|
|||
|
cols.append(cid)
|
|||
|
else:
|
|||
|
cols = [cid]
|
|||
|
|
|||
|
# visible columns
|
|||
|
dcols = self.view.cget("displaycolumns")
|
|||
|
if "#all" in dcols:
|
|||
|
dcols = cols
|
|||
|
elif len(dcols) > 0:
|
|||
|
dcols = [int(x) for x in dcols]
|
|||
|
if index == -1:
|
|||
|
dcols.append(cid)
|
|||
|
else:
|
|||
|
dcols.insert(index, cid)
|
|||
|
else:
|
|||
|
dcols = [cid]
|
|||
|
|
|||
|
self.view.configure(columns=cols, displaycolumns=dcols)
|
|||
|
|
|||
|
# configure new column
|
|||
|
column = TableColumn(
|
|||
|
tableview=self,
|
|||
|
cid=cid,
|
|||
|
text=text,
|
|||
|
image=image,
|
|||
|
command=command,
|
|||
|
anchor=anchor,
|
|||
|
width=width,
|
|||
|
minwidth=minwidth,
|
|||
|
stretch=stretch,
|
|||
|
)
|
|||
|
self._tablecols.append(column)
|
|||
|
# must be called to show the header after initially creating it
|
|||
|
# ad hoc, not sure why this should be the case;
|
|||
|
self._column_sort_header_reset()
|
|||
|
|
|||
|
# update settings after they are erased when a column is
|
|||
|
# inserted
|
|||
|
for column in self._tablecols:
|
|||
|
column.restore_settings()
|
|||
|
|
|||
|
return column
|
|||
|
|
|||
|
def purge_table_data(self):
|
|||
|
"""Erase all table and column data.
|
|||
|
|
|||
|
This method will completely destroy the table data structure.
|
|||
|
The table will need to be completely rebuilt after using this
|
|||
|
method.
|
|||
|
"""
|
|||
|
self.delete_rows()
|
|||
|
self.cidmap.clear()
|
|||
|
self.tablecolumns.clear()
|
|||
|
self.view.configure(columns=[], displaycolumns=[])
|
|||
|
|
|||
|
def unload_table_data(self):
|
|||
|
"""Unload all data from the table"""
|
|||
|
for row in self.tablerows_visible:
|
|||
|
row.hide()
|
|||
|
self.tablerows_visible.clear()
|
|||
|
|
|||
|
def load_table_data(self, clear_filters=False):
|
|||
|
"""Load records into the tableview.
|
|||
|
|
|||
|
Parameters:
|
|||
|
|
|||
|
clear_filters (bool):
|
|||
|
Specifies that the table filters should be cleared
|
|||
|
before loading the data into the view.
|
|||
|
"""
|
|||
|
if len(self.tablerows) == 0:
|
|||
|
return
|
|||
|
|
|||
|
if clear_filters:
|
|||
|
self.reset_table()
|
|||
|
|
|||
|
self.unload_table_data()
|
|||
|
|
|||
|
if self._paginated:
|
|||
|
page_start = self._rowindex.get()
|
|||
|
page_end = self._rowindex.get() + self._pagesize.get()
|
|||
|
else:
|
|||
|
page_start = 0
|
|||
|
page_end = len(self._tablerows)
|
|||
|
|
|||
|
if self._filtered:
|
|||
|
rowdata = self._tablerows_filtered[page_start:page_end]
|
|||
|
rowcount = len(self._tablerows_filtered)
|
|||
|
else:
|
|||
|
rowdata = self._tablerows[page_start:page_end]
|
|||
|
rowcount = len(self._tablerows)
|
|||
|
|
|||
|
self._pagelimit.set(ceil(rowcount / self._pagesize.get()))
|
|||
|
|
|||
|
pageindex = ceil(page_end / self._pagesize.get())
|
|||
|
pagelimit = self._pagelimit.get()
|
|||
|
self._pageindex.set(min([pagelimit, pageindex]))
|
|||
|
|
|||
|
for i, row in enumerate(rowdata):
|
|||
|
if self._stripecolor is not None and i % 2 == 0:
|
|||
|
row.show(True)
|
|||
|
else:
|
|||
|
row.show(False)
|
|||
|
self._viewdata.append(row)
|
|||
|
|
|||
|
def fill_empty_columns(self, fillvalue=""):
|
|||
|
"""Fill empty columns with the fillvalue.
|
|||
|
|
|||
|
This method can be used to fill in missing values when a column
|
|||
|
column is inserted after data has already been inserted into
|
|||
|
the tableview.
|
|||
|
|
|||
|
Parameters:
|
|||
|
|
|||
|
fillvalue (Any):
|
|||
|
A value to insert into an empty column
|
|||
|
"""
|
|||
|
rowcount = len(self._tablerows)
|
|||
|
if rowcount == 0:
|
|||
|
return
|
|||
|
colcount = len(self._tablecols)
|
|||
|
for row in self._tablerows:
|
|||
|
var = colcount - len(row._values)
|
|||
|
if var <= 0:
|
|||
|
return
|
|||
|
else:
|
|||
|
for _ in range(var):
|
|||
|
row._values.append(fillvalue)
|
|||
|
row.configure(values=row._values)
|
|||
|
|
|||
|
# CONFIGURATION
|
|||
|
|
|||
|
def get_columns(self) -> List[TableColumn]:
|
|||
|
"""Returns a list of all column objects. Same as using the
|
|||
|
`Tableview.tablecolumns` property."""
|
|||
|
return self._tablecols
|
|||
|
|
|||
|
def get_column(
|
|||
|
self, index=None, visible=False, cid=None
|
|||
|
) -> TableColumn:
|
|||
|
"""Returns the `TableColumn` object from an index or a cid.
|
|||
|
|
|||
|
If index is specified, the column index refers to the index
|
|||
|
within the original, unless the visible flag is set, in which
|
|||
|
case the index is relative to the visible columns in view.
|
|||
|
|
|||
|
If cid is specified, the column associated with the cid is
|
|||
|
return regardless of whether it is visible.
|
|||
|
|
|||
|
Parameters:
|
|||
|
|
|||
|
index (int):
|
|||
|
The numerical index of the column.
|
|||
|
|
|||
|
visible (bool):
|
|||
|
Use the index of the visible columns as they appear
|
|||
|
in the table.
|
|||
|
|
|||
|
Returns:
|
|||
|
|
|||
|
Union[TableColumn, None]:
|
|||
|
The table column object if found, otherwise None.
|
|||
|
"""
|
|||
|
if cid is not None:
|
|||
|
return self._cidmap.get(cid)
|
|||
|
|
|||
|
if not visible:
|
|||
|
# original column index
|
|||
|
try:
|
|||
|
return self._tablecols[index]
|
|||
|
except IndexError:
|
|||
|
return None
|
|||
|
else:
|
|||
|
# visible column index
|
|||
|
cols = self.view.cget("columns")
|
|||
|
if len(cols) > 0:
|
|||
|
cols = [int(x) for x in cols]
|
|||
|
else:
|
|||
|
cols = []
|
|||
|
|
|||
|
dcols = self.view.cget("displaycolumns")
|
|||
|
if "#all" in dcols:
|
|||
|
dcols = cols
|
|||
|
else:
|
|||
|
try:
|
|||
|
x = int(dcols[index])
|
|||
|
for c in self._tablecols:
|
|||
|
if c.cid == x:
|
|||
|
return c
|
|||
|
except ValueError:
|
|||
|
return None
|
|||
|
|
|||
|
def get_rows(self, visible=False, filtered=False, selected=False) -> List[TableRow]:
|
|||
|
"""Return a list of TableRow objects.
|
|||
|
|
|||
|
Return a subset of rows based on optional flags. Only ONE flag can be used
|
|||
|
at a time. If more than one flag is set to `True`, then the first flag will
|
|||
|
be used to return the data.
|
|||
|
|
|||
|
Parameters:
|
|||
|
|
|||
|
visible (bool):
|
|||
|
If true, only records in the current view will be returned.
|
|||
|
|
|||
|
filtered (bool):
|
|||
|
If True, only rows in the filtered dataset will be returned.
|
|||
|
|
|||
|
selected (bool):
|
|||
|
If True, only rows that are currently selected will be returned.
|
|||
|
|
|||
|
Returns:
|
|||
|
|
|||
|
List[TableRow]:
|
|||
|
A list of TableRow objects.
|
|||
|
"""
|
|||
|
if visible:
|
|||
|
return self._viewdata
|
|||
|
elif filtered:
|
|||
|
return self._tablerows_filtered
|
|||
|
elif selected:
|
|||
|
return [row for row in self._viewdata if row.iid in self.view.selection()]
|
|||
|
else:
|
|||
|
return self._tablerows
|
|||
|
|
|||
|
def get_row(self, index=None, visible=False, filtered=False, iid=None) -> TableRow:
|
|||
|
"""Returns the `TableRow` object from an index or the iid.
|
|||
|
|
|||
|
If an index is specified, the row index refers to the index
|
|||
|
within the original dataset. When choosing a subset of data,
|
|||
|
the visible data takes priority over filtered if both flags
|
|||
|
are set.
|
|||
|
|
|||
|
If an iid is specified, the object attached to that iid is
|
|||
|
returned regardless of whether or not it is visible or
|
|||
|
filtered.
|
|||
|
|
|||
|
Parameters:
|
|||
|
|
|||
|
index (int):
|
|||
|
The numerical index of the column.
|
|||
|
|
|||
|
iid (str):
|
|||
|
A unique column identifier.
|
|||
|
|
|||
|
visible (bool):
|
|||
|
Use the index of the visible rows as they appear
|
|||
|
in the current table view.
|
|||
|
|
|||
|
filtered (bool):
|
|||
|
Use the index of the rows within the filtered data
|
|||
|
set.
|
|||
|
|
|||
|
Returns:
|
|||
|
|
|||
|
Union[TableRow, None]:
|
|||
|
The table column object if found, otherwise None
|
|||
|
"""
|
|||
|
if iid is not None:
|
|||
|
return self.iidmap.get(iid)
|
|||
|
|
|||
|
if visible:
|
|||
|
try:
|
|||
|
return self.tablerows_visible[index]
|
|||
|
except IndexError:
|
|||
|
return None
|
|||
|
elif filtered:
|
|||
|
try:
|
|||
|
return self.tablerows_filtered[index]
|
|||
|
except IndexError:
|
|||
|
return None
|
|||
|
else:
|
|||
|
try:
|
|||
|
return self.tablerows[index]
|
|||
|
except IndexError:
|
|||
|
return None
|
|||
|
|
|||
|
# PAGE NAVIGATION
|
|||
|
|
|||
|
def _select_first_visible_item(self):
|
|||
|
try:
|
|||
|
iid = self.tablerows_visible[0].iid
|
|||
|
self.view.selection_set(iid)
|
|||
|
# must force focus, sometimes just focus on iid doesn't work
|
|||
|
self.view.focus_force()
|
|||
|
# this sets the focus on the specific row item
|
|||
|
self.view.focus(iid)
|
|||
|
# make sure the row is visible
|
|||
|
self.view.see(iid)
|
|||
|
except:
|
|||
|
pass
|
|||
|
|
|||
|
def goto_first_page(self):
|
|||
|
"""Update table with first page of data"""
|
|||
|
self._rowindex.set(0)
|
|||
|
self.load_table_data()
|
|||
|
self._select_first_visible_item()
|
|||
|
|
|||
|
def goto_last_page(self):
|
|||
|
"""Update table with the last page of data"""
|
|||
|
pagelimit = self._pagelimit.get() - 1
|
|||
|
self._rowindex.set(self.pagesize * pagelimit)
|
|||
|
self.load_table_data()
|
|||
|
self._select_first_visible_item()
|
|||
|
|
|||
|
def goto_next_page(self):
|
|||
|
"""Update table with next page of data"""
|
|||
|
if self._pageindex.get() >= self._pagelimit.get():
|
|||
|
return
|
|||
|
rowindex = self._rowindex.get()
|
|||
|
self._rowindex.set(rowindex + self.pagesize)
|
|||
|
self.load_table_data()
|
|||
|
self._select_first_visible_item()
|
|||
|
|
|||
|
def goto_prev_page(self):
|
|||
|
"""Update table with prev page of data"""
|
|||
|
if self._pageindex.get() <= 1:
|
|||
|
return
|
|||
|
rowindex = self._rowindex.get()
|
|||
|
self._rowindex.set(rowindex - self.pagesize)
|
|||
|
self.load_table_data()
|
|||
|
self._select_first_visible_item()
|
|||
|
|
|||
|
def goto_page(self, *_):
|
|||
|
"""Go to a specific page indicated by the page entry widget."""
|
|||
|
pagelimit = self._pagelimit.get()
|
|||
|
pageindex = self._pageindex.get()
|
|||
|
if pageindex > pagelimit:
|
|||
|
pageindex = pagelimit
|
|||
|
self._pageindex.set(pageindex)
|
|||
|
elif pageindex <= 0:
|
|||
|
pageindex = 1
|
|||
|
self._pageindex.set(pageindex)
|
|||
|
rowindex = (pageindex * self.pagesize) - self.pagesize
|
|||
|
self._rowindex.set(rowindex)
|
|||
|
self.load_table_data()
|
|||
|
self._select_first_visible_item()
|
|||
|
|
|||
|
# COLUMN SORTING
|
|||
|
|
|||
|
def sort_column_data(self, event=None, cid=None, sort=None):
|
|||
|
"""Sort the table rows by the specified column. This method
|
|||
|
may be trigged by an event or manually.
|
|||
|
|
|||
|
Parameters:
|
|||
|
|
|||
|
event (Event):
|
|||
|
A window event.
|
|||
|
|
|||
|
cid (int):
|
|||
|
A unique column identifier; typically the numerical
|
|||
|
index of the column relative to the original data set.
|
|||
|
|
|||
|
sort (int):
|
|||
|
Determines the sort direction. 0 = ASCENDING. 1 = DESCENDING.
|
|||
|
"""
|
|||
|
if event is not None:
|
|||
|
eo = self._get_event_objects(event)
|
|||
|
column = eo.column
|
|||
|
index = column.tableindex
|
|||
|
elif cid is not None:
|
|||
|
column: TableColumn = self.cidmap.get(int(cid))
|
|||
|
index = column.tableindex
|
|||
|
else:
|
|||
|
return
|
|||
|
|
|||
|
# update table data
|
|||
|
if self.is_filtered:
|
|||
|
tablerows = self.tablerows_filtered
|
|||
|
else:
|
|||
|
tablerows = self.tablerows
|
|||
|
|
|||
|
if sort is not None:
|
|||
|
columnsort = sort
|
|||
|
else:
|
|||
|
columnsort = self.tablecolumns[index].columnsort
|
|||
|
|
|||
|
if columnsort == ASCENDING:
|
|||
|
self._tablecols[index].columnsort = DESCENDING
|
|||
|
else:
|
|||
|
self._tablecols[index].columnsort = ASCENDING
|
|||
|
|
|||
|
try:
|
|||
|
sortedrows = sorted(
|
|||
|
tablerows, reverse=columnsort, key=lambda x: x.values[index]
|
|||
|
)
|
|||
|
except:
|
|||
|
# when data is missing, or sometimes with numbers
|
|||
|
# this is still not right, but it works most of the time
|
|||
|
# fix sometime down the road when I have time
|
|||
|
self.fill_empty_columns()
|
|||
|
sortedrows = sorted(
|
|||
|
tablerows, reverse=columnsort, key=lambda x: int(x.values[index])
|
|||
|
)
|
|||
|
if self.is_filtered:
|
|||
|
self._tablerows_filtered = sortedrows
|
|||
|
else:
|
|||
|
self._tablerows = sortedrows
|
|||
|
|
|||
|
# update headers
|
|||
|
self._column_sort_header_reset()
|
|||
|
self._column_sort_header_update(column.cid)
|
|||
|
|
|||
|
self.unload_table_data()
|
|||
|
self.load_table_data()
|
|||
|
self._select_first_visible_item()
|
|||
|
|
|||
|
# DATA SEARCH & FILTERING
|
|||
|
|
|||
|
def reset_row_filters(self):
|
|||
|
"""Remove all row level filters; unhide all rows."""
|
|||
|
self._filtered = False
|
|||
|
self.searchcriteria = ""
|
|||
|
self.unload_table_data()
|
|||
|
self.load_table_data()
|
|||
|
|
|||
|
def reset_column_filters(self):
|
|||
|
"""Remove all column level filters; unhide all columns."""
|
|||
|
cols = [col.cid for col in self.tablecolumns]
|
|||
|
self.view.configure(displaycolumns=cols)
|
|||
|
|
|||
|
def reset_row_sort(self):
|
|||
|
"""Display all table rows by original insert index"""
|
|||
|
...
|
|||
|
|
|||
|
def reset_column_sort(self):
|
|||
|
"""Display all columns by original insert index"""
|
|||
|
cols = sorted([col.cid for col in self.tablecolumns_visible], key=int)
|
|||
|
self.view.configure(displaycolumns=cols)
|
|||
|
|
|||
|
def reset_table(self):
|
|||
|
"""Remove all table data filters and column sorts"""
|
|||
|
self._filtered = False
|
|||
|
self.searchcriteria = ""
|
|||
|
try:
|
|||
|
sortedrows = sorted(self.tablerows, key=lambda x: x._sort)
|
|||
|
except IndexError:
|
|||
|
self.fill_empty_columns()
|
|||
|
sortedrows = sorted(self.tablerows, key=lambda x: x._sort)
|
|||
|
self._tablerows = sortedrows
|
|||
|
self.unload_table_data()
|
|||
|
|
|||
|
# reset the columns
|
|||
|
self.reset_column_filters()
|
|||
|
self.reset_column_sort()
|
|||
|
|
|||
|
self._column_sort_header_reset()
|
|||
|
self.goto_first_page() # needed?
|
|||
|
|
|||
|
def filter_column_to_value(self, event=None, cid=None, value=None):
|
|||
|
"""Hide all records except for records where the current
|
|||
|
column exactly matches the provided value. This method may
|
|||
|
be triggered by a window event or by specifying the column id.
|
|||
|
|
|||
|
Parameters:
|
|||
|
|
|||
|
event (Event):
|
|||
|
A window click event.
|
|||
|
|
|||
|
cid (int):
|
|||
|
A unique column identifier; typically the numerical
|
|||
|
index of the column within the original dataset.
|
|||
|
|
|||
|
value (Any):
|
|||
|
The criteria used to filter the column.
|
|||
|
"""
|
|||
|
if event is not None:
|
|||
|
eo = self._get_event_objects(event)
|
|||
|
index = eo.column.tableindex
|
|||
|
value = value or eo.row.values[index]
|
|||
|
elif cid is not None:
|
|||
|
column: TableColumn = self.cidmap.get(cid)
|
|||
|
index = column.tableindex
|
|||
|
else:
|
|||
|
return
|
|||
|
|
|||
|
self._filtered = True
|
|||
|
self.tablerows_filtered.clear()
|
|||
|
self.unload_table_data()
|
|||
|
|
|||
|
for row in self.tablerows:
|
|||
|
if row.values[index] == value:
|
|||
|
self.tablerows_filtered.append(row)
|
|||
|
|
|||
|
self._rowindex.set(0)
|
|||
|
self.load_table_data()
|
|||
|
|
|||
|
def filter_to_selected_rows(self):
|
|||
|
"""Hide all records except for the selected rows"""
|
|||
|
criteria = self.view.selection()
|
|||
|
if len(criteria) == 0:
|
|||
|
return # nothing is selected
|
|||
|
|
|||
|
if self.is_filtered:
|
|||
|
for row in self.tablerows_visible:
|
|||
|
if row.iid not in criteria:
|
|||
|
row.hide()
|
|||
|
self.tablerows_filtered.remove(row)
|
|||
|
else:
|
|||
|
self._filtered = True
|
|||
|
self.tablerows_filtered.clear()
|
|||
|
for row in self.tablerows_visible:
|
|||
|
if row.iid in criteria:
|
|||
|
self.tablerows_filtered.append(row)
|
|||
|
self._rowindex.set(0)
|
|||
|
self.load_table_data()
|
|||
|
|
|||
|
def hide_selected_rows(self):
|
|||
|
"""Hide the currently selected rows"""
|
|||
|
selected = self.view.selection()
|
|||
|
view_cnt = len(self._viewdata)
|
|||
|
hide_cnt = len(selected)
|
|||
|
self.view.detach(*selected)
|
|||
|
|
|||
|
tablerows = []
|
|||
|
for row in self.tablerows_visible:
|
|||
|
if row.iid in selected:
|
|||
|
tablerows.append(row)
|
|||
|
|
|||
|
if not self.is_filtered:
|
|||
|
self._filtered = True
|
|||
|
self._tablerows_filtered = self.tablerows.copy()
|
|||
|
|
|||
|
for row in tablerows:
|
|||
|
if self.is_filtered:
|
|||
|
self.tablerows_filtered.remove(row)
|
|||
|
|
|||
|
if hide_cnt == view_cnt:
|
|||
|
# assuming that if the count of the records on the page are
|
|||
|
# selected for hiding, then need to go to the next page
|
|||
|
# The call to `load_table_data` is duplicative, but currently
|
|||
|
# this is the only way to get this to work until I've
|
|||
|
# refactored this bit.
|
|||
|
self.load_table_data()
|
|||
|
self.goto_page()
|
|||
|
else:
|
|||
|
self.load_table_data()
|
|||
|
|
|||
|
def hide_selected_column(self, event=None, cid=None):
|
|||
|
"""Detach the selected column from the tableview. This method
|
|||
|
may be triggered by a window event or by specifying the column
|
|||
|
id.
|
|||
|
|
|||
|
Parameters:
|
|||
|
|
|||
|
event (Event):
|
|||
|
A window click event
|
|||
|
|
|||
|
cid (int):
|
|||
|
A unique column identifier; typically the numerical
|
|||
|
index of the column within the original dataset.
|
|||
|
"""
|
|||
|
if event is not None:
|
|||
|
eo = self._get_event_objects(event)
|
|||
|
column = eo.column.hide()
|
|||
|
elif cid is not None:
|
|||
|
column: TableColumn = self.cidmap.get(cid)
|
|||
|
column.hide()
|
|||
|
|
|||
|
def unhide_selected_column(self, event=None, cid=None):
|
|||
|
"""Attach the selected column to the tableview. This method
|
|||
|
may be triggered by a window event or by specifying the column
|
|||
|
id. The column is reinserted at the index in the original data
|
|||
|
set.
|
|||
|
|
|||
|
Parameters:
|
|||
|
|
|||
|
event (Event):
|
|||
|
An application click event
|
|||
|
|
|||
|
cid (int):
|
|||
|
A unique column identifier; typically the numerical
|
|||
|
index of the column within the original dataset.
|
|||
|
"""
|
|||
|
if event is not None:
|
|||
|
eo = self._get_event_objects(event)
|
|||
|
eo.column.show()
|
|||
|
elif cid is not None:
|
|||
|
column = self.cidmap.get(cid)
|
|||
|
column.show()
|
|||
|
|
|||
|
# DATA EXPORT
|
|||
|
|
|||
|
def export_all_records(self):
|
|||
|
"""Export all records to a csv file"""
|
|||
|
headers = [col.headertext for col in self.tablecolumns]
|
|||
|
records = [row.values for row in self.tablerows]
|
|||
|
self.save_data_to_csv(headers, records, self._delimiter)
|
|||
|
|
|||
|
def export_current_page(self):
|
|||
|
"""Export records on current page to csv file"""
|
|||
|
headers = [col.headertext for col in self.tablecolumns]
|
|||
|
records = [row.values for row in self.tablerows_visible]
|
|||
|
self.save_data_to_csv(headers, records, self._delimiter)
|
|||
|
|
|||
|
def export_current_selection(self):
|
|||
|
"""Export rows currently selected to csv file"""
|
|||
|
headers = [col.headertext for col in self.tablecolumns]
|
|||
|
selected = self.view.selection()
|
|||
|
records = []
|
|||
|
for iid in selected:
|
|||
|
record: TableRow = self.iidmap.get(iid)
|
|||
|
records.append(record.values)
|
|||
|
self.save_data_to_csv(headers, records, self._delimiter)
|
|||
|
|
|||
|
def export_records_in_filter(self):
|
|||
|
"""Export rows currently filtered to csv file"""
|
|||
|
headers = [col.headertext for col in self.tablecolumns]
|
|||
|
if not self.is_filtered:
|
|||
|
return
|
|||
|
records = [row.values for row in self.tablerows_filtered]
|
|||
|
self.save_data_to_csv(headers, records, self._delimiter)
|
|||
|
|
|||
|
def save_data_to_csv(self, headers, records, delimiter=","):
|
|||
|
"""Save data records to a csv file.
|
|||
|
|
|||
|
Parameters:
|
|||
|
|
|||
|
headers (List[str]):
|
|||
|
A list of header labels.
|
|||
|
|
|||
|
records (List[Tuple[...]]):
|
|||
|
A list of table records.
|
|||
|
|
|||
|
delimiter (str):
|
|||
|
The character to use for delimiting the values.
|
|||
|
"""
|
|||
|
from tkinter.filedialog import asksaveasfilename
|
|||
|
import csv
|
|||
|
|
|||
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|||
|
initialfile = f"tabledata_{timestamp}.csv"
|
|||
|
filetypes = [
|
|||
|
("CSV UTF-8 (Comma delimited)", "*.csv"),
|
|||
|
("All file types", "*.*"),
|
|||
|
]
|
|||
|
filename = asksaveasfilename(
|
|||
|
confirmoverwrite=True,
|
|||
|
filetypes=filetypes,
|
|||
|
defaultextension="csv",
|
|||
|
initialfile=initialfile,
|
|||
|
)
|
|||
|
if filename:
|
|||
|
with open(filename, "w", encoding="utf-8", newline="") as f:
|
|||
|
writer = csv.writer(f, delimiter=delimiter)
|
|||
|
writer.writerow(headers)
|
|||
|
writer.writerows(records)
|
|||
|
|
|||
|
# ROW MOVEMENT
|
|||
|
|
|||
|
def move_selected_rows_to_top(self):
|
|||
|
"""Move the selected rows to the top of the data set"""
|
|||
|
selected = self.view.selection()
|
|||
|
if len(selected) == 0:
|
|||
|
return
|
|||
|
|
|||
|
if self.is_filtered:
|
|||
|
tablerows = self.tablerows_filtered.copy()
|
|||
|
else:
|
|||
|
tablerows = self.tablerows.copy()
|
|||
|
|
|||
|
for i, iid in enumerate(selected):
|
|||
|
row = self.iidmap.get(iid)
|
|||
|
tablerows.remove(row)
|
|||
|
tablerows.insert(i, row)
|
|||
|
|
|||
|
if self.is_filtered:
|
|||
|
self._tablerows_filtered = tablerows
|
|||
|
else:
|
|||
|
self._tablerows = tablerows
|
|||
|
|
|||
|
# refresh the table data
|
|||
|
self.unload_table_data()
|
|||
|
self.load_table_data()
|
|||
|
|
|||
|
def move_selected_rows_to_bottom(self):
|
|||
|
"""Move the selected rows to the bottom of the dataset"""
|
|||
|
selected = self.view.selection()
|
|||
|
if len(selected) == 0:
|
|||
|
return
|
|||
|
|
|||
|
if self.is_filtered:
|
|||
|
tablerows = self.tablerows_filtered.copy()
|
|||
|
else:
|
|||
|
tablerows = self.tablerows.copy()
|
|||
|
|
|||
|
for iid in selected:
|
|||
|
row = self.iidmap.get(iid)
|
|||
|
tablerows.remove(row)
|
|||
|
tablerows.append(row)
|
|||
|
|
|||
|
if self.is_filtered:
|
|||
|
self._tablerows_filtered = tablerows
|
|||
|
else:
|
|||
|
self._tablerows = tablerows
|
|||
|
|
|||
|
# refresh the table data
|
|||
|
self.unload_table_data()
|
|||
|
self.load_table_data()
|
|||
|
|
|||
|
def move_selected_row_up(self):
|
|||
|
"""Move the selected rows up one position in the dataset"""
|
|||
|
selected = self.view.selection()
|
|||
|
if len(selected) == 0:
|
|||
|
return
|
|||
|
|
|||
|
if self.is_filtered:
|
|||
|
tablerows = self._tablerows_filtered.copy()
|
|||
|
else:
|
|||
|
tablerows = self.tablerows.copy()
|
|||
|
|
|||
|
for iid in selected:
|
|||
|
row = self.iidmap.get(iid)
|
|||
|
index = tablerows.index(row) - 1
|
|||
|
tablerows.remove(row)
|
|||
|
tablerows.insert(index, row)
|
|||
|
|
|||
|
if self.is_filtered:
|
|||
|
self._tablerows_filtered = tablerows
|
|||
|
else:
|
|||
|
self._tablerows = tablerows
|
|||
|
|
|||
|
# refresh the table data
|
|||
|
self.unload_table_data()
|
|||
|
self.load_table_data()
|
|||
|
|
|||
|
def move_row_down(self):
|
|||
|
"""Move the selected rows down one position in the dataset"""
|
|||
|
selected = self.view.selection()
|
|||
|
if len(selected) == 0:
|
|||
|
return
|
|||
|
|
|||
|
if self._filtered:
|
|||
|
tablerows = self._tablerows_filtered
|
|||
|
else:
|
|||
|
tablerows = self._tablerows
|
|||
|
|
|||
|
for iid in selected:
|
|||
|
row = self.iidmap.get(iid)
|
|||
|
index = tablerows.index(row) + 1
|
|||
|
tablerows.remove(row)
|
|||
|
tablerows.insert(index, row)
|
|||
|
|
|||
|
if self._filtered:
|
|||
|
self._tablerows_filtered = tablerows
|
|||
|
else:
|
|||
|
self._tablerows = tablerows
|
|||
|
|
|||
|
# refresh the table data
|
|||
|
self.unload_table_data()
|
|||
|
self.load_table_data()
|
|||
|
|
|||
|
# COLUMN MOVEMENT
|
|||
|
|
|||
|
def move_column_left(self, event=None, cid=None):
|
|||
|
"""Move column one position to the left. This can be triggered
|
|||
|
by either an event, or by passing in the `cid`, which is the
|
|||
|
index of the column relative to the original data set.
|
|||
|
|
|||
|
Parameters:
|
|||
|
|
|||
|
event (Event):
|
|||
|
An application click event.
|
|||
|
|
|||
|
cid (int):
|
|||
|
A unique column identifier; typically the index of the
|
|||
|
column relative to the original dataset.
|
|||
|
"""
|
|||
|
if event is not None:
|
|||
|
eo = self._get_event_objects(event)
|
|||
|
column = eo.column
|
|||
|
elif cid is not None:
|
|||
|
column = self.cidmap.get(cid)
|
|||
|
else:
|
|||
|
return
|
|||
|
|
|||
|
displaycols = [x.cid for x in self.tablecolumns_visible]
|
|||
|
old_index = column.displayindex
|
|||
|
if old_index == 0:
|
|||
|
return
|
|||
|
|
|||
|
new_index = column.displayindex - 1
|
|||
|
displaycols.insert(new_index, displaycols.pop(old_index))
|
|||
|
self.view.configure(displaycolumns=displaycols)
|
|||
|
|
|||
|
def move_column_right(self, event=None, cid=None):
|
|||
|
"""Move column one position to the right. This can be triggered
|
|||
|
by either an event, or by passing in the `cid`, which is the
|
|||
|
index of the column relative to the original data set.
|
|||
|
|
|||
|
Parameters:
|
|||
|
|
|||
|
event (Event):
|
|||
|
An application click event.
|
|||
|
|
|||
|
cid (int):
|
|||
|
A unique column identifier; typically the index of the
|
|||
|
column relative to the original dataset.
|
|||
|
"""
|
|||
|
if event is not None:
|
|||
|
eo = self._get_event_objects(event)
|
|||
|
column = eo.column
|
|||
|
elif cid is not None:
|
|||
|
column = self.cidmap.get(cid)
|
|||
|
else:
|
|||
|
return
|
|||
|
|
|||
|
displaycols = [x.cid for x in self.tablecolumns_visible]
|
|||
|
old_index = column.displayindex
|
|||
|
if old_index == len(displaycols) - 1:
|
|||
|
return
|
|||
|
|
|||
|
new_index = old_index + 1
|
|||
|
displaycols.insert(new_index, displaycols.pop(old_index))
|
|||
|
self.view.configure(displaycolumns=displaycols)
|
|||
|
|
|||
|
def move_column_to_first(self, event=None, cid=None):
|
|||
|
"""Move column to leftmost position. This can be triggered by
|
|||
|
either an event, or by passing in the `cid`, which is the index
|
|||
|
of the column relative to the original data set.
|
|||
|
|
|||
|
Parameters:
|
|||
|
|
|||
|
event (Event):
|
|||
|
An application click event.
|
|||
|
|
|||
|
cid (int):
|
|||
|
A unique column identifier; typically the index of the
|
|||
|
column relative to the original dataset.
|
|||
|
"""
|
|||
|
if event is not None:
|
|||
|
eo = self._get_event_objects(event)
|
|||
|
column = eo.column
|
|||
|
elif cid is not None:
|
|||
|
column = self.cidmap.get(cid)
|
|||
|
else:
|
|||
|
return
|
|||
|
|
|||
|
displaycols = [x.cid for x in self.tablecolumns_visible]
|
|||
|
old_index = column.displayindex
|
|||
|
if old_index == 0:
|
|||
|
return
|
|||
|
|
|||
|
displaycols.insert(0, displaycols.pop(old_index))
|
|||
|
self.view.configure(displaycolumns=displaycols)
|
|||
|
|
|||
|
def move_column_to_last(self, event=None, cid=None):
|
|||
|
"""Move column to the rightmost position. This can be triggered
|
|||
|
by either an event, or by passing in the `cid`, which is the
|
|||
|
index of the column relative to the original data set.
|
|||
|
|
|||
|
Parameters:
|
|||
|
|
|||
|
event (Event):
|
|||
|
An application click event.
|
|||
|
|
|||
|
cid (int):
|
|||
|
A unique column identifier; typically the index of the
|
|||
|
column relative to the original dataset.
|
|||
|
"""
|
|||
|
if event is not None:
|
|||
|
eo = self._get_event_objects(event)
|
|||
|
column = eo.column
|
|||
|
elif cid is not None:
|
|||
|
column = self.cidmap.get(cid)
|
|||
|
else:
|
|||
|
return
|
|||
|
|
|||
|
displaycols = [x.cid for x in self.tablecolumns_visible]
|
|||
|
old_index = column.displayindex
|
|||
|
if old_index == len(displaycols) - 1:
|
|||
|
return
|
|||
|
|
|||
|
new_index = len(displaycols) - 1
|
|||
|
displaycols.insert(new_index, displaycols.pop(old_index))
|
|||
|
self.view.configure(displaycolumns=displaycols)
|
|||
|
|
|||
|
# OTHER FORMATTING
|
|||
|
|
|||
|
def apply_table_stripes(self, stripecolor):
|
|||
|
"""Add stripes to even-numbered table rows as indicated by the
|
|||
|
`stripecolor` of (background, foreground). Either element may be
|
|||
|
specified as `None`, but both elements must be present.
|
|||
|
|
|||
|
Parameters:
|
|||
|
|
|||
|
stripecolor (Tuple[str, str]):
|
|||
|
A tuple of colors to apply to the table stripe. The
|
|||
|
tuple represents (background, foreground).
|
|||
|
"""
|
|||
|
style: ttk.Style = ttk.Style.get_instance()
|
|||
|
colors = style.colors
|
|||
|
if len(stripecolor) == 2:
|
|||
|
self._stripecolor = stripecolor
|
|||
|
bg, fg = stripecolor
|
|||
|
kw = {}
|
|||
|
if bg is None:
|
|||
|
kw["background"] = colors.active
|
|||
|
else:
|
|||
|
kw["background"] = bg
|
|||
|
if fg is None:
|
|||
|
kw["foreground"] = colors.inputfg
|
|||
|
else:
|
|||
|
kw["foreground"] = fg
|
|||
|
self.view.tag_configure("striped", **kw)
|
|||
|
|
|||
|
def autofit_columns(self):
|
|||
|
"""Autofit all columns in the current view"""
|
|||
|
f = font.nametofont("TkDefaultFont")
|
|||
|
pad = utility.scale_size(self, 20)
|
|||
|
col_widths = []
|
|||
|
|
|||
|
# measure header sizes
|
|||
|
for col in self.tablecolumns:
|
|||
|
width = f.measure(f"{col._headertext} {DOWNARROW}") + pad
|
|||
|
col_widths.append(width)
|
|||
|
|
|||
|
for row in self.tablerows_visible:
|
|||
|
values = row.values
|
|||
|
for i, value in enumerate(values):
|
|||
|
old_width = col_widths[i]
|
|||
|
new_width = f.measure(str(value)) + pad
|
|||
|
width = max(old_width, new_width)
|
|||
|
col_widths[i] = width
|
|||
|
|
|||
|
for i, width in enumerate(col_widths):
|
|||
|
self.view.column(i, width=width)
|
|||
|
|
|||
|
# COLUMN AND HEADER ALIGNMENT
|
|||
|
|
|||
|
def autoalign_columns(self):
|
|||
|
"""Align the columns and headers based on the data type of the
|
|||
|
values. Text is left-aligned; numbers are right-aligned. This
|
|||
|
method will have no effect if there is no data in the tables."""
|
|||
|
if len(self._tablerows) == 0:
|
|||
|
return
|
|||
|
|
|||
|
values = self._tablerows[0]._values
|
|||
|
for i, value in enumerate(values):
|
|||
|
if str(value).isnumeric():
|
|||
|
self.view.column(i, anchor=E)
|
|||
|
self.view.heading(i, anchor=E)
|
|||
|
else:
|
|||
|
self.view.column(i, anchor=W)
|
|||
|
self.view.heading(i, anchor=W)
|
|||
|
|
|||
|
def align_column_left(self, event=None, cid=None):
|
|||
|
"""Left align the column text. This can be triggered by
|
|||
|
either an event, or by passing in the `cid`, which is the index
|
|||
|
of the column relative to the original data set.
|
|||
|
|
|||
|
Parameters:
|
|||
|
|
|||
|
event (Event):
|
|||
|
An application click event.
|
|||
|
|
|||
|
cid (int):
|
|||
|
A unique column identifier; typically the index of the
|
|||
|
column relative to the original dataset.
|
|||
|
"""
|
|||
|
if event is not None:
|
|||
|
eo = self._get_event_objects(event)
|
|||
|
self.view.column(eo.column.cid, anchor=W)
|
|||
|
elif cid is not None:
|
|||
|
self.view.column(cid, anchor=W)
|
|||
|
|
|||
|
def align_column_right(self, event=None, cid=None):
|
|||
|
"""Right align the column text. This can be triggered by
|
|||
|
either an event, or by passing in the `cid`, which is the index
|
|||
|
of the column relative to the original data set.
|
|||
|
|
|||
|
Parameters:
|
|||
|
|
|||
|
event (Event):
|
|||
|
An application event.
|
|||
|
|
|||
|
cid (int):
|
|||
|
A unique column identifier; typically the index of the
|
|||
|
column relative to the original dataset.
|
|||
|
"""
|
|||
|
if event is not None:
|
|||
|
eo = self._get_event_objects(event)
|
|||
|
self.view.column(eo.column.cid, anchor=E)
|
|||
|
elif cid is not None:
|
|||
|
self.view.column(cid, anchor=E)
|
|||
|
|
|||
|
def align_column_center(self, event=None, cid=None):
|
|||
|
"""Center align the column text. This can be triggered by
|
|||
|
either an event, or by passing in the `cid`, which is the index
|
|||
|
of the column relative to the original data set.
|
|||
|
|
|||
|
Parameters:
|
|||
|
|
|||
|
event (Event):
|
|||
|
An application event.
|
|||
|
|
|||
|
cid (int):
|
|||
|
A unique column identifier; typically the index of the
|
|||
|
column relative to the original dataset.
|
|||
|
"""
|
|||
|
if event is not None:
|
|||
|
eo = self._get_event_objects(event)
|
|||
|
self.view.column(eo.column.cid, anchor=CENTER)
|
|||
|
elif cid is not None:
|
|||
|
self.view.column(cid, anchor=CENTER)
|
|||
|
|
|||
|
def align_heading_left(self, event=None, cid=None):
|
|||
|
"""Left align the heading text. This can be triggered by
|
|||
|
either an event, or by passing in the `cid`, which is the index
|
|||
|
of the heading relative to the original data set.
|
|||
|
|
|||
|
Parameters:
|
|||
|
|
|||
|
event (Event):
|
|||
|
An application event.
|
|||
|
|
|||
|
cid (int):
|
|||
|
A unique heading identifier; typically the index of the
|
|||
|
heading relative to the original dataset.
|
|||
|
"""
|
|||
|
if event is not None:
|
|||
|
eo = self._get_event_objects(event)
|
|||
|
self.view.heading(eo.column.cid, anchor=W)
|
|||
|
elif cid is not None:
|
|||
|
self.view.heading(cid, anchor=W)
|
|||
|
|
|||
|
def align_heading_right(self, event=None, cid=None):
|
|||
|
"""Right align the heading text. This can be triggered by
|
|||
|
either an event, or by passing in the `cid`, which is the index
|
|||
|
of the heading relative to the original data set.
|
|||
|
|
|||
|
Parameters:
|
|||
|
|
|||
|
event (Event):
|
|||
|
An application event.
|
|||
|
|
|||
|
cid (int):
|
|||
|
A unique heading identifier; typically the index of the
|
|||
|
heading relative to the original dataset.
|
|||
|
"""
|
|||
|
if event is not None:
|
|||
|
eo = self._get_event_objects(event)
|
|||
|
self.view.heading(eo.column.cid, anchor=E)
|
|||
|
elif cid is not None:
|
|||
|
self.view.heading(cid, anchor=E)
|
|||
|
|
|||
|
def align_heading_center(self, event=None, cid=None):
|
|||
|
"""Center align the heading text. This can be triggered by
|
|||
|
either an event, or by passing in the `cid`, which is the index
|
|||
|
of the heading relative to the original data set.
|
|||
|
|
|||
|
Parameters:
|
|||
|
|
|||
|
event (Event):
|
|||
|
An application event.
|
|||
|
|
|||
|
cid (int):
|
|||
|
A unique heading identifier; typically the index of the
|
|||
|
heading relative to the original dataset.
|
|||
|
"""
|
|||
|
if event is not None:
|
|||
|
eo = self._get_event_objects(event)
|
|||
|
self.view.heading(eo.column.cid, anchor=CENTER)
|
|||
|
elif cid is not None:
|
|||
|
self.view.heading(cid, anchor=CENTER)
|
|||
|
|
|||
|
# PRIVATE METHODS
|
|||
|
|
|||
|
def _get_event_objects(self, event):
|
|||
|
iid = self.view.identify_row(event.y)
|
|||
|
col = self.view.identify_column(event.x)
|
|||
|
cid = int(self.view.column(col, "id"))
|
|||
|
column: TableColumn = self.cidmap.get(cid)
|
|||
|
row: TableRow = self.iidmap.get(iid)
|
|||
|
data = TableEvent(column, row)
|
|||
|
return data
|
|||
|
|
|||
|
def _search_table_data(self, _):
|
|||
|
"""Search the table data for records that meet search criteria.
|
|||
|
Currently, this search locates any records that contain the
|
|||
|
specified text; it is also case insensitive.
|
|||
|
"""
|
|||
|
criteria = self._searchcriteria.get()
|
|||
|
self._filtered = True
|
|||
|
self.tablerows_filtered.clear()
|
|||
|
self.unload_table_data()
|
|||
|
for row in self.tablerows:
|
|||
|
for col in row.values:
|
|||
|
if str(criteria).lower() in str(col).lower():
|
|||
|
self.tablerows_filtered.append(row)
|
|||
|
break
|
|||
|
self._rowindex.set(0)
|
|||
|
self.load_table_data()
|
|||
|
|
|||
|
# PRIVATE METHODS - SORTING
|
|||
|
|
|||
|
def _column_sort_header_reset(self):
|
|||
|
"""Remove the sort character from the column headers"""
|
|||
|
for col in self.tablecolumns:
|
|||
|
self.view.heading(col.cid, text=col.headertext)
|
|||
|
|
|||
|
def _column_sort_header_update(self, cid):
|
|||
|
"""Add sort character to the sorted column"""
|
|||
|
column: TableColumn = self.cidmap.get(int(cid))
|
|||
|
arrow = UPARROW if column.columnsort == ASCENDING else DOWNARROW
|
|||
|
headertext = f"{column.headertext} {arrow}"
|
|||
|
self.view.heading(column.cid, text=headertext)
|
|||
|
|
|||
|
# PRIVATE METHODS - WIDGET BUILDERS
|
|||
|
|
|||
|
def _build_tableview_widget(self, coldata, rowdata, bootstyle):
|
|||
|
"""Build the data table"""
|
|||
|
if self._searchable:
|
|||
|
self._build_search_frame()
|
|||
|
|
|||
|
self.view = ttk.Treeview(
|
|||
|
master=self,
|
|||
|
columns=[x for x in range(len(coldata))],
|
|||
|
height=self._height,
|
|||
|
selectmode=EXTENDED,
|
|||
|
show=HEADINGS,
|
|||
|
bootstyle=f"{bootstyle}-table",
|
|||
|
)
|
|||
|
self.view.pack(fill=BOTH, expand=YES, side=TOP)
|
|||
|
self.hbar = ttk.Scrollbar(
|
|||
|
master=self, command=self.view.xview, orient=HORIZONTAL
|
|||
|
)
|
|||
|
self.hbar.pack(fill=X)
|
|||
|
self.view.configure(xscrollcommand=self.hbar.set)
|
|||
|
|
|||
|
if self._paginated:
|
|||
|
self._build_pagination_frame()
|
|||
|
|
|||
|
self.build_table_data(coldata, rowdata)
|
|||
|
|
|||
|
self._rightclickmenu_cell = TableCellRightClickMenu(self)
|
|||
|
self._rightclickmenu_head = TableHeaderRightClickMenu(self)
|
|||
|
self._set_widget_binding()
|
|||
|
|
|||
|
def _build_search_frame(self):
|
|||
|
"""Build the search frame containing the search widgets. This
|
|||
|
frame is only created if `searchable=True` when creating the
|
|||
|
widget.
|
|||
|
"""
|
|||
|
frame = ttk.Frame(self, padding=5)
|
|||
|
frame.pack(fill=X, side=TOP)
|
|||
|
ttk.Label(frame, text=MessageCatalog.translate("Search")).pack(side=LEFT, padx=5)
|
|||
|
searchterm = ttk.Entry(frame, textvariable=self._searchcriteria)
|
|||
|
searchterm.pack(fill=X, side=LEFT, expand=YES)
|
|||
|
searchterm.bind("<Return>", self._search_table_data)
|
|||
|
searchterm.bind("<KP_Enter>", self._search_table_data)
|
|||
|
if not self._paginated:
|
|||
|
ttk.Button(
|
|||
|
frame,
|
|||
|
text=MessageCatalog.translate("⎌"),
|
|||
|
command=self.reset_table,
|
|||
|
style="symbol.Link.TButton",
|
|||
|
).pack(side=LEFT)
|
|||
|
|
|||
|
def _build_pagination_frame(self):
|
|||
|
"""Build the frame containing the pagination widgets. This
|
|||
|
frame is only built if `pagination=True` when creating the
|
|||
|
widget.
|
|||
|
"""
|
|||
|
pageframe = ttk.Frame(self)
|
|||
|
pageframe.pack(fill=X, anchor=N)
|
|||
|
|
|||
|
ttk.Button(
|
|||
|
pageframe,
|
|||
|
text=MessageCatalog.translate("⎌"),
|
|||
|
command=self.reset_table,
|
|||
|
style="symbol.Link.TButton",
|
|||
|
).pack(side=RIGHT)
|
|||
|
|
|||
|
ttk.Separator(pageframe, orient=VERTICAL).pack(side=RIGHT, padx=10)
|
|||
|
|
|||
|
ttk.Button(
|
|||
|
master=pageframe,
|
|||
|
text="»",
|
|||
|
command=self.goto_last_page,
|
|||
|
style="symbol.Link.TButton",
|
|||
|
).pack(side=RIGHT, fill=Y)
|
|||
|
ttk.Button(
|
|||
|
master=pageframe,
|
|||
|
text="›",
|
|||
|
command=self.goto_next_page,
|
|||
|
style="symbol.Link.TButton",
|
|||
|
).pack(side=RIGHT, fill=Y)
|
|||
|
|
|||
|
ttk.Button(
|
|||
|
master=pageframe,
|
|||
|
text="‹",
|
|||
|
command=self.goto_prev_page,
|
|||
|
style="symbol.Link.TButton",
|
|||
|
).pack(side=RIGHT, fill=Y)
|
|||
|
ttk.Button(
|
|||
|
master=pageframe,
|
|||
|
text="«",
|
|||
|
command=self.goto_first_page,
|
|||
|
style="symbol.Link.TButton",
|
|||
|
).pack(side=RIGHT, fill=Y)
|
|||
|
|
|||
|
ttk.Separator(pageframe, orient=VERTICAL).pack(side=RIGHT, padx=10)
|
|||
|
|
|||
|
lbl = ttk.Label(pageframe, textvariable=self._pagelimit)
|
|||
|
lbl.pack(side=RIGHT, padx=(0, 5))
|
|||
|
ttk.Label(pageframe, text=MessageCatalog.translate("of")).pack(side=RIGHT, padx=(5, 0))
|
|||
|
|
|||
|
index = ttk.Entry(pageframe, textvariable=self._pageindex, width=4)
|
|||
|
index.pack(side=RIGHT)
|
|||
|
index.bind("<Return>", self.goto_page, "+")
|
|||
|
index.bind("<KP_Enter>", self.goto_page, "+")
|
|||
|
|
|||
|
ttk.Label(pageframe, text=MessageCatalog.translate("Page")).pack(side=RIGHT, padx=5)
|
|||
|
|
|||
|
def _build_table_rows(self, rowdata):
|
|||
|
"""Build, load, and configure the DataTableRow objects
|
|||
|
|
|||
|
Parameters:
|
|||
|
|
|||
|
rowdata (List):
|
|||
|
An iterable of row data
|
|||
|
"""
|
|||
|
for row in rowdata:
|
|||
|
self.insert_row(END, row)
|
|||
|
|
|||
|
def _build_table_columns(self, coldata):
|
|||
|
"""Build, load, and configure the DataTableColumn objects
|
|||
|
|
|||
|
Parameters:
|
|||
|
|
|||
|
coldata (List[str|Dict[str, Any]]):
|
|||
|
An iterable of column names or a dictionary of column
|
|||
|
configuration settings.
|
|||
|
"""
|
|||
|
for cid, col in enumerate(coldata):
|
|||
|
if isinstance(col, str):
|
|||
|
self.tablecolumns.append(
|
|||
|
TableColumn(
|
|||
|
tableview=self,
|
|||
|
cid=cid,
|
|||
|
text=col,
|
|||
|
)
|
|||
|
)
|
|||
|
else:
|
|||
|
if "text" not in col:
|
|||
|
col["text"] = f"Column {cid}"
|
|||
|
self.tablecolumns.append(
|
|||
|
TableColumn(tableview=self, cid=cid, **col)
|
|||
|
)
|
|||
|
|
|||
|
# PRIVATE METHODS - WIDGET BINDING
|
|||
|
|
|||
|
def _set_widget_binding(self):
|
|||
|
"""Setup the widget binding"""
|
|||
|
self.view.bind("<Double-Button-1>", self._header_double_leftclick)
|
|||
|
self.view.bind("<Button-1>", self._header_leftclick)
|
|||
|
if self.tk.call("tk", "windowingsystem") == "aqua":
|
|||
|
sequence = "<Button-2>"
|
|||
|
else:
|
|||
|
sequence = "<Button-3>"
|
|||
|
self.view.bind(sequence, self._table_rightclick)
|
|||
|
|
|||
|
# add trace to track pagesize changes
|
|||
|
self._pagesize.trace_add("write", self._trace_pagesize)
|
|||
|
|
|||
|
# def _select_pagesize(self, event):
|
|||
|
# cbo: ttk.Combobox = self.nametowidget(event.widget)
|
|||
|
# cbo.select_clear()
|
|||
|
# self.goto_first_page()
|
|||
|
|
|||
|
def _trace_pagesize(self, *_):
|
|||
|
"""Callback for changes to page size"""
|
|||
|
self.goto_first_page()
|
|||
|
|
|||
|
def _header_double_leftclick(self, event):
|
|||
|
"""Callback for double-click events on the tableview header"""
|
|||
|
region = self.view.identify_region(event.x, event.y)
|
|||
|
if region == "separator":
|
|||
|
self.autofit_columns()
|
|||
|
|
|||
|
def _header_leftclick(self, event):
|
|||
|
"""Callback for left-click events"""
|
|||
|
region = self.view.identify_region(event.x, event.y)
|
|||
|
if region == "heading":
|
|||
|
self.sort_column_data(event)
|
|||
|
|
|||
|
def _table_rightclick(self, event):
|
|||
|
"""Callback for right-click events"""
|
|||
|
region = self.view.identify_region(event.x, event.y)
|
|||
|
if region == "heading":
|
|||
|
self._rightclickmenu_head.tk_popup(event)
|
|||
|
elif region != "separator":
|
|||
|
self._rightclickmenu_cell.tk_popup(event)
|
|||
|
|
|||
|
|
|||
|
class TableCellRightClickMenu(tk.Menu):
|
|||
|
"""A right-click menu object for the tableview cells - INTERNAL"""
|
|||
|
|
|||
|
def __init__(self, master: Tableview):
|
|||
|
"""
|
|||
|
Parameters:
|
|||
|
|
|||
|
master (Tableview):
|
|||
|
The parent object
|
|||
|
"""
|
|||
|
super().__init__(master, tearoff=False)
|
|||
|
self.master: Tableview = master
|
|||
|
self.view: ttk.Treeview = master.view
|
|||
|
self.cid = None
|
|||
|
self.iid = None
|
|||
|
|
|||
|
config = {
|
|||
|
"sortascending": {
|
|||
|
"label": f'''⬆ {MessageCatalog.translate("Sort Ascending")}''',
|
|||
|
"command": self.sort_column_ascending,
|
|||
|
},
|
|||
|
"sortdescending": {
|
|||
|
"label": f'''⬇ {MessageCatalog.translate("Sort Descending")}''',
|
|||
|
"command": self.sort_column_descending,
|
|||
|
},
|
|||
|
"clearfilter": {
|
|||
|
"label": f'''{MessageCatalog.translate("⎌")} {MessageCatalog.translate("Clear filters")}''',
|
|||
|
"command": self.master.reset_row_filters,
|
|||
|
},
|
|||
|
"filterbyvalue": {
|
|||
|
"label": f'''{MessageCatalog.translate("Filter by cell's value")}''',
|
|||
|
"command": self.filter_to_cell_value,
|
|||
|
},
|
|||
|
"hiderows": {
|
|||
|
"label": f'''{MessageCatalog.translate("Hide select rows")}''',
|
|||
|
"command": self.hide_selected_rows,
|
|||
|
},
|
|||
|
"showrows": {
|
|||
|
"label": f'''{MessageCatalog.translate("Show only select rows")}''',
|
|||
|
"command": self.filter_to_selected_rows,
|
|||
|
},
|
|||
|
"exportall": {
|
|||
|
"label": f'''{MessageCatalog.translate("Export all records")}''',
|
|||
|
"command": self.export_all_records,
|
|||
|
},
|
|||
|
"exportpage": {
|
|||
|
"label": f'''{MessageCatalog.translate("Export current page")}''',
|
|||
|
"command": self.export_current_page,
|
|||
|
},
|
|||
|
"exportselection": {
|
|||
|
"label": f'''{MessageCatalog.translate("Export current selection")}''',
|
|||
|
"command": self.export_current_selection,
|
|||
|
},
|
|||
|
"exportfiltered": {
|
|||
|
"label": f'''{MessageCatalog.translate("Export records in filter")}''',
|
|||
|
"command": self.export_records_in_filter,
|
|||
|
},
|
|||
|
"moveup": {
|
|||
|
"label": f'''↑ {MessageCatalog.translate("Move up")}''',
|
|||
|
"command": self.move_row_up
|
|||
|
},
|
|||
|
"movedown": {
|
|||
|
"label": f'''↓ {MessageCatalog.translate("Move down")}''',
|
|||
|
"command": self.move_row_down,
|
|||
|
},
|
|||
|
"movetotop": {
|
|||
|
"label": f'''⤒ {MessageCatalog.translate("Move to top")}''',
|
|||
|
"command": self.move_row_to_top,
|
|||
|
},
|
|||
|
"movetobottom": {
|
|||
|
"label": f'''⤓ {MessageCatalog.translate("Move to bottom")}''',
|
|||
|
"command": self.move_row_to_bottom,
|
|||
|
},
|
|||
|
"alignleft": {
|
|||
|
"label": f'''◧ {MessageCatalog.translate("Align left")}''',
|
|||
|
"command": self.align_column_left,
|
|||
|
},
|
|||
|
"aligncenter": {
|
|||
|
"label": f'''◫ {MessageCatalog.translate("Align center")}''',
|
|||
|
"command": self.align_column_center,
|
|||
|
},
|
|||
|
"alignright": {
|
|||
|
"label": f'''◨ {MessageCatalog.translate("Align right")}''',
|
|||
|
"command": self.align_column_right,
|
|||
|
},
|
|||
|
"deleterows": {
|
|||
|
"label": f'''🞨 {MessageCatalog.translate("Delete selected rows")}''',
|
|||
|
"command": self.delete_selected_rows,
|
|||
|
},
|
|||
|
}
|
|||
|
sort_menu = tk.Menu(self, tearoff=False)
|
|||
|
sort_menu.add_command(cnf=config["sortascending"])
|
|||
|
sort_menu.add_command(cnf=config["sortdescending"])
|
|||
|
self.add_cascade(menu=sort_menu, label=f'''⇅ {MessageCatalog.translate("Sort")}''')
|
|||
|
|
|||
|
filter_menu = tk.Menu(self, tearoff=False)
|
|||
|
filter_menu.add_command(cnf=config["clearfilter"])
|
|||
|
filter_menu.add_separator()
|
|||
|
filter_menu.add_command(cnf=config["filterbyvalue"])
|
|||
|
filter_menu.add_command(cnf=config["hiderows"])
|
|||
|
filter_menu.add_command(cnf=config["showrows"])
|
|||
|
self.add_cascade(menu=filter_menu, label=f'''⧨ {MessageCatalog.translate("Filter")}''')
|
|||
|
|
|||
|
export_menu = tk.Menu(self, tearoff=False)
|
|||
|
export_menu.add_command(cnf=config["exportall"])
|
|||
|
export_menu.add_command(cnf=config["exportpage"])
|
|||
|
export_menu.add_command(cnf=config["exportselection"])
|
|||
|
export_menu.add_command(cnf=config["exportfiltered"])
|
|||
|
self.add_cascade(menu=export_menu, label=f'''↔ {MessageCatalog.translate("Export")}''')
|
|||
|
|
|||
|
move_menu = tk.Menu(self, tearoff=False)
|
|||
|
move_menu.add_command(cnf=config["moveup"])
|
|||
|
move_menu.add_command(cnf=config["movedown"])
|
|||
|
move_menu.add_command(cnf=config["movetotop"])
|
|||
|
move_menu.add_command(cnf=config["movetobottom"])
|
|||
|
self.add_cascade(menu=move_menu, label=f'''⇵ {MessageCatalog.translate("Move")}''')
|
|||
|
|
|||
|
align_menu = tk.Menu(self, tearoff=False)
|
|||
|
align_menu.add_command(cnf=config["alignleft"])
|
|||
|
align_menu.add_command(cnf=config["aligncenter"])
|
|||
|
align_menu.add_command(cnf=config["alignright"])
|
|||
|
self.add_cascade(menu=align_menu, label=f'''↦ {MessageCatalog.translate("Align")}''')
|
|||
|
self.add_command(cnf=config["deleterows"])
|
|||
|
|
|||
|
def tk_popup(self, event):
|
|||
|
"""Display the menu below the selected cell.
|
|||
|
|
|||
|
Parameters:
|
|||
|
|
|||
|
event (Event):
|
|||
|
The click event that triggers menu.
|
|||
|
"""
|
|||
|
# capture the column and item that invoked the menu
|
|||
|
self.event = event
|
|||
|
iid = self.view.identify_row(event.y)
|
|||
|
col = self.view.identify_column(event.x)
|
|||
|
|
|||
|
# show the menu below the invoking cell
|
|||
|
rootx = self.view.winfo_rootx()
|
|||
|
rooty = self.view.winfo_rooty()
|
|||
|
try:
|
|||
|
bbox = self.view.bbox(iid, col)
|
|||
|
except:
|
|||
|
return
|
|||
|
try:
|
|||
|
super().tk_popup(rootx + bbox[0], rooty + bbox[1] + bbox[3])
|
|||
|
except IndexError:
|
|||
|
pass
|
|||
|
|
|||
|
def sort_column_ascending(self):
|
|||
|
"""Sort the column in ascending order."""
|
|||
|
self.master.sort_column_data(self.event, sort=ASCENDING)
|
|||
|
|
|||
|
def sort_column_descending(self):
|
|||
|
"""Sort the column in descending order."""
|
|||
|
self.master.sort_column_data(self.event, sort=DESCENDING)
|
|||
|
|
|||
|
def filter_to_cell_value(self):
|
|||
|
"""Hide all records except for records where the current
|
|||
|
column exactly matches the current cell value."""
|
|||
|
self.master.filter_column_to_value(self.event)
|
|||
|
|
|||
|
def filter_to_selected_rows(self):
|
|||
|
"""Hide all records except for the selected rows."""
|
|||
|
self.master.filter_to_selected_rows()
|
|||
|
|
|||
|
def export_all_records(self):
|
|||
|
"""Export all records to a csv file"""
|
|||
|
self.master.export_all_records()
|
|||
|
|
|||
|
def export_current_page(self):
|
|||
|
"""Export records on current page"""
|
|||
|
self.master.export_current_page()
|
|||
|
|
|||
|
def export_current_selection(self):
|
|||
|
"""Export rows currently selected"""
|
|||
|
self.master.export_current_selection()
|
|||
|
|
|||
|
def export_records_in_filter(self):
|
|||
|
"""Export rows currently filtered"""
|
|||
|
self.master.export_records_in_filter()
|
|||
|
|
|||
|
def hide_selected_rows(self):
|
|||
|
"""Hide the selected rows"""
|
|||
|
self.master.hide_selected_rows()
|
|||
|
|
|||
|
def move_row_to_top(self):
|
|||
|
"""Move the row to the top of the data set"""
|
|||
|
self.master.move_selected_rows_to_top()
|
|||
|
|
|||
|
def move_row_to_bottom(self):
|
|||
|
"""Move the row to the bottom of the dataset"""
|
|||
|
self.master.move_selected_rows_to_bottom()
|
|||
|
|
|||
|
def move_row_up(self):
|
|||
|
"""Move the selected above the previous sibling"""
|
|||
|
self.master.move_selected_row_up()
|
|||
|
|
|||
|
def move_row_down(self):
|
|||
|
"""Move the selected row below the next sibling"""
|
|||
|
self.master.move_row_down()
|
|||
|
|
|||
|
def align_column_left(self):
|
|||
|
"Left align the column text"
|
|||
|
self.master.align_column_left(self.event)
|
|||
|
|
|||
|
def align_column_right(self):
|
|||
|
"""Right align the column text"""
|
|||
|
self.master.align_column_right(self.event)
|
|||
|
|
|||
|
def align_column_center(self):
|
|||
|
"""Center align the column text"""
|
|||
|
self.master.align_column_center(self.event)
|
|||
|
|
|||
|
def delete_selected_rows(self):
|
|||
|
"""Delete the selected rows"""
|
|||
|
iids = self.view.selection()
|
|||
|
if len(iids) > 0:
|
|||
|
# setting to prev should be in master?
|
|||
|
prev_item = self.view.prev(iids[0])
|
|||
|
self.master.delete_rows(iids=iids)
|
|||
|
self.view.focus(prev_item)
|
|||
|
self.view.selection_set(prev_item)
|
|||
|
|
|||
|
|
|||
|
class TableHeaderRightClickMenu(tk.Menu):
|
|||
|
"""A right-click menu object for the tableview header - INTERNAL"""
|
|||
|
|
|||
|
def __init__(self, master: Tableview):
|
|||
|
"""
|
|||
|
Parameters:
|
|||
|
|
|||
|
master (Tableview):
|
|||
|
The parent object
|
|||
|
"""
|
|||
|
super().__init__(master, tearoff=False)
|
|||
|
self.master: Tableview = self.master
|
|||
|
self.view: ttk.Treeview = master.view
|
|||
|
self.event = None
|
|||
|
self.columnvars = []
|
|||
|
self._show_menu = None
|
|||
|
|
|||
|
config = {
|
|||
|
"movetoright": {
|
|||
|
"label": f'''→ {MessageCatalog.translate("Move to right")}''',
|
|||
|
"command": self.move_column_right,
|
|||
|
},
|
|||
|
"movetoleft": {
|
|||
|
"label": f'''← {MessageCatalog.translate("Move to left")}''',
|
|||
|
"command": self.move_column_left,
|
|||
|
},
|
|||
|
"movetofirst": {
|
|||
|
"label": f'''⇤ {MessageCatalog.translate("Move to first")}''',
|
|||
|
"command": self.move_column_to_first,
|
|||
|
},
|
|||
|
"movetolast": {
|
|||
|
"label": f'''⇥ {MessageCatalog.translate("Move to last")}''',
|
|||
|
"command": self.move_column_to_last,
|
|||
|
},
|
|||
|
"alignleft": {
|
|||
|
"label": f'''◧ {MessageCatalog.translate("Align left")}''',
|
|||
|
"command": self.align_heading_left,
|
|||
|
},
|
|||
|
"alignright": {
|
|||
|
"label": f'''◨ {MessageCatalog.translate("Align right")}''',
|
|||
|
"command": self.align_heading_right,
|
|||
|
},
|
|||
|
"aligncenter": {
|
|||
|
"label": f'''◫ {MessageCatalog.translate("Align center")}''',
|
|||
|
"command": self.align_heading_center,
|
|||
|
},
|
|||
|
"resettable": {
|
|||
|
"label": f'''{MessageCatalog.translate("⎌")} {MessageCatalog.translate("Reset table")}''',
|
|||
|
"command": self.master.reset_table,
|
|||
|
},
|
|||
|
"deletecolumn": {
|
|||
|
"label": f'''🞨 {MessageCatalog.translate("Delete column")}''',
|
|||
|
"command": self.delete_column,
|
|||
|
},
|
|||
|
"hidecolumn": {
|
|||
|
"label": f'''◑ {MessageCatalog.translate("Hide column")}''',
|
|||
|
"command": self.hide_column,
|
|||
|
},
|
|||
|
}
|
|||
|
|
|||
|
self.add_command(cnf=config["resettable"])
|
|||
|
|
|||
|
# HIDE & SHOW
|
|||
|
self._build_show_menu()
|
|||
|
self.add_cascade(menu=self._show_menu, label=f'''± {MessageCatalog.translate("Columns")}''')
|
|||
|
self.add_separator()
|
|||
|
|
|||
|
# MOVE MENU
|
|||
|
move_menu = tk.Menu(self, tearoff=False)
|
|||
|
move_menu.add_command(cnf=config["movetoleft"])
|
|||
|
move_menu.add_command(cnf=config["movetoright"])
|
|||
|
move_menu.add_command(cnf=config["movetofirst"])
|
|||
|
move_menu.add_command(cnf=config["movetolast"])
|
|||
|
self.add_cascade(menu=move_menu, label=f'''⇄ {MessageCatalog.translate("Move")}''')
|
|||
|
|
|||
|
align_menu = tk.Menu(self, tearoff=False)
|
|||
|
align_menu.add_command(cnf=config["alignleft"])
|
|||
|
align_menu.add_command(cnf=config["aligncenter"])
|
|||
|
align_menu.add_command(cnf=config["alignright"])
|
|||
|
self.add_cascade(menu=align_menu, label=f'''↦ {MessageCatalog.translate("Align")}''')
|
|||
|
self.add_command(cnf=config["hidecolumn"])
|
|||
|
self.add_command(cnf=config["deletecolumn"])
|
|||
|
|
|||
|
def tk_popup(self, event):
|
|||
|
# capture the column and item that invoked the menu
|
|||
|
self.event = event
|
|||
|
self._build_show_menu()
|
|||
|
|
|||
|
# show the menu below the invoking cell
|
|||
|
rootx = self.view.winfo_rootx()
|
|||
|
rooty = self.view.winfo_rooty()
|
|||
|
super().tk_popup(rootx + event.x, rooty + event.y + 10)
|
|||
|
|
|||
|
def _build_show_menu(self):
|
|||
|
"""Build the show menu based on currently available columns"""
|
|||
|
if self._show_menu is not None:
|
|||
|
self._show_menu.delete(0, END)
|
|||
|
else:
|
|||
|
self._show_menu = tk.Menu(self, tearoff=False)
|
|||
|
|
|||
|
self._show_menu.add_command(
|
|||
|
label=MessageCatalog.translate("Show All"), command=self.show_all_columns
|
|||
|
)
|
|||
|
self._show_menu.add_separator()
|
|||
|
|
|||
|
displaycolumns = [x.cid for x in self.master.tablecolumns_visible]
|
|||
|
for column in self.master.tablecolumns:
|
|||
|
varname = f"column_{column.cid}"
|
|||
|
# self.columnvars.append(tk.Variable(name=varname, value=True))
|
|||
|
self._show_menu.add_checkbutton(
|
|||
|
label=column._headertext,
|
|||
|
command=lambda w=column: self.toggle_columns(w.cid),
|
|||
|
variable=varname,
|
|||
|
onvalue=True,
|
|||
|
offvalue=False,
|
|||
|
)
|
|||
|
if column.cid in displaycolumns:
|
|||
|
self.setvar(varname, True)
|
|||
|
else:
|
|||
|
self.setvar(varname, False)
|
|||
|
|
|||
|
def toggle_columns(self, cid):
|
|||
|
"""Toggles the visibility of the selected column"""
|
|||
|
variable = f"column_{cid}"
|
|||
|
toggled = self.getvar(variable)
|
|||
|
if toggled:
|
|||
|
self.master.unhide_selected_column(cid=int(cid))
|
|||
|
else:
|
|||
|
self.master.hide_selected_column(cid=int(cid))
|
|||
|
|
|||
|
def show_all_columns(self):
|
|||
|
"""Show all columns"""
|
|||
|
for var in self.columnvars:
|
|||
|
var.set(value=True)
|
|||
|
self.master.reset_column_filters()
|
|||
|
|
|||
|
def move_column_left(self):
|
|||
|
"""Move column one position to the left"""
|
|||
|
self.master.move_column_left(self.event)
|
|||
|
|
|||
|
def move_column_right(self):
|
|||
|
"""Move column on position to the right"""
|
|||
|
self.master.move_column_right(self.event)
|
|||
|
|
|||
|
def move_column_to_first(self):
|
|||
|
"""Move column to leftmost position"""
|
|||
|
self.master.move_column_to_first(self.event)
|
|||
|
|
|||
|
def move_column_to_last(self):
|
|||
|
"""Move column to rightmost position"""
|
|||
|
self.master.move_column_to_last(self.event)
|
|||
|
|
|||
|
def align_heading_left(self):
|
|||
|
"""Left align the column header"""
|
|||
|
self.master.align_heading_left(self.event)
|
|||
|
|
|||
|
def align_heading_right(self):
|
|||
|
"""Right align the column header"""
|
|||
|
self.master.align_heading_right(self.event)
|
|||
|
|
|||
|
def align_heading_center(self):
|
|||
|
"""Center align the column header"""
|
|||
|
self.master.align_heading_center(self.event)
|
|||
|
|
|||
|
def delete_column(self):
|
|||
|
"""Delete the selected column"""
|
|||
|
eo = self.master._get_event_objects(self.event)
|
|||
|
eo.column.delete()
|
|||
|
|
|||
|
def hide_column(self):
|
|||
|
"""Hide the selected column"""
|
|||
|
eo = self.master._get_event_objects(self.event)
|
|||
|
eo.column.hide()
|