From 9de4fc4cc49ebbc8dd20e2757092c8b3338f0330 Mon Sep 17 00:00:00 2001 From: ragardner Date: Tue, 22 Oct 2024 19:04:02 +0100 Subject: [PATCH] 7.2.18 #### Fixed: - Inserting rows/columns with hidden rows/columns sometimes resulted in incorrect rows/columns being displayed - Treeview function `insert()` when using parameter `index` sometimes resulted in treeview items being displayed in the wrong locations #### Changed: - Using function `set_currently_selected()` or any function/setter which does the same internally will now trigger a select event like creating selection boxes does - iids in Treeview mode are now case sensitive - Treeview function `tree_build()` when parameter `safety` is `False` will no longer check for missing ids in the iid column - Treeview function `get_children()` now gets item ids from the row index which provides them in the same order as in the treeview - Add parameter `run_binding` to treeview functions `selection_add()`, `selection_set()` - Slight color change for top left rectangle bars #### Added: - Initialization parameters `default_header` and `default_row_index` can now optionally be set to `None` to not display anything in empty header cells / row index cells - Parameters `lower` and `include_text_column` to treeview function `tree_build()` - Treeview function `bulk_insert()` - Treeview function `get_nodes()` behaves exactly the same as `get_children()` except it retrieves item ids from the tree nodes `dict` not the row index. - Treeview function `descendants()` which returns a generator - Treeview property `tree_selected` --- docs/CHANGELOG.md | 20 ++- docs/DOCUMENTATION.md | 130 +++++++++++++++++- tksheet/functions.py | 18 ++- tksheet/main_table.py | 36 ++--- tksheet/other_classes.py | 1 - tksheet/row_index.py | 8 +- tksheet/sheet.py | 278 +++++++++++++++++++++++++-------------- tksheet/themes.py | 4 +- 8 files changed, 361 insertions(+), 134 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 0c3af76..d3f482d 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,23 @@ ### Version 7.2.18 -#### +#### Fixed: +- Inserting rows/columns with hidden rows/columns sometimes resulted in incorrect rows/columns being displayed +- Treeview function `insert()` when using parameter `index` sometimes resulted in treeview items being displayed in the wrong locations + +#### Changed: +- Using function `set_currently_selected()` or any function/setter which does the same internally will now trigger a select event like creating selection boxes does +- iids in Treeview mode are now case sensitive +- Treeview function `tree_build()` when parameter `safety` is `False` will no longer check for missing ids in the iid column +- Treeview function `get_children()` now gets item ids from the row index which provides them in the same order as in the treeview +- Add parameter `run_binding` to treeview functions `selection_add()`, `selection_set()` +- Slight color change for top left rectangle bars + +#### Added: +- Initialization parameters `default_header` and `default_row_index` can now optionally be set to `None` to not display anything in empty header cells / row index cells +- Parameters `lower` and `include_text_column` to treeview function `tree_build()` +- Treeview function `bulk_insert()` +- Treeview function `get_nodes()` behaves exactly the same as `get_children()` except it retrieves item ids from the tree nodes `dict` not the row index. +- Treeview function `descendants()` which returns a generator +- Treeview property `tree_selected` ### Version 7.2.17 #### Changed: diff --git a/docs/DOCUMENTATION.md b/docs/DOCUMENTATION.md index 036e7d8..8bd0d09 100644 --- a/docs/DOCUMENTATION.md +++ b/docs/DOCUMENTATION.md @@ -310,8 +310,8 @@ def __init__( header: None | list[object] = None, row_index: None | list[object] = None, index: None | list[object] = None, - default_header: Literal["letters", "numbers", "both"] = "letters", - default_row_index: Literal["letters", "numbers", "both"] = "numbers", + default_header: Literal["letters", "numbers", "both"] | None = "letters", + default_row_index: Literal["letters", "numbers", "both"] | None = "numbers", data_reference: None | Sequence[Sequence[object]] = None, data: None | Sequence[Sequence[object]] = None, # either (start row, end row, "rows"), (start column, end column, "rows") or @@ -5760,6 +5760,54 @@ sheet.insert( ) ``` +___ + +#### **Insert multiple items** + +```python +bulk_insert( + data: list[list[object]], + parent: str = "", + index: None | int | Literal["end"] = None, + iid_column: int | None = None, + text_column: int | None | str = None, + create_selections: bool = False, + include_iid_column: bool = True, + include_text_column: bool = True, +) -> dict[str, int] +``` +Parameters: +- `parent` is the `iid` of the parent item (if any). If left as `""` then the items will not have a parent. +- `index` is the row number for the items to be placed at, leave as `None` for the end. +- `iid_column` if left as `None` iids will be automatically generated for the new items, else you can specify a column in the `data` which contains the iids. +- `text_column`: + - If left as `None` there will be no displayed text next to the items. + - A text column can be provided in the data and `text_column` set to an `int` representing its index to provide the displayed text for the items. + - Or if a `str` is used all items will have that `str` as their displayed text. +- `create_selections` when `True` selects the row that has just been created. +- `include_iid_column` when `False` excludes the iid column from the inserted rows. +- `include_text_column` when the `text_column` is an `int` setting this to `False` excludes that column from the treeview. + +Notes: +- Returns a `dict[str, int]` of key: new iids, value: their data row number. + +Example: +```python +sheet.insert( + iid="top level", + text="Top level", + values=["cell A1", "cell B1"], +) +sheet.insert( + parent="top level", + iid="mid level", + text="Mid level", + values=["cell A2", "cell B2"], +) +``` + +___ + #### **Build a tree from data** This takes a list of lists where sublists are rows and a few arguments to bulk insert items into the treeview. **Note that**: @@ -5776,8 +5824,10 @@ tree_build( open_ids: Iterator[str] | None = None, safety: bool = True, ncols: int | None = None, + lower: bool = False, include_iid_column: bool = True, include_parent_column: bool = True, + include_text_column: bool = True, ) -> Sheet ``` Parameters: @@ -5791,8 +5841,10 @@ Parameters: - In the case of empty iid cells the row will be ignored. - In the case of duplicate iids they will be renamed and `"DUPLICATED_"` will be attached to the end. - `ncols` is like maximum columns, an `int` which limits the number of columns that are included in the loaded data. +- `lower` makes all item ids - iids lower case. - `include_iid_column` when `False` excludes the iid column from the inserted rows. - `include_parent_column` when `False` excludes the parent column from the inserted rows. +- `include_text_column` when the `text_column` is an `int` setting this to `False` excludes that column from the treeview. Notes: - Returns the `Sheet` object. @@ -5812,18 +5864,24 @@ sheet.tree_build( ) ``` +___ + #### **Reset the treeview** ```python tree_reset() -> Sheet ``` +___ + #### **Get treeview iids that are open** ```python tree_get_open() -> set[str] ``` +___ + #### **Set the open treeview iids** ```python @@ -5831,6 +5889,8 @@ tree_set_open(open_ids: Iterator[str]) -> Sheet ``` - Any other iids are closed as a result. +___ + #### **Open treeview iids** ```python @@ -5838,6 +5898,8 @@ tree_open(*items, redraw: bool = True) -> Sheet ``` - Opens all given iids. +___ + #### **Close treeview iids** ```python @@ -5845,6 +5907,8 @@ tree_close(*items, redraw: bool = True) -> Sheet ``` - Closes all given iids. +___ + #### **Set or get an iids attributes** ```python @@ -5875,6 +5939,8 @@ Notes: } ``` +___ + #### **Get an iids row number** ```python @@ -5882,6 +5948,8 @@ itemrow(item: str) -> int ``` - Includes hidden rows in counting row numbers. +___ + #### **Get a row numbers item** ```python @@ -5889,6 +5957,8 @@ rowitem(row: int, data_index: bool = False) -> str | None ``` - Includes hidden rows in counting row numbers. See [here](https://github.com/ragardner/tksheet/wiki/Version-7#hiding-rows) for more information. +___ + #### **Get treeview children** ```python @@ -5899,6 +5969,32 @@ get_children(item: None | str = None) -> Generator[str] - Use an empty `str` (`""`) to get all top level iids in the treeview. - Use an iid to get the children for that particular iid. Does not include all descendants. +```python +get_nodes(item: None | str = None) -> Generator[str]: +``` +- Exactly the same as above but instead of retrieving iids in the order that they appear in the treeview it retrieves iids from the internal `dict` which may not be ordered. + +___ + +#### **Get item descendants** + +```python +descendants(item: str, check_open: bool = False) -> Generator[str]: +``` +- Returns a generator which yields item ids in the order that they appear in the treeview. + +___ + +#### **Get the currently selected treeview item** + +```python +@property +tree_selected() -> str | None: +``` +- Returns the item id of the currently selected box row. If nothing is selected returns `None`. + +___ + #### **Delete treeview items** ```python @@ -5907,6 +6003,8 @@ del_items(*items) -> Sheet - `*items` the iids of items to delete. - Also deletes all item descendants. +___ + #### **Move items to a new parent** ```python @@ -5915,6 +6013,8 @@ set_children(parent: str, *newchildren) -> Sheet - `parent` the new parent for the items. - `*newchildren` the items to move. +___ + #### **Move an item to a new parent** ```python @@ -5928,6 +6028,8 @@ move(item: str, parent: str, index: int | None = None) -> Sheet - Use an `int` to move the item to an index within its parents children (or within top level items if moving to the top). - `reattach()` is exactly the same as `move()`. +___ + #### **Check an item exists** ```python @@ -5935,6 +6037,8 @@ exists(item: str) -> bool ``` - `item` - a treeview iid. +___ + #### **Get an items parent** ```python @@ -5942,6 +6046,8 @@ parent(item: str) -> str ``` - `item` - a treeview iid. +___ + #### **Get an items index** ```python @@ -5949,6 +6055,8 @@ index(item: str) -> int ``` - `item` - a treeview iid. +___ + #### **Check if an item is currently displayed** ```python @@ -5956,6 +6064,8 @@ item_displayed(item: str) -> bool ``` - `item` - a treeview iid. +___ + #### **Display an item** Make sure an items parents are all open, does **not** scroll to the item. @@ -5965,6 +6075,8 @@ display_item(item: str, redraw: bool = False) -> Sheet ``` - `item` - a treeview iid. +___ + #### **Scroll to an item** - Make sure an items parents are all open and scrolls to the item. @@ -5974,6 +6086,8 @@ scroll_to_item(item: str, redraw: bool = False) -> Sheet ``` - `item` - a treeview iid. +___ + #### **Get currently selected items** ```python @@ -5985,25 +6099,33 @@ Notes: Parameters: - `cells` when `True` any selected cells will also qualify as selected items. +___ + #### **Set selected items** ```python -selection_set(*items, redraw: bool = True) -> Sheet +selection_set(*items, run_binding: bool = True, redraw: bool = True) -> Sheet ``` - Sets selected rows (items). +___ + #### **Add selected items** ```python -selection_add(*items, redraw: bool = True) -> Sheet +selection_add(*items, run_binding: bool = True, redraw: bool = True) -> Sheet ``` +___ + #### **Remove selections** ```python selection_remove(*items, redraw: bool = True) -> Sheet ``` +___ + #### **Toggle selections** ```python diff --git a/tksheet/functions.py b/tksheet/functions.py index 3f0f398..ecb4ae7 100644 --- a/tksheet/functions.py +++ b/tksheet/functions.py @@ -18,6 +18,7 @@ ) from functools import partial from itertools import islice, repeat +from typing import Literal from .formatters import ( to_bool, @@ -363,12 +364,15 @@ def idx_param_to_int(idx: str | int | None) -> int | None: return alpha2idx(idx) -def get_n2a(n: int = 0, _type: str = "numbers") -> str: +def get_n2a(n: int = 0, _type: Literal["letters", "numbers", "both"] | None = "numbers") -> str: if _type == "letters": return num2alpha(n) elif _type == "numbers": return f"{n + 1}" - return f"{num2alpha(n)} {n + 1}" + elif _type == "both": + return f"{num2alpha(n)} {n + 1}" + elif _type is None: + return "" def get_index_of_gap_in_sorted_integer_seq_forward( @@ -514,6 +518,16 @@ def index_exists(seq: Sequence[object], index: int) -> bool: return False +def add_to_displayed(displayed: list[int], to_add: Iterator[int]) -> list[int]: + # assumes to_add is sorted in reverse + for i in reversed(to_add): + ins = bisect_left(displayed, i) + displayed[ins:] = [e + 1 for e in islice(displayed, ins, None)] + for i in reversed(to_add): + displayed.insert(bisect_left(displayed, i), i) + return displayed + + def move_elements_by_mapping( seq: list[object], new_idxs: dict[int, int], diff --git a/tksheet/main_table.py b/tksheet/main_table.py index f73be7b..e329949 100644 --- a/tksheet/main_table.py +++ b/tksheet/main_table.py @@ -49,7 +49,9 @@ try_to_bool, ) from .functions import ( + add_to_displayed, b_index, + cell_right_within_box, consecutive_ranges, decompress_load, diff_gen, @@ -73,7 +75,6 @@ new_tk_event, pickle_obj, pickled_event_dict, - cell_right_within_box, rounded_box_coords, span_idxs_post_move, try_binding, @@ -4317,16 +4318,7 @@ def add_columns( if isinstance(displayed_columns, list): self.displayed_columns = displayed_columns elif not self.all_columns_displayed: - # push displayed indexes by one for every inserted column - self.displayed_columns.sort() - # highest index is first in columns - up_to = len(self.displayed_columns) - for cn in columns: - self.displayed_columns.insert((last_ins := bisect_left(self.displayed_columns, cn)), cn) - self.displayed_columns[last_ins + 1 : up_to] = [ - i + 1 for i in islice(self.displayed_columns, last_ins + 1, up_to) - ] - up_to = last_ins + self.displayed_columns = add_to_displayed(self.displayed_columns, columns) cws = self.get_column_widths() if column_widths and next(reversed(column_widths)) > len(cws): for i in reversed(range(len(cws), len(cws) + next(reversed(column_widths)) - len(cws))): @@ -4417,7 +4409,7 @@ def rc_add_columns(self, event: object = None): else: numcols = 1 displayed_ins_col = len(self.col_positions) - 1 - data_ins_col = int(displayed_ins_col) + data_ins_col = self.total_data_cols() if ( isinstance(self.PAR.ops.paste_insert_column_limit, int) and self.PAR.ops.paste_insert_column_limit < displayed_ins_col + numcols @@ -4462,16 +4454,7 @@ def add_rows( if isinstance(displayed_rows, list): self.displayed_rows = displayed_rows elif not self.all_rows_displayed: - # push displayed indexes by one for every inserted row - self.displayed_rows.sort() - # highest index is first in rows - up_to = len(self.displayed_rows) - for rn in rows: - self.displayed_rows.insert((last_ins := bisect_left(self.displayed_rows, rn)), rn) - self.displayed_rows[last_ins + 1 : up_to] = [ - i + 1 for i in islice(self.displayed_rows, last_ins + 1, up_to) - ] - up_to = last_ins + self.displayed_rows = add_to_displayed(self.displayed_rows, rows) rhs = self.get_row_heights() if row_heights and next(reversed(row_heights)) > len(rhs): default_row_height = self.get_default_row_height() @@ -4561,7 +4544,7 @@ def rc_add_rows(self, event: object = None): else: numrows = 1 displayed_ins_row = len(self.row_positions) - 1 - data_ins_row = int(displayed_ins_row) + data_ins_row = self.total_data_rows() if ( isinstance(self.PAR.ops.paste_insert_row_limit, int) and self.PAR.ops.paste_insert_row_limit < displayed_ins_row + numrows @@ -5847,6 +5830,7 @@ def set_currently_selected( c: int | None = None, item: int | None = None, box: tuple[int, int, int, int] | None = None, + run_binding: bool = True, ) -> None: if isinstance(item, int) and item in self.selection_boxes: selection_box = self.selection_boxes[item] @@ -5862,6 +5846,8 @@ def set_currently_selected( selection_box.type_, selection_box.fill_iid, ) + if run_binding: + self.run_selection_binding(selection_box.type_) return # currently selected is pointed at any selection box with "box" coordinates if isinstance(box, tuple): @@ -5878,6 +5864,8 @@ def set_currently_selected( selection_box.type_, selection_box.fill_iid, ) + if run_binding: + self.run_selection_binding(selection_box.type_) return # currently selected is just pointed at a coordinate # find the top most box there, requires r and c @@ -5891,6 +5879,8 @@ def set_currently_selected( selection_box.type_, selection_box.fill_iid, ) + if run_binding: + self.run_selection_binding(selection_box.type_) return # wasn't provided an item and couldn't find a box at coords so select cell if r < len(self.row_positions) - 1 and c < len(self.col_positions) - 1: diff --git a/tksheet/other_classes.py b/tksheet/other_classes.py index 3a5e424..307b2e1 100644 --- a/tksheet/other_classes.py +++ b/tksheet/other_classes.py @@ -6,7 +6,6 @@ from functools import partial from typing import Literal - pickle_obj = partial(pickle.dumps, protocol=pickle.HIGHEST_PROTOCOL) FontTuple = namedtuple("FontTuple", "family size style") diff --git a/tksheet/row_index.py b/tksheet/row_index.py index 21f9323..a53bd50 100644 --- a/tksheet/row_index.py +++ b/tksheet/row_index.py @@ -1206,6 +1206,8 @@ def auto_set_index_width(self, end_row: int, only_rows: list) -> bool: new_w = self.MT.get_txt_w(f"{end_row}") + 20 elif self.PAR.ops.default_row_index == "both": new_w = self.MT.get_txt_w(f"{end_row + 1} {num2alpha(end_row)}") + 20 + elif self.PAR.ops.default_row_index is None: + new_w = 20 elif self.PAR.ops.auto_resize_row_index is True: new_w = self.get_index_text_width(only_rows=only_rows) else: @@ -1380,7 +1382,7 @@ def redraw_tree_arrow( # x1 + 5 + indent + small_mod + small_mod, # y1 + mid_y + small_mod + small_mod, # ) - + # POINTS FOR A LINE THAT STOPS AT ROW LINE # points = ( # # the upper point @@ -1390,7 +1392,7 @@ def redraw_tree_arrow( # x1 + 5 + indent + small_mod + small_mod, # y2 - mid_y + small_mod + small_mod, # ) - + # POINTS FOR A HORIZONTAL LINE points = ( # the left point @@ -1400,7 +1402,7 @@ def redraw_tree_arrow( x1 + 5 + indent + small_mod + small_mod + small_mod + small_mod, y1 + mid_y, ) - + if self.hidd_tree_arrow: t, sh = self.hidd_tree_arrow.popitem() self.coords(t, points) diff --git a/tksheet/sheet.py b/tksheet/sheet.py index 7503823..cbce164 100644 --- a/tksheet/sheet.py +++ b/tksheet/sheet.py @@ -19,6 +19,7 @@ product, repeat, ) +from operator import attrgetter from timeit import default_timer from tkinter import ttk from typing import Literal @@ -110,8 +111,8 @@ def __init__( header: None | list[object] = None, row_index: None | list[object] = None, index: None | list[object] = None, - default_header: Literal["letters", "numbers", "both"] = "letters", - default_row_index: Literal["letters", "numbers", "both"] = "numbers", + default_header: Literal["letters", "numbers", "both"] | None = "letters", + default_row_index: Literal["letters", "numbers", "both"] | None = "numbers", data_reference: None | Sequence[Sequence[object]] = None, data: None | Sequence[Sequence[object]] = None, # either (start row, end row, "rows"), (start column, end column, "rows") or @@ -4715,8 +4716,10 @@ def tree_build( open_ids: Iterator[str] | None = None, safety: bool = True, ncols: int | None = None, + lower: bool = False, include_iid_column: bool = True, include_parent_column: bool = True, + include_text_column: bool = True, ) -> Sheet: self.reset(cell_options=False, column_widths=False, header=False, redraw=False) if text_column is None: @@ -4727,7 +4730,12 @@ def tree_build( for rn, row in enumerate(data): if safety and ncols > (lnr := len(row)): row += self.MT.get_empty_row_seq(rn, end=ncols, start=lnr) - iid, pid = row[iid_column].lower(), row[parent_column].lower() + if lower: + iid = row[iid_column].lower() + pid = row[parent_column].lower() + else: + iid = row[iid_column] + pid = row[parent_column] if safety: if not iid: continue @@ -4736,7 +4744,7 @@ def tree_build( x = 1 while iid in tally_of_ids: new = f"{row[iid_column]}_DUPLICATED_{x}" - iid = new.lower() + iid = new.lower() if lower else new x += 1 tally_of_ids[iid] += 1 row[iid_column] = new @@ -4755,14 +4763,14 @@ def tree_build( else: self.RI.tree[iid].parent = "" self.RI.tree_rns[iid] = rn - for n in self.RI.tree.values(): - if n.parent is None: - n.parent = "" - newrow = self.MT.get_empty_row_seq(len(data), ncols) - newrow[iid_column] = n.iid - self.RI.tree_rns[n.iid] = len(data) - data.append(newrow) - + if safety: + for n in self.RI.tree.values(): + if n.parent is None: + n.parent = "" + newrow = self.MT.get_empty_row_seq(len(data), ncols) + newrow[iid_column] = n.iid + self.RI.tree_rns[n.iid] = len(data) + data.append(newrow) insert_rows = partial( self.insert_rows, idx=0, @@ -4773,37 +4781,22 @@ def tree_build( push_ops=push_ops, redraw=False, ) - - if include_iid_column and include_parent_column: - insert_rows(rows=[[self.RI.tree[iid]] + data[self.RI.tree_rns[iid]] for iid in self.get_children()]) - - elif include_iid_column and not include_parent_column: - exclude = {parent_column} - insert_rows( - rows=[ - [self.RI.tree[iid]] + [e for i, e in enumerate(data[self.RI.tree_rns[iid]]) if i not in exclude] - for iid in self.get_children() - ] - ) - - elif include_parent_column and not include_iid_column: - exclude = {iid_column} + exclude = set() + if not include_iid_column: + exclude.add(iid_column) + if not include_parent_column: + exclude.add(parent_column) + if isinstance(text_column, int) and not include_text_column: + exclude.add(text_column) + if exclude: insert_rows( rows=[ [self.RI.tree[iid]] + [e for i, e in enumerate(data[self.RI.tree_rns[iid]]) if i not in exclude] - for iid in self.get_children() + for iid in self.get_nodes() ] ) - - elif not include_iid_column and not include_parent_column: - exclude = {iid_column, parent_column} - insert_rows( - rows=[ - [self.RI.tree[iid]] + [e for i, e in enumerate(data[self.RI.tree_rns[iid]]) if i not in exclude] - for iid in self.get_children() - ] - ) - + else: + insert_rows(rows=[[self.RI.tree[iid]] + data[self.RI.tree_rns[iid]] for iid in self.get_nodes()]) self.MT.all_rows_displayed = False self.MT.displayed_rows = list(range(len(self.MT._row_index))) self.RI.tree_rns = {n.iid: i for i, n in enumerate(self.MT._row_index)} @@ -4840,7 +4833,7 @@ def tree_set_open(self, open_ids: Iterator[str]) -> Sheet: deselect_all=False, data_indexes=True, ) - open_ids = set(filter(self.exists, map(str.lower, open_ids))) + open_ids = set(filter(self.exists, open_ids)) self.RI.tree_open_ids = set() if open_ids: self.show_rows( @@ -4924,58 +4917,129 @@ def insert( values: None | list[object] = None, create_selections: bool = False, ) -> str: - if iid is None: + """ + Insert an item into the treeview + """ + if not iid: i = 0 while (iid := f"{num2alpha(i)}") in self.RI.tree: i += 1 - iid, pid = iid.lower(), parent.lower() - if not iid: - raise ValueError("iid cannot be empty string.") if iid in self.RI.tree: raise ValueError(f"iid '{iid}' already exists.") - if iid == pid: - raise ValueError(f"iid '{iid}' cannot be equal to parent '{pid}'.") - if pid and pid not in self.RI.tree: + if iid == parent: + raise ValueError(f"iid '{iid}' cannot be equal to parent '{parent}'.") + if parent and parent not in self.RI.tree: raise ValueError(f"parent '{parent}' does not exist.") if text is None: text = iid - parent_node = self.RI.tree[pid] if parent else "" + parent_node = self.RI.tree[parent] if parent else "" self.RI.tree[iid] = Node(text, iid, parent_node) if parent_node: if isinstance(index, int): - idx = self.RI.tree_rns[pid] + index + 1 - for count, cid in enumerate(self.get_children(pid)): - if count >= index: - break - idx += sum(1 for _ in self.RI.get_iid_descendants(cid)) - self.RI.tree[pid].children.insert(index, self.RI.tree[iid]) + datarn = self.RI.tree_rns[parent] + index + 1 + datarn += sum( + sum(1 for _ in self.RI.get_iid_descendants(cid)) for cid in islice(self.get_children(parent), index) + ) + self.RI.tree[parent].children.insert(index, self.RI.tree[iid]) else: - idx = self.RI.tree_rns[pid] + sum(1 for _ in self.RI.get_iid_descendants(pid)) + 1 - self.RI.tree[pid].children.append(self.RI.tree[iid]) + datarn = self.RI.tree_rns[parent] + sum(1 for _ in self.RI.get_iid_descendants(parent)) + 1 + self.RI.tree[parent].children.append(self.RI.tree[iid]) else: if isinstance(index, int): - idx = index - if index: - for count, cid in enumerate(self.get_children("")): - if count >= index: - break - idx += sum(1 for _ in self.RI.get_iid_descendants(cid)) + 1 + datarn = index + if index and (datarn := self.top_index_row(datarn)) is None: + datarn = len(self.MT._row_index) else: - idx = len(self.MT._row_index) + datarn = len(self.MT._row_index) if values is None: values = [] self.insert_rows( - idx=idx, rows=[[self.RI.tree[iid]] + values], + idx=datarn, row_index=True, create_selections=create_selections, fill=False, ) - self.RI.tree_rns[iid] = idx - if pid and (pid not in self.RI.tree_open_ids or not self.item_displayed(pid)): - self.hide_rows(idx, deselect_all=False, data_indexes=True) + self.RI.tree_rns[iid] = datarn + if parent and (parent not in self.RI.tree_open_ids or not self.item_displayed(parent)): + self.hide_rows(datarn, deselect_all=False, data_indexes=True) return iid + def bulk_insert( + self, + data: list[list[object]], + parent: str = "", + index: None | int | Literal["end"] = None, + iid_column: int | None = None, + text_column: int | None | str = None, + create_selections: bool = False, + include_iid_column: bool = True, + include_text_column: bool = True, + ) -> dict[str, int]: + """ + Insert multiple items into the treeview at once, under the same parent + """ + to_insert = [] + pid = parent + if pid and pid not in self.RI.tree: + raise ValueError(f"parent '{parent}' does not exist.") + parent_node = self.RI.tree[pid] if parent else "" + if parent_node: + if isinstance(index, int): + datarn = self.RI.tree_rns[pid] + index + 1 + datarn += sum( + sum(1 for _ in self.RI.get_iid_descendants(cid)) for cid in islice(self.get_children(pid), index) + ) + else: + datarn = self.RI.tree_rns[pid] + sum(1 for _ in self.RI.get_iid_descendants(pid)) + 1 + else: + if isinstance(index, int): + datarn = index + if index and (datarn := self.top_index_row(datarn)) is None: + datarn = len(self.MT._row_index) + else: + datarn = len(self.MT._row_index) + i = 0 + rns_to_add = {} + for rn, r in enumerate(data, start=datarn): + if iid_column is None: + while (iid := f"{num2alpha(i)}") in self.RI.tree: + i += 1 + else: + iid = r[iid_column] + self.RI.tree[iid] = Node( + r[text_column] if isinstance(text_column, int) else text_column if isinstance(text_column, str) else "", + iid, + parent_node, + ) + if parent_node: + if isinstance(index, int): + self.RI.tree[pid].children.insert(index, self.RI.tree[iid]) + else: + self.RI.tree[pid].children.append(self.RI.tree[iid]) + exclude = set() + if isinstance(iid_column, int) and not include_iid_column: + exclude.add(iid_column) + if isinstance(text_column, int) and not include_text_column: + exclude.add(text_column) + if exclude: + to_insert.append([self.RI.tree[iid]] + [e for c, e in enumerate(r) if c not in exclude]) + else: + to_insert.append([self.RI.tree[iid]] + r) + rns_to_add[iid] = rn + self.insert_rows( + rows=to_insert, + idx=datarn, + row_index=True, + create_selections=create_selections, + fill=False, + ) + for iid, rn in rns_to_add.items(): + self.RI.tree_rns[iid] = rn + if pid and (pid not in self.RI.tree_open_ids or not self.item_displayed(pid)): + self.hide_rows(range(datarn, datarn + len(to_insert)), deselect_all=False, data_indexes=True) + return rns_to_add + def item( self, item: str, @@ -4990,12 +5054,11 @@ def item( If no options are set then returns DotDict of options for item Else returns Sheet """ - if not (item := item.lower()) or item not in self.RI.tree: + if item not in self.RI.tree: raise ValueError(f"Item '{item}' does not exist.") if isinstance(iid, str): if iid in self.RI.tree: raise ValueError(f"Cannot rename '{iid}', it already exists.") - iid = iid.lower() self.RI.tree[item].iid = iid self.RI.tree[iid] = self.RI.tree.pop(item) self.RI.tree_rns[iid] = self.RI.tree_rns.pop(item) @@ -5040,9 +5103,9 @@ def item( def itemrow(self, item: str) -> int: try: - return self.RI.tree_rns[item.lower()] + return self.RI.tree_rns[item] except Exception: - raise ValueError(f"item '{item.lower()}' does not exist.") + raise ValueError(f"item '{item}' does not exist.") def rowitem(self, row: int, data_index: bool = False) -> str | None: try: @@ -5053,12 +5116,21 @@ def rowitem(self, row: int, data_index: bool = False) -> str | None: return None def get_children(self, item: None | str = None) -> Generator[str]: + if item is None: + for iid in self.get_children(""): + yield iid + yield from self.RI.get_iid_descendants(iid) + elif item == "": + yield from map(attrgetter("iid"), self.RI.gen_top_nodes()) + else: + yield from (n.iid for n in self.RI.tree[item].children) + + def get_nodes(self, item: None | str = None) -> Generator[str]: if item is None: for n in self.RI.tree.values(): if not n.parent: yield n.iid - for iid in self.RI.get_iid_descendants(n.iid): - yield iid + yield from self.RI.get_iid_descendants(n.iid) elif item == "": yield from (n.iid for n in self.RI.tree.values() if not n.parent) else: @@ -5071,7 +5143,6 @@ def del_items(self, *items) -> Sheet: rows_to_del = [] iids_to_del = [] for item in unpack(items): - item = item.lower() if item not in self.RI.tree: continue rows_to_del.append(self.RI.tree_rns[item]) @@ -5109,9 +5180,9 @@ def move(self, item: str, parent: str, index: int | None = None) -> Sheet: 'parent' can be an empty str which will put the item at top level Performance is not great """ - if (item := item.lower()) and item not in self.RI.tree: + if item not in self.RI.tree: raise ValueError(f"Item '{item}' does not exist.") - if (parent := parent.lower()) and parent not in self.RI.tree: + if parent and parent not in self.RI.tree: raise ValueError(f"Parent '{parent}' does not exist.") mapping = {} to_show = [] @@ -5188,11 +5259,8 @@ def move(self, item: str, parent: str, index: int | None = None) -> Sheet: item_node.parent = parent_node parent_node.children.insert(index, item_node) else: - if index is None: - new_r = self.top_index_row((sum(1 for _ in self.RI.gen_top_nodes()) - 1)) - else: - if (new_r := self.top_index_row(index)) is None: - new_r = self.top_index_row((sum(1 for _ in self.RI.gen_top_nodes()) - 1)) + if index is None or (new_r := self.top_index_row(index)) is None: + new_r = self.top_index_row(sum(1 for _ in self.RI.gen_top_nodes()) - 1) if item_r < new_r: r_ctr = ( new_r @@ -5225,15 +5293,15 @@ def move(self, item: str, parent: str, index: int | None = None) -> Sheet: reattach = move def exists(self, item: str) -> bool: - return item.lower() in self.RI.tree + return item in self.RI.tree def parent(self, item: str) -> str: - if (item := item.lower()) not in self.RI.tree: + if item not in self.RI.tree: raise ValueError(f"Item '{item}' does not exist.") return self.RI.tree[item].parent.iid if self.RI.tree[item].parent else self.RI.tree[item].parent def index(self, item: str) -> int: - if (item := item.lower()) not in self.RI.tree: + if item not in self.RI.tree: raise ValueError(f"Item '{item}' does not exist.") if not self.RI.tree[item].parent: find_node = self.RI.tree[item] @@ -5245,7 +5313,7 @@ def item_displayed(self, item: str) -> bool: Check if an item (row) is currently displayed on the sheet - Does not check if the item is visible to the user """ - if (item := item.lower()) not in self.RI.tree: + if item not in self.RI.tree: raise ValueError(f"Item '{item}' does not exist.") return self.RI.tree_rns[item] in self.MT.displayed_rows @@ -5256,7 +5324,7 @@ def display_item(self, item: str, redraw: bool = False) -> Sheet: - Unlike the ttk treeview 'see' function this function does **NOT** scroll to the item """ - if (item := item.lower()) not in self.RI.tree: + if item not in self.RI.tree: raise ValueError(f"Item '{item}' does not exist.") if self.RI.tree[item].parent: self.show_rows( @@ -5270,7 +5338,7 @@ def scroll_to_item(self, item: str, redraw: bool = False) -> Sheet: """ Scrolls to an item and ensures that it is displayed """ - if (item := item.lower()) not in self.RI.tree: + if item not in self.RI.tree: raise ValueError(f"Item '{item}' does not exist.") self.display_item(item, redraw=False) self.see( @@ -5288,19 +5356,29 @@ def selection(self, cells: bool = False) -> list[str]: self.MT._row_index[self.MT.displayed_rows[rn]].iid for rn in self.get_selected_rows(get_cells_as_rows=cells) ] - def selection_set(self, *items, redraw: bool = True) -> Sheet: - if any(item.lower() in self.RI.tree for item in unpack(items)): - boxes_to_hide = tuple(self.MT.selection_boxes) - self.selection_add(*items, redraw=False) - for iid in boxes_to_hide: - self.MT.hide_selection_box(iid) + @property + def tree_selected(self) -> str | None: + """ + Get the iid at the currently selected box + """ + if selected := self.selected: + return self.rowitem(selected.row) + return None + + def selection_set(self, *items, run_binding: bool = True, redraw: bool = True) -> Sheet: + boxes_to_hide = tuple(self.MT.selection_boxes) + self.selection_add(*items, run_binding=False, redraw=False) + for iid in boxes_to_hide: + self.MT.hide_selection_box(iid) + if run_binding: + self.MT.run_selection_binding("rows") return self.set_refresh_timer(redraw) - def selection_add(self, *items, redraw: bool = True) -> Sheet: + def selection_add(self, *items, run_binding: bool = True, redraw: bool = True) -> Sheet: to_open = [] quick_displayed_check = set(self.MT.displayed_rows) for item in unpack(items): - if self.RI.tree_rns[(item := item.lower())] not in quick_displayed_check and self.RI.tree[item].parent: + if self.RI.tree_rns[item] not in quick_displayed_check and self.RI.tree[item].parent: to_open.extend(list(self.RI.get_iid_ancestors(item))) if to_open: self.show_rows( @@ -5312,7 +5390,7 @@ def selection_add(self, *items, redraw: bool = True) -> Sheet: sorted( bisect_left( self.MT.displayed_rows, - self.RI.tree_rns[item.lower()], + self.RI.tree_rns[item], ) for item in unpack(items) ) @@ -5326,12 +5404,13 @@ def selection_add(self, *items, redraw: bool = True) -> Sheet: set_current=True, ext=True, ) - self.MT.run_selection_binding("rows") + if run_binding: + self.MT.run_selection_binding("rows") return self.set_refresh_timer(redraw) def selection_remove(self, *items, redraw: bool = True) -> Sheet: for item in unpack(items): - if (item := item.lower()) not in self.RI.tree: + if item not in self.RI.tree: continue try: self.deselect(bisect_left(self.MT.displayed_rows, self.RI.tree_rns[item]), redraw=False) @@ -5344,7 +5423,7 @@ def selection_toggle(self, *items, redraw: bool = True) -> Sheet: add = [] remove = [] for item in unpack(items): - if (item := item.lower()) in self.RI.tree: + if item in self.RI.tree: if item in selected: remove.append(item) else: @@ -5353,6 +5432,9 @@ def selection_toggle(self, *items, redraw: bool = True) -> Sheet: self.selection_add(*add, redraw=False) return self.set_refresh_timer(redraw) + def descendants(self, item: str, check_open: bool = False) -> Generator[str]: + return self.RI.get_iid_descendants(item, check_open=check_open) + # Functions not in docs def event_generate(self, *args, **kwargs) -> None: diff --git a/tksheet/themes.py b/tksheet/themes.py index d1d927e..b440308 100644 --- a/tksheet/themes.py +++ b/tksheet/themes.py @@ -22,7 +22,7 @@ "index_selected_cells_bg": "#D3E3FD", "index_selected_cells_fg": "black", "top_left_bg": "#F9FBFD", - "top_left_fg": "#C7C7C7", + "top_left_fg": "#d9d9d9", "top_left_fg_highlight": "#747775", "table_bg": "#FFFFFF", "table_grid_fg": "#E1E1E1", @@ -97,7 +97,7 @@ "index_selected_cells_bg": "#CAEAD8", "index_selected_cells_fg": "#107C41", "top_left_bg": "#F5F5F5", - "top_left_fg": "#b7b7b7", + "top_left_fg": "#d9d9d9", "top_left_fg_highlight": "#5f6368", "table_bg": "#FFFFFF", "table_grid_fg": "#bfbfbf",