psiutils 0.2.26__tar.gz → 0.2.28__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 (70) hide show
  1. {psiutils-0.2.26 → psiutils-0.2.28}/PKG-INFO +3 -1
  2. {psiutils-0.2.26 → psiutils-0.2.28}/pyproject.toml +9 -1
  3. psiutils-0.2.28/src/psiutils/__init__.py +1 -0
  4. psiutils-0.2.28/src/psiutils/_scrolling_canvas.py +83 -0
  5. psiutils-0.2.28/src/psiutils/_version.py +1 -0
  6. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/constants.py +9 -27
  7. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/date_picker.py +39 -25
  8. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/menus.py +2 -2
  9. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/treeview.py +112 -26
  10. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/utilities.py +0 -14
  11. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/widgets.py +5 -7
  12. psiutils-0.2.26/src/psiutils/__init__.py +0 -0
  13. psiutils-0.2.26/src/psiutils/_date_picker.py +0 -403
  14. psiutils-0.2.26/src/psiutils/_version.py +0 -1
  15. {psiutils-0.2.26 → psiutils-0.2.28}/README.md +0 -0
  16. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/_about_frame.py +0 -0
  17. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/_logger.py +0 -0
  18. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/_notify.py +0 -0
  19. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/buttons.py +0 -0
  20. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/drag_manager.py +0 -0
  21. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/errors.py +0 -0
  22. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icecream_init.py +0 -0
  23. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/backup.png +0 -0
  24. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/build.png +0 -0
  25. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/cancel.png +0 -0
  26. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/check.png +0 -0
  27. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/checkbox_checked.png +0 -0
  28. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/checkbox_unchecked.png +0 -0
  29. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/clear.png +0 -0
  30. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/code.png +0 -0
  31. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/compare.png +0 -0
  32. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/copy_clipboard.png +0 -0
  33. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/copy_docs.png +0 -0
  34. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/delete.png +0 -0
  35. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/diff.png +0 -0
  36. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/done.png +0 -0
  37. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/download.png +0 -0
  38. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/edit.png +0 -0
  39. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/gear.png +0 -0
  40. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/new.png +0 -0
  41. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/next.png +0 -0
  42. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/open.png +0 -0
  43. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/pause.png +0 -0
  44. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/preferences.png +0 -0
  45. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/previous.png +0 -0
  46. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/process.png +0 -0
  47. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/redo.png +0 -0
  48. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/refresh.png +0 -0
  49. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/rename.png +0 -0
  50. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/report.png +0 -0
  51. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/reset.png +0 -0
  52. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/restore.png +0 -0
  53. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/restore_database.png +0 -0
  54. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/restore_page.png +0 -0
  55. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/revert.png +0 -0
  56. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/save.png +0 -0
  57. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/script.png +0 -0
  58. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/search.png +0 -0
  59. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/send.png +0 -0
  60. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/start.png +0 -0
  61. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/update.png +0 -0
  62. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/upgrade.png +0 -0
  63. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/upload.png +0 -0
  64. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/icons/windows.png +0 -0
  65. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/images/icon-error.png +0 -0
  66. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/images/icon-info.png +0 -0
  67. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/images/icon-query.png +0 -0
  68. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/known_paths.py +0 -0
  69. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/messagebox.py +0 -0
  70. {psiutils-0.2.26 → psiutils-0.2.28}/src/psiutils/text.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: psiutils
3
- Version: 0.2.26
3
+ Version: 0.2.28
4
4
  Summary: Various TKinter utilities.
5
5
  Author: Jeff
6
6
  Author-email: Jeff <<jeffwatkins2000@gmail.com>>
@@ -15,7 +15,9 @@ Requires-Dist: psiconfig>=0.0.14
15
15
  Requires-Dist: pycairo>=1.28.0
16
16
  Requires-Dist: pygments>=2.19.2
17
17
  Requires-Dist: pygobject>=3.54.2
18
+ Requires-Dist: pytest>=9.0.2
18
19
  Requires-Dist: structlog>=25.4.0
20
+ Requires-Dist: tkcalendar>=1.6.1
19
21
  Requires-Dist: tkinterweb>=4.4.4
20
22
  Requires-Dist: tkinterweb-tkhtml>=1.1.1
21
23
  Requires-Dist: tomli>=2.2.1
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "psiutils"
3
- version = "0.2.26"
3
+ version = "0.2.28"
4
4
  description = "Various TKinter utilities."
5
5
  authors = [{name = "Jeff", "email" = "<jeffwatkins2000@gmail.com>"}]
6
6
  requires-python = '>= 3.10, < 3.13'
@@ -20,13 +20,21 @@ dependencies = [
20
20
  "pycairo>=1.28.0",
21
21
  "pygments>=2.19.2",
22
22
  "pygobject>=3.54.2",
23
+ "pytest>=9.0.2",
23
24
  "structlog>=25.4.0",
25
+ "tkcalendar>=1.6.1",
24
26
  "tkinterweb>=4.4.4",
25
27
  "tkinterweb-tkhtml>=1.1.1",
26
28
  "tomli>=2.2.1",
27
29
  "tomli-w>=1.2.0",
28
30
  ]
