tksheet 7.5.4__tar.gz → 7.5.7__tar.gz

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.
Files changed (28) hide show
  1. {tksheet-7.5.4/tksheet.egg-info → tksheet-7.5.7}/PKG-INFO +1 -1
  2. {tksheet-7.5.4 → tksheet-7.5.7}/pyproject.toml +1 -1
  3. {tksheet-7.5.4 → tksheet-7.5.7}/tksheet/__init__.py +1 -1
  4. {tksheet-7.5.4 → tksheet-7.5.7}/tksheet/column_headers.py +230 -87
  5. {tksheet-7.5.4 → tksheet-7.5.7}/tksheet/constants.py +1 -0
  6. {tksheet-7.5.4 → tksheet-7.5.7}/tksheet/find_window.py +4 -4
  7. {tksheet-7.5.4 → tksheet-7.5.7}/tksheet/formatters.py +4 -2
  8. {tksheet-7.5.4 → tksheet-7.5.7}/tksheet/functions.py +134 -57
  9. {tksheet-7.5.4 → tksheet-7.5.7}/tksheet/main_table.py +360 -631
  10. tksheet-7.5.7/tksheet/menus.py +494 -0
  11. {tksheet-7.5.4 → tksheet-7.5.7}/tksheet/other_classes.py +3 -0
  12. {tksheet-7.5.4 → tksheet-7.5.7}/tksheet/row_index.py +235 -92
  13. {tksheet-7.5.4 → tksheet-7.5.7}/tksheet/sheet.py +117 -52
  14. {tksheet-7.5.4 → tksheet-7.5.7}/tksheet/sheet_options.py +5 -1
  15. {tksheet-7.5.4 → tksheet-7.5.7}/tksheet/tksheet_types.py +1 -0
  16. tksheet-7.5.7/tksheet/tooltip.py +335 -0
  17. {tksheet-7.5.4 → tksheet-7.5.7/tksheet.egg-info}/PKG-INFO +1 -1
  18. {tksheet-7.5.4 → tksheet-7.5.7}/tksheet.egg-info/SOURCES.txt +2 -0
  19. {tksheet-7.5.4 → tksheet-7.5.7}/LICENSE.txt +0 -0
  20. {tksheet-7.5.4 → tksheet-7.5.7}/README.md +0 -0
  21. {tksheet-7.5.4 → tksheet-7.5.7}/setup.cfg +0 -0
  22. {tksheet-7.5.4 → tksheet-7.5.7}/tksheet/colors.py +0 -0
  23. {tksheet-7.5.4 → tksheet-7.5.7}/tksheet/sorting.py +0 -0
  24. {tksheet-7.5.4 → tksheet-7.5.7}/tksheet/text_editor.py +0 -0
  25. {tksheet-7.5.4 → tksheet-7.5.7}/tksheet/themes.py +0 -0
  26. {tksheet-7.5.4 → tksheet-7.5.7}/tksheet/top_left_rectangle.py +0 -0
  27. {tksheet-7.5.4 → tksheet-7.5.7}/tksheet.egg-info/dependency_links.txt +0 -0
  28. {tksheet-7.5.4 → tksheet-7.5.7}/tksheet.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tksheet
3
- Version: 7.5.4
3
+ Version: 7.5.7
4
4
  Summary: Tkinter table / sheet and treeview widget
5
5
  Author-email: ragardner <github@ragardner.simplelogin.com>
6
6
  License: Copyright (c) 2019 ragardner and open source contributors
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
  name = "tksheet"
7
7
  description = "Tkinter table / sheet and treeview widget"
8
8
  readme = "README.md"
9
- version = "7.5.4"
9
+ version = "7.5.7"
10
10
  authors = [{ name = "ragardner", email = "github@ragardner.simplelogin.com" }]
11
11
  requires-python = ">=3.8"
12
12
  license = {file = "LICENSE.txt"}
@@ -4,7 +4,7 @@
4
4
  tksheet - A Python tkinter table widget
