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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tksheet/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(0, weight=1)
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=0, sticky="nswe")
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="", cursor="hand2", highlightthickness=1)
153
- self.find_previous_arrow.bind("<Button-1>", find_prev_func)
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="", cursor="hand2", highlightthickness=1)
157
- self.find_next_arrow.bind("<Button-1>", find_next_func)
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="🔎", cursor="hand2", highlightthickness=1)
162
- self.in_selection.bind("<Button-1>", self.toggle_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.bind("<Button-1>", close_func)
167
- self.close.grid(row=0, column=4)
209
+ self.close.grid(row=0, column=5, sticky="nswe")
168
210
 
169
- for widget in (self.find_previous_arrow, self.find_next_arrow, self.in_selection, self.close):
170
- widget.bind("<Enter>", lambda w, widget=widget: self.enter_label(widget=widget))
171
- widget.bind("<Leave>", lambda w, widget=widget: self.leave_label(widget=widget))
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
- widget.config(
179
- highlightbackground=self.fg,
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
- if widget == self.in_selection and self.find_in_selection:
185
- return
186
- widget.config(
187
- highlightbackground=self.bg,
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
- for widget in (self.find_previous_arrow, self.find_next_arrow, self.in_selection, self.close):
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