tksheet 7.5.4__py3-none-any.whl → 7.5.7__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/__init__.py +1 -1
- tksheet/column_headers.py +230 -87
- tksheet/constants.py +1 -0
- tksheet/find_window.py +4 -4
- tksheet/formatters.py +4 -2
- tksheet/functions.py +134 -57
- tksheet/main_table.py +360 -631
- tksheet/menus.py +494 -0
- tksheet/other_classes.py +3 -0
- tksheet/row_index.py +235 -92
- tksheet/sheet.py +117 -52
- tksheet/sheet_options.py +5 -1
- tksheet/tksheet_types.py +1 -0
- tksheet/tooltip.py +335 -0
- {tksheet-7.5.4.dist-info → tksheet-7.5.7.dist-info}/METADATA +1 -1
- tksheet-7.5.7.dist-info/RECORD +24 -0
- {tksheet-7.5.4.dist-info → tksheet-7.5.7.dist-info}/WHEEL +1 -1
- tksheet-7.5.4.dist-info/RECORD +0 -22
- {tksheet-7.5.4.dist-info → tksheet-7.5.7.dist-info}/licenses/LICENSE.txt +0 -0
- {tksheet-7.5.4.dist-info → tksheet-7.5.7.dist-info}/top_level.txt +0 -0
tksheet/functions.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
+
import copy
|
3
4
|
import csv
|
4
5
|
import io
|
5
6
|
import re
|
@@ -9,6 +10,7 @@ from collections import deque
|
|
9
10
|
from collections.abc import Callable, Generator, Hashable, Iterable, Iterator, Sequence
|
10
11
|
from difflib import SequenceMatcher
|
11
12
|
from itertools import chain, islice, repeat
|
13
|
+
from types import ModuleType
|
12
14
|
from typing import Any, Literal
|
13
15
|
|
14
16
|
from .colors import color_map
|
@@ -16,7 +18,6 @@ from .constants import align_value_error, symbols_set
|
|
16
18
|
from .formatters import to_bool
|
17
19
|
from .other_classes import DotDict, EventDataDict, Highlight, Loc, Span
|
18
20
|
|
19
|
-
lines_re = re.compile(r"[^\n]+")
|
20
21
|
ORD_A = ord("A")
|
21
22
|
|
22
23
|
|
@@ -33,8 +34,8 @@ def wrap_text(
|
|
33
34
|
line_width = 0
|
34
35
|
if wrap == "c":
|
35
36
|
current_line = []
|
36
|
-
for
|
37
|
-
for char in
|
37
|
+
for line in text.split("\n"):
|
38
|
+
for char in line:
|
38
39
|
try:
|
39
40
|
char_width = widths[char]
|
40
41
|
except KeyError:
|
@@ -75,8 +76,8 @@ def wrap_text(
|
|
75
76
|
space_width = char_width_fn(" ")
|
76
77
|
current_line = []
|
77
78
|
|
78
|
-
for
|
79
|
-
for i, word in enumerate(
|
79
|
+
for line in text.split("\n"):
|
80
|
+
for i, word in enumerate(line.split()):
|
80
81
|
# if we're going to next word and
|
81
82
|
# if a space fits on the end of the current line we add one
|
82
83
|
if i and line_width + space_width < max_width:
|
@@ -155,10 +156,10 @@ def wrap_text(
|
|
155
156
|
line_width = 0
|
156
157
|
|
157
158
|
else:
|
158
|
-
for
|
159
|
+
for line in text.split("\n"):
|
159
160
|
line_width = 0
|
160
161
|
current_line = []
|
161
|
-
for char in
|
162
|
+
for char in line:
|
162
163
|
try:
|
163
164
|
char_width = widths[char]
|
164
165
|
except KeyError:
|
@@ -205,6 +206,16 @@ def get_data_from_clipboard(
|
|
205
206
|
return [[data]]
|
206
207
|
|
207
208
|
|
209
|
+
def widget_descendants(widget: tk.Misc) -> list[tk.Misc]:
|
210
|
+
result = []
|
211
|
+
queue = deque([widget])
|
212
|
+
while queue:
|
213
|
+
current_widget = queue.popleft()
|
214
|
+
result.append(current_widget)
|
215
|
+
queue.extend(current_widget.winfo_children())
|
216
|
+
return result
|
217
|
+
|
218
|
+
|
208
219
|
def recursive_bind(widget: tk.Misc, event: str, callback: Callable) -> None:
|
209
220
|
widget.bind(event, callback)
|
210
221
|
for child in widget.winfo_children():
|
@@ -461,6 +472,8 @@ def get_dropdown_kwargs(
|
|
461
472
|
search_function: Callable = dropdown_search_function,
|
462
473
|
validate_input: bool = True,
|
463
474
|
text: None | str = None,
|
475
|
+
edit_data: bool = True,
|
476
|
+
default_value: Any = None,
|
464
477
|
) -> dict:
|
465
478
|
return {
|
466
479
|
"values": [] if values is None else values,
|
@@ -472,6 +485,8 @@ def get_dropdown_kwargs(
|
|
472
485
|
"search_function": search_function,
|
473
486
|
"validate_input": validate_input,
|
474
487
|
"text": text,
|
488
|
+
"edit_data": edit_data,
|
489
|
+
"default_value": default_value,
|
475
490
|
}
|
476
491
|
|
477
492
|
|
@@ -484,6 +499,7 @@ def get_dropdown_dict(**kwargs) -> dict:
|
|
484
499
|
"validate_input": kwargs["validate_input"],
|
485
500
|
"text": kwargs["text"],
|
486
501
|
"state": kwargs["state"],
|
502
|
+
"default_value": kwargs["default_value"],
|
487
503
|
}
|
488
504
|
|
489
505
|
|
@@ -765,6 +781,14 @@ def add_to_displayed(displayed: list[int], to_add: Iterable[int]) -> list[int]:
|
|
765
781
|
return displayed
|
766
782
|
|
767
783
|
|
784
|
+
def push_displayed(displayed: list[int], to_add: Iterable[int]) -> list[int]:
|
785
|
+
# assumes to_add is sorted
|
786
|
+
for i in to_add:
|
787
|
+
ins = bisect_left(displayed, i)
|
788
|
+
displayed[ins:] = [e + 1 for e in islice(displayed, ins, None)]
|
789
|
+
return displayed
|
790
|
+
|
791
|
+
|
768
792
|
def move_elements_by_mapping(
|
769
793
|
seq: list[Any],
|
770
794
|
new_idxs: dict[int, int],
|
@@ -873,50 +897,38 @@ def rounded_box_coords(
|
|
873
897
|
x2: float,
|
874
898
|
y2: float,
|
875
899
|
radius: int = 5,
|
876
|
-
) -> tuple[float]:
|
900
|
+
) -> tuple[float, ...]:
|
901
|
+
# Handle case where rectangle is too small for rounding
|
877
902
|
if y2 - y1 < 2 or x2 - x1 < 2:
|
878
|
-
return x1, y1, x2, y1, x2, y2, x1, y2
|
903
|
+
return (x1, y1, x2, y1, x2, y2, x1, y2, x1, y1)
|
904
|
+
# Coordinates for a closed rectangle with rounded corners
|
879
905
|
return (
|
880
906
|
x1 + radius,
|
881
|
-
y1,
|
882
|
-
x1 + radius,
|
883
|
-
y1,
|
907
|
+
y1, # Top side start
|
884
908
|
x2 - radius,
|
885
|
-
y1,
|
886
|
-
x2 - radius,
|
887
|
-
y1,
|
909
|
+
y1, # Top side end
|
888
910
|
x2,
|
889
|
-
y1,
|
911
|
+
y1, # Top-right corner
|
890
912
|
x2,
|
891
913
|
y1 + radius,
|
892
914
|
x2,
|
893
|
-
|
894
|
-
x2,
|
895
|
-
y2 - radius,
|
896
|
-
x2,
|
897
|
-
y2 - radius,
|
915
|
+
y2 - radius, # Right side
|
898
916
|
x2,
|
899
|
-
y2,
|
900
|
-
x2 - radius,
|
901
|
-
y2,
|
917
|
+
y2, # Bottom-right corner
|
902
918
|
x2 - radius,
|
903
919
|
y2,
|
904
920
|
x1 + radius,
|
905
|
-
y2,
|
906
|
-
x1 + radius,
|
907
|
-
y2,
|
921
|
+
y2, # Bottom side
|
908
922
|
x1,
|
909
|
-
y2,
|
910
|
-
x1,
|
911
|
-
y2 - radius,
|
923
|
+
y2, # Bottom-left corner
|
912
924
|
x1,
|
913
925
|
y2 - radius,
|
914
926
|
x1,
|
915
|
-
y1 + radius,
|
927
|
+
y1 + radius, # Left side
|
916
928
|
x1,
|
917
|
-
y1
|
918
|
-
x1,
|
919
|
-
y1,
|
929
|
+
y1, # Top-left corner
|
930
|
+
x1 + radius,
|
931
|
+
y1, # Close the shape
|
920
932
|
)
|
921
933
|
|
922
934
|
|
@@ -1195,14 +1207,14 @@ PATTERN_ALL = re.compile(r"^:$") # ":"
|
|
1195
1207
|
|
1196
1208
|
def span_a2i(a: str) -> int | None:
|
1197
1209
|
n = 0
|
1198
|
-
for c in a:
|
1210
|
+
for c in a.upper():
|
1199
1211
|
n = n * 26 + ord(c) - ORD_A + 1
|
1200
1212
|
return n - 1
|
1201
1213
|
|
1202
1214
|
|
1203
1215
|
def span_a2n(a: str) -> int | None:
|
1204
1216
|
n = 0
|
1205
|
-
for c in a:
|
1217
|
+
for c in a.upper():
|
1206
1218
|
n = n * 26 + ord(c) - ORD_A + 1
|
1207
1219
|
return n
|
1208
1220
|
|
@@ -1239,7 +1251,7 @@ def key_to_span(
|
|
1239
1251
|
return coords_to_span(widget=widget, from_r=None, from_c=None, upto_r=None, upto_c=None)
|
1240
1252
|
|
1241
1253
|
# Validate input type
|
1242
|
-
elif not isinstance(key, (str, int, slice,
|
1254
|
+
elif not isinstance(key, (str, int, slice, tuple, list)):
|
1243
1255
|
return f"Key type must be either str, int, list, tuple or slice, not '{type(key).__name__}'."
|
1244
1256
|
|
1245
1257
|
try:
|
@@ -1265,22 +1277,34 @@ def key_to_span(
|
|
1265
1277
|
)
|
1266
1278
|
|
1267
1279
|
# Sequence key: various span formats
|
1268
|
-
elif isinstance(key, (
|
1269
|
-
if (
|
1270
|
-
|
1271
|
-
|
1272
|
-
|
1273
|
-
|
1274
|
-
|
1275
|
-
|
1276
|
-
|
1277
|
-
|
1278
|
-
|
1279
|
-
|
1280
|
-
|
1281
|
-
|
1282
|
-
|
1283
|
-
|
1280
|
+
elif isinstance(key, (tuple, list)):
|
1281
|
+
if len(key) == 2:
|
1282
|
+
if (isinstance(key[0], int) or key[0] is None) and (isinstance(key[1], int) or key[1] is None):
|
1283
|
+
# Single cell or partial span: (row, col)
|
1284
|
+
r_int = isinstance(key[0], int)
|
1285
|
+
c_int = isinstance(key[1], int)
|
1286
|
+
return span_dict(
|
1287
|
+
from_r=key[0] if r_int else 0,
|
1288
|
+
from_c=key[1] if c_int else 0,
|
1289
|
+
upto_r=key[0] + 1 if r_int else None,
|
1290
|
+
upto_c=key[1] + 1 if c_int else None,
|
1291
|
+
widget=widget,
|
1292
|
+
)
|
1293
|
+
|
1294
|
+
elif isinstance(key[0], int) and isinstance(key[1], str):
|
1295
|
+
# Single cell with column letter: (row 0, col A)
|
1296
|
+
c_int = span_a2i(key[1])
|
1297
|
+
return span_dict(
|
1298
|
+
from_r=key[0],
|
1299
|
+
from_c=c_int,
|
1300
|
+
upto_r=key[0] + 1,
|
1301
|
+
upto_c=c_int + 1,
|
1302
|
+
widget=widget,
|
1303
|
+
)
|
1304
|
+
|
1305
|
+
else:
|
1306
|
+
return f"'{key}' could not be converted to span."
|
1307
|
+
|
1284
1308
|
elif len(key) == 4:
|
1285
1309
|
# Full span coordinates: (from_r, from_c, upto_r, upto_c)
|
1286
1310
|
return coords_to_span(
|
@@ -1290,7 +1314,7 @@ def key_to_span(
|
|
1290
1314
|
upto_r=key[2],
|
1291
1315
|
upto_c=key[3],
|
1292
1316
|
)
|
1293
|
-
elif len(key) == 2 and all(isinstance(k, (
|
1317
|
+
elif len(key) == 2 and all(isinstance(k, (tuple, list)) for k in key):
|
1294
1318
|
# Start and end points: ((from_r, from_c), (upto_r, upto_c))
|
1295
1319
|
return coords_to_span(
|
1296
1320
|
widget=widget,
|
@@ -1327,11 +1351,12 @@ def key_to_span(
|
|
1327
1351
|
widget=widget,
|
1328
1352
|
)
|
1329
1353
|
elif m := PATTERN_COL.match(key):
|
1354
|
+
c_int = span_a2i(m[1])
|
1330
1355
|
return span_dict(
|
1331
1356
|
from_r=None,
|
1332
|
-
from_c=
|
1357
|
+
from_c=c_int,
|
1333
1358
|
upto_r=None,
|
1334
|
-
upto_c=
|
1359
|
+
upto_c=c_int + 1,
|
1335
1360
|
widget=widget,
|
1336
1361
|
)
|
1337
1362
|
elif m := PATTERN_CELL.match(key):
|
@@ -1499,6 +1524,17 @@ def del_named_span_options_nested(
|
|
1499
1524
|
del options[k][type_]
|
1500
1525
|
|
1501
1526
|
|
1527
|
+
def mod_note(options: dict, key: int | tuple[int, int], note: str | None, readonly: bool = True) -> dict:
|
1528
|
+
if note is not None:
|
1529
|
+
if key not in options:
|
1530
|
+
options[key] = {}
|
1531
|
+
options[key]["note"] = {"note": note, "readonly": readonly}
|
1532
|
+
else:
|
1533
|
+
if key in options and "note" in options[key]:
|
1534
|
+
del options[key]["note"]
|
1535
|
+
return options
|
1536
|
+
|
1537
|
+
|
1502
1538
|
def add_highlight(
|
1503
1539
|
options: dict,
|
1504
1540
|
key: int | tuple[int, int],
|
@@ -1733,7 +1769,7 @@ def get_vertical_gridline_points(
|
|
1733
1769
|
positions: list[float],
|
1734
1770
|
start: int,
|
1735
1771
|
end: int,
|
1736
|
-
) -> list[float]:
|
1772
|
+
) -> list[int | float]:
|
1737
1773
|
return list(
|
1738
1774
|
chain.from_iterable(
|
1739
1775
|
(
|
@@ -1749,3 +1785,44 @@ def get_vertical_gridline_points(
|
|
1749
1785
|
for c in range(start, end)
|
1750
1786
|
)
|
1751
1787
|
)
|
1788
|
+
|
1789
|
+
|
1790
|
+
def safe_copy(value: Any) -> Any:
|
1791
|
+
"""
|
1792
|
+
Attempts to create a deep copy of the input value. If copying fails,
|
1793
|
+
returns the original value.
|
1794
|
+
|
1795
|
+
Args:
|
1796
|
+
value: Any Python object to be copied
|
1797
|
+
|
1798
|
+
Returns:
|
1799
|
+
A deep copy of the value if possible, otherwise the original value
|
1800
|
+
"""
|
1801
|
+
try:
|
1802
|
+
# Try deep copy first for most objects
|
1803
|
+
return copy.deepcopy(value)
|
1804
|
+
except Exception:
|
1805
|
+
try:
|
1806
|
+
# For types that deepcopy might fail on, try shallow copy
|
1807
|
+
return copy.copy(value)
|
1808
|
+
except Exception:
|
1809
|
+
try:
|
1810
|
+
# For built-in immutable types, return as-is
|
1811
|
+
if isinstance(value, (int, float, str, bool, bytes, tuple, frozenset)):
|
1812
|
+
return value
|
1813
|
+
# For None
|
1814
|
+
if value is None:
|
1815
|
+
return None
|
1816
|
+
# For functions, return same function
|
1817
|
+
if isinstance(value, Callable):
|
1818
|
+
return value
|
1819
|
+
# For modules
|
1820
|
+
if isinstance(value, ModuleType):
|
1821
|
+
return value
|
1822
|
+
# For classes
|
1823
|
+
if isinstance(value, type):
|
1824
|
+
return value
|
1825
|
+
except Exception:
|
1826
|
+
pass
|
1827
|
+
# If all copy attempts fail, return original value
|
1828
|
+
return value
|