tksheet 7.4.6__py3-none-any.whl → 7.4.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tksheet/main_table.py CHANGED
@@ -10,6 +10,7 @@ from functools import partial
10
10
  from itertools import accumulate, chain, cycle, filterfalse, islice, repeat
11
11
  from math import ceil, floor
12
12
  from operator import itemgetter
13
+ from re import IGNORECASE, escape, sub
13
14
  from tkinter import TclError
14
15
  from typing import Any, Literal
15
16
 
@@ -30,7 +31,7 @@ from .constants import (
30
31
  text_editor_to_unbind,
31
32
  val_modifying_options,
32
33
  )
33
- from .find_window import FindWindow
34
+ from .find_window import FindWindow, replacer
34
35
  from .formatters import (
35
36
  data_to_str,
36
37
  format_data,
@@ -45,19 +46,21 @@ from .functions import (
45
46
  bisect_in,
46
47
  box_gen_coords,
47
48
  box_is_single_cell,
49
+ cell_down_within_box,
48
50
  cell_right_within_box,
49
51
  color_tup,
50
52
  consecutive_ranges,
51
53
  data_to_displayed_idxs,
52
54
  diff_gen,
53
- down_cell_within_box,
54
55
  event_dict,
55
56
  event_has_char_key,
56
57
  event_opens_dropdown_or_checkbox,
57
58
  float_to_int,
58
59
  gen_coords,
59
60
  gen_formatted,
61
+ get_bg_fg,
60
62
  get_data_from_clipboard,
63
+ get_menu_kwargs,
61
64
  get_new_indexes,
62
65
  get_seq_without_gaps_at_index,
63
66
  index_exists,
@@ -86,7 +89,6 @@ from .other_classes import (
86
89
  Box_nt,
87
90
  Box_st,
88
91
  Box_t,
89
- DotDict,
90
92
  DropdownStorage,
91
93
  EditorStorageBase,
92
94
  EventDataDict,
@@ -136,6 +138,7 @@ class MainTable(tk.Canvas):
136
138
  self.dropdown = DropdownStorage()
137
139
  self.text_editor = TextEditorStorage()
138
140
  self.find_window = EditorStorageBase()
141
+ self.find_window_left_x_pc = 1
139
142
  self.event_linker = {
140
143
  "<<Copy>>": self.ctrl_c,
141
144
  "<<Cut>>": self.ctrl_x,
@@ -145,6 +148,7 @@ class MainTable(tk.Canvas):
145
148
  "<<Redo>>": self.redo,
146
149
  "<<SelectAll>>": self.select_all,
147
150
  }
151
+ self.enabled_bindings = set()
148
152
 
149
153
  self.disp_ctrl_outline = {}
150
154
  self.disp_text = {}
@@ -190,7 +194,10 @@ class MainTable(tk.Canvas):
190
194
  self.extra_double_b1_func = None
191
195
  self.extra_rc_func = None
192
196
 
197
+ self.extra_end_replace_all_func = None
198
+
193
199
  self.edit_validation_func = None
200
+ self.bulk_table_edit_validation_func = None
194
201
 
195
202
  self.extra_begin_sort_cells_func = None
196
203
  self.extra_end_sort_cells_func = None
@@ -477,10 +484,45 @@ class MainTable(tk.Canvas):
477
484
  else:
478
485
  self.deselect()
479
486
 
480
- def get_find_window_dimensions_coords(self, w_width: int) -> tuple[int, int, int, int]:
487
+ def get_find_window_dimensions_coords(self, w_width: int | None) -> tuple[int, int, int, int]:
488
+ if w_width is None:
489
+ w_width = self.winfo_width()
481
490
  width = min(self.get_txt_w("X" * 23), w_width - 7)
482
- # w, h, x, y
483
- return width, self.min_row_height, self.canvasx(max(0, w_width - width - 7)), self.canvasy(7)
491
+ height = self.min_row_height
492
+ if self.find_window.window and self.find_window.window.replace_visible:
493
+ height *= 2
494
+ # Position from left based on percentage
495
+ xpos = w_width * self.find_window_left_x_pc
496
+ # Clamp to stay within canvas bounds
497
+ xpos = min(xpos, w_width - width - 7) # Don’t exceed right edge
498
+ xpos = max(0, xpos) # Don’t go left of 0
499
+ return width, height, self.canvasx(xpos), self.canvasy(7)
500
+
501
+ def reposition_find_window(self, w_width: int | None = None) -> None:
502
+ if w_width is None:
503
+ w_width = self.winfo_width()
504
+ w, h, x, y = self.get_find_window_dimensions_coords(w_width=w_width)
505
+ self.coords(self.find_window.canvas_id, x, y)
506
+ self.itemconfig(
507
+ self.find_window.canvas_id,
508
+ width=w,
509
+ height=h,
510
+ state="normal",
511
+ )
512
+
513
+ def drag_find_window(self, event: tk.Event) -> None:
514
+ """Receives a tkinter b1-motion event, is bound to a label on the find window"""
515
+ # Convert screen coordinates to canvas window coordinates
516
+ window_x = event.x_root - self.winfo_rootx()
517
+ # Get the visible canvas width
518
+ visible_width = self.winfo_width()
519
+ if visible_width > 0:
520
+ # Calculate the new percentage using widget-relative coordinates
521
+ new_pc = window_x / visible_width
522
+ # Clamp the percentage between 0 and 1
523
+ self.find_window_left_x_pc = min(max(new_pc, 0), 1)
524
+ # Reposition the find window based on the updated percentage
525
+ self.reposition_find_window()
484
526
 
485
527
  def open_find_window(
486
528
  self,
@@ -488,7 +530,7 @@ class MainTable(tk.Canvas):
488
530
  focus: bool = True,
489
531
  ) -> Literal["break"]:
490
532
  if self.find_window.open:
491
- self.close_find_window()
533
+ self.find_window.window.tktext.focus_set()
492
534
  return "break"
493
535
  width, height, x, y = self.get_find_window_dimensions_coords(w_width=self.winfo_width())
494
536
  if not self.find_window.window:
@@ -497,14 +539,12 @@ class MainTable(tk.Canvas):
497
539
  find_prev_func=self.find_previous,
498
540
  find_next_func=self.find_next,
499
541
  close_func=self.close_find_window,
542
+ replace_func=self.replace_next,
543
+ replace_all_func=self.replace_all,
544
+ toggle_replace_func=self.reposition_find_window,
545
+ drag_func=self.drag_find_window,
500
546
  )
501
547
  self.find_window.canvas_id = self.create_window((x, y), window=self.find_window.window, anchor="nw")
502
- for b in chain(self.PAR.ops.escape_bindings, self.PAR.ops.find_bindings):
503
- self.find_window.tktext.bind(b, self.close_find_window)
504
- for b in chain(self.PAR.ops.find_next_bindings, ("<Return>", "<KP_Enter>")):
505
- self.find_window.tktext.bind(b, self.find_next)
506
- for b in self.PAR.ops.find_previous_bindings:
507
- self.find_window.tktext.bind(b, self.find_previous)
508
548
  else:
509
549
  self.coords(self.find_window.canvas_id, x, y)
510
550
  if not self.find_window.open:
@@ -512,21 +552,12 @@ class MainTable(tk.Canvas):
512
552
  self.find_window.open = True
513
553
  self.find_window.window.reset(
514
554
  **{
515
- "menu_kwargs": DotDict(
516
- {
517
- "font": self.PAR.ops.table_font,
518
- "foreground": self.PAR.ops.popup_menu_fg,
519
- "background": self.PAR.ops.popup_menu_bg,
520
- "activebackground": self.PAR.ops.popup_menu_highlight_bg,
521
- "activeforeground": self.PAR.ops.popup_menu_highlight_fg,
522
- }
523
- ),
555
+ "menu_kwargs": get_menu_kwargs(self.PAR.ops),
524
556
  "sheet_ops": self.PAR.ops,
525
557
  "border_color": self.PAR.ops.table_selected_box_cells_fg,
526
- "bg": self.PAR.ops.table_editor_bg,
527
- "fg": self.PAR.ops.table_editor_fg,
528
- "select_bg": self.PAR.ops.table_editor_select_bg,
529
- "select_fg": self.PAR.ops.table_editor_select_fg,
558
+ "grid_color": self.PAR.ops.table_grid_fg,
559
+ **get_bg_fg(self.PAR.ops),
560
+ "replace_enabled": "replace" in self.enabled_bindings or "all" in self.enabled_bindings,
530
561
  }
531
562
  )
532
563
  self.itemconfig(self.find_window.canvas_id, width=width, height=height)
@@ -534,96 +565,223 @@ class MainTable(tk.Canvas):
534
565
  self.find_window.tktext.focus_set()
535
566
  return "break"
536
567
 
568
+ def replace_next(self, event: tk.Misc | None = None) -> None:
569
+ find = self.find_window.get().lower()
570
+ replace = self.find_window.window.get_replace()
571
+ sel = self.selected
572
+ if sel:
573
+ datarn, datacn = self.datarn(sel.row), self.datacn(sel.column)
574
+ m = self.find_match(find, datarn, datacn)
575
+ if m:
576
+ current = f"{self.get_cell_data(datarn, datacn, True)}"
577
+ new = sub(escape(find), replacer(find, replace, current), current, flags=IGNORECASE)
578
+ event_data = event_dict(
579
+ name="end_edit_table",
580
+ sheet=self.PAR.name,
581
+ widget=self,
582
+ cells_table={(datarn, datacn): self.get_cell_data(datarn, datacn)},
583
+ key="replace_next",
584
+ value=new,
585
+ loc=Loc(sel.row, sel.column),
586
+ row=sel.row,
587
+ column=sel.column,
588
+ boxes=self.get_boxes(),
589
+ selected=self.selected,
590
+ data={(datarn, datacn): new},
591
+ )
592
+ value, event_data = self.single_edit_run_validation(datarn, datacn, event_data)
593
+ if value is not None and (
594
+ self.set_cell_data_undo(
595
+ r=datarn,
596
+ c=datacn,
597
+ datarn=datarn,
598
+ datacn=datacn,
599
+ value=value,
600
+ redraw=False,
601
+ )
602
+ ):
603
+ try_binding(self.extra_end_edit_cell_func, event_data)
604
+ if self.find_window.window.find_in_selection:
605
+ found_next = self.find_see_and_set(self.find_within(find))
606
+ else:
607
+ found_next = self.find_see_and_set(self.find_all_cells(find))
608
+ if not found_next and not self.find_window.window.find_in_selection:
609
+ self.deselect()
610
+
611
+ def replace_all(self, event: tk.Misc | None = None) -> None:
612
+ find = self.find_window.get().lower()
613
+ replace = self.find_window.window.get_replace()
614
+ tree = self.PAR.ops.treeview
615
+ event_data = self.new_event_dict("edit_table")
616
+ boxes = self.get_boxes()
617
+ event_data["selection_boxes"] = boxes
618
+ if self.find_window.window.find_in_selection:
619
+ iterable = chain.from_iterable(
620
+ (
621
+ box_gen_coords(
622
+ *box.coords,
623
+ start_r=box.coords.from_r,
624
+ start_c=box.coords.from_c,
625
+ reverse=False,
626
+ all_rows_displayed=self.all_rows_displayed,
627
+ all_cols_displayed=self.all_columns_displayed,
628
+ displayed_rows=self.displayed_rows,
629
+ displayed_cols=self.displayed_columns,
630
+ )
631
+ for box in self.selection_boxes.values()
632
+ )
633
+ )
634
+ else:
635
+ iterable = box_gen_coords(
636
+ from_r=0,
637
+ from_c=0,
638
+ upto_r=self.total_data_rows(include_index=False),
639
+ upto_c=self.total_data_cols(include_header=False),
640
+ start_r=0,
641
+ start_c=0,
642
+ reverse=False,
643
+ )
644
+ for r, c in iterable:
645
+ m = self.find_match(find, r, c)
646
+ if m and (
647
+ (tree or self.all_rows_displayed or bisect_in(self.displayed_rows, r))
648
+ and (self.all_columns_displayed or bisect_in(self.displayed_columns, c))
649
+ ):
650
+ current = f"{self.get_cell_data(r, c, True)}"
651
+ new = sub(escape(find), replacer(find, replace, current), current, flags=IGNORECASE)
652
+ if not self.edit_validation_func or (
653
+ self.edit_validation_func
654
+ and (new := self.edit_validation_func(mod_event_val(event_data, new, (r, c)))) is not None
655
+ ):
656
+ event_data = self.event_data_set_cell(
657
+ r,
658
+ c,
659
+ new,
660
+ event_data,
661
+ )
662
+ event_data = self.bulk_edit_validation(event_data)
663
+ if event_data["cells"]["table"]:
664
+ self.refresh()
665
+ if self.undo_enabled:
666
+ self.undo_stack.append(stored_event_dict(event_data))
667
+ try_binding(self.extra_end_replace_all_func, event_data, "end_edit_table")
668
+ self.sheet_modified(event_data)
669
+ self.PAR.emit_event("<<SheetModified>>", event_data)
670
+
537
671
  def find_see_and_set(
538
- self,
539
- coords: tuple[int, int, int | None] | None,
540
- just_see: bool = False,
672
+ self, coords: tuple[int, int, int | None] | None, within: bool | None = None
541
673
  ) -> tuple[int, int]:
542
674
  if coords:
543
675
  row, column, item = coords
544
- if not self.all_rows_displayed:
545
- row = self.disprn(row)
546
- if not self.all_columns_displayed:
547
- column = self.dispcn(column)
548
- if not just_see:
549
- if self.find_window.window.find_in_selection:
550
- self.set_currently_selected(row, column, item=item)
551
- else:
552
- self.select_cell(row, column, redraw=False)
553
- if not self.see(row, column):
676
+ if self.PAR.ops.treeview:
677
+ self.PAR.scroll_to_item(self.PAR.rowitem(row, data_index=True))
678
+ disp_row = self.disprn(row) if not self.all_rows_displayed else row
679
+ disp_col = self.dispcn(column) if not self.all_columns_displayed else column
680
+ if within or (self.find_window.window and self.find_window.window.find_in_selection):
681
+ self.set_currently_selected(disp_row, disp_col, item=item)
682
+ else:
683
+ self.select_cell(disp_row, disp_col, redraw=False)
684
+ if not self.see(disp_row, disp_col):
554
685
  self.refresh()
555
686
  return coords
556
687
 
557
688
  def find_match(self, find: str, r: int, c: int) -> bool:
558
- return (
559
- not find
560
- and (not self.get_valid_cell_data_as_str(r, c, True).lower() or not f"{self.get_cell_data(r, c)}".lower())
561
- ) or (
562
- find
563
- and (
564
- find in self.get_valid_cell_data_as_str(r, c, True).lower()
565
- or find in f"{self.get_cell_data(r, c)}".lower()
566
- )
567
- )
568
-
569
- def find_within_match(self, find: str, r: int, c: int) -> bool:
570
- if not self.all_rows_displayed:
571
- r = self.datarn(r)
572
- if not self.all_columns_displayed:
573
- c = self.datacn(c)
574
- return self.find_match(find, r, c)
689
+ try:
690
+ value = self.data[r][c]
691
+ except Exception:
692
+ value = ""
693
+ kwargs = self.get_cell_kwargs(r, c, key="format")
694
+ if kwargs:
695
+ # assumed given formatter class has __str__() or value attribute
696
+ value = data_to_str(value, **kwargs) if kwargs["formatter"] is None else str(value)
697
+ if value is None:
698
+ return find == ""
699
+ elif not find:
700
+ return str(value) == ""
701
+ else:
702
+ return find in str(value).lower()
575
703
 
576
704
  def find_within_current_box(
577
- self,
578
- current_box: SelectionBox,
579
- find: str,
580
- reverse: bool,
581
- ) -> None | tuple[int, int]:
582
- start_row, start_col = next_cell(
705
+ self, current_box: SelectionBox, find: str, reverse: bool
706
+ ) -> None | tuple[int, int, int]:
707
+ start_r, start_c = next_cell(
583
708
  *current_box.coords,
584
709
  self.selected.row,
585
710
  self.selected.column,
586
711
  reverse=reverse,
587
712
  )
588
- _, _, r2, c2 = current_box.coords
589
- for r, c in box_gen_coords(start_row, start_col, c2, r2, reverse=reverse):
590
- if self.find_within_match(find, r, c):
591
- return (r, c, current_box.fill_iid)
592
- return None
713
+ return next(
714
+ (
715
+ (r, c, current_box.fill_iid)
716
+ for r, c in box_gen_coords(
717
+ *current_box.coords,
718
+ start_r,
719
+ start_c,
720
+ reverse=reverse,
721
+ all_rows_displayed=self.all_rows_displayed,
722
+ all_cols_displayed=self.all_columns_displayed,
723
+ displayed_rows=self.displayed_rows,
724
+ displayed_cols=self.displayed_columns,
725
+ no_wrap=True,
726
+ )
727
+ if (
728
+ self.find_match(find, r, c) # will not show hidden rows
729
+ and (self.all_rows_displayed or bisect_in(self.displayed_rows, r))
730
+ and (self.all_columns_displayed or bisect_in(self.displayed_columns, c))
731
+ )
732
+ ),
733
+ None,
734
+ )
593
735
 
594
- def find_within_non_current_boxes(
595
- self,
596
- current_id: int,
597
- find: str,
598
- reverse: bool,
599
- ) -> None | tuple[int, int]:
736
+ def find_within_non_current_boxes(self, current_id: int, find: str, reverse: bool) -> None | tuple[int, int, int]:
737
+ fn = partial(
738
+ box_gen_coords,
739
+ reverse=reverse,
740
+ all_rows_displayed=self.all_rows_displayed,
741
+ all_cols_displayed=self.all_columns_displayed,
742
+ displayed_rows=self.displayed_rows,
743
+ displayed_cols=self.displayed_columns,
744
+ )
600
745
  if reverse:
601
746
  # iterate backwards through selection boxes from the box before current
602
747
  idx = next(i for i, k in enumerate(reversed(self.selection_boxes)) if k == current_id)
603
- for item, box in chain(
604
- islice(reversed(self.selection_boxes.items()), idx + 1, None),
605
- islice(reversed(self.selection_boxes.items()), 0, idx),
606
- ):
607
- for r, c in gen_coords(*box.coords, reverse=reverse):
608
- if self.find_within_match(find, r, c):
609
- return (r, c, item)
748
+ return next(
749
+ (
750
+ (r, c, item)
751
+ for item, box in chain(
752
+ islice(reversed(self.selection_boxes.items()), idx + 1, None),
753
+ islice(reversed(self.selection_boxes.items()), 0, idx),
754
+ )
755
+ for r, c in fn(*box.coords, box.coords.upto_r - 1, box.coords.upto_c - 1)
756
+ if (
757
+ self.find_match(find, r, c) # will not show hidden rows
758
+ and (self.all_rows_displayed or bisect_in(self.displayed_rows, r))
759
+ and (self.all_columns_displayed or bisect_in(self.displayed_columns, c))
760
+ )
761
+ ),
762
+ None,
763
+ )
610
764
  else:
611
765
  # iterate forwards through selection boxes from the box after current
612
766
  idx = next(i for i, k in enumerate(self.selection_boxes) if k == current_id)
613
- for item, box in chain(
614
- islice(self.selection_boxes.items(), idx + 1, None),
615
- islice(self.selection_boxes.items(), 0, idx),
616
- ):
617
- for r, c in gen_coords(*box.coords, reverse=reverse):
618
- if self.find_within_match(find, r, c):
619
- return (r, c, item)
620
- return None
767
+ return next(
768
+ (
769
+ (r, c, item)
770
+ for item, box in chain(
771
+ islice(self.selection_boxes.items(), idx + 1, None),
772
+ islice(self.selection_boxes.items(), 0, idx),
773
+ )
774
+ for r, c in fn(*box.coords, box.coords.from_r, box.coords.from_c)
775
+ if (
776
+ self.find_match(find, r, c)
777
+ and (self.all_rows_displayed or bisect_in(self.displayed_rows, r))
778
+ and (self.all_columns_displayed or bisect_in(self.displayed_columns, c))
779
+ )
780
+ ),
781
+ None,
782
+ )
621
783
 
622
- def find_within(
623
- self,
624
- find: str,
625
- reverse: bool = False,
626
- ) -> tuple[int, int, int] | None:
784
+ def find_within(self, find: str, reverse: bool = False) -> tuple[int, int, int] | None:
627
785
  if not self.selected:
628
786
  return None
629
787
  current_box = self.selection_boxes[self.selected.fill_iid]
@@ -640,70 +798,73 @@ class MainTable(tk.Canvas):
640
798
  return coord
641
799
  return None
642
800
 
643
- def find_all_cells(
644
- self,
645
- find: str,
646
- reverse: bool = False,
647
- ) -> tuple[int, int, None] | None:
801
+ def find_all_cells(self, find: str, reverse: bool = False) -> tuple[int, int, None] | None:
802
+ tree = self.PAR.ops.treeview
803
+ totalrows = self.total_data_rows(include_index=False)
804
+ totalcols = self.total_data_cols(include_header=False)
648
805
  if self.selected:
649
- row, col = next_cell(
806
+ start_r, start_c = next_cell(
650
807
  0,
651
808
  0,
652
- len(self.row_positions) - 1,
653
- len(self.col_positions) - 1,
654
- self.selected.row,
655
- self.selected.column,
809
+ totalrows,
810
+ totalcols,
811
+ self.datarn(self.selected.row),
812
+ self.datacn(self.selected.column),
656
813
  reverse=reverse,
657
814
  )
658
815
  else:
659
- row, col = 0, 0
660
- row, col = self.datarn(row), self.datacn(col)
661
- result = next(
816
+ start_r, start_c = 0, 0
817
+ return next(
662
818
  (
663
- (r, c)
819
+ (r, c, None)
664
820
  for r, c in box_gen_coords(
665
- start_row=row,
666
- start_col=col,
667
- total_cols=self.total_data_cols(include_header=False),
668
- total_rows=self.total_data_rows(include_index=False),
821
+ from_r=0,
822
+ from_c=0,
823
+ upto_r=totalrows,
824
+ upto_c=totalcols,
825
+ start_r=start_r,
826
+ start_c=start_c,
669
827
  reverse=reverse,
670
828
  )
671
829
  if (
672
- (self.all_rows_displayed or bisect_in(self.displayed_rows, r))
830
+ self.find_match(find, r, c)
831
+ and (tree or self.all_rows_displayed or bisect_in(self.displayed_rows, r))
673
832
  and (self.all_columns_displayed or bisect_in(self.displayed_columns, c))
674
- and self.find_match(find, r, c)
675
833
  )
676
834
  ),
677
835
  None,
678
836
  )
679
- if result:
680
- return result + (None,)
681
- return None
682
837
 
683
- def find_next(self, event: tk.Misc | None = None) -> Literal["break"]:
684
- find = self.find_window.get().lower()
838
+ def replace_toggle(self, event: tk.Event | None) -> None:
685
839
  if not self.find_window.open:
686
840
  self.open_find_window(focus=False)
687
- if self.find_window.window.find_in_selection:
688
- self.find_see_and_set(self.find_within(find))
689
- else:
690
- self.find_see_and_set(self.find_all_cells(find))
691
- return "break"
841
+ if not self.find_window.window.replace_visible:
842
+ self.find_window.window.toggle_replace_window()
843
+ self.find_window.window.replace_tktext.focus_set()
692
844
 
693
- def find_previous(self, event: tk.Misc | None = None) -> Literal["break"]:
694
- find = self.find_window.get().lower()
695
- if not self.find_window.open:
845
+ def find_next(
846
+ self,
847
+ event: tk.Misc | None = None,
848
+ within: bool | None = None,
849
+ find: str | None = None,
850
+ reverse: bool = False,
851
+ ) -> Literal["break"]:
852
+ if find is None:
853
+ find = self.find_window.get().lower()
854
+ if find is None and not self.find_window.open:
696
855
  self.open_find_window(focus=False)
697
- if self.find_window.window.find_in_selection:
698
- self.find_see_and_set(self.find_within(find, reverse=True))
856
+ if within or (self.find_window.window and self.find_window.window.find_in_selection):
857
+ self.find_see_and_set(self.find_within(find, reverse=reverse), within=within)
699
858
  else:
700
- self.find_see_and_set(self.find_all_cells(find, reverse=True))
859
+ self.find_see_and_set(self.find_all_cells(find, reverse=reverse), within=within)
701
860
  return "break"
702
861
 
703
- def close_find_window(
704
- self,
705
- event: tk.Misc | None = None,
706
- ) -> None:
862
+ def find_previous(
863
+ self, event: tk.Misc | None = None, within: bool | None = None, find: str | None = None
864
+ ) -> Literal["break"]:
865
+ return self.find_next(find=find, within=within, reverse=True)
866
+
867
+ def close_find_window(self, event: tk.Misc | None = None) -> None:
707
868
  if self.find_window.open:
708
869
  self.itemconfig(self.find_window.canvas_id, state="hidden")
709
870
  self.find_window.open = False
@@ -910,11 +1071,13 @@ class MainTable(tk.Canvas):
910
1071
  else:
911
1072
  self.clipboard_append(s.getvalue())
912
1073
  self.update_idletasks()
913
- self.refresh()
914
- for r1, c1, r2, c2 in boxes:
915
- self.show_ctrl_outline(canvas="table", start_cell=(c1, r1), end_cell=(c2, r2))
1074
+ event_data = self.bulk_edit_validation(event_data)
916
1075
  if event_data["cells"]["table"]:
917
- self.undo_stack.append(stored_event_dict(event_data))
1076
+ self.refresh()
1077
+ for r1, c1, r2, c2 in boxes:
1078
+ self.show_ctrl_outline(canvas="table", start_cell=(c1, r1), end_cell=(c2, r2))
1079
+ if self.undo_enabled:
1080
+ self.undo_stack.append(stored_event_dict(event_data))
918
1081
  try_binding(self.extra_end_ctrl_x_func, event_data, "end_ctrl_x")
919
1082
  self.sheet_modified(event_data)
920
1083
  self.PAR.emit_event("<<Cut>>", event_data)
@@ -965,6 +1128,7 @@ class MainTable(tk.Canvas):
965
1128
  value=val,
966
1129
  event_data=event_data,
967
1130
  )
1131
+ event_data = self.bulk_edit_validation(event_data)
968
1132
  if event_data["cells"]["table"]:
969
1133
  if undo and self.undo_enabled:
970
1134
  self.undo_stack.append(stored_event_dict(event_data))
@@ -1030,7 +1194,6 @@ class MainTable(tk.Canvas):
1030
1194
  for _ in range(int(lastbox_numcols / new_data_numcols)):
1031
1195
  data[rn].extend(r.copy())
1032
1196
  new_data_numcols *= int(lastbox_numcols / new_data_numcols)
1033
- event_data["data"] = data
1034
1197
  added_rows = 0
1035
1198
  added_cols = 0
1036
1199
  total_data_cols = None
@@ -1069,6 +1232,11 @@ class MainTable(tk.Canvas):
1069
1232
  ): "cells"
1070
1233
  }
1071
1234
  event_data["selection_boxes"] = boxes
1235
+ for ndr, r in enumerate(range(selected_r, selected_r_adjusted_new_data_numrows)):
1236
+ datarn = self.datarn(r)
1237
+ for ndc, c in enumerate(range(selected_c, selected_c_adjusted_new_data_numcols)):
1238
+ event_data["data"][(datarn, self.datacn(c))] = data[ndr][ndc]
1239
+
1072
1240
  if not try_binding(self.extra_begin_ctrl_v_func, event_data, "begin_ctrl_v"):
1073
1241
  return
1074
1242
  # the order of actions here is important:
@@ -1210,8 +1378,10 @@ class MainTable(tk.Canvas):
1210
1378
  event_data["selected"] = self.selected
1211
1379
  self.see(selected_r, selected_c, redraw=False)
1212
1380
  self.refresh()
1381
+ event_data = self.bulk_edit_validation(event_data)
1213
1382
  if event_data["cells"]["table"] or event_data["added"]["rows"] or event_data["added"]["columns"]:
1214
- self.undo_stack.append(stored_event_dict(event_data))
1383
+ if self.undo_enabled:
1384
+ self.undo_stack.append(stored_event_dict(event_data))
1215
1385
  try_binding(self.extra_end_ctrl_v_func, event_data, "end_ctrl_v")
1216
1386
  self.sheet_modified(event_data)
1217
1387
  self.PAR.emit_event("<<Paste>>", event_data)
@@ -1243,18 +1413,33 @@ class MainTable(tk.Canvas):
1243
1413
  val,
1244
1414
  event_data,
1245
1415
  )