29
31
 
32
+ [tool.hatch.version]
33
+ source = "vcs"
34
+
35
+ [tool.hatch.build.hooks.vcs]
36
+ version-file = "src/psiutils/_version.py"
37
+
30
38
  [dependency-groups]
31
39
  dev = ['pytest']
32
40
 
@@ -0,0 +1 @@
1
+ from ._version import __version__
@@ -0,0 +1,83 @@
1
+
2
+ import tkinter as tk
3
+ from tkinter import ttk
4
+
5
+
6
+ class ScrollingCanvas(tk.Frame):
7
+ """
8
+ A reusable scrolling container.
9
+
10
+ This widget embeds a Frame (`self.content`) inside a Canvas,
11
+ with a vertical scrollbar that activates when the content
12
+ exceeds the visible height.
13
+
14
+ IMPORTANT:
15
+ - The canvas controls scrolling
16
+ - Widgets must be added to `self.content`, not the canvas
17
+ """
18
+
19
+ def __init__(
20
+ self,
21
+ master,
22
+ *,
23
+ relief,
24
+ borderwidth,
25
+ **kwargs):
26
+ super().__init__(master, relief=relief, borderwidth=borderwidth)
27
+
28
+ self.grid_propagate(False)
29
+
30
+ self.rowconfigure(0, weight=1)
31
+ self.columnconfigure(0, weight=1)
32
+
33
+ self.canvas = tk.Canvas(
34
+ self,
35
+ borderwidth=0,
36
+ highlightthickness=0,
37
+ )
38
+ self.canvas.grid(row=0, column=0, sticky="nsew")
39
+
40
+ self.scrollbar = ttk.Scrollbar(
41
+ self,
42
+ orient="vertical",
43
+ command=self.canvas.yview,
44
+ )
45
+ self.scrollbar.grid(row=0, column=1, sticky="ns")
46
+
47
+ self.canvas.configure(yscrollcommand=self.scrollbar.set)
48
+
49
+ self.content = tk.Frame(self.canvas)
50
+
51
+ self.window_id = self.canvas.create_window(
52
+ (0, 0),
53
+ window=self.content,
54
+ anchor="nw",
55
+ )
56
+
57
+ # Geometry management
58
+ self.content.bind("<Configure>", self._on_content_configure)
59
+ self.canvas.bind("<Configure>", self._on_canvas_configure)
60
+
61
+ def _on_content_configure(self, event=None):
62
+ """
63
+ Update the scrollable region to include all content.
64
+
65
+ Using after_idle ensures Tk has finished laying out widgets
66
+ before bbox("all") is computed — without this, the scrollbar
67
+ may behave incorrectly.
68
+ """
69
+ self.after_idle(
70
+ lambda: self.canvas.configure(
71
+ scrollregion=self.canvas.bbox("all")
72
+ )
73
+ )
74
+
75
+ def _on_canvas_configure(self, event):
76
+ """
77
+ Keep the content frame the same width as the canvas.
78
+
79
+ CRITICAL:
80
+ - Width is synced
81
+ - Height is NEVER synced (this would break scrolling)
82
+ """
83
+ self.canvas.itemconfigure(self.window_id, width=event.width)
@@ -0,0 +1 @@
1
+ __version__ = '0.2.28'
@@ -1,33 +1,12 @@
1
1
  """Constants for the tkinter psiutils."""
2
2
  from enum import Enum, auto
3
3
 
4
- # from .utilities import invert
5
4
  from .known_paths import get_documents_dir, get_downloads_dir
6
5
 
7
6
  DEFAULT_GEOMETRY = '500x400'
8
7
 
9
- # # TODO is this needed with Status?
10
- # DIALOG_STATUS: dict = {
11
- # 'yes': True,
12
- # 'no': False,
13
- # 'cancel': None,
14
- # 'null': 0,
15
- # 'undefined': 0,
16
- # 'exit': 1,
17
- # 'ok': 2,
18
- # 'updated': 3,
19
- # 'error': 4,
20
- # }
21
- # DIALOG_STATUS = invert(DIALOG_STATUS)
22
-
23
- # # TODO is this needed with Mode?
24
- # MODES: dict[int, str] | dict[str, int] = {
25
- # 0: 'view',
26
- # 1: 'new',
27
- # 2: 'edit',
28
- # 3: 'delete'
29
- # }
30
- # MODES = invert(MODES)
8
+ DOCUMENTS_DIR = get_documents_dir()
9
+ DOWNLOADS_DIR = get_downloads_dir()
31
10
 
32
11
  # GUI
33
12
  PAD = 5
@@ -35,6 +14,7 @@ PADR = (0, PAD)
35
14
  PADL = (PAD, 0)
36
15
  PADT = (PAD, 0)
37
16
  PADB = (0, PAD)
17
+
38
18
  LARGE_FONT = ('Arial', 16)
39
19
  BOLD_FONT = ('Arial', 12, 'bold')
40
20
 
@@ -68,10 +48,6 @@ class Pad():
68
48
  S = (0, PAD)
69
49
 
70
50
 
71
- DOCUMENTS_DIR = get_documents_dir()
72
- DOWNLOADS_DIR = get_downloads_dir()
73
-
74
-
75
51
  class Mode(Enum):
