tksheet 7.4.7__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/__init__.py +1 -1
- tksheet/column_headers.py +2 -0
- tksheet/constants.py +195 -0
- tksheet/find_window.py +324 -30
- tksheet/functions.py +89 -39
- tksheet/main_table.py +443 -288
- tksheet/row_index.py +9 -9
- tksheet/sheet.py +138 -488
- tksheet/sheet_options.py +4 -0
- tksheet/tksheet_types.py +3 -0
- {tksheet-7.4.7.dist-info → tksheet-7.4.8.dist-info}/METADATA +1 -1
- tksheet-7.4.8.dist-info/RECORD +22 -0
- tksheet-7.4.7.dist-info/RECORD +0 -22
- {tksheet-7.4.7.dist-info → tksheet-7.4.8.dist-info}/LICENSE.txt +0 -0
- {tksheet-7.4.7.dist-info → tksheet-7.4.8.dist-info}/WHEEL +0 -0
- {tksheet-7.4.7.dist-info → tksheet-7.4.8.dist-info}/top_level.txt +0 -0
tksheet/find_window.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
+
import re
|
3
4
|
import tkinter as tk
|
4
5
|
from collections.abc import Callable
|
5
6
|
from typing import Any, Literal
|
@@ -10,6 +11,8 @@ from .other_classes import DotDict
|
|
10
11
|
|
11
12
|
|
12
13
|
class FindWindowTkText(tk.Text):
|
14
|
+
"""Custom Text widget for the FindWindow class."""
|
15
|
+
|
13
16
|
def __init__(
|
14
17
|
self,
|
15
18
|
parent: tk.Misc,
|
@@ -42,6 +45,7 @@ class FindWindowTkText(tk.Text):
|
|
42
45
|
select_bg: str,
|
43
46
|
select_fg: str,
|
44
47
|
) -> None:
|
48
|
+
"""Reset the text widget's appearance and menu options."""
|
45
49
|
self.config(
|
46
50
|
font=font,
|
47
51
|
background=bg,
|
@@ -84,10 +88,12 @@ class FindWindowTkText(tk.Text):
|
|
84
88
|
)
|
85
89
|
|
86
90
|
def rc(self, event: Any) -> None:
|
91
|
+
"""Show the right-click popup menu."""
|
87
92
|
self.focus_set()
|
88
93
|
self.rc_popup_menu.tk_popup(event.x_root, event.y_root)
|
89
94
|
|
90
95
|
def delete_key(self, event: Any = None) -> None:
|
96
|
+
"""Handle the Delete key based on editor configuration."""
|
91
97
|
if self.editor_del_key == "forward":
|
92
98
|
return
|
93
99
|
elif not self.editor_del_key:
|
@@ -101,38 +107,60 @@ class FindWindowTkText(tk.Text):
|
|
101
107
|
return "break"
|
102
108
|
|
103
109
|
def select_all(self, event: Any = None) -> Literal["break"]:
|
110
|
+
"""Select all text in the widget."""
|
104
111
|
self.tag_add(tk.SEL, "1.0", tk.END)
|
105
112
|
self.mark_set(tk.INSERT, tk.END)
|
106
|
-
# self.see(tk.INSERT)
|
107
113
|
return "break"
|
108
114
|
|
109
115
|
def cut(self, event: Any = None) -> Literal["break"]:
|
116
|
+
"""Cut selected text."""
|
110
117
|
self.event_generate(f"<{ctrl_key}-x>")
|
111
118
|
self.event_generate("<KeyRelease>")
|
112
119
|
return "break"
|
113
120
|
|
114
121
|
def copy(self, event: Any = None) -> Literal["break"]:
|
122
|
+
"""Copy selected text."""
|
115
123
|
self.event_generate(f"<{ctrl_key}-c>")
|
116
124
|
return "break"
|
117
125
|
|
118
126
|
def paste(self, event: Any = None) -> Literal["break"]:
|
127
|
+
"""Paste text from clipboard."""
|
119
128
|
self.event_generate(f"<{ctrl_key}-v>")
|
120
129
|
self.event_generate("<KeyRelease>")
|
121
130
|
return "break"
|
122
131
|
|
123
132
|
def undo(self, event: Any = None) -> Literal["break"]:
|
133
|
+
"""Undo the last action."""
|
124
134
|
self.event_generate(f"<{ctrl_key}-z>")
|
125
135
|
self.event_generate("<KeyRelease>")
|
126
136
|
return "break"
|
127
137
|
|
128
138
|
|
139
|
+
class Tooltip(tk.Toplevel):
|
140
|
+
def __init__(self, parent: tk.Misc, text: str, bg: str, fg: str) -> None:
|
141
|
+
super().__init__(parent)
|
142
|
+
self.withdraw()
|
143
|
+
self.overrideredirect(True)
|
144
|
+
self.label = tk.Label(self, text=text, background=bg, foreground=fg, relief="flat", borderwidth=0)
|
145
|
+
self.label.pack()
|
146
|
+
self.text = text
|
147
|
+
self.config(background=bg, highlightbackground=bg, highlightthickness=0)
|
148
|
+
self.update_idletasks()
|
149
|
+
|
150
|
+
|
129
151
|
class FindWindow(tk.Frame):
|
152
|
+
"""A frame containing find and replace functionality with label highlighting and tooltips."""
|
153
|
+
|
130
154
|
def __init__(
|
131
155
|
self,
|
132
156
|
parent: tk.Misc,
|
133
157
|
find_next_func: Callable,
|
134
158
|
find_prev_func: Callable,
|
135
159
|
close_func: Callable,
|
160
|
+
replace_func: Callable,
|
161
|
+
replace_all_func: Callable,
|
162
|
+
toggle_replace_func: Callable,
|
163
|
+
drag_func: Callable,
|
136
164
|
) -> None:
|
137
165
|
super().__init__(
|
138
166
|
parent,
|
@@ -140,81 +168,244 @@ class FindWindow(tk.Frame):
|
|
140
168
|
height=0,
|
141
169
|
bd=0,
|
142
170
|
)
|
143
|
-
self.grid_columnconfigure(
|
171
|
+
self.grid_columnconfigure(1, weight=1)
|
172
|
+
self.grid_columnconfigure(4, uniform="group1")
|
173
|
+
self.grid_columnconfigure(5, uniform="group2")
|
144
174
|
self.grid_rowconfigure(0, weight=1)
|
175
|
+
self.grid_rowconfigure(2, weight=1)
|
145
176
|
self.grid_propagate(False)
|
146
177
|
self.parent = parent
|
178
|
+
self.tooltip_after_id = None
|
179
|
+
self.tooltip_last_x = None
|
180
|
+
self.tooltip_last_y = None
|
181
|
+
self.tooltip_widget = None # Added to track the current widget
|
182
|
+
self.tooltip = None
|
183
|
+
|
184
|
+
self.find_next_func = find_next_func
|
185
|
+
self.find_prev_func = find_prev_func
|
186
|
+
self.replace_func = replace_func
|
187
|
+
self.toggle_replace_func = toggle_replace_func
|
188
|
+
self.drag_func = drag_func
|
189
|
+
self.close_func = close_func
|
190
|
+
|
191
|
+
self.toggle_replace = tk.Label(self, text="↓", cursor="sb_h_double_arrow", highlightthickness=1)
|
192
|
+
self.toggle_replace.grid(row=0, column=0, sticky="ns")
|
193
|
+
self.toggle_replace.grid_remove()
|
194
|
+
|
147
195
|
self.tktext = FindWindowTkText(self)
|
148
|
-
self.tktext.grid(row=0, column=
|
149
|
-
self.bg = None
|
150
|
-
self.fg = None
|
196
|
+
self.tktext.grid(row=0, column=1, sticky="nswe")
|
151
197
|
|
152
|
-
self.find_previous_arrow = tk.Label(self, text="
|
153
|
-
self.find_previous_arrow.
|
154
|
-
self.find_previous_arrow.grid(row=0, column=1)
|
198
|
+
self.find_previous_arrow = tk.Label(self, text="↑", cursor="hand2", highlightthickness=1)
|
199
|
+
self.find_previous_arrow.grid(row=0, column=2)
|
155
200
|
|
156
|
-
self.find_next_arrow = tk.Label(self, text="
|
157
|
-
self.find_next_arrow.
|
158
|
-
self.find_next_arrow.grid(row=0, column=2)
|
201
|
+
self.find_next_arrow = tk.Label(self, text="↓", cursor="hand2", highlightthickness=1)
|
202
|
+
self.find_next_arrow.grid(row=0, column=3)
|
159
203
|
|
160
204
|
self.find_in_selection = False
|
161
|
-
self.in_selection = tk.Label(self, text="
|
162
|
-
self.in_selection.
|
163
|
-
self.in_selection.grid(row=0, column=3)
|
205
|
+
self.in_selection = tk.Label(self, text="≡", cursor="hand2", highlightthickness=1)
|
206
|
+
self.in_selection.grid(row=0, column=4)
|
164
207
|
|
165
208
|
self.close = tk.Label(self, text="✕", cursor="hand2", highlightthickness=1)
|
166
|
-
self.close.
|
167
|
-
self.close.grid(row=0, column=4)
|
209
|
+
self.close.grid(row=0, column=5, sticky="nswe")
|
168
210
|
|
169
|
-
|
170
|
-
|
171
|
-
|
211
|
+
self.separator = tk.Frame(self, height=1)
|
212
|
+
self.separator.grid(row=1, column=1, columnspan=3, sticky="we")
|
213
|
+
self.separator.grid_remove()
|
214
|
+
|
215
|
+
self.replace_tktext = FindWindowTkText(self)
|
216
|
+
self.replace_tktext.grid(row=2, column=1, columnspan=4, sticky="nswe")
|
217
|
+
self.replace_tktext.grid_remove()
|
218
|
+
|
219
|
+
self.replace_next = tk.Label(self, text="→", cursor="hand2", highlightthickness=1)
|
220
|
+
self.replace_next.grid(row=2, column=4, sticky="nswe")
|
221
|
+
self.replace_next.grid_remove()
|
222
|
+
|
223
|
+
self.replace_all = tk.Label(self, text="⟳", cursor="hand2", highlightthickness=1)
|
224
|
+
self.replace_all.grid(row=2, column=5, sticky="nswe")
|
225
|
+
self.replace_all.grid_remove()
|
226
|
+
|
227
|
+
self.tktext.bind("<Tab>", self.handle_tab)
|
228
|
+
self.replace_tktext.bind("<Tab>", self.handle_tab)
|
229
|
+
self.tktext.bind("<Return>", self.handle_return)
|
230
|
+
self.replace_tktext.bind("<Return>", self.handle_return)
|
231
|
+
|
232
|
+
self.bind_label(self.toggle_replace, self.toggle_replace_window, self.drag_func)
|
233
|
+
self.bind_label(self.find_previous_arrow, find_prev_func)
|
234
|
+
self.bind_label(self.find_next_arrow, find_next_func)
|
235
|
+
self.bind_label(self.in_selection, self.toggle_in_selection)
|
236
|
+
self.bind_label(self.close, close_func)
|
237
|
+
self.bind_label(self.replace_next, replace_func)
|
238
|
+
self.bind_label(self.replace_all, replace_all_func)
|
239
|
+
|
240
|
+
self.replace_visible = False
|
241
|
+
self.bg = None
|
242
|
+
self.fg = None
|
243
|
+
self.pressed_label = None
|
172
244
|
|
173
245
|
for b in ("Option", "Alt"):
|
174
246
|
for c in ("l", "L"):
|
175
247
|
recursive_bind(self, f"<{b}-{c}>", self.toggle_in_selection)
|
176
248
|
|
249
|
+
action_labels = [
|
250
|
+
(self.toggle_replace, "Toggle Replace"),
|
251
|
+
(self.find_previous_arrow, "Previous Match"),
|
252
|
+
(self.find_next_arrow, "Next Match"),
|
253
|
+
(self.in_selection, "Find in Selection"),
|
254
|
+
(self.close, "Close"),
|
255
|
+
(self.replace_next, "Replace"),
|
256
|
+
(self.replace_all, "Replace All"),
|
257
|
+
]
|
258
|
+
for widget, text in action_labels:
|
259
|
+
widget.tooltip_text = text
|
260
|
+
widget.bind("<Enter>", self.on_enter)
|
261
|
+
widget.bind("<Leave>", self.on_leave)
|
262
|
+
|
263
|
+
def bind_label(self, label: tk.Label, func: Callable, motion_func: Callable | None = None) -> None:
|
264
|
+
"""Bind press, release, and optional motion events with highlight changes."""
|
265
|
+
|
266
|
+
def on_press(event: tk.Event) -> None:
|
267
|
+
label.config(highlightbackground=self.border_color, highlightcolor=self.border_color)
|
268
|
+
self.pressed_label = label
|
269
|
+
|
270
|
+
def on_release(event: tk.Event) -> None:
|
271
|
+
self.pressed_label = None
|
272
|
+
if 0 <= event.x < label.winfo_width() and 0 <= event.y < label.winfo_height():
|
273
|
+
label.config(highlightbackground=self.fg, highlightcolor=self.fg)
|
274
|
+
func(event)
|
275
|
+
else:
|
276
|
+
label.config(highlightbackground=self.bg, highlightcolor=self.fg)
|
277
|
+
|
278
|
+
label.bind("<Button-1>", on_press)
|
279
|
+
label.bind("<ButtonRelease-1>", on_release)
|
280
|
+
if motion_func:
|
281
|
+
label.bind("<B1-Motion>", motion_func)
|
282
|
+
|
283
|
+
def on_enter(self, event: tk.Event) -> None:
|
284
|
+
"""Handle mouse entering a widget."""
|
285
|
+
widget = event.widget
|
286
|
+
self.enter_label(widget)
|
287
|
+
self.tooltip_widget = widget
|
288
|
+
self.tooltip_last_x, self.tooltip_last_y = get_mouse_coords(widget)
|
289
|
+
self.start_tooltip_timer()
|
290
|
+
|
291
|
+
def on_leave(self, event: tk.Event) -> None:
|
292
|
+
"""Handle mouse leaving a widget."""
|
293
|
+
widget = event.widget
|
294
|
+
self.leave_label(widget)
|
295
|
+
self.hide_tooltip()
|
296
|
+
self.cancel_tooltip()
|
297
|
+
self.tooltip_widget = None
|
298
|
+
|
177
299
|
def enter_label(self, widget: tk.Misc) -> None:
|
178
|
-
|
179
|
-
|
180
|
-
highlightcolor=self.fg
|
181
|
-
)
|
300
|
+
"""Highlight label on hover if no label is pressed."""
|
301
|
+
if self.pressed_label is None:
|
302
|
+
widget.config(highlightbackground=self.fg, highlightcolor=self.fg)
|
182
303
|
|
183
304
|
def leave_label(self, widget: tk.Misc) -> None:
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
highlightcolor=self.fg
|
189
|
-
|
305
|
+
"""Remove highlight on leave unless toggled or pressed."""
|
306
|
+
if self.pressed_label is None:
|
307
|
+
if widget == self.in_selection and self.find_in_selection:
|
308
|
+
return
|
309
|
+
widget.config(highlightbackground=self.bg, highlightcolor=self.fg)
|
310
|
+
|
311
|
+
def focus_find(self, event: tk.Misc = None) -> Literal["break"]:
|
312
|
+
widget = self.focus_get()
|
313
|
+
if widget == self.tktext:
|
314
|
+
self.tktext.select_all()
|
315
|
+
else:
|
316
|
+
self.tktext.focus_set()
|
317
|
+
return "break"
|
318
|
+
|
319
|
+
def focus_replace(self, event: tk.Misc = None) -> Literal["break"]:
|
320
|
+
widget = self.focus_get()
|
321
|
+
if widget == self.replace_tktext:
|
322
|
+
self.replace_tktext.select_all()
|
323
|
+
else:
|
324
|
+
self.replace_tktext.focus_set()
|
325
|
+
return "break"
|
326
|
+
|
327
|
+
def toggle_replace_window(self, event: tk.Misc = None) -> None:
|
328
|
+
"""Toggle visibility of the replace window."""
|
329
|
+
if self.replace_visible:
|
330
|
+
self.replace_tktext.grid_remove()
|
331
|
+
self.replace_next.grid_remove()
|
332
|
+
self.replace_all.grid_remove()
|
333
|
+
self.separator.grid_remove()
|
334
|
+
self.toggle_replace.config(text="↓")
|
335
|
+
self.toggle_replace.grid(row=0, column=0, rowspan=1, sticky="ns")
|
336
|
+
self.replace_visible = False
|
337
|
+
elif self.replace_enabled:
|
338
|
+
self.separator.grid()
|
339
|
+
self.replace_tktext.grid()
|
340
|
+
self.replace_next.grid()
|
341
|
+
self.replace_all.grid()
|
342
|
+
self.toggle_replace.config(text="↑")
|
343
|
+
self.toggle_replace.grid(row=0, column=0, rowspan=3, sticky="ns")
|
344
|
+
self.replace_visible = True
|
345
|
+
self.toggle_replace_func()
|
190
346
|
|
191
347
|
def toggle_in_selection(self, event: tk.Misc) -> None:
|
348
|
+
"""Toggle the find-in-selection state."""
|
192
349
|
self.find_in_selection = not self.find_in_selection
|
193
350
|
self.enter_label(self.in_selection)
|
194
351
|
self.leave_label(self.in_selection)
|
195
352
|
|
353
|
+
def handle_tab(self, event: tk.Event) -> Literal["break"]:
|
354
|
+
"""Switch focus between find and replace text widgets."""
|
355
|
+
if not self.replace_visible:
|
356
|
+
self.toggle_replace_window()
|
357
|
+
if event.widget == self.tktext:
|
358
|
+
self.replace_tktext.focus_set()
|
359
|
+
elif event.widget == self.replace_tktext:
|
360
|
+
self.tktext.focus_set()
|
361
|
+
return "break"
|
362
|
+
|
363
|
+
def handle_return(self, event: tk.Event) -> Literal["break"]:
|
364
|
+
"""Trigger find or replace based on focused widget."""
|
365
|
+
if event.widget == self.tktext:
|
366
|
+
self.find_next_func()
|
367
|
+
elif event.widget == self.replace_tktext:
|
368
|
+
self.replace_func()
|
369
|
+
return "break"
|
370
|
+
|
196
371
|
def get(self) -> str:
|
372
|
+
"""Return the find text."""
|
197
373
|
return self.tktext.get("1.0", "end-1c")
|
198
374
|
|
375
|
+
def get_replace(self) -> str:
|
376
|
+
"""Return the replace text."""
|
377
|
+
return self.replace_tktext.get("1.0", "end-1c")
|
378
|
+
|
199
379
|
def get_num_lines(self) -> int:
|
380
|
+
"""Return the number of lines in the find text."""
|
200
381
|
return int(self.tktext.index("end-1c").split(".")[0])
|
201
382
|
|
202
383
|
def set_text(self, text: str = "") -> None:
|
384
|
+
"""Set the find text."""
|
203
385
|
self.tktext.delete(1.0, "end")
|
204
386
|
self.tktext.insert(1.0, text)
|
205
387
|
|
206
388
|
def reset(
|
207
389
|
self,
|
208
390
|
border_color: str,
|
391
|
+
grid_color: str,
|
209
392
|
menu_kwargs: DotDict,
|
210
393
|
sheet_ops: DotDict,
|
211
394
|
bg: str,
|
212
395
|
fg: str,
|
213
396
|
select_bg: str,
|
214
397
|
select_fg: str,
|
398
|
+
replace_enabled: bool,
|
215
399
|
) -> None:
|
400
|
+
"""Reset styles and configurations."""
|
401
|
+
self.replace_enabled = replace_enabled
|
402
|
+
if replace_enabled:
|
403
|
+
self.toggle_replace.grid()
|
404
|
+
else:
|
405
|
+
self.toggle_replace.grid_remove()
|
216
406
|
self.bg = bg
|
217
407
|
self.fg = fg
|
408
|
+
self.border_color = border_color
|
218
409
|
self.tktext.reset(
|
219
410
|
menu_kwargs=menu_kwargs,
|
220
411
|
sheet_ops=sheet_ops,
|
@@ -224,7 +415,24 @@ class FindWindow(tk.Frame):
|
|
224
415
|
select_bg=select_bg,
|
225
416
|
select_fg=select_fg,
|
226
417
|
)
|
227
|
-
|
418
|
+
self.replace_tktext.reset(
|
419
|
+
menu_kwargs=menu_kwargs,
|
420
|
+
sheet_ops=sheet_ops,
|
421
|
+
font=menu_kwargs.font,
|
422
|
+
bg=bg,
|
423
|
+
fg=fg,
|
424
|
+
select_bg=select_bg,
|
425
|
+
select_fg=select_fg,
|
426
|
+
)
|
427
|
+
for widget in (
|
428
|
+
self.find_previous_arrow,
|
429
|
+
self.find_next_arrow,
|
430
|
+
self.in_selection,
|
431
|
+
self.close,
|
432
|
+
self.toggle_replace,
|
433
|
+
self.replace_next,
|
434
|
+
self.replace_all,
|
435
|
+
):
|
228
436
|
widget.config(
|
229
437
|
font=menu_kwargs.font,
|
230
438
|
bg=bg,
|
@@ -240,3 +448,89 @@ class FindWindow(tk.Frame):
|
|
240
448
|
highlightcolor=border_color,
|
241
449
|
highlightthickness=1,
|
242
450
|
)
|
451
|
+
self.separator.config(background=grid_color)
|
452
|
+
|
453
|
+
for b in sheet_ops.find_bindings:
|
454
|
+
recursive_bind(self, b, self.focus_find)
|
455
|
+
for b in sheet_ops.toggle_replace_bindings:
|
456
|
+
recursive_bind(self, b, self.focus_replace)
|
457
|
+
|
458
|
+
for b in sheet_ops.escape_bindings:
|
459
|
+
recursive_bind(self, b, self.close_func)
|
460
|
+
|
461
|
+
for b in sheet_ops.find_next_bindings:
|
462
|
+
recursive_bind(self, b, self.find_next_func)
|
463
|
+
for b in sheet_ops.find_previous_bindings:
|
464
|
+
recursive_bind(self, b, self.find_prev_func)
|
465
|
+
|
466
|
+
def start_tooltip_timer(self) -> None:
|
467
|
+
self.tooltip_after_id = self.after(400, self.check_and_show_tooltip)
|
468
|
+
|
469
|
+
def check_and_show_tooltip(self) -> None:
|
470
|
+
"""Check if the mouse position has changed and show tooltip if stationary."""
|
471
|
+
if self.tooltip_widget is None:
|
472
|
+
return
|
473
|
+
current_x, current_y = get_mouse_coords(self.tooltip_widget)
|
474
|
+
if current_x < 0 or current_y < 0: # Mouse outside window
|
475
|
+
return
|
476
|
+
# Allow 2-pixel tolerance for minor movements
|
477
|
+
if abs(current_x - self.tooltip_last_x) <= 2 and abs(current_y - self.tooltip_last_y) <= 2:
|
478
|
+
self.show_tooltip(self.tooltip_widget)
|
479
|
+
else:
|
480
|
+
self.tooltip_last_x = current_x
|
481
|
+
self.tooltip_last_y = current_y
|
482
|
+
self.tooltip_after_id = self.after(400, self.check_and_show_tooltip)
|
483
|
+
|
484
|
+
def show_tooltip(self, widget: tk.Misc) -> None:
|
485
|
+
"""Show the tooltip at the specified position."""
|
486
|
+
bg = self.bg if self.bg is not None else "white"
|
487
|
+
fg = self.fg if self.fg is not None else "black"
|
488
|
+
self.tooltip = Tooltip(self, widget.tooltip_text, bg, fg)
|
489
|
+
# Use current mouse position instead of recorded position
|
490
|
+
self.tooltip.deiconify()
|
491
|
+
show_x = max(0, self.winfo_toplevel().winfo_pointerx() - self.tooltip.winfo_width() - 5)
|
492
|
+
show_y = self.winfo_toplevel().winfo_pointery()
|
493
|
+
self.tooltip.wm_geometry(f"+{show_x}+{show_y - 10}")
|
494
|
+
|
495
|
+
def cancel_tooltip(self):
|
496
|
+
"""Cancel any scheduled tooltip."""
|
497
|
+
if self.tooltip_after_id is not None:
|
498
|
+
self.after_cancel(self.tooltip_after_id)
|
499
|
+
self.tooltip_after_id = None
|
500
|
+
|
501
|
+
def hide_tooltip(self):
|
502
|
+
"""Hide the tooltip."""
|
503
|
+
if self.tooltip is not None:
|
504
|
+
self.tooltip.destroy()
|
505
|
+
self.tooltip = None
|
506
|
+
|
507
|
+
|
508
|
+
def replacer(find: str, replace: str, current: str) -> Callable[[re.Match], str]:
|
509
|
+
"""Create a replacement function for re.sub with special empty string handling."""
|
510
|
+
|
511
|
+
def _replacer(match: re.Match) -> str:
|
512
|
+
if find:
|
513
|
+
return replace
|
514
|
+
else:
|
515
|
+
if len(current) == 0:
|
516
|
+
return replace
|
517
|
+
else:
|
518
|
+
return match.group(0)
|
519
|
+
|
520
|
+
return _replacer
|
521
|
+
|
522
|
+
|
523
|
+
def get_mouse_coords(widget: tk.Misc) -> tuple[int, int]:
|
524
|
+
# Get absolute mouse coordinates (relative to screen)
|
525
|
+
mouse_x = widget.winfo_pointerx()
|
526
|
+
mouse_y = widget.winfo_pointery()
|
527
|
+
|
528
|
+
# Get widget's position relative to the screen
|
529
|
+
widget_x = widget.winfo_rootx()
|
530
|
+
widget_y = widget.winfo_rooty()
|
531
|
+
|
532
|
+
# Calculate coordinates relative to the widget
|
533
|
+
relative_x = mouse_x - widget_x
|
534
|
+
relative_y = mouse_y - widget_y
|
535
|
+
|
536
|
+
return relative_x, relative_y
|
tksheet/functions.py
CHANGED
@@ -203,6 +203,12 @@ def recursive_bind(widget: tk.Misc, event: str, callback: Callable) -> None:
|
|
203
203
|
recursive_bind(child, event, callback)
|
204
204
|
|
205
205
|
|
206
|
+
def recursive_unbind(widget: tk.Misc, event: str) -> None:
|
207
|
+
widget.unbind(event)
|
208
|
+
for child in widget.winfo_children():
|
209
|
+
recursive_unbind(child, event)
|
210
|
+
|
211
|
+
|
206
212
|
def tksheet_type_error(kwarg: str, valid_types: list[str], not_type: Any) -> str:
|
207
213
|
valid_types = ", ".join(f"{type_}" for type_ in valid_types)
|
208
214
|
return f"Argument '{kwarg}' must be one of the following types: {valid_types}, not {type(not_type)}."
|
@@ -334,7 +340,7 @@ def event_dict(
|
|
334
340
|
selection_boxes={} if boxes is None else boxes,
|
335
341
|
selected=() if selected is None else selected,
|
336
342
|
being_selected=() if being_selected is None else being_selected,
|
337
|
-
data=
|
343
|
+
data={} if data is None else data,
|
338
344
|
key="" if key is None else key,
|
339
345
|
value=None if value is None else value,
|
340
346
|
loc=() if loc is None else loc,
|
@@ -414,6 +420,27 @@ def push_n(num: int, sorted_seq: Sequence[int]) -> int:
|
|
414
420
|
return num + lo
|
415
421
|
|
416
422
|
|
423
|
+
def get_menu_kwargs(ops: DotDict[str, Any]) -> DotDict[str, Any]:
|
424
|
+
return DotDict(
|
425
|
+
{
|
426
|
+
"font": ops.table_font,
|
427
|
+
"foreground": ops.popup_menu_fg,
|
428
|
+
"background": ops.popup_menu_bg,
|
429
|
+
"activebackground": ops.popup_menu_highlight_bg,
|
430
|
+
"activeforeground": ops.popup_menu_highlight_fg,
|
431
|
+
}
|
432
|
+
)
|
433
|
+
|
434
|
+
|
435
|
+
def get_bg_fg(ops: DotDict[str, Any]) -> dict[str, str]:
|
436
|
+
return {
|
437
|
+
"bg": ops.table_editor_bg,
|
438
|
+
"fg": ops.table_editor_fg,
|
439
|
+
"select_bg": ops.table_editor_select_bg,
|
440
|
+
"select_fg": ops.table_editor_select_fg,
|
441
|
+
}
|
442
|
+
|
443
|
+
|
417
444
|
def get_dropdown_kwargs(
|
418
445
|
values: list[Any] | None = None,
|
419
446
|
set_value: Any = None,
|
@@ -645,7 +672,7 @@ def color_tup(color: str) -> tuple[int, int, int]:
|
|
645
672
|
return int(res[1:3], 16), int(res[3:5], 16), int(res[5:], 16)
|
646
673
|
|
647
674
|
|
648
|
-
def
|
675
|
+
def cell_down_within_box(
|
649
676
|
r: int,
|
650
677
|
c: int,
|
651
678
|
r1: int,
|
@@ -909,45 +936,68 @@ def gen_coords(
|
|
909
936
|
|
910
937
|
|
911
938
|
def box_gen_coords(
|
912
|
-
|
913
|
-
|
914
|
-
|
915
|
-
|
916
|
-
|
939
|
+
from_r: int,
|
940
|
+
from_c: int,
|
941
|
+
upto_r: int,
|
942
|
+
upto_c: int,
|
943
|
+
start_r: int,
|
944
|
+
start_c: int,
|
945
|
+
reverse: bool,
|
946
|
+
all_rows_displayed: bool = True,
|
947
|
+
all_cols_displayed: bool = True,
|
948
|
+
displayed_cols: list[int] | None = None,
|
949
|
+
displayed_rows: list[int] | None = None,
|
950
|
+
no_wrap: bool = False,
|
917
951
|
) -> Generator[tuple[int, int]]:
|
918
|
-
if
|
919
|
-
|
920
|
-
|
921
|
-
|
922
|
-
|
923
|
-
|
924
|
-
|
925
|
-
|
926
|
-
|
927
|
-
|
928
|
-
|
929
|
-
|
930
|
-
|
931
|
-
|
932
|
-
|
933
|
-
|
934
|
-
|
935
|
-
|
952
|
+
# Initialize empty lists if None
|
953
|
+
if displayed_rows is None:
|
954
|
+
displayed_rows = []
|
955
|
+
if displayed_cols is None:
|
956
|
+
displayed_cols = []
|
957
|
+
|
958
|
+
# Adjust row indices based on displayed_rows
|
959
|
+
if not all_rows_displayed:
|
960
|
+
from_r = displayed_rows[from_r]
|
961
|
+
upto_r = displayed_rows[upto_r - 1] + 1
|
962
|
+
start_r = displayed_rows[start_r]
|
963
|
+
# Adjust column indices based on displayed_cols (fixing original bug)
|
964
|
+
if not all_cols_displayed:
|
965
|
+
from_c = displayed_cols[from_c]
|
966
|
+
upto_c = displayed_cols[upto_c - 1] + 1
|
967
|
+
start_c = displayed_cols[start_c]
|
968
|
+
|
969
|
+
if not reverse:
|
970
|
+
# Forward direction
|
971
|
+
# Part 1: From (start_r, start_c) to the end of the box
|
972
|
+
for c in range(start_c, upto_c):
|
973
|
+
yield (start_r, c)
|
974
|
+
for r in range(start_r + 1, upto_r):
|
975
|
+
for c in range(from_c, upto_c):
|
976
|
+
yield (r, c)
|
977
|
+
if not no_wrap:
|
978
|
+
# Part 2: Wrap around from beginning to just before (start_r, start_c)
|
979
|
+
for r in range(from_r, start_r):
|
980
|
+
for c in range(from_c, upto_c):
|
981
|
+
yield (r, c)
|
982
|
+
if start_c > from_c: # Only if there are columns before start_c
|
983
|
+
for c in range(from_c, start_c):
|
984
|
+
yield (start_r, c)
|
936
985
|
else:
|
937
|
-
#
|
938
|
-
|
939
|
-
|
940
|
-
|
941
|
-
for
|
942
|
-
for
|
943
|
-
yield (
|
944
|
-
|
945
|
-
|
946
|
-
for
|
947
|
-
|
948
|
-
|
949
|
-
|
950
|
-
|
986
|
+
# Reverse direction
|
987
|
+
# Part 1: From (start_r, start_c) backwards to the start of the box
|
988
|
+
for c in range(start_c, from_c - 1, -1):
|
989
|
+
yield (start_r, c)
|
990
|
+
for r in range(start_r - 1, from_r - 1, -1):
|
991
|
+
for c in range(upto_c - 1, from_c - 1, -1):
|
992
|
+
yield (r, c)
|
993
|
+
if not no_wrap:
|
994
|
+
# Part 2: Wrap around from end to just after (start_r, start_c)
|
995
|
+
for r in range(upto_r - 1, start_r, -1):
|
996
|
+
for c in range(upto_c - 1, from_c - 1, -1):
|
997
|
+
yield (r, c)
|
998
|
+
if start_c < upto_c - 1: # Only if there are columns after start_c
|
999
|
+
for c in range(upto_c - 1, start_c, -1):
|
1000
|
+
yield (start_r, c)
|
951
1001
|
|
952
1002
|
|
953
1003
|
def next_cell(
|