1416
+ event_data = self.bulk_edit_validation(event_data)
1246
1417
  if event_data["cells"]["table"]:
1247
1418
  self.refresh()
1248
- self.undo_stack.append(stored_event_dict(event_data))
1419
+ if self.undo_enabled:
1420
+ self.undo_stack.append(stored_event_dict(event_data))
1249
1421
  try_binding(self.extra_end_delete_key_func, event_data, "end_delete")
1250
1422
  self.sheet_modified(event_data)
1251
1423
  self.PAR.emit_event("<<Delete>>", event_data)
1252
1424
  return event_data
1253
1425
 
1254
- def event_data_set_cell(self, datarn: int, datacn: int, value: Any, event_data: dict) -> EventDataDict:
1426
+ def event_data_set_cell(self, datarn: int, datacn: int, value: Any, event_data: EventDataDict) -> EventDataDict:
1427
+ """If bulk_table_edit_validation_func -> only updates event_data.data"""
1255
1428
  if self.input_valid_for_cell(datarn, datacn, value):
1256
- event_data["cells"]["table"][(datarn, datacn)] = self.get_cell_data(datarn, datacn)
1257
- self.set_cell_data(datarn, datacn, value)
1429
+ if self.bulk_table_edit_validation_func:
1430
+ event_data["data"][(datarn, datacn)] = value
1431
+ else:
1432
+ event_data["cells"]["table"][(datarn, datacn)] = self.get_cell_data(datarn, datacn)
1433
+ self.set_cell_data(datarn, datacn, value)
1434
+ return event_data
1435
+
1436
+ def bulk_edit_validation(self, event_data: EventDataDict) -> EventDataDict:
1437
+ if self.bulk_table_edit_validation_func:
1438
+ self.bulk_table_edit_validation_func(event_data)
1439
+ for (datarn, datacn), value in event_data["data"].items():
1440
+ if self.input_valid_for_cell(datarn, datacn, value):
1441
+ event_data["cells"]["table"][(datarn, datacn)] = self.get_cell_data(datarn, datacn)
1442
+ self.set_cell_data(datarn, datacn, value)
1258
1443
  return event_data