5
5
  """
6
6
 
7
- __version__ = "7.5.4"
7
+ __version__ = "7.5.7"
8
8
 
9
9
  from .colors import (
10
10
  color_map,
@@ -24,22 +24,28 @@ from .functions import (
24
24
  event_dict,
25
25
  event_has_char_key,
26
26
  event_opens_dropdown_or_checkbox,
27
+ get_bg_fg,
27
28
  get_menu_kwargs,
28
29
  get_n2a,
29
30
  int_x_tuple,
30
31
  is_contiguous,
31
32
  mod_event_val,
32
33
  new_tk_event,
34
+ recursive_bind,
33
35
  rounded_box_coords,
36
+ safe_copy,
34
37
  stored_event_dict,
35
38
  try_b_index,
36
39
  try_binding,
40
+ widget_descendants,
37
41
  wrap_text,
38
42
  )
43
+ from .menus import build_empty_rc_menu, build_header_rc_menu
39
44
  from .other_classes import DraggedRowColumn, DropdownStorage, EventDataDict, TextEditorStorage
40
45
  from .row_index import RowIndex
41
46
  from .sorting import sort_column, sort_rows_by_column, sort_tree_rows_by_column
42
47
  from .text_editor import TextEditor
48
+ from .tooltip import Tooltip
43
49
 
44
50
 
45
51
  class ColumnHeaders(tk.Canvas):
@@ -55,6 +61,18 @@ class ColumnHeaders(tk.Canvas):
55
61
  self.MT = None # is set from within MainTable() __init__
56
62
  self.RI: RowIndex | None = None # is set from within MainTable() __init__
57
63
  self.TL = None # is set from within TopLeftRectangle() __init__
64
+ self.tooltip = Tooltip(
65
+ **{
66
+ "parent": self,
67
+ "sheet_ops": self.ops,
68
+ "menu_kwargs": get_menu_kwargs(self.ops),
69
+ **get_bg_fg(self.ops),
70
+ "scrollbar_style": f"Sheet{self.PAR.unique_id}.Vertical.TScrollbar",
71
+ }
72
+ )
73
+ self.tooltip_widgets = widget_descendants(self.tooltip)
74
+ self.tooltip_coords, self.tooltip_after_id, self.tooltip_showing = None, None, False
75
+ recursive_bind(self.tooltip, "<Leave>", self.close_tooltip_save)
58
76
  self.current_cursor = ""
59
77
  self.popup_menu_loc = None
60
78
  self.extra_begin_edit_cell_func = None
@@ -106,7 +124,7 @@ class ColumnHeaders(tk.Canvas):
106
124
  self.disp_resize_lines = {}
107
125
  self.disp_dropdown = {}
108
126
  self.disp_checkbox = {}
109
- self.disp_boxes = set()
127
+ self.disp_corners = set()
110
128
  self.hidd_text = {}
111
129
  self.hidd_high = {}
112
130
  self.hidd_grid = {}
@@ -114,7 +132,7 @@ class ColumnHeaders(tk.Canvas):
114
132
  self.hidd_resize_lines = {}
115
133
  self.hidd_dropdown = {}
116
134
  self.hidd_checkbox = {}
117
- self.hidd_boxes = set()
135
+ self.hidd_corners = set()
118
136
 
119
137
  self.align = kwargs["header_align"]
120
138
  self.basic_bindings()
@@ -177,12 +195,14 @@ class ColumnHeaders(tk.Canvas):
177
195
  c = len(self.MT.col_positions) - 1
178
196
  if self.MT.rc_popup_menus_enabled:
179
197
  popup_menu = self.MT.empty_rc_popup_menu
198
+ build_empty_rc_menu(self.MT, popup_menu)
180
199
  elif self.col_selection_enabled and not self.currently_resizing_width and not self.currently_resizing_height:
181
200
  c = self.MT.identify_col(x=event.x)
182
201
  if c < len(self.MT.col_positions) - 1:
183
202
  if self.MT.col_selected(c):
184
203
  if self.MT.rc_popup_menus_enabled:
185
204
  popup_menu = self.ch_rc_popup_menu
205
+ build_header_rc_menu(self.MT, popup_menu, c)
186
206
  else:
187
207
  if self.MT.single_selection_enabled and self.MT.rc_select_enabled:
188
208
  self.select_col(c, redraw=True)
@@ -190,11 +210,10 @@ class ColumnHeaders(tk.Canvas):
190
210
  self.toggle_select_col(c, redraw=True)
191
211
  if self.MT.rc_popup_menus_enabled:
192
212
  popup_menu = self.ch_rc_popup_menu
213
+ build_header_rc_menu(self.MT, popup_menu, c)
193
214
  try_binding(self.extra_rc_func, event)
194
215
  if popup_menu is not None:
195
216
  self.popup_menu_loc = c
196
- self.MT.popup_menu_disable_edit_if_readonly(popup_menu)
197
- self.MT.popup_menu_disable_undo_redo(popup_menu)
198
217
  popup_menu.tk_popup(event.x_root, event.y_root)
199
218
 
200
219
  def ctrl_b1_press(self, event: Any) -> None:
@@ -880,7 +899,7 @@ class ColumnHeaders(tk.Canvas):
880
899
  event_data = self.MT.new_event_dict("edit_table")
881
900
  try_binding(self.MT.extra_begin_sort_cells_func, event_data)
882
901
  if key is None:
883
- key = self.PAR.ops.sort_key
902
+ key = self.ops.sort_key
884
903
  for c in columns:
885
904
  datacn = self.MT.datacn(c)
886
905
  for r, val in enumerate(
@@ -932,7 +951,7 @@ class ColumnHeaders(tk.Canvas):
932
951
  column = self.MT.datacn(self.MT.selected.column)
933
952
  if try_binding(self.ch_extra_begin_sort_rows_func, event_data, "begin_move_rows"):
934
953
  if key is None:
935
- key = self.PAR.ops.sort_key
954
+ key = self.ops.sort_key
936
955
  disp_new_idxs, disp_row_ctr = {}, 0
937
956
  if self.ops.treeview:
938
957
  new_nodes_order, data_new_idxs = sort_tree_rows_by_column(
@@ -961,7 +980,7 @@ class ColumnHeaders(tk.Canvas):
961
980
  if (idx := try_b_index(self.MT.displayed_rows, old_idx)) is not None:
962
981
  disp_new_idxs[idx] = disp_row_ctr
963
982
  disp_row_ctr += 1
964
- if self.PAR.ops.treeview:
983
+ if self.ops.treeview:
965
984
  data_new_idxs, disp_new_idxs, event_data = self.MT.move_rows_adjust_options_dict(
966
985
  data_new_idxs=data_new_idxs,
967
986
  data_old_idxs=dict(zip(data_new_idxs.values(), data_new_idxs)),
@@ -1253,44 +1272,60 @@ class ColumnHeaders(tk.Canvas):
1253
1272
  redrawn = False
1254
1273
  kwargs = self.get_cell_kwargs(datacn, key="highlight")
1255
1274
  if kwargs:
1256
- fill = kwargs[0]
1257
- if fill and not fill.startswith("#"):
1258
- fill = color_map[fill]
1275
+ high_bg = kwargs[0]
1276
+ if high_bg and not high_bg.startswith("#"):
1277
+ high_bg = color_map[high_bg]
1259
1278
  if "columns" in selections and c in selections["columns"]:
1260
1279
  txtfg = (
1261
1280
  self.ops.header_selected_columns_fg
1262
1281
  if kwargs[1] is None or self.ops.display_selected_fg_over_highlights
1263
1282
  else kwargs[1]
1264
1283
  )
1265
- if fill:
1266
- fill = (
1267
- f"#{int((int(fill[1:3], 16) + int(sel_cols_bg[1:3], 16)) / 2):02X}"
1268
- + f"{int((int(fill[3:5], 16) + int(sel_cols_bg[3:5], 16)) / 2):02X}"
1269
- + f"{int((int(fill[5:], 16) + int(sel_cols_bg[5:], 16)) / 2):02X}"
1270
- )
1284
+ redrawn = self.redraw_highlight(
1285
+ fc + 1,
1286
+ 0,
1287
+ sc,
1288
+ self.current_height - 1,
1289
+ fill=self.ops.header_selected_columns_bg
1290
+ if high_bg is None
1291
+ else (
1292
+ f"#{int((int(high_bg[1:3], 16) + int(sel_cols_bg[1:3], 16)) / 2):02X}"
1293
+ + f"{int((int(high_bg[3:5], 16) + int(sel_cols_bg[3:5], 16)) / 2):02X}"
1294
+ + f"{int((int(high_bg[5:], 16) + int(sel_cols_bg[5:], 16)) / 2):02X}"
1295
+ ),
1296
+ outline=self.ops.header_fg if has_dd and self.ops.show_dropdown_borders else "",
1297
+ )
1271
1298
  elif "cells" in selections and c in selections["cells"]:
1272
1299
  txtfg = (
1273
1300
  self.ops.header_selected_cells_fg
1274
1301
  if kwargs[1] is None or self.ops.display_selected_fg_over_highlights
1275
1302
  else kwargs[1]
1276
1303
  )
1277
- if fill:
1278
- fill = (
1279
- f"#{int((int(fill[1:3], 16) + int(sel_cells_bg[1:3], 16)) / 2):02X}"
1280
- + f"{int((int(fill[3:5], 16) + int(sel_cells_bg[3:5], 16)) / 2):02X}"
1281
- + f"{int((int(fill[5:], 16) + int(sel_cells_bg[5:], 16)) / 2):02X}"
1282
- )
1283
- else:
1284
- txtfg = self.ops.header_fg if kwargs[1] is None else kwargs[1]
1285
- if fill:
1286
1304
  redrawn = self.redraw_highlight(
1287
1305
  fc + 1,
1288
1306
  0,
1289
1307
  sc,
1290
1308
  self.current_height - 1,
1291
- fill=fill,
1309
+ fill=self.ops.header_selected_cells_bg
1310
+ if high_bg is None
1311
+ else (
1312
+ f"#{int((int(high_bg[1:3], 16) + int(sel_cells_bg[1:3], 16)) / 2):02X}"
1313
+ + f"{int((int(high_bg[3:5], 16) + int(sel_cells_bg[3:5], 16)) / 2):02X}"
1314
+ + f"{int((int(high_bg[5:], 16) + int(sel_cells_bg[5:], 16)) / 2):02X}"
1315
+ ),
1292
1316
  outline=self.ops.header_fg if has_dd and self.ops.show_dropdown_borders else "",
1293
1317
  )
1318
+ else:
1319
+ txtfg = self.ops.header_fg if kwargs[1] is None else kwargs[1]
1320
+ if high_bg:
1321
+ redrawn = self.redraw_highlight(
1322
+ fc + 1,
1323
+ 0,
1324
+ sc,
1325
+ self.current_height - 1,
1326
+ fill=high_bg,
1327
+ outline=self.ops.header_fg if has_dd and self.ops.show_dropdown_borders else "",
1328
+ )
1294
1329
  elif not kwargs:
1295
1330
  if "columns" in selections and c in selections["columns"]:
1296
1331
  txtfg = self.ops.header_selected_columns_fg
@@ -1471,6 +1506,17 @@ class ColumnHeaders(tk.Canvas):
1471
1506
  self.MT.char_widths[self.header_font][c] = wd
1472
1507
  return wd
1473
1508
 
1509
+ def redraw_corner(self, x: float, y: float) -> None:
1510
+ if self.hidd_corners:
1511
+ iid = self.hidd_corners.pop()
1512
+ self.coords(iid, x - 10, y, x, y, x, y + 10)
1513
+ self.itemconfig(iid, fill=self.ops.header_grid_fg, state="normal")
1514
+ self.disp_corners.add(iid)
1515
+ else:
1516
+ self.disp_corners.add(
1517
+ self.create_polygon(x - 10, y, x, y, x, y + 10, fill=self.ops.header_grid_fg, tags="lift")
1518
+ )
1519
+
1474
1520
  def redraw_grid_and_text(
1475
1521
  self,
1476
1522
  last_col_line_pos: float,
@@ -1496,6 +1542,8 @@ class ColumnHeaders(tk.Canvas):
1496
1542
  self.disp_dropdown = {}
1497
1543
  self.hidd_checkbox.update(self.disp_checkbox)
1498
1544
  self.disp_checkbox = {}
1545
+ self.hidd_corners.update(self.disp_corners)
1546
+ self.disp_corners = set()
1499
1547
  self.visible_col_dividers = {}
1500
1548
  self.col_height_resize_bbox = (
1501
1549
  scrollpos_left,
@@ -1533,12 +1581,12 @@ class ColumnHeaders(tk.Canvas):
1533
1581
  )
1534
1582
  self.redraw_gridline(points=points, fill=self.ops.header_grid_fg, width=1)
1535
1583
 
1536
- sel_cols_bg = (
1584
+ sel_cells_bg = (
1537
1585
  self.ops.header_selected_cells_bg
1538
1586
  if self.ops.header_selected_cells_bg.startswith("#")
1539
1587
  else color_map[self.ops.header_selected_cells_bg]
1540
1588
  )
1541
- sel_cells_bg = (
1589
+ sel_cols_bg = (
1542
1590
  self.ops.header_selected_columns_bg
1543
1591
  if self.ops.header_selected_columns_bg.startswith("#")
1544
1592
  else color_map[self.ops.header_selected_columns_bg]
@@ -1548,6 +1596,7 @@ class ColumnHeaders(tk.Canvas):
1548
1596
  dd_coords = self.dropdown.get_coords()
1549
1597
  wrap = self.ops.header_wrap
1550
1598
  txt_h = self.MT.header_txt_height
1599
+ note_corners = self.ops.note_corners
1551
1600
  for c in range(text_start_col, text_end_col):
1552
1601
  draw_y = 3
1553
1602
  cleftgridln = self.MT.col_positions[c]
@@ -1626,6 +1675,10 @@ class ColumnHeaders(tk.Canvas):
1626
1675
  or (align[-1] == "n" and cleftgridln + 5 > scrollpos_right)
1627
1676
  ):
1628
1677
  continue
1678
+
1679
+ if note_corners and max_width > 5 and datacn in self.cell_options and "note" in self.cell_options[datacn]:
1680
+ self.redraw_corner(crightgridln, 0)
1681
+
1629
1682
  text = self.cell_str(datacn, fix=False)
1630
1683
  if not text:
1631
1684
  continue
@@ -1648,6 +1701,7 @@ class ColumnHeaders(tk.Canvas):
1648
1701
  fill=fill,
1649
1702
  font=font,
1650
1703
  anchor=align,
1704
+ tags=("lift", "t", f"{c}"),
1651
1705
  )
1652
1706
  else:
1653
1707
  self.itemconfig(
@@ -1657,6 +1711,7 @@ class ColumnHeaders(tk.Canvas):
1657
1711
  font=font,
1658
1712
  anchor=align,
1659
1713
  state="normal",
1714
+ tags=("lift", "t", f"{c}"),
1660
1715
  )
1661
1716
  else:
1662
1717
  iid = self.create_text(
@@ -1666,7 +1721,7 @@ class ColumnHeaders(tk.Canvas):
1666
1721
  fill=fill,
1667
1722
  font=font,
1668
1723
  anchor=align,
1669
- tag="lift",
1724
+ tags=("lift", "t", f"{c}"),
1670
1725
  )
1671
1726
  self.disp_text[iid] = True
1672
1727
  else:
@@ -1681,6 +1736,7 @@ class ColumnHeaders(tk.Canvas):
1681
1736
  fill=fill,
1682
1737
  font=font,
1683
1738
  anchor=align,
1739
+ tags=("lift", "t", f"{c}"),
1684
1740
  )
1685
1741
  else:
1686
1742
  self.itemconfig(
@@ -1690,6 +1746,7 @@ class ColumnHeaders(tk.Canvas):
1690
1746
  font=font,
1691
1747
  anchor=align,
1692
1748
  state="normal",
1749
+ tags=("lift", "t", f"{c}"),
1693
1750
  )
1694
1751
  else:
1695
1752
  iid = self.create_text(
@@ -1699,7 +1756,7 @@ class ColumnHeaders(tk.Canvas):
1699
1756
  fill=fill,
1700
1757
  font=font,
1701
1758
  anchor=align,
1702
- tag="lift",
1759
+ tags=("lift", "t", f"{c}"),
1703
1760
  )
1704
1761
  self.disp_text[iid] = True
1705
1762
  draw_y += self.MT.header_txt_height
@@ -1709,11 +1766,115 @@ class ColumnHeaders(tk.Canvas):
1709
1766
  if showing:
1710
1767
  self.itemconfig(iid, state="hidden")
1711
1768
  dct[iid] = False
1769
+ for iid in self.hidd_corners:
1770
+ self.itemconfig(iid, state="hidden")
1712
1771
  self.tag_raise("lift")
1713
1772
  if self.disp_resize_lines:
1714
1773
  self.tag_raise("rw")
1774
+ self.tag_bind("t", "<Enter>", self.enter_text)
1775
+ self.tag_bind("t", "<Leave>", self.leave_text)
1715
1776
  return True
1716
1777
 
1778
+ def enter_text(self, event: tk.Event | None = None) -> None:
1779
+ if self.text_editor.open or self.dropdown.open:
1780
+ return
1781
+ can_x, can_y = self.canvasx(event.x), self.canvasy(event.y)
1782
+ for i in self.find_overlapping(can_x - 1, can_y - 1, can_x + 1, can_y + 1):
1783
+ try:
1784
+ if (coords := self.gettags(i)[2]) == self.tooltip_coords:
1785
+ return
1786
+ self.tooltip_coords = coords
1787
+ self.tooltip_last_x, self.tooltip_last_y = self.winfo_pointerx(), self.winfo_pointery()
1788
+ self.start_tooltip_timer()
1789
+ return
1790
+ except Exception:
1791
+ continue
1792
+
1793
+ def leave_text(self, event: tk.Event | None = None) -> None:
1794
+ if self.tooltip_after_id is not None:
1795
+ self.after_cancel(self.tooltip_after_id)
1796
+ self.tooltip_after_id = None
1797
+ if self.tooltip_showing:
1798
+ if self.winfo_containing(self.winfo_pointerx(), self.winfo_pointery()) not in self.tooltip_widgets:
1799
+ self.close_tooltip_save()
1800
+ else:
1801
+ self.tooltip_coords = None
1802
+
1803
+ def start_tooltip_timer(self) -> None:
1804
+ self.tooltip_after_id = self.after(1000, self.check_and_show_tooltip)
1805
+
1806
+ def check_and_show_tooltip(self, event: tk.Event | None = None) -> None:
1807
+ current_x, current_y = self.winfo_pointerx(), self.winfo_pointery()
1808
+ if current_x < 0 or current_y < 0:
1809
+ return
1810
+ if abs(current_x - self.tooltip_last_x) <= 1 and abs(current_y - self.tooltip_last_y) <= 1:
1811
+ self.show_tooltip()
1812
+ else:
1813
+ self.tooltip_last_x, self.tooltip_last_y = current_x, current_y
1814
+ self.tooltip_after_id = self.after(400, self.check_and_show_tooltip)
1815
+
1816
+ def hide_tooltip(self):
1817
+ self.tooltip.withdraw()
1818
+ self.tooltip_showing, self.tooltip_coords = False, None
1819
+
1820
+ def show_tooltip(self) -> None:
1821
+ c = int(self.tooltip_coords)
1822
+ datacn = self.MT.datacn(c)
1823
+ kws = self.get_cell_kwargs(datacn, key="note")
1824
+ if not self.ops.tooltips and not kws and not self.ops.user_can_create_notes:
1825
+ return
1826
+ cell_readonly = self.get_cell_kwargs(datacn, "readonly") or not self.MT.index_edit_cell_enabled()
1827
+ if kws:
1828
+ note = kws["note"]
1829
+ note_readonly = kws["readonly"]
1830
+ elif self.ops.user_can_create_notes:
1831
+ note = ""
1832
+ note_readonly = bool(cell_readonly)
1833
+ else:
1834
+ note = None
1835
+ note_readonly = True
1836
+ self.tooltip.reset(
1837
+ **{
1838
+ "text": f"{self.get_cell_data(datacn, none_to_empty_str=True)}",
1839
+ "cell_readonly": cell_readonly,
1840
+ "note": note,
1841
+ "note_readonly": note_readonly,
1842
+ "row": 0,
1843
+ "col": c,
1844
+ "menu_kwargs": get_menu_kwargs(self.ops),
1845
+ **get_bg_fg(self.ops),
1846
+ "user_can_create_notes": self.ops.user_can_create_notes,
1847
+ "note_only": not self.ops.tooltips and isinstance(note, str),
1848
+ "width": self.ops.tooltip_width,
1849
+ "height": self.ops.tooltip_height,
1850
+ }
1851
+ )
1852
+ self.tooltip.set_position(self.tooltip_last_x - 4, self.tooltip_last_y - 4)
1853
+ self.tooltip_showing = True
1854
+
1855
+ def close_tooltip_save(self, event: tk.Event | None = None) -> None:
1856
+ widget = self.winfo_containing(self.winfo_pointerx(), self.winfo_pointery())
1857
+ if any(widget == tw for tw in self.tooltip_widgets):
1858
+ try:
1859
+ if self.tooltip.notebook.index("current") == 0:
1860
+ self.tooltip.content_text.focus_set()
1861
+ else:
1862
+ self.tooltip.note_text.focus_set()
1863
+ except Exception:
1864
+ self.tooltip.content_text.focus_set()
1865
+ return
1866
+ if not self.tooltip.cell_readonly:
1867
+ _, c, cell, note = self.tooltip.get()
1868
+ datacn = self.MT.datacn(c)
1869
+ event_data = self.new_single_edit_event(c, datacn, "??", self.get_cell_data(datacn), cell)
1870
+ self.do_single_edit(c, datacn, event_data, cell)
1871
+ if not self.tooltip.note_readonly:
1872
+ span = self.PAR.span(None, datacn, None, datacn + 1).options(table=False, header=True)
1873
+ self.PAR.note(span, note=note if note else None, readonly=False)
1874
+ self.hide_tooltip()
1875
+ self.MT.refresh()
1876
+ self.focus_set()
1877
+
1717
1878
  def get_redraw_selections(self, startc: int, endc: int) -> dict[str, set[int]]:
1718
1879
  d = defaultdict(set)
1719
1880
  for _, box in self.MT.get_selection_items():
@@ -1942,6 +2103,18 @@ class ColumnHeaders(tk.Canvas):
1942
2103
  self.itemconfig(self.text_editor.canvas_id, state="hidden")
1943
2104
  self.text_editor.open = False
1944
2105
 
2106
+ def do_single_edit(self, c: int, datacn: int, event_data: EventDataDict, val):
2107
+ edited = False
2108
+ set_data = partial(self.set_cell_data_undo, c=c, datacn=datacn, check_input_valid=False)
2109
+ if self.MT.edit_validation_func:
2110
+ val = self.MT.edit_validation_func(event_data)
2111
+ if val is not None and self.input_valid_for_cell(datacn, val):
2112
+ edited = set_data(value=val)
2113
+ elif self.input_valid_for_cell(datacn, val):
2114
+ edited = set_data(value=val)
2115
+ if edited:
2116
+ try_binding(self.extra_end_edit_cell_func, event_data)
2117
+
1945
2118
  # c is displayed col
1946
2119
  def close_text_editor(self, event: tk.Event) -> Literal["break"] | None:
1947
2120
  # checking if text editor should be closed or not
@@ -1965,33 +2138,8 @@ class ColumnHeaders(tk.Canvas):
1965
2138
  text_editor_value = self.text_editor.get()
1966
2139
  c = self.text_editor.column
1967
2140
  datacn = c if self.MT.all_columns_displayed else self.MT.displayed_columns[c]
1968
- event_data = event_dict(
1969
- name="end_edit_header",
1970
- sheet=self.PAR.name,
1971
- widget=self,
1972
- cells_header={datacn: self.get_cell_data(datacn)},
1973
- key=event.keysym,
1974
- value=text_editor_value,
1975
- loc=c,
1976
- column=c,
1977
- boxes=self.MT.get_boxes(),
1978
- selected=self.MT.selected,
1979
- )
1980
- edited = False
1981
- set_data = partial(
1982
- self.set_cell_data_undo,
1983
- c=c,
1984
- datacn=datacn,
1985
- check_input_valid=False,
1986
- )
1987
- if self.MT.edit_validation_func:
1988
- text_editor_value = self.MT.edit_validation_func(event_data)
1989
- if text_editor_value is not None and self.input_valid_for_cell(datacn, text_editor_value):
1990
- edited = set_data(value=text_editor_value)
1991
- elif self.input_valid_for_cell(datacn, text_editor_value):
1992
- edited = set_data(value=text_editor_value)
1993
- if edited:
1994
- try_binding(self.extra_end_edit_cell_func, event_data)
2141
+ event_data = self.new_single_edit_event(c, datacn, event.keysym, self.get_cell_data(datacn), text_editor_value)
2142
+ self.do_single_edit(c, datacn, event_data, text_editor_value)
1995
2143
  self.MT.recreate_all_selection_boxes()
1996
2144
  self.hide_text_editor_and_dropdown()
1997
2145
  if event.keysym != "FocusOut":
@@ -2125,18 +2273,7 @@ class ColumnHeaders(tk.Canvas):
2125
2273
  if c is not None and selection is not None:
2126
2274
  datacn = c if self.MT.all_columns_displayed else self.MT.displayed_columns[c]
2127
2275
  kwargs = self.get_cell_kwargs(datacn, key="dropdown")
2128
- event_data = event_dict(
2129
- name="end_edit_header",
2130
- sheet=self.PAR.name,
2131
- widget=self,
2132
- cells_header={datacn: self.get_cell_data(datacn)},
2133
- key="??",
2134
- value=selection,
2135
- loc=c,
2136
- column=c,
2137
- boxes=self.MT.get_boxes(),
2138
- selected=self.MT.selected,
2139
- )
2276
+ event_data = self.new_single_edit_event(c, datacn, "??", self.get_cell_data(datacn), selection)
2140
2277
  try_binding(kwargs["select_function"], event_data)
2141
2278
  selection = selection if not self.MT.edit_validation_func else self.MT.edit_validation_func(event_data)
2142
2279
  if selection is not None:
@@ -2308,12 +2445,14 @@ class ColumnHeaders(tk.Canvas):
2308
2445
  def get_value_for_empty_cell(self, datacn: int, c_ops: bool = True) -> Any:
2309
2446
  if self.get_cell_kwargs(datacn, key="checkbox", cell=c_ops):
2310
2447
  return False
2311
- elif (
2312
- (kwargs := self.get_cell_kwargs(datacn, key="dropdown", cell=c_ops))
2313
- and kwargs["validate_input"]
2314
- and kwargs["values"]
2315
- ):
2316
- return kwargs["values"][0]
2448
+ elif (kwargs := self.get_cell_kwargs(datacn, key="dropdown", cell=c_ops)) and kwargs["validate_input"]:
2449
+ if kwargs["default_value"] is None:
2450
+ if kwargs["values"]:
2451
+ return safe_copy(kwargs["values"][0])
2452
+ else:
2453
+ return ""
2454
+ else:
2455
+ return safe_copy(kwargs["default_value"])
2317
2456
  else:
2318
2457
  return ""
2319
2458
 
@@ -2365,18 +2504,7 @@ class ColumnHeaders(tk.Canvas):
2365
2504
  else:
2366
2505
  value = False
2367
2506
  self.set_cell_data_undo(c, datacn=datacn, value=value, cell_resize=False)
2368
- event_data = event_dict(
2369
- name="end_edit_header",
2370
- sheet=self.PAR.name,
2371
- widget=self,
2372
- cells_header={datacn: pre_edit_value},
2373
- key="??",
2374
- value=value,
2375
- loc=c,
2376
- column=c,
2377
- boxes=self.MT.get_boxes(),
2378
- selected=self.MT.selected,
2379
- )
2507
+ event_data = self.new_single_edit_event(c, datacn, "??", pre_edit_value, value)
2380
2508
  if kwargs["check_function"] is not None:
2381
2509
  kwargs["check_function"](event_data)
2382
2510
  try_binding(self.extra_end_edit_cell_func, event_data)
@@ -2388,3 +2516,18 @@ class ColumnHeaders(tk.Canvas):
2388
2516
  return self.cell_options[datacn] if key is None else self.cell_options[datacn].get(key, {})
2389
2517
  else:
2390
2518
  return {}
2519
+
2520
+ def new_single_edit_event(self, c: int, datacn: int, k: str, before_val: Any, after_val: Any) -> EventDataDict:
2521
+ return event_dict(
2522
+ name="end_edit_header",
2523
+ sheet=self.PAR.name,
2524
+ widget=self,
2525
+ cells_header={datacn: before_val},
2526
+ key=k,
2527
+ value=after_val,
2528
+ loc=c,
2529
+ column=c,
2530
+ boxes=self.MT.get_boxes(),
2531
+ selected=self.MT.selected,
2532
+ data={datacn: after_val},
2533
+ )
@@ -21,6 +21,7 @@ named_span_types: set[str] = {
21
21
  "checkbox",
22
22
  "readonly",
23
23
  "align",
24
+ "note",
24
25
  }
25
26
 
26
27
  emitted_events: set[str] = {
@@ -7,7 +7,7 @@ from typing import Any, Literal
7
7
 
8
8
  from .constants import ctrl_key, rc_binding
9
9
  from .functions import recursive_bind
10
- from .other_classes import DotDict
10
+ from .other_classes import DotDict, FontTuple
11
11
 
12
12
 
13
13
  class FindWindowTkText(tk.Text):
@@ -39,7 +39,7 @@ class FindWindowTkText(tk.Text):
39
39
  self,
40
40
  menu_kwargs: dict,
41
41
  sheet_ops: dict,
42
- font: tuple,
42
+ font: FontTuple,
43
43
  bg: str,
44
44
  fg: str,
45
45
  select_bg: str,
@@ -159,7 +159,7 @@ class FindWindowTkText(tk.Text):
159
159
  return "break"
160
160
 
161
161
 
162
- class Tooltip(tk.Toplevel):
162
+ class LabelTooltip(tk.Toplevel):
163
163
  def __init__(self, parent: tk.Misc, text: str, bg: str, fg: str) -> None:
164
164
  super().__init__(parent)
165
165
  self.withdraw()
@@ -510,7 +510,7 @@ class FindWindow(tk.Frame):
510
510
  """Show the tooltip at the specified position."""
511
511
  bg = self.bg if self.bg is not None else "white"
512
512
  fg = self.fg if self.fg is not None else "black"
513
- self.tooltip = Tooltip(self, widget.tooltip_text, bg, fg)
513
+ self.tooltip = LabelTooltip(self, widget.tooltip_text, bg, fg)
514
514
  # Use current mouse position instead of recorded position
515
515
  self.tooltip.deiconify()
516
516
  show_x = max(0, self.winfo_toplevel().winfo_pointerx() - self.tooltip.winfo_width() - 5)
@@ -227,11 +227,13 @@ def data_to_str(
227
227
  ) -> str:
228
228
  if not isinstance(value, datatypes):
229
229
  return invalid_value
230
- if value is None and nullable:
230
+ elif value is None and nullable:
231
231
  return ""
232
- return to_str_function(value, **kwargs)
232
+ else:
233
+ return to_str_function(value, **kwargs)
233
234
 
234
235
 
236
+ # Only used if MainTable.cell_str is called with get_displayed=False
235
237
  def get_data_with_valid_check(value="", datatypes: tuple[()] | tuple[Any] | Any = (), invalid_value="NA"):
236
238
  if isinstance(value, datatypes):
237
239
  return value