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()
|