1259
1444
 
1260
1445
  def get_args_for_move_columns(
@@ -1562,6 +1747,7 @@ class MainTable(tk.Canvas):
1562
1747
  event_data: EventDataDict | None = None,
1563
1748
  undo_modification: EventDataDict | None = None,
1564
1749
  node_change: None | tuple[str, str, int] = None,
1750
+ manage_tree: bool = True,
1565
1751
  ) -> tuple[dict[int, int], dict[int, int], EventDataDict]:
1566
1752
  self.saved_row_heights = {}
1567
1753
  if not isinstance(totalrows, int):
@@ -1583,7 +1769,7 @@ class MainTable(tk.Canvas):
1583
1769
 
1584
1770
  if move_data:
1585
1771
  maxidx = len_to_idx(totalrows)
1586
- if self.PAR.ops.treeview:
1772
+ if manage_tree and self.PAR.ops.treeview:
1587
1773
  two_step_move = self.RI.move_rows_mod_nodes(
1588
1774
  data_new_idxs=data_new_idxs,
1589
1775
  data_old_idxs=data_old_idxs,
@@ -1615,7 +1801,7 @@ class MainTable(tk.Canvas):
1615
1801
  self.RI.cell_options = {full_new_idxs[k]: v for k, v in self.RI.cell_options.items()}
1616
1802
  self.RI.tree_rns = {v: full_new_idxs[k] for v, k in self.RI.tree_rns.items()}
1617
1803
  self.displayed_rows = sorted(full_new_idxs[k] for k in self.displayed_rows)
1618
- if self.PAR.ops.treeview:
1804
+ if manage_tree and self.PAR.ops.treeview:
1619
1805
  next(two_step_move)
1620
1806
 
1621
1807
  if self.named_spans:
@@ -1814,38 +2000,28 @@ class MainTable(tk.Canvas):
1814
2000
  self.purge_redo_stack()
1815
2001
 
1816
2002
  def edit_cells_using_modification(self, modification: dict, event_data: dict) -> EventDataDict:
1817
- # row index
1818
- if self.PAR.ops.treeview:
1819
- for datarn, v in modification["cells"]["index"].items():
1820
- if not self.edit_validation_func or (
1821
- self.edit_validation_func
1822
- and (v := self.edit_validation_func(mod_event_val(event_data, v, row=datarn))) is not None
1823
- ):
2003
+ treeview = self.PAR.ops.treeview
2004
+ for datarn, v in modification["cells"]["index"].items():
2005
+ if not self.edit_validation_func or (
2006
+ self.edit_validation_func
2007
+ and (v := self.edit_validation_func(mod_event_val(event_data, v, row=datarn))) is not None
2008
+ ):
2009
+ if treeview:
1824
2010
  self._row_index[datarn].text = v
1825
- else:
1826
- for datarn, v in modification["cells"]["index"].items():
1827
- if not self.edit_validation_func or (
1828
- self.edit_validation_func
1829
- and (v := self.edit_validation_func(mod_event_val(event_data, v, row=datarn))) is not None
1830
- ):
2011
+ else:
1831
2012
  self._row_index[datarn] = v
1832
-
1833
- # header
1834
2013
  for datacn, v in modification["cells"]["header"].items():
1835
2014
  if not self.edit_validation_func or (
1836
2015
  self.edit_validation_func
1837
2016
  and (v := self.edit_validation_func(mod_event_val(event_data, v, column=datacn))) is not None
1838
2017
  ):
1839
2018
  self._headers[datacn] = v
1840
-
1841
- # table
1842
2019
  for k, v in modification["cells"]["table"].items():
1843
2020
  if not self.edit_validation_func or (
1844
2021
  self.edit_validation_func
1845
2022
  and (v := self.edit_validation_func(mod_event_val(event_data, v, loc=k))) is not None
1846
2023
  ):
1847
- self.set_cell_data(k[0], k[1], v)
1848
-
2024
+ event_data = self.event_data_set_cell(k[0], k[1], v, event_data)
1849
2025
  return event_data
1850
2026
 
1851
2027
  def save_cells_using_modification(self, modification: EventDataDict, event_data: EventDataDict) -> EventDataDict:
@@ -2004,6 +2180,7 @@ class MainTable(tk.Canvas):
2004
2180
  if not saved_cells:
2005
2181
  event_data = self.save_cells_using_modification(modification, event_data)
2006
2182
  event_data = self.edit_cells_using_modification(modification, event_data)
2183
+ event_data = self.bulk_edit_validation(event_data)
2007
2184
 
2008
2185
  elif modification["eventname"].startswith("add"):
2009
2186
  event_data["eventname"] = modification["eventname"].replace("add", "delete")
@@ -2039,8 +2216,10 @@ class MainTable(tk.Canvas):
2039
2216
  redraw: bool = True,
2040
2217
  r_pc: float = 0.0,
2041
2218
  c_pc: float = 0.0,
2219
+ index: bool = True,
2042
2220
  ) -> bool:
2043
- need_redraw = False
2221
+ need_y_redraw = False
2222
+ need_x_redraw = False
2044
2223
  vis_info = self.cell_visibility_info(r, c)
2045
2224
  yvis, xvis = vis_info["yvis"], vis_info["xvis"]
2046
2225
  top_left_x, top_left_y, bottom_right_x, bottom_right_y = vis_info["visible_region"]
@@ -2067,7 +2246,7 @@ class MainTable(tk.Canvas):
2067
2246
  y - 1 if y > 1 else y,
2068
2247
  ]
2069
2248
  self.set_yviews(*args, redraw=False)
2070
- need_redraw = True
2249
+ need_y_redraw = True
2071
2250
  else:
2072
2251
  if r is not None and not keep_yscroll:
2073
2252
  y = max(
@@ -2079,7 +2258,7 @@ class MainTable(tk.Canvas):
2079
2258
  y - 1 if y > 1 else y,
2080
2259
  ]
2081
2260
  self.set_yviews(*args, redraw=False)
2082
- need_redraw = True
2261
+ need_y_redraw = True
2083
2262
  # x scroll
2084
2263
  if not check_cell_visibility or (check_cell_visibility and not xvis) and len(self.col_positions) > 1:
2085
2264
  if bottom_right_corner is None:
@@ -2102,7 +2281,7 @@ class MainTable(tk.Canvas):
2102
2281
  x - 1 if x > 1 else x,
2103
2282
  ]
2104
2283
  self.set_xviews(*args, redraw=False)
2105
- need_redraw = True
2284
+ need_x_redraw = True
2106
2285
  else:
2107
2286
  if c is not None and not keep_xscroll:
2108
2287
  x = max(
@@ -2114,8 +2293,24 @@ class MainTable(tk.Canvas):
2114
2293
  x - 1 if x > 1 else x,
2115
2294
  ]
2116
2295
  self.set_xviews(*args, redraw=False)
2117
- need_redraw = True
2118
- if redraw and need_redraw:
2296
+ need_x_redraw = True
2297
+ # the index may have resized after scrolling making x calculation wrong
2298
+ if need_x_redraw and index and self.PAR.ops.auto_resize_row_index and self.show_index:
2299
+ self.main_table_redraw_grid_and_text(redraw_header=False, redraw_row_index=False, redraw_table=False)
2300
+ self.see(
2301
+ r=r,
2302
+ c=c,
2303
+ keep_yscroll=keep_yscroll,
2304
+ keep_xscroll=keep_xscroll,
2305
+ check_cell_visibility=check_cell_visibility,
2306
+ redraw=redraw,
2307
+ r_pc=r_pc,
2308
+ c_pc=c_pc,
2309
+ index=False,
2310
+ )
2311
+ if (need_y_redraw or need_x_redraw) and self.find_window.open:
2312
+ self.reposition_find_window() # prevent it from appearing to move around
2313
+ if redraw and (need_y_redraw or need_x_redraw):
2119
2314
  self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True)
