psiutils 0.2.26__tar.gz → 0.2.27__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.
- {psiutils-0.2.26 → psiutils-0.2.27}/PKG-INFO +3 -1
- {psiutils-0.2.26 → psiutils-0.2.27}/pyproject.toml +9 -1
- psiutils-0.2.27/src/psiutils/__init__.py +1 -0
- psiutils-0.2.27/src/psiutils/_scrolling_canvas.py +83 -0
- psiutils-0.2.27/src/psiutils/_version.py +1 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/constants.py +9 -27
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/date_picker.py +39 -25
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/menus.py +2 -2
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/treeview.py +109 -22
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/utilities.py +0 -14
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/widgets.py +5 -7
- psiutils-0.2.26/src/psiutils/__init__.py +0 -0
- psiutils-0.2.26/src/psiutils/_date_picker.py +0 -403
- psiutils-0.2.26/src/psiutils/_version.py +0 -1
- {psiutils-0.2.26 → psiutils-0.2.27}/README.md +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/_about_frame.py +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/_logger.py +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/_notify.py +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/buttons.py +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/drag_manager.py +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/errors.py +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icecream_init.py +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/backup.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/build.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/cancel.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/check.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/checkbox_checked.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/checkbox_unchecked.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/clear.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/code.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/compare.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/copy_clipboard.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/copy_docs.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/delete.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/diff.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/done.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/download.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/edit.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/gear.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/new.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/next.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/open.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/pause.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/preferences.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/previous.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/process.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/redo.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/refresh.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/rename.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/report.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/reset.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/restore.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/restore_database.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/restore_page.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/revert.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/save.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/script.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/search.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/send.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/start.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/update.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/upgrade.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/upload.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/icons/windows.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/images/icon-error.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/images/icon-info.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/images/icon-query.png +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/known_paths.py +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/messagebox.py +0 -0
- {psiutils-0.2.26 → psiutils-0.2.27}/src/psiutils/text.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: psiutils
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.27
|
|
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.
|
|
3
|
+
version = "0.2.27"
|
|
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.27'
|
|
@@ -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
|
-
|
|
10
|
-
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
105
|
+
self.increment_button.grid(row=0, column=column, padx=PAD)
|
|
101
106
|
|
|
102
|
-
|
|
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
|
-
|
|
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=
|
|
132
|
-
month=
|
|
133
|
-
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
|
-
|
|
149
|
+
day, month, year = map(int, self._date_input.get().split('/'))
|
|
146
150
|
return datetime(
|
|
147
|
-
year=int(
|
|
148
|
-
month=int(
|
|
149
|
-
day=int(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
342
|
+
self.time_pickers[name].grid(row=0, column=column, rowspan=2, sticky=tk.W)
|
|
329
343
|
|
|
330
344
|
column += 1
|
|
331
345
|
|
|
332
|
-
|
|
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
|
-
|
|
353
|
+
self.increment_buttons[name].grid(row=0, column=column, padx=PAD)
|
|
340
354
|
|
|
341
|
-
|
|
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
|
-
|
|
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],
|
|
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
|
-
|
|
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(
|
|
150
|
+
class CheckTreeView(Treeview):
|
|
44
151
|
def __init__(
|
|
45
152
|
self,
|
|
46
153
|
master,
|
|
@@ -54,8 +161,6 @@ class CheckTreeView(ttk.Treeview):
|
|
|
54
161
|
super().__init__(master, **kwargs)
|
|
55
162
|
self.column_defs = column_defs
|
|
56
163
|
self["show"] = "tree headings"
|
|
57
|
-
self._configure_columns()
|
|
58
|
-
|
|
59
164
|
(
|
|
60
165
|
self.unchecked_image,
|
|
61
166
|
self.checked_image
|
|
@@ -80,24 +185,6 @@ class CheckTreeView(ttk.Treeview):
|
|
|
80
185
|
checked = ImageTk.PhotoImage(checked_img)
|
|
81
186
|
return (unchecked, checked)
|
|
82
187
|
|
|
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
188
|
def populate(self, items: list[tuple], checked: bool = False) -> None:
|
|
102
189
|
self.delete(*self.get_children())
|
|
103
190
|
item_checked = (self.checked_image
|
|
@@ -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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|