tksheet 7.4.7__py3-none-any.whl → 7.4.9__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,23 +565,123 @@ 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
 
@@ -559,86 +690,98 @@ class MainTable(tk.Canvas):
559
690
  value = self.data[r][c]
560
691
  except Exception:
561
692
  value = ""
562
- kwargs = self.get_cell_kwargs(r, c, key=None)
693
+ kwargs = self.get_cell_kwargs(r, c, key="format")
563
694
  if kwargs:
564
- if "dropdown" in kwargs:
565
- kwargs = kwargs["dropdown"]
566
- if kwargs["text"] is not None and find in str(kwargs["text"]).lower():
567
- return True
568
- elif "checkbox" in kwargs:
569
- kwargs = kwargs["checkbox"]
570
- if find in str(kwargs["text"]).lower() or (not find and find in "False"):
571
- return True
572
- elif "format" in kwargs:
573
- if kwargs["formatter"] is None:
574
- if find in data_to_str(value, **kwargs).lower():
575
- return True
576
- # assumed given formatter class has __str__() or value attribute
577
- elif find in str(value).lower() or find in str(value.value).lower():
578
- return True
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)
579
697
  if value is None:
580
698
  return find == ""
699
+ elif not find:
700
+ return str(value) == ""
581
701
  else:
582
702
  return find in str(value).lower()
583
703
 
584
- def find_within_match(self, find: str, r: int, c: int) -> bool:
585
- if not self.all_rows_displayed:
586
- r = self.datarn(r)
587
- if not self.all_columns_displayed:
588
- c = self.datacn(c)
589
- return self.find_match(find, r, c)
590
-
591
704
  def find_within_current_box(
592
- self,
593
- current_box: SelectionBox,
594
- find: str,
595
- reverse: bool,
596
- ) -> None | tuple[int, int]:
597
- 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(
598
708
  *current_box.coords,
599
709
  self.selected.row,
600
710
  self.selected.column,
601
711
  reverse=reverse,
602
712
  )
603
- _, _, r2, c2 = current_box.coords
604
- for r, c in box_gen_coords(start_row, start_col, c2, r2, reverse=reverse):
605
- if self.find_within_match(find, r, c):
606
- return (r, c, current_box.fill_iid)
607
- 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
+ )
608
735
 
609
- def find_within_non_current_boxes(
610
- self,
611
- current_id: int,
612
- find: str,
613
- reverse: bool,
614
- ) -> 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
+ )
615
745
  if reverse:
616
746
  # iterate backwards through selection boxes from the box before current
617
747
  idx = next(i for i, k in enumerate(reversed(self.selection_boxes)) if k == current_id)
618
- for item, box in chain(
619
- islice(reversed(self.selection_boxes.items()), idx + 1, None),
620
- islice(reversed(self.selection_boxes.items()), 0, idx),
621
- ):
622
- for r, c in gen_coords(*box.coords, reverse=reverse):
623
- if self.find_within_match(find, r, c):
624
- 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
+ )
625
764
  else:
626
765
  # iterate forwards through selection boxes from the box after current
627
766
  idx = next(i for i, k in enumerate(self.selection_boxes) if k == current_id)
628
- for item, box in chain(
629
- islice(self.selection_boxes.items(), idx + 1, None),
630
- islice(self.selection_boxes.items(), 0, idx),
631
- ):
632
- for r, c in gen_coords(*box.coords, reverse=reverse):
633
- if self.find_within_match(find, r, c):
634
- return (r, c, item)
635
- 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
+ )
636
783
 
637
- def find_within(
638
- self,
639
- find: str,
640
- reverse: bool = False,
641
- ) -> tuple[int, int, int] | None:
784
+ def find_within(self, find: str, reverse: bool = False) -> tuple[int, int, int] | None:
642
785
  if not self.selected:
643
786
  return None
644
787
  current_box = self.selection_boxes[self.selected.fill_iid]
@@ -655,70 +798,73 @@ class MainTable(tk.Canvas):
655
798
  return coord
656
799
  return None
657
800
 
658
- def find_all_cells(
659
- self,
660
- find: str,
661
- reverse: bool = False,
662
- ) -> 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)
663
805
  if self.selected:
664
- row, col = next_cell(
806
+ start_r, start_c = next_cell(
665
807
  0,
666
808
  0,
667
- len(self.row_positions) - 1,
668
- len(self.col_positions) - 1,
669
- self.selected.row,
670
- self.selected.column,
809
+ totalrows,
810
+ totalcols,
811
+ self.datarn(self.selected.row),
812
+ self.datacn(self.selected.column),
671
813
  reverse=reverse,
672
814
  )
673
815
  else:
674
- row, col = 0, 0
675
- row, col = self.datarn(row), self.datacn(col)
676
- result = next(
816
+ start_r, start_c = 0, 0
817
+ return next(
677
818
  (
678
- (r, c)
819
+ (r, c, None)
679
820
  for r, c in box_gen_coords(
680
- start_row=row,
681
- start_col=col,
682
- total_cols=self.total_data_cols(include_header=False),
683
- 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,
684
827
  reverse=reverse,
685
828
  )
686
829
  if (
687
830
  self.find_match(find, r, c)
688
- and (self.all_rows_displayed or bisect_in(self.displayed_rows, r))
831
+ and (tree or self.all_rows_displayed or bisect_in(self.displayed_rows, r))
689
832
  and (self.all_columns_displayed or bisect_in(self.displayed_columns, c))
690
833
  )
691
834
  ),
692
835
  None,
693
836
  )
694
- if result:
695
- return result + (None,)
696
- return None
697
837
 
698
- def find_next(self, event: tk.Misc | None = None) -> Literal["break"]:
699
- find = self.find_window.get().lower()
838
+ def replace_toggle(self, event: tk.Event | None) -> None:
700
839
  if not self.find_window.open:
701
840
  self.open_find_window(focus=False)
702
- if self.find_window.window.find_in_selection:
703
- self.find_see_and_set(self.find_within(find))
704
- else:
705
- self.find_see_and_set(self.find_all_cells(find))
706
- 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()
707
844
 
708
- def find_previous(self, event: tk.Misc | None = None) -> Literal["break"]:
709
- find = self.find_window.get().lower()
710
- 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:
711
855
  self.open_find_window(focus=False)
712
- if self.find_window.window.find_in_selection:
713
- 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)
714
858
  else:
715
- 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)
716
860
  return "break"
717
861
 
718
- def close_find_window(
719
- self,
720
- event: tk.Misc | None = None,
721
- ) -> 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:
722
868
  if self.find_window.open:
723
869
  self.itemconfig(self.find_window.canvas_id, state="hidden")
724
870
  self.find_window.open = False
@@ -925,11 +1071,13 @@ class MainTable(tk.Canvas):
925
1071
  else:
926
1072
  self.clipboard_append(s.getvalue())
927
1073
  self.update_idletasks()
928
- self.refresh()
929
- for r1, c1, r2, c2 in boxes:
930
- self.show_ctrl_outline(canvas="table", start_cell=(c1, r1), end_cell=(c2, r2))
1074
+ event_data = self.bulk_edit_validation(event_data)
931
1075
  if event_data["cells"]["table"]:
932
- 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))
933
1081
  try_binding(self.extra_end_ctrl_x_func, event_data, "end_ctrl_x")
934
1082
  self.sheet_modified(event_data)
935
1083
  self.PAR.emit_event("<<Cut>>", event_data)
@@ -980,6 +1128,7 @@ class MainTable(tk.Canvas):
980
1128
  value=val,
981
1129
  event_data=event_data,
982
1130
  )
1131
+ event_data = self.bulk_edit_validation(event_data)
983
1132
  if event_data["cells"]["table"]:
984
1133
  if undo and self.undo_enabled:
985
1134
  self.undo_stack.append(stored_event_dict(event_data))
@@ -1045,7 +1194,6 @@ class MainTable(tk.Canvas):
1045
1194
  for _ in range(int(lastbox_numcols / new_data_numcols)):
1046
1195
  data[rn].extend(r.copy())
1047
1196
  new_data_numcols *= int(lastbox_numcols / new_data_numcols)
1048
- event_data["data"] = data
1049
1197
  added_rows = 0
1050
1198
  added_cols = 0
1051
1199
  total_data_cols = None
@@ -1084,6 +1232,11 @@ class MainTable(tk.Canvas):
1084
1232
  ): "cells"
1085
1233
  }
1086
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
+
1087
1240
  if not try_binding(self.extra_begin_ctrl_v_func, event_data, "begin_ctrl_v"):
1088
1241
  return
1089
1242
  # the order of actions here is important:
@@ -1225,8 +1378,10 @@ class MainTable(tk.Canvas):
1225
1378
  event_data["selected"] = self.selected
1226
1379
  self.see(selected_r, selected_c, redraw=False)
1227
1380
  self.refresh()
1381
+ event_data = self.bulk_edit_validation(event_data)
1228
1382
  if event_data["cells"]["table"] or event_data["added"]["rows"] or event_data["added"]["columns"]:
1229
- self.undo_stack.append(stored_event_dict(event_data))
1383
+ if self.undo_enabled:
1384
+ self.undo_stack.append(stored_event_dict(event_data))
1230
1385
  try_binding(self.extra_end_ctrl_v_func, event_data, "end_ctrl_v")