2120
2315
  return True
2121
2316
  return False
@@ -2534,7 +2729,7 @@ class MainTable(tk.Canvas):
2534
2729
  ):
2535
2730
  self.select_cell(r - 1, c, redraw=True)
2536
2731
 
2537
- def arrowkey_LEFT(self, event: Any = None) -> None:
2732
+ def arrowkey_LEFT(self, event: Any = None) -> Literal["break"]:
2538
2733
  if not self.selected:
2539
2734
  return
2540
2735
  r = self.selected.row
@@ -2550,6 +2745,7 @@ class MainTable(tk.Canvas):
2550
2745
  self.single_selection_enabled or self.toggle_selection_enabled
2551
2746
  ):
2552
2747
  self.select_cell(r, c - 1, redraw=True)
2748
+ return "break"
2553
2749
 
2554
2750
  def arrowkey_DOWN(self, event: Any = None) -> None:
2555
2751
  if not self.selected:
@@ -2719,13 +2915,7 @@ class MainTable(tk.Canvas):
2719
2915
  self.empty_rc_popup_menu,
2720
2916
  ):
2721
2917
  menu.delete(0, "end")
2722
- mnkwgs = {
2723
- "font": self.PAR.ops.table_font,
2724
- "foreground": self.PAR.ops.popup_menu_fg,
2725
- "background": self.PAR.ops.popup_menu_bg,
2726
- "activebackground": self.PAR.ops.popup_menu_highlight_bg,
2727
- "activeforeground": self.PAR.ops.popup_menu_highlight_fg,
2728
- }
2918
+ mnkwgs = get_menu_kwargs(self.PAR.ops)
2729
2919
  if self.rc_popup_menus_enabled and self.CH.edit_cell_enabled:
2730
2920
  self.menu_add_command(
2731
2921
  self.CH.ch_rc_popup_menu,
@@ -3137,11 +3327,13 @@ class MainTable(tk.Canvas):
3137
3327
  self.undo_enabled = True
3138
3328
  self._tksheet_bind("undo_bindings", self.undo)
3139
3329
  self._tksheet_bind("redo_bindings", self.redo)
3140
- if binding in ("find",):
3330
+ if binding in ("all", "find"):
3141
3331
  self.find_enabled = True
3142
3332
  self._tksheet_bind("find_bindings", self.open_find_window)
3143
3333
  self._tksheet_bind("find_next_bindings", self.find_next)
3144
3334
  self._tksheet_bind("find_previous_bindings", self.find_previous)
3335
+ if binding in ("all", "replace"):
3336
+ self._tksheet_bind("toggle_replace_bindings", self.replace_toggle)
3145
3337
  if binding in bind_del_columns:
3146
3338
  self.rc_delete_column_enabled = True
3147
3339
  self.rc_popup_menus_enabled = True
@@ -3184,6 +3376,7 @@ class MainTable(tk.Canvas):
3184
3376
  # has to be specifically enabled
3185
3377
  if binding in ("ctrl_click_select", "ctrl_select"):
3186
3378
  self.ctrl_select_enabled = True
3379
+ self.enabled_bindings.add(binding)
3187
3380
 
3188
3381
  def _tksheet_bind(self, bindings_key: str, func: Callable) -> None:
3189
3382
  for widget in (self, self.RI, self.CH, self.TL):
@@ -3193,6 +3386,10 @@ class MainTable(tk.Canvas):
3193
3386
  def _disable_binding(self, binding: Binding) -> None:
3194
3387
  if binding == "disable_all":
3195
3388
  binding = "all"
3389
+ if binding == "all":
3390
+ self.enabled_bindings = set()
3391
+ else:
3392
+ self.enabled_bindings.discard(binding)
3196
3393
  if binding in (
3197
3394
  "all",
3198
3395
  "single",
@@ -3311,6 +3508,8 @@ class MainTable(tk.Canvas):
3311
3508
  self._tksheet_unbind("find_next_bindings")
3312
3509
  self._tksheet_unbind("find_previous_bindings")
3313
3510
  self.close_find_window()
3511
+ if binding in ("all", "replace"):
3512
+ self._tksheet_unbind("toggle_replace_bindings")
3314
3513
 
3315
3514
  def _tksheet_unbind(self, *keys) -> None:
3316
3515
  for widget in (self, self.RI, self.CH, self.TL):
@@ -5572,8 +5771,7 @@ class MainTable(tk.Canvas):
5572
5771
 
5573
5772
  def total_data_cols(self, include_header: bool = True) -> int:
5574
5773
  h_total = len(self._headers) if include_header and isinstance(self._headers, (list, tuple)) else 0
5575
- # map() for some reason is 15% faster than max(key=len) using python 3.11 windows 11
5576
- d_total = max(map(len, self.data), default=0)
5774
+ d_total = max(map(len, self.data), default=0) # max(map(len, )) is faster
5577
5775
  return max(h_total, d_total)
5578
5776
 
5579
5777
  def total_data_rows(self, include_index: bool = True) -> int:
@@ -6112,10 +6310,12 @@ class MainTable(tk.Canvas):
6112
6310
  text_end_row = grid_end_row - 1 if grid_end_row == len(self.row_positions) else grid_end_row
6113
6311
  text_start_col = grid_start_col - 1 if grid_start_col else grid_start_col
6114
6312
  text_end_col = grid_end_col - 1 if grid_end_col == len(self.col_positions) else grid_end_col
6115
-
6313
+ # manage find window
6314
+ if self.find_window.open:
6315
+ self.reposition_find_window()
6116
6316
  # check if auto resizing row index
6117
6317
  changed_w = False
6118
- if self.PAR.ops.auto_resize_row_index and redraw_row_index and self.show_index:
6318
+ if self.PAR.ops.auto_resize_row_index and self.show_index:
6119
6319
  changed_w = self.RI.auto_set_index_width(
6120
6320
  end_row=grid_end_row,
6121
6321
  only_rows=map(self.datarn, range(text_start_row, text_end_row)),
@@ -6129,16 +6329,6 @@ class MainTable(tk.Canvas):
6129
6329
  # important vars
6130
6330
  x_stop = min(last_col_line_pos, scrollpos_right)
6131
6331
  y_stop = min(last_row_line_pos, scrollpos_bot)
6132
- # manage find window
6133
- if self.find_window.open:
6134
- w, h, x, y = self.get_find_window_dimensions_coords(w_width=self.winfo_width())
6135
- self.coords(self.find_window.canvas_id, x, y)
6136
- self.itemconfig(
6137
- self.find_window.canvas_id,
6138
- width=w,
6139
- height=h,
6140
- state="normal",
6141
- )
6142
6332
  # redraw table
6143
6333
  if redraw_table:
6144
6334
  # reset canvas item storage
@@ -7184,15 +7374,7 @@ class MainTable(tk.Canvas):
7184
7374
  w = self.col_positions[c + 1] - x + 1
7185
7375
  h = self.row_positions[r + 1] - y + 1
7186
7376
  kwargs = {
7187
- "menu_kwargs": DotDict(
7188
- {
7189
- "font": self.PAR.ops.table_font,
7190
- "foreground": self.PAR.ops.popup_menu_fg,
7191
- "background": self.PAR.ops.popup_menu_bg,
7192
- "activebackground": self.PAR.ops.popup_menu_highlight_bg,
7193
- "activeforeground": self.PAR.ops.popup_menu_highlight_fg,
7194
- }
7195
- ),
7377
+ "menu_kwargs": get_menu_kwargs(self.PAR.ops),
7196
7378
  "sheet_ops": self.PAR.ops,
7197
7379
  "border_color": self.PAR.ops.table_selected_box_cells_fg,
7198
7380
  "text": text,
@@ -7200,10 +7382,7 @@ class MainTable(tk.Canvas):
7200
7382
  "width": w,
7201
7383
  "height": h,
7202
7384
  "show_border": True,
7203
- "bg": self.PAR.ops.table_editor_bg,
7204
- "fg": self.PAR.ops.table_editor_fg,
7205
- "select_bg": self.PAR.ops.table_editor_select_bg,
7206
- "select_fg": self.PAR.ops.table_editor_select_fg,
7385
+ **get_bg_fg(self.PAR.ops),
7207
7386
  "align": self.get_cell_align(r, c),
7208
7387
  "r": r,
7209
7388
  "c": c,
@@ -7335,7 +7514,7 @@ class MainTable(tk.Canvas):
7335
7514
  self.focus_set()
7336
7515
  return
7337
7516
  # setting cell data with text editor value
7338
- text_editor_value = self.text_editor.get()
7517
+ value = self.text_editor.get()
7339
7518
  r, c = self.text_editor.coords
7340
7519
  datarn, datacn = self.datarn(r), self.datacn(c)
7341
7520
  event_data = event_dict(
@@ -7344,30 +7523,26 @@ class MainTable(tk.Canvas):
7344
7523
  widget=self,
7345
7524
  cells_table={(datarn, datacn): self.get_cell_data(datarn, datacn)},
7346
7525
  key=event.keysym,
7347
- value=text_editor_value,
7526
+ value=value,
7348
7527
  loc=Loc(r, c),
7349
7528
  row=r,
7350
7529
  column=c,
7351
7530
  boxes=self.get_boxes(),
7352
7531
  selected=self.selected,
7532
+ data={(datarn, datacn): value},
7353
7533
  )
7534
+ value, event_data = self.single_edit_run_validation(datarn, datacn, event_data)
7354
7535
  edited = False
7355
- set_data = partial(
7356
- self.set_cell_data_undo,
7357
- r=r,
7358
- c=c,
7359
- datarn=datarn,
7360
- datacn=datacn,
7361
- redraw=False,
7362
- check_input_valid=False,
7363
- )
7364
- if self.edit_validation_func:
7365
- text_editor_value = self.edit_validation_func(event_data)
7366
- if text_editor_value is not None and self.input_valid_for_cell(datarn, datacn, text_editor_value):
7367
- edited = set_data(value=text_editor_value)
7368
- elif self.input_valid_for_cell(datarn, datacn, text_editor_value):
7369
- edited = set_data(value=text_editor_value)
7370
- if edited:
7536
+ if value is not None and (
7537
+ edited := self.set_cell_data_undo(
7538
+ r=r,
7539
+ c=c,
7540
+ datarn=datarn,
7541
+ datacn=datacn,
7542
+ value=value,
7543
+ redraw=False,
7544
+ )
7545
+ ):
7371
7546
  try_binding(self.extra_end_edit_cell_func, event_data)
7372
7547
  if (
7373
7548
  r is not None
@@ -7376,48 +7551,55 @@ class MainTable(tk.Canvas):
7376
7551
  and r == self.selected.row
7377
7552
  and c == self.selected.column
7378
7553
  and (self.single_selection_enabled or self.toggle_selection_enabled)
7379
- and (edited or self.cell_equal_to(datarn, datacn, text_editor_value))
7554
+ and (edited or self.cell_equal_to(datarn, datacn, value))
7380
7555
  ):
7381
- r1, c1, r2, c2 = self.selection_boxes[self.selected.fill_iid].coords
7382
- numcols = c2 - c1
7383
- numrows = r2 - r1
7384
- if numcols == 1 and numrows == 1:
7385
- if event.keysym == "Return":
7386
- if self.PAR.ops.edit_cell_return == "right":
7387
- self.select_right(r, c)
7388
- if self.PAR.ops.edit_cell_return == "down":
7389
- self.select_down(r, c)
7390
- elif event.keysym == "Tab":
7391
- if self.PAR.ops.edit_cell_tab == "right":
7392
- self.select_right(r, c)
7393
- if self.PAR.ops.edit_cell_tab == "down":
7394
- self.select_down(r, c)
7395
- else:
7396
- if event.keysym == "Return":
7397
- if self.PAR.ops.edit_cell_return == "right":
7398
- new_r, new_c = cell_right_within_box(r, c, r1, c1, r2, c2, numrows, numcols)
7399
- elif self.PAR.ops.edit_cell_return == "down":
7400
- new_r, new_c = down_cell_within_box(r, c, r1, c1, r2, c2, numrows, numcols)
7401
- else:
7402
- new_r, new_c = None, None
7403
- elif event.keysym == "Tab":
7404
- if self.PAR.ops.edit_cell_tab == "right":
7405
- new_r, new_c = cell_right_within_box(r, c, r1, c1, r2, c2, numrows, numcols)
7406
- elif self.PAR.ops.edit_cell_tab == "down":
7407
- new_r, new_c = down_cell_within_box(r, c, r1, c1, r2, c2, numrows, numcols)
7408
- else:
7409
- new_r, new_c = None, None
7410
- else:
7411
- new_r, new_c = None, None
7412
- if isinstance(new_r, int):
7413
- self.set_currently_selected(new_r, new_c, item=self.selected.fill_iid)
7414
- self.see(new_r, new_c)
7556
+ self.go_to_next_cell(r, c, event.keysym)
7415
7557
  self.recreate_all_selection_boxes()
7416
7558
  self.hide_text_editor_and_dropdown()
7417
7559
  if event.keysym != "FocusOut":
7418
7560
  self.focus_set()
7419
7561
  return "break"
7420
7562
 
7563
+ def go_to_next_cell(self, r: int, c: int, key: Any = "Return") -> None:
7564
+ r1, c1, r2, c2 = self.selection_boxes[self.selected.fill_iid].coords
7565
+ numrows, numcols = r2 - r1, c2 - c1
7566
+ if key == "Return":
7567
+ direction = self.PAR.ops.edit_cell_return
7568
+ elif key == "Tab":
7569
+ direction = self.PAR.ops.edit_cell_tab
7570
+ else:
7571
+ return
7572
+ if numcols == 1 and numrows == 1:
7573
+ if direction == "right":
7574
+ self.select_right(r, c)
7575
+ elif direction == "down":
7576
+ self.select_down(r, c)
7577
+ else:
7578
+ if direction == "right":
7579
+ new_r, new_c = cell_right_within_box(r, c, r1, c1, r2, c2, numrows, numcols)
7580
+ elif direction == "down":
7581
+ new_r, new_c = cell_down_within_box(r, c, r1, c1, r2, c2, numrows, numcols)
7582
+ if direction in ("right", "down"):
7583
+ self.set_currently_selected(new_r, new_c, item=self.selected.fill_iid)
7584
+ self.see(new_r, new_c)
7585
+
7586
+ def single_edit_run_validation(
7587
+ self, datarn: int, datacn: int, event_data: EventDataDict
7588
+ ) -> tuple[Any, EventDataDict]:
7589
+ value = event_data.value
7590
+ if self.edit_validation_func and (new_value := self.edit_validation_func(event_data)) is not None:
7591
+ value = new_value
7592
+ event_data["data"][(datarn, datacn)] = value
7593
+ event_data["value"] = value
7594
+ if self.bulk_table_edit_validation_func:
7595
+ self.bulk_table_edit_validation_func(event_data)
7596
+ if (datarn, datacn) in event_data["data"]:
7597
+ if event_data["data"][(datarn, datacn)] is not None:
7598
+ value = event_data["data"][(datarn, datacn)]
7599
+ else:
7600
+ value = None
7601
+ return value, event_data
7602
+
7421
7603
  def select_right(self, r: int, c: int) -> None:
7422
7604
  self.select_cell(r, c + 1 if c < len(self.col_positions) - 2 else c)
7423
7605
  self.see(
@@ -7628,13 +7810,19 @@ class MainTable(tk.Canvas):
7628
7810
  column=c,
7629
7811
  boxes=self.get_boxes(),
7630
7812
  selected=self.selected,
7813
+ data={(datarn, datacn): selection},
7631
7814
  )
7632
7815
  try_binding(kwargs["select_function"], event_data)
7633
- selection = selection if not self.edit_validation_func else self.edit_validation_func(event_data)
7634
- if selection is not None:
7635
- edited = self.set_cell_data_undo(r, c, datarn=datarn, datacn=datacn, value=selection, redraw=not redraw)
7636
- if edited:
7637
- try_binding(self.extra_end_edit_cell_func, event_data)
7816
+ selection, event_data = self.single_edit_run_validation(datarn, datacn, event_data)
7817
+ if selection is not None and self.set_cell_data_undo(
7818
+ r,
7819
+ c,
7820
+ datarn=datarn,
7821
+ datacn=datacn,
7822
+ value=selection,
7823
+ redraw=not redraw,
7824
+ ):
7825
+ try_binding(self.extra_end_edit_cell_func, event_data)
7638
7826
  self.recreate_all_selection_boxes()
7639
7827
  self.focus_set()
7640
7828
  self.hide_text_editor_and_dropdown(redraw=redraw)
@@ -7653,12 +7841,12 @@ class MainTable(tk.Canvas):
7653
7841
  self.focus_set()
7654
7842
  return closed_dd_coords
7655
7843
 
7656
- def mouseclick_outside_editor_or_dropdown_all_canvases(self):
7844
+ def mouseclick_outside_editor_or_dropdown_all_canvases(self) -> tuple[int, int] | None:
7657
7845
  self.CH.mouseclick_outside_editor_or_dropdown()
7658
7846
  self.RI.mouseclick_outside_editor_or_dropdown()
7659
7847
  return self.mouseclick_outside_editor_or_dropdown()
7660
7848
 
7661
- def hide_dropdown_editor_all_canvases(self):
7849
+ def hide_dropdown_editor_all_canvases(self) -> None:
7662
7850
  self.hide_text_editor_and_dropdown(redraw=False)
7663
7851
  self.RI.hide_text_editor_and_dropdown(redraw=False)
7664
7852
  self.CH.hide_text_editor_and_dropdown(redraw=False)
@@ -7706,6 +7894,7 @@ class MainTable(tk.Canvas):
7706
7894
  column=c,
7707
7895
  boxes=self.get_boxes(),
7708
7896
  selected=self.selected,
7897
+ data={(datarn, datacn): value},
7709
7898
  )
7710
7899
  if kwargs["check_function"] is not None:
7711
7900
  kwargs["check_function"](event_data)
@@ -7900,7 +8089,10 @@ class MainTable(tk.Canvas):
7900
8089
  kwargs = self.get_cell_kwargs(datarn, datacn, key="checkbox")
7901
8090
  if kwargs:
7902
8091
  return f"{kwargs['text']}"
7903
- value = self.data[datarn][datacn] if len(self.data) > datarn and len(self.data[datarn]) > datacn else ""
8092
+ try:
8093
+ value = self.data[datarn][datacn]
8094
+ except Exception:
8095
+ value = ""
7904
8096
  kwargs = self.get_cell_kwargs(datarn, datacn, key="format")
7905
8097
  if kwargs:
7906
8098
  if kwargs["formatter"] is None:
@@ -7915,28 +8107,24 @@ class MainTable(tk.Canvas):
7915
8107
  else:
7916
8108
  # assumed given formatter class has get_data_with_valid_check()
7917
8109
  return f"{value.get_data_with_valid_check()}"
7918
- return "" if value is None else value if isinstance(value, str) else f"{value}"
8110
+ else:
8111
+ return "" if value is None else value if isinstance(value, str) else f"{value}"
7919
8112
 
7920
8113
  def get_cell_data(
7921
8114
  self,
7922
8115
  datarn: int,
7923
8116
  datacn: int,
7924
- get_displayed: bool = False,
7925
8117
  none_to_empty_str: bool = False,
7926
8118
  fmt_kw: dict | None = None,
7927
- **kwargs,
7928
8119
  ) -> Any:
7929
- if get_displayed:
7930
- return self.get_valid_cell_data_as_str(datarn, datacn, get_displayed=True)
7931
- value = (
7932
- self.data[datarn][datacn]
7933
- if len(self.data) > datarn and len(self.data[datarn]) > datacn
7934
- else self.get_value_for_empty_cell(datarn, datacn)
7935
- )
8120
+ try: # when successful try is more than twice as fast as len check
8121
+ value = self.data[datarn][datacn]
8122
+ except Exception:
8123
+ value = self.get_value_for_empty_cell(datarn, datacn)
7936
8124
  kwargs = self.get_cell_kwargs(datarn, datacn, key="format")
7937
8125
  if kwargs and kwargs["formatter"] is not None:
7938
8126
  value = value.value # assumed given formatter class has value attribute
7939
- if isinstance(fmt_kw, dict):
8127
+ if fmt_kw:
7940
8128
  value = format_data(value=value, **fmt_kw)
7941
8129
  return "" if (value is None and none_to_empty_str) else value
7942
8130
 
@@ -7953,7 +8141,7 @@ class MainTable(tk.Canvas):
7953
8141
  return False
7954
8142
  elif "format" in kwargs:
7955
8143
  return True
7956
- elif self.cell_equal_to(datarn, datacn, value, ignore_empty=ignore_empty) or (
8144
+ elif self.cell_equal_to(datarn, datacn, value, ignore_empty=ignore_empty, check_fmt=False) or (
7957
8145
  (dropdown := kwargs.get("dropdown", {})) and dropdown["validate_input"] and value not in dropdown["values"]
7958
8146
  ):
7959
8147
  return False
@@ -7962,22 +8150,20 @@ class MainTable(tk.Canvas):
7962
8150
  else:
7963
8151
  return True
7964
8152
 
7965
- def cell_equal_to(self, datarn: int, datacn: int, value: Any, ignore_empty: bool = False, **kwargs) -> bool:
7966
- v = self.get_cell_data(datarn, datacn)
7967
- kwargs = self.get_cell_kwargs(datarn, datacn, key="format")
7968
- if kwargs and kwargs["formatter"] is None:
7969
- if ignore_empty:
7970
- if not (x := format_data(value=value, **kwargs)) and not v:
7971
- return False
7972
- return v == x
7973
- return v == format_data(value=value, **kwargs)
7974
- # assumed if there is a formatter class in cell then it has a
7975
- # __eq__() function anyway
7976
- # else if there is not a formatter class in cell and cell is not formatted
7977
- # then compare value as is
7978
- if ignore_empty and not v and not value:
7979
- return False
7980
- return v == value
8153
+ def cell_equal_to(
8154
+ self,
8155
+ datarn: int,
8156
+ datacn: int,
8157
+ new: Any,
8158
+ ignore_empty: bool = False,
8159
+ check_fmt: bool = True,
8160
+ ) -> bool:
8161
+ current = self.get_cell_data(datarn, datacn)
8162
+ if check_fmt:
8163
+ kws = self.get_cell_kwargs(datarn, datacn, key="format")
8164
+ if kws and kws["formatter"] is None:
8165
+ new = format_data(value=new, **kws)
8166
+ return current == new and not (ignore_empty and not current and not new)
7981
8167
 
7982
8168
  def get_cell_clipboard(self, datarn: int, datacn: int) -> str | int | float | bool:
7983
8169
  value = self.data[datarn][datacn] if len(self.data) > datarn and len(self.data[datarn]) > datacn else ""