76
52
  VIEW = auto()
77
53
  NEW = auto()
@@ -90,3 +66,9 @@ class Status(Enum):
90
66
  UPDATED = 4
91
67
  ERROR = 5
92
68
  WARNING = 6
69
+
70
+
71
+ class WidgetState(str, Enum):
72
+ NORMAL = 'normal'
73
+ READONLY = 'readonly'
74
+ DISABLED = 'disabled'
@@ -17,7 +17,7 @@ TIME_WIDTH = 3
17
17
  INCREMENT_BUTTON_SIZE = 2
18
18
  INCREMENT_BUTTON_FONT_SIZE = 8
19
19
  DATE_FORMAT = '%d/%m/%Y'
20
- PICKER_DATE_PATTERN = 'dd/mm/yyyy'
20
+ PICKER_DATE_PATTERN = 'dd/MM/yyyy'
21
21
  MAX_HOURS = 23
22
22
  MAX_MINS = 59
23
23
  TALL_COMBO_PADDING = 6
@@ -72,6 +72,11 @@ class DatePicker(tk.Frame):
72
72
  padding=0,)
73
73
  style.configure('Tall.TCombobox', padding=TALL_COMBO_PADDING)
74
74
 
75
+ # Exposed for testing and integration
76
+ self.date_picker = None
77
+ self.increment_button = None
78
+ self.decrement_button = None
79
+
75
80
  main_frame = self._picker()
76
81
  main_frame.pack()
77
82
 
@@ -86,27 +91,27 @@ class DatePicker(tk.Frame):
86
91
  frame = ttk.Frame(self)
87
92
 
88
93
  column = 0
89
- date_picker = self._date_picker(frame, self._date_input)
90
- date_picker.grid(row=0, column=column, rowspan=2, sticky=tk.NS)
94
+ self.date_picker = self._date_picker(frame, self._date_input)
95
+ self.date_picker.grid(row=0, column=column, rowspan=2, sticky=tk.NS)
91
96
 
92
97
  column += 1