1231
1386
  self.sheet_modified(event_data)
1232
1387
  self.PAR.emit_event("<<Paste>>", event_data)
@@ -1258,18 +1413,33 @@ class MainTable(tk.Canvas):
1258
1413
  val,
1259
1414
  event_data,
1260
1415
  )
1416
+ event_data = self.bulk_edit_validation(event_data)
1261
1417
  if event_data["cells"]["table"]:
1262
1418
  self.refresh()
1263
- self.undo_stack.append(stored_event_dict(event_data))
1419
+ if self.undo_enabled:
1420
+ self.undo_stack.append(stored_event_dict(event_data))
1264
1421
  try_binding(self.extra_end_delete_key_func, event_data, "end_delete")
1265
1422
  self.sheet_modified(event_data)
1266
1423
  self.PAR.emit_event("<<Delete>>", event_data)
1267
1424
  return event_data
1268
1425
 
1269
- 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"""
1270
1428
  if self.input_valid_for_cell(datarn, datacn, value):
1271
- event_data["cells"]["table"][(datarn, datacn)] = self.get_cell_data(datarn, datacn)
1272
- 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)
1273
1443
  return event_data
1274
1444
 
1275
1445
  def get_args_for_move_columns(
@@ -1577,6 +1747,7 @@ class MainTable(tk.Canvas):
1577
1747
  event_data: EventDataDict | None = None,
1578
1748
  undo_modification: EventDataDict | None = None,
1579
1749
  node_change: None | tuple[str, str, int] = None,
1750
+ manage_tree: bool = True,
1580
1751
  ) -> tuple[dict[int, int], dict[int, int], EventDataDict]:
1581
1752
  self.saved_row_heights = {}
1582
1753
  if not isinstance(totalrows, int):
@@ -1598,7 +1769,7 @@ class MainTable(tk.Canvas):
1598
1769
 
1599
1770
  if move_data:
1600
1771
  maxidx = len_to_idx(totalrows)
1601
- if self.PAR.ops.treeview:
1772
+ if manage_tree and self.PAR.ops.treeview:
1602
1773
  two_step_move = self.RI.move_rows_mod_nodes(
1603
1774
  data_new_idxs=data_new_idxs,
1604
1775
  data_old_idxs=data_old_idxs,
@@ -1630,7 +1801,7 @@ class MainTable(tk.Canvas):
1630
1801
  self.RI.cell_options = {full_new_idxs[k]: v for k, v in self.RI.cell_options.items()}
1631
1802
  self.RI.tree_rns = {v: full_new_idxs[k] for v, k in self.RI.tree_rns.items()}
1632
1803
  self.displayed_rows = sorted(full_new_idxs[k] for k in self.displayed_rows)
1633
- if self.PAR.ops.treeview:
1804
+ if manage_tree and self.PAR.ops.treeview:
1634
1805
  next(two_step_move)
1635
1806
 
1636
1807
  if self.named_spans:
@@ -1829,38 +2000,28 @@ class MainTable(tk.Canvas):
1829
2000
  self.purge_redo_stack()
1830
2001
 
1831
2002
  def edit_cells_using_modification(self, modification: dict, event_data: dict) -> EventDataDict:
1832
- # row index
1833
- if self.PAR.ops.treeview:
1834
- for datarn, v in modification["cells"]["index"].items():
1835
- if not self.edit_validation_func or (
1836
- self.edit_validation_func
1837
- and (v := self.edit_validation_func(mod_event_val(event_data, v, row=datarn))) is not None
1838
- ):
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:
1839
2010
  self._row_index[datarn].text = v
1840
- else:
1841
- for datarn, v in modification["cells"]["index"].items():
1842
- if not self.edit_validation_func or (
1843
- self.edit_validation_func
1844
- and (v := self.edit_validation_func(mod_event_val(event_data, v, row=datarn))) is not None
1845
- ):
2011
+ else:
1846
2012
  self._row_index[datarn] = v
1847
-
1848
- # header
1849
2013
  for datacn, v in modification["cells"]["header"].items():
1850
2014
  if not self.edit_validation_func or (
1851
2015
  self.edit_validation_func
1852
2016
  and (v := self.edit_validation_func(mod_event_val(event_data, v, column=datacn))) is not None
1853
2017
  ):
1854
2018
  self._headers[datacn] = v
1855
-
1856
- # table
1857
2019
  for k, v in modification["cells"]["table"].items():
1858
2020
  if not self.edit_validation_func or (
1859
2021
  self.edit_validation_func
1860
2022
  and (v := self.edit_validation_func(mod_event_val(event_data, v, loc=k))) is not None
1861
2023
  ):
1862
- self.set_cell_data(k[0], k[1], v)
1863
-
2024
+ event_data = self.event_data_set_cell(k[0], k[1], v, event_data)
1864
2025
  return event_data
1865
2026
 
1866
2027
  def save_cells_using_modification(self, modification: EventDataDict, event_data: EventDataDict) -> EventDataDict:
@@ -2019,6 +2180,7 @@ class MainTable(tk.Canvas):
2019
2180
  if not saved_cells:
2020
2181
  event_data = self.save_cells_using_modification(modification, event_data)
2021
2182
  event_data = self.edit_cells_using_modification(modification, event_data)
2183
+ event_data = self.bulk_edit_validation(event_data)
2022
2184
 
2023
2185
  elif modification["eventname"].startswith("add"):
2024
2186
  event_data["eventname"] = modification["eventname"].replace("add", "delete")
@@ -2146,6 +2308,8 @@ class MainTable(tk.Canvas):
2146
2308
  c_pc=c_pc,
2147
2309
  index=False,
2148
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
2149
2313
  if redraw and (need_y_redraw or need_x_redraw):
2150
2314
  self.main_table_redraw_grid_and_text(redraw_header=True, redraw_row_index=True)
2151
2315
  return True
@@ -2751,13 +2915,7 @@ class MainTable(tk.Canvas):
2751
2915
  self.empty_rc_popup_menu,
2752
2916
  ):
2753
2917
  menu.delete(0, "end")
2754
- mnkwgs = {
2755
- "font": self.PAR.ops.table_font,
2756
- "foreground": self.PAR.ops.popup_menu_fg,
2757
- "background": self.PAR.ops.popup_menu_bg,
2758
- "activebackground": self.PAR.ops.popup_menu_highlight_bg,
2759
- "activeforeground": self.PAR.ops.popup_menu_highlight_fg,
2760
- }
2918
+ mnkwgs = get_menu_kwargs(self.PAR.ops)
2761
2919
  if self.rc_popup_menus_enabled and self.CH.edit_cell_enabled:
2762
2920
  self.menu_add_command(
2763
2921
  self.CH.ch_rc_popup_menu,
@@ -3169,11 +3327,13 @@ class MainTable(tk.Canvas):
3169
3327
  self.undo_enabled = True
3170
3328
  self._tksheet_bind("undo_bindings", self.undo)
3171
3329
  self._tksheet_bind("redo_bindings", self.redo)
3172
- if binding in ("find",):
3330
+ if binding in ("all", "find"):
3173
3331
  self.find_enabled = True
3174
3332
  self._tksheet_bind("find_bindings", self.open_find_window)
3175
3333
  self._tksheet_bind("find_next_bindings", self.find_next)
3176
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)
3177
3337
  if binding in bind_del_columns:
3178
3338
  self.rc_delete_column_enabled = True
3179
3339
  self.rc_popup_menus_enabled = True
@@ -3216,6 +3376,7 @@ class MainTable(tk.Canvas):
3216
3376
  # has to be specifically enabled
3217
3377
  if binding in ("ctrl_click_select", "ctrl_select"):
3218
3378
  self.ctrl_select_enabled = True
3379
+ self.enabled_bindings.add(binding)
3219
3380
 
3220
3381
  def _tksheet_bind(self, bindings_key: str, func: Callable) -> None:
3221
3382
  for widget in (self, self.RI, self.CH, self.TL):
@@ -3225,6 +3386,10 @@ class MainTable(tk.Canvas):
3225
3386
  def _disable_binding(self, binding: Binding) -> None:
3226
3387
  if binding == "disable_all":
3227
3388
  binding = "all"
3389
+ if binding == "all":
3390
+ self.enabled_bindings = set()
3391
+ else:
3392
+ self.enabled_bindings.discard(binding)
3228
3393
  if binding in (
3229
3394
  "all",
3230
3395
  "single",
@@ -3343,6 +3508,8 @@ class MainTable(tk.Canvas):
3343
3508
  self._tksheet_unbind("find_next_bindings")
3344
3509
  self._tksheet_unbind("find_previous_bindings")
3345
3510
  self.close_find_window()
3511
+ if binding in ("all", "replace"):
3512
+ self._tksheet_unbind("toggle_replace_bindings")
3346
3513
 
3347
3514
  def _tksheet_unbind(self, *keys) -> None:
3348
3515
  for widget in (self, self.RI, self.CH, self.TL):
@@ -4182,7 +4349,7 @@ class MainTable(tk.Canvas):
4182
4349
  return self.data
4183
4350
 
4184
4351
  def get_cell_dimensions(self, datarn: int, datacn: int) -> tuple[int, int]:
4185
- txt = self.get_valid_cell_data_as_str(datarn, datacn, get_displayed=True)
4352
+ txt = self.cell_str(datarn, datacn, get_displayed=True)
4186
4353
  if txt:
4187
4354
  self.txt_measure_canvas.itemconfig(self.txt_measure_canvas_text, text=txt, font=self.PAR.ops.table_font)
4188
4355
  b = self.txt_measure_canvas.bbox(self.txt_measure_canvas_text)
@@ -4311,7 +4478,7 @@ class MainTable(tk.Canvas):
4311
4478
  sum(
4312
4479
  1
4313
4480
  for _ in wrap_text(
4314
- text=self.get_valid_cell_data_as_str(datarn, datacn, get_displayed=True),
4481
+ text=self.cell_str(datarn, datacn, get_displayed=True),
4315
4482
  max_width=self.get_cell_max_width(datarn, dispcn),
4316
4483
  max_lines=float("inf"),
4317
4484
  char_width_fn=self.wrap_get_char_w,
@@ -4357,7 +4524,7 @@ class MainTable(tk.Canvas):
4357
4524
  w = min_column_width if width is None else width
4358
4525
  w = hw if (hw := self.CH.get_cell_dimensions(datacn)[0]) > w else min_column_width
4359
4526
  for datarn in iterrows:
4360
- if txt := self.get_valid_cell_data_as_str(datarn, datacn, get_displayed=True):
4527
+ if txt := self.cell_str(datarn, datacn, get_displayed=True):
4361
4528
  qconf(qtxtm, text=txt, font=qfont)
4362
4529
  b = qbbox(qtxtm)
4363
4530
  tw = b[2] - b[0] + added_w_space
@@ -6073,7 +6240,7 @@ class MainTable(tk.Canvas):
6073
6240
  cells["dropdown"][(datarn, datacn)] = kwargs
6074
6241
  elif kwargs := self.get_cell_kwargs(datarn, datacn, key="checkbox"):
6075
6242
  cells["checkbox"][(datarn, datacn)] = kwargs
6076
- cells[(datarn, datacn)] = self.get_valid_cell_data_as_str(datarn, datacn, get_displayed=True)
6243
+ cells[(datarn, datacn)] = self.cell_str(datarn, datacn, get_displayed=True)
6077
6244
  return cells
6078
6245
 
6079
6246
  def wrap_get_char_w(self, c: str) -> int:
@@ -6143,7 +6310,9 @@ class MainTable(tk.Canvas):
6143
6310
  text_end_row = grid_end_row - 1 if grid_end_row == len(self.row_positions) else grid_end_row
6144
6311
  text_start_col = grid_start_col - 1 if grid_start_col else grid_start_col
6145
6312
  text_end_col = grid_end_col - 1 if grid_end_col == len(self.col_positions) else grid_end_col
6146
-
6313
+ # manage find window
6314
+ if self.find_window.open:
6315
+ self.reposition_find_window()
6147
6316
  # check if auto resizing row index
6148
6317
  changed_w = False
6149
6318
  if self.PAR.ops.auto_resize_row_index and self.show_index:
@@ -6160,16 +6329,6 @@ class MainTable(tk.Canvas):
6160
6329
  # important vars
6161
6330
  x_stop = min(last_col_line_pos, scrollpos_right)
6162
6331
  y_stop = min(last_row_line_pos, scrollpos_bot)
6163
- # manage find window
6164
- if self.find_window.open:
6165
- w, h, x, y = self.get_find_window_dimensions_coords(w_width=self.winfo_width())
6166
- self.coords(self.find_window.canvas_id, x, y)
6167
- self.itemconfig(
6168
- self.find_window.canvas_id,
6169
- width=w,
6170
- height=h,
6171
- state="normal",
6172
- )
6173
6332
  # redraw table
6174
6333
  if redraw_table:
6175
6334
  # reset canvas item storage
@@ -7215,15 +7374,7 @@ class MainTable(tk.Canvas):
7215
7374
  w = self.col_positions[c + 1] - x + 1
7216
7375
  h = self.row_positions[r + 1] - y + 1
7217
7376
  kwargs = {
7218
- "menu_kwargs": DotDict(
7219
- {
7220
- "font": self.PAR.ops.table_font,
7221
- "foreground": self.PAR.ops.popup_menu_fg,
7222
- "background": self.PAR.ops.popup_menu_bg,
7223
- "activebackground": self.PAR.ops.popup_menu_highlight_bg,
7224
- "activeforeground": self.PAR.ops.popup_menu_highlight_fg,
7225
- }
7226
- ),
7377
+ "menu_kwargs": get_menu_kwargs(self.PAR.ops),
7227
7378
  "sheet_ops": self.PAR.ops,
7228
7379
  "border_color": self.PAR.ops.table_selected_box_cells_fg,
7229
7380
  "text": text,
@@ -7231,10 +7382,7 @@ class MainTable(tk.Canvas):
7231
7382
  "width": w,
7232
7383
  "height": h,
7233
7384
  "show_border": True,
7234
- "bg": self.PAR.ops.table_editor_bg,
7235
- "fg": self.PAR.ops.table_editor_fg,
7236
- "select_bg": self.PAR.ops.table_editor_select_bg,
7237
- "select_fg": self.PAR.ops.table_editor_select_fg,
7385
+ **get_bg_fg(self.PAR.ops),
7238
7386
  "align": self.get_cell_align(r, c),
7239
7387
  "r": r,
7240
7388
  "c": c,
@@ -7366,7 +7514,7 @@ class MainTable(tk.Canvas):
7366
7514
  self.focus_set()
7367
7515
  return
7368
7516
  # setting cell data with text editor value
7369
- text_editor_value = self.text_editor.get()
7517
+ value = self.text_editor.get()
7370
7518
  r, c = self.text_editor.coords
7371
7519
  datarn, datacn = self.datarn(r), self.datacn(c)
7372
7520
  event_data = event_dict(
@@ -7375,30 +7523,26 @@ class MainTable(tk.Canvas):
7375
7523
  widget=self,
7376
7524
  cells_table={(datarn, datacn): self.get_cell_data(datarn, datacn)},
7377
7525
  key=event.keysym,
7378
- value=text_editor_value,
7526
+ value=value,
7379
7527
  loc=Loc(r, c),
7380
7528
  row=r,
7381
7529
  column=c,
7382
7530
  boxes=self.get_boxes(),
7383
7531
  selected=self.selected,
7532
+ data={(datarn, datacn): value},
7384
7533
  )
7534
+ value, event_data = self.single_edit_run_validation(datarn, datacn, event_data)
7385
7535
  edited = False
7386
- set_data = partial(
7387
- self.set_cell_data_undo,
7388
- r=r,
7389
- c=c,
7390
- datarn=datarn,
7391
- datacn=datacn,
7392
- redraw=False,
7393
- check_input_valid=False,
7394
- )
7395
- if self.edit_validation_func:
7396
- text_editor_value = self.edit_validation_func(event_data)
7397
- if text_editor_value is not None and self.input_valid_for_cell(datarn, datacn, text_editor_value):
7398
- edited = set_data(value=text_editor_value)
7399
- elif self.input_valid_for_cell(datarn, datacn, text_editor_value):
7400
- edited = set_data(value=text_editor_value)
7401
- 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
+ ):
7402
7546
  try_binding(self.extra_end_edit_cell_func, event_data)
7403
7547
  if (
7404
7548
  r is not None
@@ -7407,48 +7551,55 @@ class MainTable(tk.Canvas):
7407
7551
  and r == self.selected.row
7408
7552
  and c == self.selected.column
7409
7553
  and (self.single_selection_enabled or self.toggle_selection_enabled)
7410
- and (edited or self.cell_equal_to(datarn, datacn, text_editor_value))
7554
+ and (edited or self.cell_equal_to(datarn, datacn, value))
7411
7555
  ):
7412
- r1, c1, r2, c2 = self.selection_boxes[self.selected.fill_iid].coords
7413
- numcols = c2 - c1
7414
- numrows = r2 - r1
7415
- if numcols == 1 and numrows == 1:
7416
- if event.keysym == "Return":
7417
- if self.PAR.ops.edit_cell_return == "right":
7418
- self.select_right(r, c)
7419
- if self.PAR.ops.edit_cell_return == "down":
7420
- self.select_down(r, c)
7421
- elif event.keysym == "Tab":
7422
- if self.PAR.ops.edit_cell_tab == "right":
7423
- self.select_right(r, c)
7424
- if self.PAR.ops.edit_cell_tab == "down":
7425
- self.select_down(r, c)
7426
- else:
7427
- if event.keysym == "Return":
7428
- if self.PAR.ops.edit_cell_return == "right":
7429
- new_r, new_c = cell_right_within_box(r, c, r1, c1, r2, c2, numrows, numcols)
7430
- elif self.PAR.ops.edit_cell_return == "down":
7431
- new_r, new_c = down_cell_within_box(r, c, r1, c1, r2, c2, numrows, numcols)
7432
- else:
7433
- new_r, new_c = None, None
7434
- elif event.keysym == "Tab":
7435
- if self.PAR.ops.edit_cell_tab == "right":
7436
- new_r, new_c = cell_right_within_box(r, c, r1, c1, r2, c2, numrows, numcols)
7437
- elif self.PAR.ops.edit_cell_tab == "down":
7438
- new_r, new_c = down_cell_within_box(r, c, r1, c1, r2, c2, numrows, numcols)
7439
- else:
7440
- new_r, new_c = None, None
7441
- else:
7442
- new_r, new_c = None, None
7443
- if isinstance(new_r, int):
7444
- self.set_currently_selected(new_r, new_c, item=self.selected.fill_iid)
7445
- self.see(new_r, new_c)
7556
+ self.go_to_next_cell(r, c, event.keysym)
7446
7557
  self.recreate_all_selection_boxes()
7447
7558
  self.hide_text_editor_and_dropdown()
7448
7559
  if event.keysym != "FocusOut":
7449
7560
  self.focus_set()
7450
7561
  return "break"
7451
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
+
7452
7603
  def select_right(self, r: int, c: int) -> None:
7453
7604
  self.select_cell(r, c + 1 if c < len(self.col_positions) - 2 else c)
7454
7605
  self.see(
@@ -7659,13 +7810,19 @@ class MainTable(tk.Canvas):
7659
7810
  column=c,
7660
7811
  boxes=self.get_boxes(),
7661
7812
  selected=self.selected,
7813
+ data={(datarn, datacn): selection},
7662
7814
  )
7663
7815
  try_binding(kwargs["select_function"], event_data)
7664
- selection = selection if not self.edit_validation_func else self.edit_validation_func(event_data)
7665
- if selection is not None:
7666
- edited = self.set_cell_data_undo(r, c, datarn=datarn, datacn=datacn, value=selection, redraw=not redraw)
7667
- if edited:
7668
- 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)
7669
7826
  self.recreate_all_selection_boxes()
7670
7827
  self.focus_set()
7671
7828
  self.hide_text_editor_and_dropdown(redraw=redraw)
@@ -7737,6 +7894,7 @@ class MainTable(tk.Canvas):
7737
7894
  column=c,
7738
7895
  boxes=self.get_boxes(),
7739
7896
  selected=self.selected,
7897
+ data={(datarn, datacn): value},
7740
7898
  )
7741
7899
  if kwargs["check_function"] is not None:
7742
7900
  kwargs["check_function"](event_data)
@@ -7917,28 +8075,30 @@ class MainTable(tk.Canvas):
7917
8075
  for datarn in range(len(self.data)):
7918
8076
  self.set_cell_data(datarn, datacn, get_val(datarn, datacn), expand_sheet=False)
7919
8077
 
7920
- # deals with possibility of formatter class being in self.data cell
7921
- # if cell is formatted - possibly returns invalid_value kwarg if
7922
- # cell value is not in datatypes kwarg
7923
- # if get displayed is true then Nones are replaced by ""
7924
- def get_valid_cell_data_as_str(self, datarn: int, datacn: int, get_displayed: bool = False, **kwargs) -> str:
8078
+ def cell_str(self, datarn: int, datacn: int, get_displayed: bool = False, **kwargs) -> str:
8079
+ """
8080
+ deals with possibility of formatter class being in self.data cell
8081
+ if cell is formatted - possibly returns invalid_value kwarg if
8082
+ cell value is not in datatypes kwarg
8083
+ if get displayed is true then Nones are replaced by
8084
+ """
8085
+ kwargs = self.get_cell_kwargs(datarn, datacn, key=None)
7925
8086
  if get_displayed:
7926
- kwargs = self.get_cell_kwargs(datarn, datacn, key="dropdown")
7927
- if kwargs:
7928
- if kwargs["text"] is not None:
7929
- return f"{kwargs['text']}"
7930
- else:
7931
- kwargs = self.get_cell_kwargs(datarn, datacn, key="checkbox")
7932
- if kwargs:
7933
- return f"{kwargs['text']}"
7934
- value = self.data[datarn][datacn] if len(self.data) > datarn and len(self.data[datarn]) > datacn else ""
7935
- kwargs = self.get_cell_kwargs(datarn, datacn, key="format")
7936
- if kwargs:
7937
- if kwargs["formatter"] is None:
8087
+ if kwargs and "dropdown" in kwargs:
8088
+ if kwargs["dropdown"]["text"] is not None:
8089
+ return f"{kwargs['dropdown']['text']}"
8090
+ elif kwargs and "checkbox" in kwargs:
8091
+ return f"{kwargs['checkbox']['text']}"
8092
+ try:
8093
+ value = self.data[datarn][datacn]
8094
+ except Exception:
8095
+ value = ""
8096
+ if "format" in kwargs:
8097
+ if kwargs["format"]["formatter"] is None:
7938
8098
  if get_displayed:
7939
- return data_to_str(value, **kwargs)
8099
+ return data_to_str(value, **kwargs["format"])
7940
8100
  else:
7941
- return f"{get_data_with_valid_check(value, **kwargs)}"
8101
+ return f"{get_data_with_valid_check(value, **kwargs['format'])}"
7942
8102
  else:
7943
8103
  if get_displayed:
7944
8104
  # assumed given formatter class has __str__()
@@ -7946,7 +8106,8 @@ class MainTable(tk.Canvas):
7946
8106
  else:
7947
8107
  # assumed given formatter class has get_data_with_valid_check()
7948
8108
  return f"{value.get_data_with_valid_check()}"
7949
- return "" if value is None else value if isinstance(value, str) else f"{value}"
8109
+ else:
8110
+ return "" if value is None else value if isinstance(value, str) else f"{value}"
7950
8111
 
7951
8112
  def get_cell_data(
7952
8113
  self,
@@ -7955,19 +8116,20 @@ class MainTable(tk.Canvas):
7955
8116
  get_displayed: bool = False,
7956
8117
  none_to_empty_str: bool = False,
7957
8118
  fmt_kw: dict | None = None,
7958
- **kwargs,
7959
8119
  ) -> Any:
7960
8120
  if get_displayed:
7961
- return self.get_valid_cell_data_as_str(datarn, datacn, get_displayed=True)
7962
- value = (
7963
- self.data[datarn][datacn]
7964
- if len(self.data) > datarn and len(self.data[datarn]) > datacn
7965
- else self.get_value_for_empty_cell(datarn, datacn)
7966
- )
8121
+ if fmt_kw:
8122
+ return format_data(value=self.cell_str(datarn, datacn, get_displayed=True), **fmt_kw)
8123
+ else:
8124
+ return self.cell_str(datarn, datacn, get_displayed=True)
8125
+ try: # when successful try is more than twice as fast as len check
8126
+ value = self.data[datarn][datacn]
8127
+ except Exception:
8128
+ value = self.get_value_for_empty_cell(datarn, datacn)
7967
8129
  kwargs = self.get_cell_kwargs(datarn, datacn, key="format")
7968
8130
  if kwargs and kwargs["formatter"] is not None:
7969
8131
  value = value.value # assumed given formatter class has value attribute
7970
- if isinstance(fmt_kw, dict):
8132
+ if fmt_kw:
7971
8133
  value = format_data(value=value, **fmt_kw)
7972
8134
  return "" if (value is None and none_to_empty_str) else value
7973
8135
 
@@ -7984,7 +8146,7 @@ class MainTable(tk.Canvas):
7984
8146
  return False
7985
8147
  elif "format" in kwargs:
7986
8148
  return True
7987
- elif self.cell_equal_to(datarn, datacn, value, ignore_empty=ignore_empty) or (
8149
+ elif self.cell_equal_to(datarn, datacn, value, ignore_empty=ignore_empty, check_fmt=False) or (
7988
8150
  (dropdown := kwargs.get("dropdown", {})) and dropdown["validate_input"] and value not in dropdown["values"]
7989
8151
  ):
7990
8152
  return False
@@ -7993,22 +8155,20 @@ class MainTable(tk.Canvas):
7993
8155
  else:
7994
8156
  return True
7995
8157
 
7996
- def cell_equal_to(self, datarn: int, datacn: int, value: Any, ignore_empty: bool = False, **kwargs) -> bool:
7997
- v = self.get_cell_data(datarn, datacn)
7998
- kwargs = self.get_cell_kwargs(datarn, datacn, key="format")
7999
- if kwargs and kwargs["formatter"] is None:
8000
- if ignore_empty:
8001
- if not (x := format_data(value=value, **kwargs)) and not v:
8002
- return False
8003
- return v == x
8004
- return v == format_data(value=value, **kwargs)
8005
- # assumed if there is a formatter class in cell then it has a
8006
- # __eq__() function anyway
8007
- # else if there is not a formatter class in cell and cell is not formatted
8008
- # then compare value as is
8009
- if ignore_empty and not v and not value:
8010
- return False
8011
- return v == value
8158
+ def cell_equal_to(
8159
+ self,
8160
+ datarn: int,
8161
+ datacn: int,
8162
+ new: Any,
8163
+ ignore_empty: bool = False,
8164
+ check_fmt: bool = True,
8165
+ ) -> bool:
8166
+ current = self.get_cell_data(datarn, datacn)
8167
+ if check_fmt:
8168
+ kws = self.get_cell_kwargs(datarn, datacn, key="format")
8169
+ if kws and kws["formatter"] is None:
8170
+ new = format_data(value=new, **kws)
8171
+ return current == new and not (ignore_empty and not current and not new)
8012
8172
 
8013
8173
  def get_cell_clipboard(self, datarn: int, datacn: int) -> str | int | float | bool:
8014
8174
  value = self.data[datarn][datacn] if len(self.data) > datarn and len(self.data[datarn]) > datacn else ""