93
- button = ttk.Button(
98
+ self.increment_button = ttk.Button(
94
99
  frame,
95
100
  text=txt.INCREMENT_ARROW,
96
101
  command=partial(self._date_increment, self._date_input),
97
102
  width=INCREMENT_BUTTON_SIZE,
98
103
  style='Increment.TButton',
99
104
  )
100
- button.grid(row=0, column=column, padx=PAD)
105
+ self.increment_button.grid(row=0, column=column, padx=PAD)
101
106
 
102
- button = ttk.Button(
107
+ self.decrement_button = ttk.Button(
103
108
  frame,
104
109
  text=txt.DECREMENT_ARROW,
105
110
  command=partial(self._date_increment, self._date_input, -1),
106
111
  width=INCREMENT_BUTTON_SIZE,
107
112
  style='Increment.TButton',
108
113
  )
109
- button.grid(row=1, column=column, padx=PAD)
114
+ self.decrement_button.grid(row=1, column=column, padx=PAD)
110
115
 
111
116
  column += 1
112
117
 
@@ -124,13 +129,12 @@ class DatePicker(tk.Frame):
124
129
  Returns:
125
130
  DateEntry: A configured calendar date entry widget.
126
131
  """
127
- event_date = datetime.now()
128
132
  return DateEntry(
129
133
  master,
130
134
  date_pattern=PICKER_DATE_PATTERN,
131
- year=event_date.year,
132
- month=event_date.month,
133
- day=event_date.day,
135
+ year=self.date.year,
136
+ month=self.date.month,
137
+ day=self.date.day,
134
138
  textvariable=textvariable,
135
139
  )
136
140
 
@@ -142,11 +146,11 @@ class DatePicker(tk.Frame):
142
146
  Returns:
143
147
  datetime: The selected date parsed from the input field.
144
148
  """
145
- date_elements = self._date_input.get().split('/')
149
+ day, month, year = map(int, self._date_input.get().split('/'))
146
150
  return datetime(
147
- year=int(date_elements[2]),
148
- month=int(date_elements[1]),
149
- day=int(date_elements[0])
151
+ year=int(year),
152
+ month=int(month),
153
+ day=int(day)
150
154
  )
151
155
 
152
156
  @date.setter
@@ -244,6 +248,11 @@ class TimePicker(tk.Frame):
244
248
  style = ttk.Style()
245
249
  style.configure('Increment.TButton', font=('Helvetica', 8))
246
250
 
251
+ # Exposed for testing and integration
252
+ self.time_pickers = {}
253
+ self.increment_buttons = {}
254
+ self.decrement_buttons = {}
255
+
247
256
  main_frame = self._picker()
248
257
  main_frame.grid(row=0, column=0)
249
258
 
@@ -260,14 +269,17 @@ class TimePicker(tk.Frame):
260
269
  if self.use_labels:
261
270
  row = self._label_row(frame, row)
262
271
 
263
- hour_timer = self._timer_element(frame, self._hour_input, MAX_HOURS)
272
+ hour_timer = self._timer_element(
273
+ frame, self._hour_input, 'hours', MAX_HOURS)
264
274
  hour_timer.grid(row=row, column=HOUR_COL)
265
275
 
266
- minute_timer = self._timer_element(frame, self._minute_input)
276
+ minute_timer = self._timer_element(
277
+ frame, self._minute_input, 'minutes')
267
278
  minute_timer.grid(row=row, column=MINUTE_COL)
268
279
 
269
280
  if self.use_seconds:
270
- second_timer = self._timer_element(frame, self._second_input)
281
+ second_timer = self._timer_element(
282
+ frame, self._second_input, 'seconds')
271
283
  second_timer.grid(row=row, column=SECOND_COL)
272
284
 
273
285
  return frame
@@ -280,7 +292,8 @@ class TimePicker(tk.Frame):
280
292
  aligned with their corresponding timer controls.
281
293
 
282
294
  Args:
283
- frame (tk.Frame): The parent container in which the labels are placed.
295
+ frame (tk.Frame): The parent container
296
+ in which the labels are placed.
284
297
  row (int): The grid row index to place the labels on.
285
298
 
286
299
  Returns:
@@ -301,6 +314,7 @@ class TimePicker(tk.Frame):
301
314
  self,
302
315
  master: tk.Frame,
303
316
  textvariable: tk.StringVar,
317
+ name: str,
304
318
  max_value: int = MAX_MINS,
305
319
  ) -> tk.Frame:
306
320
  """
@@ -318,34 +332,34 @@ class TimePicker(tk.Frame):
318
332
  frame = ttk.Frame(master)
319
333
  column = 0
320
334
 
321
- combobox = ttk.Combobox(
335
+ self.time_pickers[name] = ttk.Combobox(
322
336
  frame,
323
337
  textvariable=textvariable,
324
338
  values=[f'{x:02d}' for x in range(max_value+1)],
325
339
  width=TIME_WIDTH,
326
340
  style='Tall.TCombobox',
327
341
  )
328
- combobox.grid(row=0, column=column, rowspan=2, sticky=tk.W)
342
+ self.time_pickers[name].grid(row=0, column=column, rowspan=2, sticky=tk.W)
329
343
 
330
344
  column += 1
331
345
 
332
- button = ttk.Button(
346
+ self.increment_buttons[name] = ttk.Button(
333
347
  frame,
334
348
  text=txt.INCREMENT_ARROW,
335
349
  command=partial(self._time_increment, textvariable, 1, max_value),
336
350
  width=INCREMENT_BUTTON_SIZE,
337
351
  style='Increment.TButton',
338
352
  )
339
- button.grid(row=0, column=column, padx=PAD)
353
+ self.increment_buttons[name].grid(row=0, column=column, padx=PAD)
340
354
 
341
- button = ttk.Button(
355
+ self.decrement_buttons[name] = ttk.Button(
342
356
  frame,
343
357
  text=txt.DECREMENT_ARROW,
344
358
  command=partial(self._time_increment, textvariable, -1, max_value),
345
359
  width=INCREMENT_BUTTON_SIZE,
346
360
  style='Increment.TButton',
347
361
  )
348
- button.grid(row=1, column=column, padx=PAD)
362
+ self.decrement_buttons[name].grid(row=1, column=column, padx=PAD)
349
363
  return frame
350
364
 
351
365
  def _time_increment(
@@ -44,8 +44,8 @@ class MenuItem():
44
44
  def __repr__(self) -> str:
45
45
  return f'MenuItem: {self.text}'
46
46
 
47
- def enable(self) -> None:
48
- enable_menu_items(self.menu, [self], True)
47
+ def enable(self, enable: bool = True) -> None:
48
+ enable_menu_items(self.menu, [self], enable)
49
49
 
50
50
  def disable(self) -> None:
51
51
  enable_menu_items(self.menu, [self], False)
@@ -5,12 +5,119 @@ from tkinter import ttk
5
5
  import dateutil # type: ignore
6
6
  from dateutil.parser import parse # type: ignore
7
7
  from PIL import Image, ImageTk
8
+ from dataclasses import dataclass
8
9
 
9
10
  CHECK_BOX_SIZE = (20, 20)
10
11
 
11
12
 
12
- def sort_treeview(tree: ttk.Treeview, col: int, reverse: bool) -> None:
13
+ @dataclass
14
+ class ColumnDefn():
15
+ name: str
16
+ heading: str
17
+ width: int
18
+
19
+
20
+ class Treeview(ttk.Treeview):
21
+ def __init__(self, master, column_defs: dict = None, **kwargs) -> None:
22
+ super().__init__(master, **kwargs)
23
+ if not column_defs:
24
+ column_defs = {}
25
+ self.column_defs = column_defs
26
+
27
+ self._configure_columns()
28
+
29
+ def _configure_columns(self) -> None:
30
+ column_ids = [col_defn.name for col_defn in self.column_defs]
31
+ self["columns"] = column_ids[1:]
32
+
33
+ # Configure each column
34
+ for index, col_defn in enumerate(self.column_defs):
35
+ if index == 0:
36
+ self.column(
37
+ "#0",
38
+ width=col_defn.width,
39
+ minwidth=col_defn.width,
40
+ stretch=False,
41
+ anchor="center")
42
+ self._heading('#0', col_defn.heading)
43
+ else:
44
+ self.column(
45
+ col_defn.name,
46
+ width=col_defn.width,
47
+ anchor="w",
48
+ stretch=True)
49
+ self._heading(col_defn.name, col_defn.heading)
50
+
51
+ @property
52
+ def columns(self) -> dict[int, str]:
53
+ return {
54
+ col_defn.name: column-1
55
+ for column, col_defn in enumerate(self.column_defs)
56
+ }
57
+
58
+ def _heading(self, col_id: str, heading: str) -> Treeview.heading:
59
+ return self.heading(
60
+ col_id,
61
+ text=heading,
62
+ command=lambda c=col_id: self._sort_columns(c, False)
63
+ )
64
+
65
+ def populate(self, values: dict[tuples]) -> None:
66
+ self.delete(*self.get_children())
67
+ for item in values:
68
+ item = self.insert('', 'end', values=item)
69
+
70
+
71
+ def select_item(self, column: int | str, value: str) -> None:
72
+ if isinstance(column, str):
73
+ column = self.columns[column]
74
+
75
+ for iid in self.get_children():
76
+ values = self.item(iid, "values")
77
+ if values[column] == value:
78
+ self.selection_set(iid)
79
+ break
80
+
81
+ def _sort_columns(self, col: int, reverse: bool) -> None:
82
+ """Sort the Treeview by column."""
83
+ children = [
84
+ (self.set(child, col), child) for child in self.get_children('')
85
+ ]
86
+ date_children = self._get_date_children(children)
87
+ if date_children:
88
+ children = date_children
89
+ try:
90
+ children.sort(key=lambda t: float(t[0]), reverse=reverse)
91
+ except TypeError:
92
+ children.sort(key=lambda t: t[0], reverse=reverse)
93
+ except ValueError:
94
+ children.sort(reverse=reverse)
95
+
96
+ for index, (val, child) in enumerate(children):
97
+ self.move(child, '', index)
98
+
99
+ self.heading(col, command=lambda: self._sort_columns(col, not reverse))
100
+
101
+ def _get_date_children(self, children) -> list:
102
+ try:
103
+ date_children = []
104
+ for child in children:
105
+ if len(child[0]) < 8:
106
+ is_date = False
107
+ break
108
+ date = parse(child[0])
109
+ date_children.append((date, child[1]))
110
+ return date_children
111
+ except dateutil.parser._parser.ParserError:
112
+ return []
113
+
114
+
115
+
116
+ def sort_treeview(tree: Treeview, col: int, reverse: bool) -> None:
13
117
  """Sort the Treeview by column."""
118
+ print('*** psiutils "sort_treeview" called: DEPRECATED ***')
119
+ print('Use psiutils.Treeviw class instead!!!')
120
+
14
121
  children = [
15
122
  (tree.set(child, col), child) for child in tree.get_children('')
16
123
  ]
@@ -40,7 +147,7 @@ def sort_treeview(tree: ttk.Treeview, col: int, reverse: bool) -> None:
40
147
  tree.heading(col, command=lambda: sort_treeview(tree, col, not reverse))
41
148
 
42
149
 
43
- class CheckTreeView(ttk.Treeview):
150
+ class CheckTreeView(Treeview):
44
151
  def __init__(
45
152
  self,
46
153
  master,
@@ -51,11 +158,8 @@ class CheckTreeView(ttk.Treeview):
51
158
  :param column_defs: a tuple defining column (key, text, width)
52
159
  Other parameters are passed to the `TreeView`.
53
160
  """
54
- super().__init__(master, **kwargs)
55
- self.column_defs = column_defs
161
+ super().__init__(master, column_defs, **kwargs)
56
162
  self["show"] = "tree headings"
57
- self._configure_columns()
58
-
59
163
  (
60
164
  self.unchecked_image,
61
165
  self.checked_image
@@ -80,29 +184,11 @@ class CheckTreeView(ttk.Treeview):
80
184
  checked = ImageTk.PhotoImage(checked_img)
81
185
  return (unchecked, checked)
82
186
 
83
- def _configure_columns(self) -> None:
84
- column_ids = [col[0] for col in self.column_defs]
85
- self["columns"] = column_ids[1:]
86
-
87
- # Configure each column
88
- for index, (col_id, heading, width) in enumerate(self.column_defs):
89
- if index == 0:
90
- self.column(
91
- "#0",
92
- width=width,
93
- minwidth=width,
94
- stretch=False,
95
- anchor="center")
96
- self.heading("#0", text=heading)
97
- else:
98
- self.column(col_id, width=width, anchor="w", stretch=True)
99
- self.heading(col_id, text=heading)
100
-
101
- def populate(self, items: list[tuple], checked: bool = False) -> None:
187
+ def populate(self, values: list[tuple], checked: bool = False) -> None:
102
188
  self.delete(*self.get_children())
103
189
  item_checked = (self.checked_image
104
190
  if checked else self.unchecked_image)
105
- for item in items:
191
+ for item in values:
106
192
  iid = self.insert(
107
193
  parent='',
108
194
  index='end',
@@ -38,11 +38,6 @@ def resource_path(base: Path, relative_path: Path):
38
38
  return Path(base_path, relative_path)
39
39
 
40
40
 
41
- class Enum():
42
- def __init__(self, values: dict) -> None:
43
- self.values = invert(values)
44
-
45
-
46
41
  def confirm_delete(parent: Any) -> str:
47
42
  question = txt.DELETE_THESE_ITEMS
48
43
  return tk.messagebox.askquestion(
@@ -67,15 +62,6 @@ def create_directories(path: str | Path) -> bool:
67
62
  return True
68
63
 
69
64
 
70
- def invert(enum: dict) -> dict:
71
- """Add the inverse items to a dictionary."""
72
- output = {}
73
- for key, item in enum.items():
74
- output[key] = item
75
- output[item] = key
76
- return output
77
-
78
-
79
65
  def enable_frame(parent: tk.Frame, enable: bool = True) -> None:
80
66
  state = tk.NORMAL if enable else tk.DISABLED
81
67
  for child in parent.winfo_children():
@@ -4,17 +4,15 @@ from tkinter import ttk
4
4
  import contextlib
5
5
 
6
6
 
7
- from .constants import PAD, COLOURS
8
- from ._about_frame import AboutFrame
7
+ from psiutils.constants import PAD, COLOURS
8
+ from psiutils._about_frame import AboutFrame as AboutFrameMaster
9
+ from psiutils._scrolling_canvas import ScrollingCanvas as ScrollingCanvasMaster
9
10
 
10
11
  HAND = 'hand2'
11
12
  DIM_TEXT = '#555'
12
13
 
13
-
14
- class About(AboutFrame):
15
- def __init__(self, parent, app_name, about_text, parent_file, data_dir):
16
- super().__init__(parent, app_name, about_text, parent_file, data_dir)
17
- pass
14
+ About = AboutFrameMaster
15
+ ScrollingCanvas = ScrollingCanvasMaster
18
16
 
19
17
 
20
18
  class PsiText(tk.Text):
File without changes
@@ -1,403 +0,0 @@
1
- import tkinter as tk
2
- from tkinter import ttk
3
- from datetime import datetime, timedelta
4
- from dataclasses import dataclass
5
- from functools import partial
6
- from tkcalendar import DateEntry
7
- from dateutil.parser import parse
8
-
9
- from text import Text
10
-
11
- txt = Text()
12
-
13
- PAD = 2
14
- TIME_WIDTH = 3
15
- INCREMENT_BUTTON_SIZE = 2
16
- INCREMENT_BUTTON_FONT_SIZE = 8
17
- DATE_FORMAT = '%d/%m/%Y'
18
- PICKER_DATE_PATTERN = 'dd/mm/yyyy'
19
- MAX_HOURS = 23
20
- MAX_MINS = 59
21
- TALL_COMBO_PADDING = 6
22
-
23
- HOUR_COL = 0
24
- MINUTE_COL = 1
25
- SECOND_COL = 2
26
-
27
-
28
- class DatePicker(tk.Frame):
29
- """
30
- A Tkinter widget for selecting a date using a calendar picker
31
- with increment and decrement controls.
32
-
33
- The widget displays a date entry field backed by a StringVar and
34
- allows the user to adjust the selected date one day at a time
35
- using arrow buttons.
36
-
37
- Example:
38
- picker = DatePicker(root)
39
- picker.pack()
40
-
41
- selected_date = picker.date
42
- """
43
- def __init__(
44
- self, master: tk.Frame,
45
- initial_date: datetime = None,
46
- date_format: str = ''):
47
- """
48
- Initialize the DatePicker widget.
49
-
50
- Args:
51
- master (tk.Frame): Parent widget.
52
- initial_date (datetime, optional): Initial date to display.
53
- Defaults to the current date.
54
- date_format (str, optional): Format string used to display
55
- the date. Defaults to DATE_FORMAT.
56
- """
57
- super().__init__(master)
58
- if not initial_date:
59
- initial_date = datetime.now()
60
- if not date_format:
61
- date_format = DATE_FORMAT
62
-
63
- self._date_input = tk.StringVar(
64
- value=initial_date.strftime(DATE_FORMAT))
65
-
66
- style = ttk.Style()
67
- style.configure(
68
- 'Increment.TButton',
69
- font=('Helvetica', INCREMENT_BUTTON_FONT_SIZE),
70
- padding=0,)
71
- style.configure('Tall.TCombobox', padding=TALL_COMBO_PADDING)
72
-
73
- main_frame = self._picker()
74
- main_frame.pack()
75
-
76
- def _picker(self) -> tk.Frame:
77
- """
78
- Create and layout the main date picker UI.
79
-
80
- Returns:
81
- tk.Frame: The populated frame containing the date entry
82
- and increment/decrement buttons.
83
- """
84
- frame = ttk.Frame(self)
85
-
86
- column = 0
87
- date_picker = self._date_picker(frame, self._date_input)
88
- date_picker.grid(row=0, column=column, rowspan=2, sticky=tk.NS)
89
-
90
- column += 1
91
- button = ttk.Button(
92
- frame,
93
- text=txt.INCREMENT_ARROW,
94
- command=partial(self._date_increment, self._date_input),
95
- width=INCREMENT_BUTTON_SIZE,
96
- style='Increment.TButton',
97
- )
98
- button.grid(row=0, column=column, padx=PAD)
99
-
100
- button = ttk.Button(
101
- frame,
102
- text=txt.DECREMENT_ARROW,
103
- command=partial(self._date_increment, self._date_input, -1),
104
- width=INCREMENT_BUTTON_SIZE,
105
- style='Increment.TButton',
106
- )
107
- button.grid(row=1, column=column, padx=PAD)
108
-
109
- column += 1
110
-
111
- return frame
112
-
113
- def _date_picker(
114
- self, master: tk.Frame, textvariable: tk.StringVar) -> DateEntry:
115
- """
116
- Create a calendar-based date entry widget.
117
-
118
- Args:
119
- master (tk.Frame): Parent container.
120
- textvariable (tk.StringVar): Variable bound to the selected date.
121
-
122
- Returns:
123
- DateEntry: A configured calendar date entry widget.
124
- """
125
- event_date = datetime.now()
126
- return DateEntry(
127
- master,
128
- date_pattern=PICKER_DATE_PATTERN,
129
- year=event_date.year,
130
- month=event_date.month,
131
- day=event_date.day,
132
- textvariable=textvariable,
133
- )
134
-
135
- @property
136
- def date(self) -> datetime:
137
- """
138
- Get the currently selected date.
139
-
140
- Returns:
141
- datetime: The selected date parsed from the input field.
142
- """
143
- return parse(self._date_input.get())
144
-
145
- @date.setter
146
- def date(self, value: datetime) -> None:
147
- self._date_input.set(value.strftime(DATE_FORMAT))
148
-
149
- def _date_increment(
150
- self,
151
- textvariable: tk.StringVar,
152
- increment: int = 1,
153
- ) -> None:
154
- """
155
- Increment or decrement the selected date by a number of days.
156
-
157
- Args:
158
- textvariable (tk.StringVar): The date value to update.
159
- increment (int): Number of days to add or subtract.
160
- Defaults to 1.
161
- """
162
- date = parse(textvariable.get(), dayfirst=True).date()
163
- new_date = date + timedelta(days=increment)
164
- textvariable.set(new_date.strftime(DATE_FORMAT))
165
-
166
-
167
- @dataclass
168
- class Time():
169
- """
170
- Simple value object representing a time selection.
171
-
172
- Attributes:
173
- hour (int): Hour value (0-24).
174
- minute (int): Minute value (0-59).
175
- second (int): Second value (0-59).
176
- """
177
- hour: int = 0
178
- minute: int = 0
179
- second: int = 0
180
-
181
- def on(self, date: datetime) -> datetime:
182
- """Return a datetime by applying this time to the given date."""
183
- return datetime(
184
- year=date.year,
185
- month=date.month,
186
- day=date.day,
187
- hour=self.hour,
188
- minute=self.minute,
189
- second=self.second,
190
- )
191
-
192
- DAY_START = Time(0, 0, 0)
193
- MIDNIGHT = Time(23, 59, 59)
194
-
195
-
196
- class TimePicker(tk.Frame):
197
- """
198
- A Tkinter widget for selecting a time using comboboxes and
199
- increment/decrement buttons.
200
-
201
- The widget supports hours and minutes by default, with optional
202
- seconds and optional column labels.
203
-
204
- Example:
205
- picker = TimePicker(root, use_seconds=True, use_labels=True)
206
- picker.pack()
207
-
208
- selected_time = picker.time
209
- print(selected_time.hour, selected_time.minute, selected_time.second)
210
- """
211
- def __init__(
212
- self,
213
- master: tk.Frame,
214
- time: Time = Time(0, 0, 0),
215
- use_seconds: bool = False,
216
- use_labels: bool = False):
217
- """
218
- Initialize the TimePicker widget.
219
-
220
- Args:
221
- master (tk.Frame): Parent widget.
222
- time (Time): The initial time setting (hour, minute, second)
223
- use_seconds (bool): Whether to include a seconds selector.
224
- use_labels (bool): Whether to show labels above each selector.
225
- """
226
- super().__init__(master)
227
- self.use_seconds = use_seconds
228
- self.use_labels = use_labels
229
-
230
- self._hour_input = tk.StringVar(value=f'{time.hour:02d}')
231
- self._minute_input = tk.StringVar(value=f'{time.minute:02d}')
232
- self._second_input = tk.StringVar(value=f'{time.second:02d}')
233
-
234
- style = ttk.Style()
235
- style.configure('Increment.TButton', font=('Helvetica', 8))
236
-
237
- main_frame = self._picker()
238
- main_frame.grid(row=0, column=0)
239
-
240
- def _picker(self) -> tk.Frame:
241
- """
242
- Create and layout the main picker UI.
243
-
244
- Returns:
245
- tk.Frame: The populated frame containing the time controls.
246
- """
247
- frame = ttk.Frame(self)
248
-
249
- row = 0
250
- if self.use_labels:
251
- row = self._label_row(frame, row)
252
-
253
- hour_timer = self._timer_element(frame, self._hour_input, MAX_HOURS)
254
- hour_timer.grid(row=row, column=HOUR_COL)
255
-
256
- minute_timer = self._timer_element(frame, self._minute_input)
257
- minute_timer.grid(row=row, column=MINUTE_COL)
258
-
259
- if self.use_seconds:
260
- second_timer = self._timer_element(frame, self._second_input)
261
- second_timer.grid(row=row, column=SECOND_COL)
262
-
263
- return frame
264
-
265
- def _label_row(self, frame: tk.Frame, row: int) -> int:
266
- """
267
- Create and place the label row for the time picker.
268
-
269
- Adds column headers for hours and minutes, and optionally seconds,
270
- aligned with their corresponding timer controls.
271
-
272
- Args:
273
- frame (tk.Frame): The parent container in which the labels are placed.
274
- row (int): The grid row index to place the labels on.
275
-
276
- Returns:
277
- int: The row index used for the labels (unchanged).
278
- """
279
- label = ttk.Label(frame, text='Hour')
280
- label.grid(row=row, column=HOUR_COL, sticky=tk.W)
281
-
282
- label = ttk.Label(frame, text='Mins')
283
- label.grid(row=row, column=MINUTE_COL, sticky=tk.W)
284
-
285
- if self.use_seconds:
286
- label = ttk.Label(frame, text='Secs')
287
- label.grid(row=row, column=SECOND_COL, sticky=tk.E)
288
- return row + 1
289
-
290
- def _timer_element(
291
- self,
292
- master: tk.Frame,
293
- textvariable: tk.StringVar,
294
- max_value: int = MAX_MINS,
295
- ) -> tk.Frame:
296
- """
297
- Create a single timer control consisting of a combobox and
298
- increment/decrement buttons.
299
-
300
- Args:
301
- master (tk.Frame): Parent container.
302
- textvariable (tk.StringVar): Variable bound to the combobox value.
303
- max_value (int): Maximum allowed value (wraps around).
304
-
305
- Returns:
306
- tk.Frame: The assembled timer element.
307
- """
308
- frame = ttk.Frame(master)
309
- column = 0
310
-
311
- combobox = ttk.Combobox(
312
- frame,
313
- textvariable=textvariable,
314
- values=[f'{x:02d}' for x in range(max_value+1)],
315
- width=TIME_WIDTH,
316
- style='Tall.TCombobox',
317
- )
318
- combobox.grid(row=0, column=column, rowspan=2, sticky=tk.W)
319
-
320
- column += 1
321
-
322
- button = ttk.Button(
323
- frame,
324
- text=txt.INCREMENT_ARROW,
325
- command=partial(self._time_increment, textvariable, 1, max_value),
326
- width=INCREMENT_BUTTON_SIZE,
327
- style='Increment.TButton',
328
- )
329
- button.grid(row=0, column=column, padx=PAD)
330
-
331
- button = ttk.Button(
332
- frame,
333
- text=txt.DECREMENT_ARROW,
334
- command=partial(self._time_increment, textvariable, -1, max_value),
335
- width=INCREMENT_BUTTON_SIZE,
336
- style='Increment.TButton',
337
- )
338
- button.grid(row=1, column=column, padx=PAD)
339
- return frame
340
-
341
- def _time_increment(
342
- self,
343
- textvariable: tk.StringVar,
344
- increment,
345
- max_value) -> None:
346
- """
347
- Increment or decrement a time value with wrap-around behavior.
348
-
349
- Args:
350
- textvariable (tk.StringVar): The value to update.
351
- increment (int): Amount to add (usually ±1).
352
- max_value (int): Maximum allowed value.
353
- """
354
- value = int(textvariable.get()) + increment
355
- if value < 0:
356
- value = max_value
357
- if value > max_value:
358
- value = 0
359
- textvariable.set(f'{value:02d}')
360
-
361
- @property
362
- def time(self) -> Time:
363
- """
364
- Get the currently selected time as a Time object.
365
-
366
- Returns:
367
- Time: The selected hour, minute, and second values.
368
- """
369
- return Time(self.hour, self.minute, self.second)
370
-
371
- @time.setter
372
- def time(self, value: Time) -> None:
373
- self._hour_input.set(f'{value.hour:02d}')
374
- self._minute_input.set(f'{value.minute:02d}')
375
- self._second_input.set(f'{value.second:02d}')
376
-
377
- @property
378
- def hour(self) -> int:
379
- """Return the selected hour value."""
380
- return int(self._hour_input.get())
381
-
382
- @property
383
- def minute(self) -> int:
384
- """Return the selected minute value."""
385
- return int(self._minute_input.get())
386
-
387
-
388
- @property
389
- def second(self) -> int:
390
- """Return the selected second value."""
391
- return int(self._second_input.get())
392
-
393
-
394
- def on(self, date: datetime) -> datetime:
395
- """Return a datetime by applying this time to the given date."""
396
- return datetime(
397
- year=date.year,
398
- month=date.month,
399
- day=date.day,
400
- hour=self.hour,
401
- minute=self.minute,
402
- second=self.second,
403
- )
@@ -1 +0,0 @@
1
- __version__ = '0.2.26'
File without changes