scustomtkinter 0.2.1__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.
- scustomtkinter/__init__.py +81 -0
- scustomtkinter/assets/images/error.png +0 -0
- scustomtkinter/assets/images/info.png +0 -0
- scustomtkinter/assets/images/warning.png +0 -0
- scustomtkinter/assets/sCTkThemes.json +1963 -0
- scustomtkinter/license_morecustomtkinterwidgets.txt +22 -0
- scustomtkinter/sctk_button_primary.py +366 -0
- scustomtkinter/sctk_button_secondary.py +354 -0
- scustomtkinter/sctk_button_tertiary.py +375 -0
- scustomtkinter/sctk_checkbox.py +270 -0
- scustomtkinter/sctk_combobox.py +155 -0
- scustomtkinter/sctk_core.py +15 -0
- scustomtkinter/sctk_dial.py +1472 -0
- scustomtkinter/sctk_dialog.py +701 -0
- scustomtkinter/sctk_dialog_toplevel.py +268 -0
- scustomtkinter/sctk_entry_primary.py +400 -0
- scustomtkinter/sctk_entry_secondary.py +342 -0
- scustomtkinter/sctk_file_explorer.py +879 -0
- scustomtkinter/sctk_frame.py +210 -0
- scustomtkinter/sctk_frame_labeled_primary.py +667 -0
- scustomtkinter/sctk_frame_labeled_secondary.py +598 -0
- scustomtkinter/sctk_frequency_display.py +636 -0
- scustomtkinter/sctk_label_primary.py +283 -0
- scustomtkinter/sctk_label_secondary.py +275 -0
- scustomtkinter/sctk_label_tertiary.py +278 -0
- scustomtkinter/sctk_messagebox.py +275 -0
- scustomtkinter/sctk_notebook.py +1103 -0
- scustomtkinter/sctk_optionmenu_border_mixin.py +203 -0
- scustomtkinter/sctk_optionmenu_primary.py +378 -0
- scustomtkinter/sctk_optionmenu_secondary.py +346 -0
- scustomtkinter/sctk_path_chooser.py +516 -0
- scustomtkinter/sctk_progress_bar.py +274 -0
- scustomtkinter/sctk_radiobutton.py +360 -0
- scustomtkinter/sctk_scroll_mixin.py +546 -0
- scustomtkinter/sctk_scrollable_frame.py +795 -0
- scustomtkinter/sctk_scrollbar.py +313 -0
- scustomtkinter/sctk_segmentedbutton.py +424 -0
- scustomtkinter/sctk_selector.py +581 -0
- scustomtkinter/sctk_separator.py +308 -0
- scustomtkinter/sctk_slider.py +264 -0
- scustomtkinter/sctk_smeter.py +405 -0
- scustomtkinter/sctk_smeter_bar.py +557 -0
- scustomtkinter/sctk_spinbox.py +664 -0
- scustomtkinter/sctk_switch.py +463 -0
- scustomtkinter/sctk_tableview.py +869 -0
- scustomtkinter/sctk_tabview.py +409 -0
- scustomtkinter/sctk_textbox_primary.py +291 -0
- scustomtkinter/sctk_textbox_secondary.py +225 -0
- scustomtkinter/sctk_toplevel.py +153 -0
- scustomtkinter/themeable_widget.py +938 -0
- scustomtkinter-0.2.1.dist-info/METADATA +336 -0
- scustomtkinter-0.2.1.dist-info/RECORD +101 -0
- scustomtkinter-0.2.1.dist-info/WHEEL +5 -0
- scustomtkinter-0.2.1.dist-info/entry_points.txt +2 -0
- scustomtkinter-0.2.1.dist-info/licenses/LICENSE +21 -0
- scustomtkinter-0.2.1.dist-info/licenses/scustomtkinter/license_morecustomtkinterwidgets.txt +22 -0
- scustomtkinter-0.2.1.dist-info/top_level.txt +2 -0
- scustomtkinter_pygubu/__init__.py +1 -0
- scustomtkinter_pygubu/designer/__init__.py +0 -0
- scustomtkinter_pygubu/designer/plugin.py +1854 -0
- scustomtkinter_pygubu/designer/properties.py +247 -0
- scustomtkinter_pygubu/sCTkButtonPrimarybo.py +46 -0
- scustomtkinter_pygubu/sCTkButtonSecondarybo.py +45 -0
- scustomtkinter_pygubu/sCTkButtonTertiarybo.py +44 -0
- scustomtkinter_pygubu/sCTkCheckBoxbo.py +45 -0
- scustomtkinter_pygubu/sCTkComboBoxbo.py +45 -0
- scustomtkinter_pygubu/sCTkCorebo.py +120 -0
- scustomtkinter_pygubu/sCTkDialbo.py +371 -0
- scustomtkinter_pygubu/sCTkDialogbo.py +397 -0
- scustomtkinter_pygubu/sCTkEntryPrimarybo.py +100 -0
- scustomtkinter_pygubu/sCTkEntrySecondarybo.py +98 -0
- scustomtkinter_pygubu/sCTkFileExplorerbo.py +189 -0
- scustomtkinter_pygubu/sCTkFrameLabeledPrimarybo.py +69 -0
- scustomtkinter_pygubu/sCTkFrameLabeledSecondarybo.py +71 -0
- scustomtkinter_pygubu/sCTkFramebo.py +46 -0
- scustomtkinter_pygubu/sCTkFrequencyDisplaybo.py +175 -0
- scustomtkinter_pygubu/sCTkLabelPrimarybo.py +45 -0
- scustomtkinter_pygubu/sCTkLabelSecondarybo.py +44 -0
- scustomtkinter_pygubu/sCTkLabelTertiarybo.py +45 -0
- scustomtkinter_pygubu/sCTkNotebookbo.py +392 -0
- scustomtkinter_pygubu/sCTkOptionMenuPrimarybo.py +45 -0
- scustomtkinter_pygubu/sCTkOptionMenuSecondarybo.py +39 -0
- scustomtkinter_pygubu/sCTkPathChooserbo.py +204 -0
- scustomtkinter_pygubu/sCTkProgressBarbo.py +45 -0
- scustomtkinter_pygubu/sCTkRadioButtonbo.py +45 -0
- scustomtkinter_pygubu/sCTkSMeterBarbo.py +153 -0
- scustomtkinter_pygubu/sCTkSMeterbo.py +101 -0
- scustomtkinter_pygubu/sCTkScrollableFramebo.py +66 -0
- scustomtkinter_pygubu/sCTkScrollbarbo.py +48 -0
- scustomtkinter_pygubu/sCTkSegmentedButtonbo.py +47 -0
- scustomtkinter_pygubu/sCTkSelectorbo.py +167 -0
- scustomtkinter_pygubu/sCTkSeparatorbo.py +141 -0
- scustomtkinter_pygubu/sCTkSliderbo.py +45 -0
- scustomtkinter_pygubu/sCTkSpinboxbo.py +200 -0
- scustomtkinter_pygubu/sCTkSwitchbo.py +45 -0
- scustomtkinter_pygubu/sCTkTableviewbo.py +188 -0
- scustomtkinter_pygubu/sCTkTabviewbo.py +226 -0
- scustomtkinter_pygubu/sCTkTextboxPrimarybo.py +45 -0
- scustomtkinter_pygubu/sCTkTextboxSecondarybo.py +45 -0
- scustomtkinter_pygubu/sCTkToplevelbo.py +55 -0
- scustomtkinter_pygubu/sCTkWidgetSetForPygubuDesigner.py +141 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
"""
|
|
3
|
+
sCTkLabelTertiary
|
|
4
|
+
|
|
5
|
+
A theme-compliant, low-emphasis inline description label widget -- the
|
|
6
|
+
lowest-emphasis of the library's three label tiers (see also sCTkLabelPrimary,
|
|
7
|
+
sCTkLabelSecondary). Inherits directly from ctk.CTkLabel so CustomTkinter
|
|
8
|
+
handles native rendering; this class layers automatic light/dark theme
|
|
9
|
+
resolution and a distinct enabled/disabled visual state on top.
|
|
10
|
+
|
|
11
|
+
Base class order matters here: `class sCTkLabelTertiary(ctk.CTkLabel,
|
|
12
|
+
ThemeableWidget)` puts the native CTk class first, so every `super()` call in
|
|
13
|
+
this file's own methods resolves to ctk.CTkLabel -- and, beneath it,
|
|
14
|
+
tkinter.Misc -- never to ThemeableWidget. ThemeableWidget's own
|
|
15
|
+
configure()/cget()/_set_appearance_mode() overrides have been removed entirely
|
|
16
|
+
for this reason (see themeable_widget.py's docstring); this widget owns all of
|
|
17
|
+
its own runtime color-swapping logic.
|
|
18
|
+
|
|
19
|
+
Unlike the button/checkbox family, "disabled" here is purely visual (dimmed
|
|
20
|
+
text color) -- CTkLabel has no native click handling to block in the first
|
|
21
|
+
place.
|
|
22
|
+
|
|
23
|
+
Internal state attribute is named self._custom_current_state, matching the
|
|
24
|
+
convention used across the rest of the library -- an earlier version of this
|
|
25
|
+
file used self._current_state instead, inconsistent both within the label
|
|
26
|
+
family (sCTkLabelSecondary already used _custom_current_state) and against
|
|
27
|
+
every other themed widget. Purely an internal rename; doesn't affect the
|
|
28
|
+
public state()/get_state() API.
|
|
29
|
+
"""
|
|
30
|
+
from typing import Any, Optional
|
|
31
|
+
import customtkinter as ctk
|
|
32
|
+
from .themeable_widget import ThemeableWidget
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class sCTkLabelTertiary(ctk.CTkLabel, ThemeableWidget):
|
|
36
|
+
"""Themeable, low-emphasis inline description label.
|
|
37
|
+
|
|
38
|
+
Adds to native ctk.CTkLabel:
|
|
39
|
+
- Automatic light/dark theme resolution from sCTkThemes.json (via
|
|
40
|
+
ThemeableWidget.__init__ -- see that class's docstring for what it does,
|
|
41
|
+
and just as importantly, what it no longer does).
|
|
42
|
+
- A distinct enabled/disabled visual state -- purely a text-color dim,
|
|
43
|
+
since labels have no native interactivity to block.
|
|
44
|
+
- Pygubu Designer property introspection for `state`, `fg_color`, and
|
|
45
|
+
`text_color` via a single-argument configure() call, plus a cget()
|
|
46
|
+
override so querying "state" doesn't raise (native CTkLabel has no
|
|
47
|
+
real "state" option to cget in the first place).
|
|
48
|
+
|
|
49
|
+
Colors are passed through to configure() as raw (light, dark) tuples rather
|
|
50
|
+
than pre-resolved to a single value, so CustomTkinter's own appearance-mode
|
|
51
|
+
tracking repaints them automatically on a light/dark switch -- the same
|
|
52
|
+
approach validated on sCTkComboBox, sCTkSegmentedButton, and the button
|
|
53
|
+
family. Not separately re-confirmed for this specific widget.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, master: Optional[Any] = None, **kwargs: Any) -> None:
|
|
57
|
+
"""
|
|
58
|
+
Args:
|
|
59
|
+
master: Parent container.
|
|
60
|
+
**kwargs: `state` is pulled out explicitly below (applied after
|
|
61
|
+
construction, via self.configure(), rather than passed to the
|
|
62
|
+
native constructor). Everything else is either a native
|
|
63
|
+
CTkLabel argument (e.g. `text`, `font`) or a theme-key override
|
|
64
|
+
(see the "sCTkLabelTertiary" block in sCTkThemes.json).
|
|
65
|
+
"""
|
|
66
|
+
# 1. Capture the initial state before ThemeableWidget's own kwargs pass,
|
|
67
|
+
# so it doesn't get merged into final_kw and sent to the native
|
|
68
|
+
# constructor.
|
|
69
|
+
# NO PLACEHOLDER TEXT. CustomTkinter defaults `text` to the class
|
|
70
|
+
# name, so a CTkLabel built without text reads "CTkLabel". Nothing wants
|
|
71
|
+
# that in a real interface, and the Designer made it worse: clearing
|
|
72
|
+
# the text field removes the property, the canvas rebuilds the widget
|
|
73
|
+
# from what is left, and the placeholder appeared in place of the
|
|
74
|
+
# blank that was asked for.
|
|
75
|
+
kwargs.setdefault("text", "")
|
|
76
|
+
state_init = kwargs.pop("state", "normal")
|
|
77
|
+
|
|
78
|
+
# 2. Fire our shared theme logic. This resolves final_kw
|
|
79
|
+
# (construction-time properties) and the disabled color map. See
|
|
80
|
+
# ThemeableWidget.__init__ for what actually happens here.
|
|
81
|
+
ThemeableWidget.__init__(self, kwargs)
|
|
82
|
+
|
|
83
|
+
# 3. Deep-copy the resolved map onto this instance, so later changes
|
|
84
|
+
# here never leak back into the shared theme registry.
|
|
85
|
+
self._local_defaults = dict(self.final_kw)
|
|
86
|
+
self._custom_disabled_map = dict(self._widget_disabled_map)
|
|
87
|
+
|
|
88
|
+
# Remove any stray "state" key from final_kw -- already extracted
|
|
89
|
+
# above, but guards against it being reintroduced by a theme override.
|
|
90
|
+
self.final_kw.pop("state", None)
|
|
91
|
+
|
|
92
|
+
# 4. Initialize CustomTkinter natively with the clean final kwargs array.
|
|
93
|
+
super().__init__(master, **self.final_kw)
|
|
94
|
+
|
|
95
|
+
# 5. Apply the requested initial state now that the native widget exists.
|
|
96
|
+
self._custom_current_state = "normal"
|
|
97
|
+
self.configure(state=state_init)
|
|
98
|
+
|
|
99
|
+
# 6. Register lifecycle handshake hook, notifying Pygubu-style consumers
|
|
100
|
+
# that construction is complete.
|
|
101
|
+
self._finalize_themeable_lifecycle()
|
|
102
|
+
|
|
103
|
+
def _set_appearance_mode(self, mode_string: str) -> None:
|
|
104
|
+
"""
|
|
105
|
+
Forwards CustomTkinter's internal light/dark mode change notification to
|
|
106
|
+
the native widget.
|
|
107
|
+
|
|
108
|
+
No longer manually re-triggers _update_current_visual_state(). That
|
|
109
|
+
method now passes raw (light, dark) tuples straight through to
|
|
110
|
+
configure() instead of pre-resolving to a single color, so CTk's own
|
|
111
|
+
appearance-mode tracking should repaint correctly on its own -- the
|
|
112
|
+
same approach validated on sCTkComboBox, sCTkSegmentedButton, and the
|
|
113
|
+
button family.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
mode_string: The new appearance mode ("Light" or "Dark"), as passed
|
|
117
|
+
by CustomTkinter's internal appearance-mode change machinery.
|
|
118
|
+
"""
|
|
119
|
+
if hasattr(super(), "_set_appearance_mode"):
|
|
120
|
+
try:
|
|
121
|
+
super()._set_appearance_mode(mode_string)
|
|
122
|
+
except Exception:
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
def configure(self, *args: Any, **kwargs: Any) -> Any:
|
|
126
|
+
"""
|
|
127
|
+
Standard widget configuration, with Pygubu/positional-argument handling.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
*args: At most one positional argument is meaningful:
|
|
131
|
+
- a dict: merged into kwargs and processed normally below.
|
|
132
|
+
- the literal string "state": returns a Tkinter-style
|
|
133
|
+
(name, name, name, default, current) tuple.
|
|
134
|
+
- "fg_color" or "text_color": returns the same style of tuple,
|
|
135
|
+
with `current` reflecting the disabled or normal value as
|
|
136
|
+
appropriate. Note the returned value is str(value), where
|
|
137
|
+
value may itself be a (light, dark) tuple rather than a
|
|
138
|
+
single resolved color -- a known limitation shared with the
|
|
139
|
+
wider Pygubu-query investigation set aside elsewhere in this
|
|
140
|
+
project, not fixed here.
|
|
141
|
+
- anything else: forwarded directly to the native widget's
|
|
142
|
+
configure(), which does not support single-argument property
|
|
143
|
+
queries for arbitrary properties (same limitation).
|
|
144
|
+
**kwargs: Standard CTkLabel configuration options, plus: passing
|
|
145
|
+
`state=...` routes through self.state() rather than being
|
|
146
|
+
forwarded as-is.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
The query tuple described above for the single-argument case, or
|
|
150
|
+
whatever super().configure() returns for the keyword-argument case
|
|
151
|
+
(typically None).
|
|
152
|
+
"""
|
|
153
|
+
# args is always a tuple -- args[0] is the actual value passed, whether
|
|
154
|
+
# that's a string or a dict. An earlier version of this method compared
|
|
155
|
+
# the wrapped tuple directly (`pname = args`), so the query branches
|
|
156
|
+
# below never matched anything. Don't reintroduce that.
|
|
157
|
+
if len(args) == 1:
|
|
158
|
+
if isinstance(args[0], dict):
|
|
159
|
+
kwargs = {**args[0], **kwargs}
|
|
160
|
+
else:
|
|
161
|
+
pname = args[0]
|
|
162
|
+
if pname == "state":
|
|
163
|
+
return ("state", "state", "state", "normal", str(self.state()))
|
|
164
|
+
|
|
165
|
+
if pname in ["fg_color", "text_color"]:
|
|
166
|
+
current_state = str(self.state()).lower()
|
|
167
|
+
val = self._custom_disabled_map.get(pname) if current_state == "disabled" else self._local_defaults.get(pname)
|
|
168
|
+
return (pname, pname, pname, self._query_value(self._theme_default(pname)), self._query_value(val))
|
|
169
|
+
|
|
170
|
+
return self._configure_query(pname)
|
|
171
|
+
|
|
172
|
+
# Runtime overrides have to reach the map a repaint reads, or
|
|
173
|
+
# _update_current_visual_state() puts the theme value straight back --
|
|
174
|
+
# see ThemeableWidget._record_theme_overrides().
|
|
175
|
+
self._record_theme_overrides(kwargs)
|
|
176
|
+
|
|
177
|
+
if "state" in kwargs:
|
|
178
|
+
self.state(kwargs.pop("state"))
|
|
179
|
+
|
|
180
|
+
# Theme values cleared to "" are dropped; `text=""` is kept, so a
|
|
181
|
+
# label can actually be blanked. See ThemeableWidget._CONTENT_KEYS.
|
|
182
|
+
self._drop_cleared(kwargs)
|
|
183
|
+
|
|
184
|
+
if kwargs:
|
|
185
|
+
return super().configure(**kwargs)
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
# Tkinter/CTk convention binds .config to .configure as a SEPARATE class
|
|
189
|
+
# attribute -- it does not automatically track whichever configure() a
|
|
190
|
+
# subclass defines. Without this line, calling .config(...) on an instance
|
|
191
|
+
# would silently skip this entire override and land on the native widget's
|
|
192
|
+
# configure() directly, bypassing theming and state handling entirely.
|
|
193
|
+
config = configure
|
|
194
|
+
|
|
195
|
+
def cget(self, attribute_name: str) -> Any:
|
|
196
|
+
"""
|
|
197
|
+
Intercepts "state" queries so they route through state() instead of
|
|
198
|
+
being forwarded to the native widget, which has no real "state" option
|
|
199
|
+
to cget in the first place and would otherwise raise.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
attribute_name: The property name being queried.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
self.state() if attribute_name is "state" (case-insensitive),
|
|
206
|
+
otherwise whatever the native CTkLabel.cget() returns.
|
|
207
|
+
"""
|
|
208
|
+
if str(attribute_name).lower() == "state":
|
|
209
|
+
return self.state()
|
|
210
|
+
return super().cget(attribute_name)
|
|
211
|
+
|
|
212
|
+
def get_state(self) -> str:
|
|
213
|
+
"""Equivalent to calling state() with no argument."""
|
|
214
|
+
return self.state()
|
|
215
|
+
|
|
216
|
+
def state(self, mode: Optional[str] = None) -> str:
|
|
217
|
+
"""
|
|
218
|
+
Gets or sets the widget's enabled/disabled visual state.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
mode: If None, returns the current state without changing anything.
|
|
222
|
+
Otherwise, only the literal string "disabled" (case-insensitive)
|
|
223
|
+
is treated as disabled; anything in ("normal", "enabled",
|
|
224
|
+
"active") is treated as enabled. Any other value matches
|
|
225
|
+
neither branch and leaves the state unchanged.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
The resulting state ("normal" or "disabled", lowercase).
|
|
229
|
+
"""
|
|
230
|
+
if mode is None:
|
|
231
|
+
return str(getattr(self, "_custom_current_state", "normal")).lower()
|
|
232
|
+
|
|
233
|
+
mode = mode.lower()
|
|
234
|
+
if mode in ("normal", "enabled", "active"):
|
|
235
|
+
self._custom_current_state = "normal"
|
|
236
|
+
self._update_current_visual_state()
|
|
237
|
+
elif mode == "disabled":
|
|
238
|
+
self._custom_current_state = "disabled"
|
|
239
|
+
self._update_current_visual_state()
|
|
240
|
+
|
|
241
|
+
return self._custom_current_state
|
|
242
|
+
|
|
243
|
+
def _update_current_visual_state(self) -> None:
|
|
244
|
+
"""
|
|
245
|
+
Recomputes and applies this widget's colors from the theme file, based
|
|
246
|
+
on the current state.
|
|
247
|
+
|
|
248
|
+
Called after construction and on every state() change.
|
|
249
|
+
|
|
250
|
+
Passes raw (light, dark) tuples straight through to configure() instead
|
|
251
|
+
of resolving to a single color first, so CTk's native tracking can
|
|
252
|
+
handle appearance-mode repaints without help from _set_appearance_mode.
|
|
253
|
+
|
|
254
|
+
text_color is required to be present in whichever map is active (the
|
|
255
|
+
top-level block for normal state, disabled_map for disabled) -- if
|
|
256
|
+
it's missing, this raises immediately rather than silently falling
|
|
257
|
+
back to a default or silently omitting the property. An earlier
|
|
258
|
+
version fell back to CTk's own global ThemeManager default; per this
|
|
259
|
+
project's design, ThemeableWidget-based widgets are meant to fail hard
|
|
260
|
+
on incomplete theme data, not paper over it.
|
|
261
|
+
"""
|
|
262
|
+
is_disabled = self._custom_current_state == "disabled"
|
|
263
|
+
target_map = self._custom_disabled_map if is_disabled else self._local_defaults
|
|
264
|
+
|
|
265
|
+
if target_map.get("text_color") is None:
|
|
266
|
+
raise KeyError(
|
|
267
|
+
f"'{getattr(self, '_THEME_BLOCK_NAME', None) or self.__class__.__name__}' "
|
|
268
|
+
f"theme block is missing 'text_color' in its "
|
|
269
|
+
f"{'disabled_map' if is_disabled else 'top-level'} section of sCTkThemes.json."
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
config_payload = {}
|
|
273
|
+
for key in ("fg_color", "text_color", "font"):
|
|
274
|
+
val = target_map.get(key)
|
|
275
|
+
if val is not None:
|
|
276
|
+
config_payload[key] = val
|
|
277
|
+
|
|
278
|
+
super().configure(**config_payload)
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
"""
|
|
3
|
+
sCTkMessage
|
|
4
|
+
|
|
5
|
+
An advanced, themeable dialog window system subclassed from ctk.CTkToplevel.
|
|
6
|
+
Supports customizable single prompt text or dual choice prompts returning boolean states.
|
|
7
|
+
Derived from Message class by Fastattack, 2024.
|
|
8
|
+
https://github.com/fastattackv/MoreCustomTkinterWidget
|
|
9
|
+
"""
|
|
10
|
+
import os
|
|
11
|
+
import textwrap
|
|
12
|
+
|
|
13
|
+
import customtkinter as ctk
|
|
14
|
+
from .themeable_widget import ThemeableWidget
|
|
15
|
+
|
|
16
|
+
from PIL import Image
|
|
17
|
+
from typing import Literal, Union, Tuple, Optional
|
|
18
|
+
|
|
19
|
+
from .sctk_button_primary import sCTkButtonPrimary
|
|
20
|
+
from .sctk_button_secondary import sCTkButtonSecondary
|
|
21
|
+
from .sctk_label_primary import sCTkLabelPrimary
|
|
22
|
+
|
|
23
|
+
class sCTkMessagebox(ctk.CTkToplevel, ThemeableWidget):
|
|
24
|
+
"""Advanced themeable message dialog window supporting single or dual prompt states."""
|
|
25
|
+
|
|
26
|
+
# Required at the TOP LEVEL of the theme block.
|
|
27
|
+
# type_font styles the "[ERROR]" / "[INFO]" marker, which is deliberately
|
|
28
|
+
# distinct from the message body's `font`. It was hardcoded as
|
|
29
|
+
# ("Arial", 12, "bold") at both sites below, so the theme file had no way
|
|
30
|
+
# to reach it while every other value on those lines was themed.
|
|
31
|
+
_REQUIRED_THEME_KEYS = ("font", "text_color", "fg_color", "type_font")
|
|
32
|
+
|
|
33
|
+
# WHITELIST GUARD. Native CTkToplevel names fg_color explicitly and passes
|
|
34
|
+
# everything else through to tkinter.Toplevel, which raises TclError on any
|
|
35
|
+
# option it doesn't know. Theme keys like `font` and `text_color` are for
|
|
36
|
+
# this widget's own label, NOT for the window, so final_kw is filtered down
|
|
37
|
+
# to this set before the native constructor sees it.
|
|
38
|
+
_NATIVE_CTKTOPLEVEL_KWARGS = frozenset({"fg_color"})
|
|
39
|
+
|
|
40
|
+
# There is no disabled_map and no state(): this is a modal dialog. It grabs
|
|
41
|
+
# input on construction and destroys itself on dismissal, so there is no
|
|
42
|
+
# window of time in which a disabled appearance would mean anything.
|
|
43
|
+
|
|
44
|
+
def __init__(self,
|
|
45
|
+
title: str,
|
|
46
|
+
message: str,
|
|
47
|
+
typ: Literal["info", "warning", "error"],
|
|
48
|
+
master: any = None,
|
|
49
|
+
buttons: Literal["ok", "yes_no"] = "ok",
|
|
50
|
+
ok_text: str = "Ok",
|
|
51
|
+
yes_text: str = "Yes",
|
|
52
|
+
no_text: str = "No",
|
|
53
|
+
width: int = 400,
|
|
54
|
+
*args, **kwargs):
|
|
55
|
+
|
|
56
|
+
# 1. Fire our shared theme logic first to extract registry configurations safely
|
|
57
|
+
ThemeableWidget.__init__(self, kwargs)
|
|
58
|
+
|
|
59
|
+
# 2. 🛠️ THE MUTATION SAFEGUARD DEEP COPY:
|
|
60
|
+
self._local_defaults = dict(self.final_kw)
|
|
61
|
+
self._validate_theme_keys()
|
|
62
|
+
|
|
63
|
+
# 3. Initialize CTkToplevel natively.
|
|
64
|
+
#
|
|
65
|
+
# FIX: this previously forwarded the RAW kwargs dict rather than the
|
|
66
|
+
# resolved final_kw, so the theme block never reached the native
|
|
67
|
+
# constructor at all -- the dialog window itself was unthemed, and
|
|
68
|
+
# ThemeableWidget's resolution work was discarded for everything
|
|
69
|
+
# except the two keys read back manually below. Passing final_kw
|
|
70
|
+
# through the whitelist above fixes that and simultaneously protects
|
|
71
|
+
# the native constructor: a caller passing font= to this widget would
|
|
72
|
+
# previously have had it forwarded to tkinter.Toplevel, which raises
|
|
73
|
+
# TclError on an option it doesn't recognize.
|
|
74
|
+
native_kwargs = {k: v for k, v in self.final_kw.items()
|
|
75
|
+
if k in self._NATIVE_CTKTOPLEVEL_KWARGS}
|
|
76
|
+
super().__init__(master=master, *args, **native_kwargs)
|
|
77
|
+
self.withdraw()
|
|
78
|
+
|
|
79
|
+
self._result = None
|
|
80
|
+
self.lift()
|
|
81
|
+
self.attributes("-topmost", True)
|
|
82
|
+
self.resizable(False, False)
|
|
83
|
+
self.grab_set()
|
|
84
|
+
self.title(title)
|
|
85
|
+
|
|
86
|
+
# No fallbacks: validated at construction, so both lookups resolve.
|
|
87
|
+
font_config = self._local_defaults.get("font")
|
|
88
|
+
text_color_config = self._local_defaults.get("text_color")
|
|
89
|
+
|
|
90
|
+
# 4. Custom Local Icon Asset Extraction Loops
|
|
91
|
+
# 4. 🔑 DYNAMIC ASSET PATH ANCHOR: Safely navigates down into assets/images/
|
|
92
|
+
current_script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
93
|
+
images_dir = os.path.normpath(os.path.join(current_script_dir, "assets", "images"))
|
|
94
|
+
|
|
95
|
+
light_icon_path = os.path.join(images_dir, f"{typ}.png")
|
|
96
|
+
dark_icon_path = os.path.join(images_dir, f"{typ}_dark.png")
|
|
97
|
+
|
|
98
|
+
# Fallback to standard light version if an explicit dark-theme variant is missing
|
|
99
|
+
if not os.path.exists(dark_icon_path):
|
|
100
|
+
dark_icon_path = light_icon_path
|
|
101
|
+
|
|
102
|
+
if os.path.exists(light_icon_path):
|
|
103
|
+
try:
|
|
104
|
+
pil_light = Image.open(light_icon_path)
|
|
105
|
+
pil_dark = Image.open(dark_icon_path)
|
|
106
|
+
ctk_image = ctk.CTkImage(light_image=pil_light, dark_image=pil_dark, size=(85, 85))
|
|
107
|
+
self.image_label = sCTkLabelPrimary(self, text="", image=ctk_image, width=85, height=85)
|
|
108
|
+
self.image_label.grid(row=0, column=0, padx=(15, 5), pady=20, sticky="n")
|
|
109
|
+
except Exception as image_err:
|
|
110
|
+
# Graceful extraction safety recovery trace
|
|
111
|
+
print(f"⚠️ sCustomTkinter Warning -> Image format initialization error: {image_err}")
|
|
112
|
+
self.image_label = sCTkLabelPrimary(
|
|
113
|
+
self, text=f"[{typ.upper()}]",
|
|
114
|
+
font=self._local_defaults.get("type_font"))
|
|
115
|
+
self.image_label.grid(row=0, column=0, padx=(15, 5), pady=20, sticky="n")
|
|
116
|
+
else:
|
|
117
|
+
# Safe text-based interface fallback if filesystem layout tracks vanish
|
|
118
|
+
print(f"⚠️ sCustomTkinter Warning -> Missing visual alert asset from target path: {light_icon_path}")
|
|
119
|
+
self.image_label = sCTkLabelPrimary(
|
|
120
|
+
self, text=f"[{typ.upper()}]",
|
|
121
|
+
font=self._local_defaults.get("type_font"))
|
|
122
|
+
self.image_label.grid(row=0, column=0, padx=(15, 5), pady=20, sticky="n")
|
|
123
|
+
|
|
124
|
+
max_text_width_pixels = width - 180
|
|
125
|
+
char_limit_per_line = max(20, int(max_text_width_pixels / 11.5))
|
|
126
|
+
wrapped_message = "\n".join(textwrap.wrap(message, width=char_limit_per_line))
|
|
127
|
+
|
|
128
|
+
self.label = sCTkLabelPrimary(self, text=wrapped_message, font=font_config, text_color=text_color_config, justify="left", anchor="w", wraplength=max_text_width_pixels)
|
|
129
|
+
self.label.grid(row=0, column=1, padx=(10, 35), pady=20, sticky="w")
|
|
130
|
+
|
|
131
|
+
if buttons == "yes_no":
|
|
132
|
+
self.yes_button = sCTkButtonPrimary(self, text=yes_text, command=self.on_yes)
|
|
133
|
+
self.yes_button.grid(row=1, column=0, padx=(15, 5), pady=15, sticky="ew")
|
|
134
|
+
self.no_button = sCTkButtonSecondary(self, text=no_text, command=self.on_no)
|
|
135
|
+
self.no_button.grid(row=1, column=1, padx=(5, 15), pady=15, sticky="ew")
|
|
136
|
+
self.bind("<Return>", self.on_yes)
|
|
137
|
+
self.grid_columnconfigure(0, weight=1, uniform="dialog_buttons")
|
|
138
|
+
self.grid_columnconfigure(1, weight=1, uniform="dialog_buttons")
|
|
139
|
+
else:
|
|
140
|
+
self.ok_button = sCTkButtonPrimary(self, text=ok_text, command=self.on_ok)
|
|
141
|
+
self.ok_button.grid(row=1, column=0, columnspan=2, padx=15, pady=15)
|
|
142
|
+
self.bind("<Return>", self.on_ok)
|
|
143
|
+
self.grid_columnconfigure(0, weight=1)
|
|
144
|
+
self.grid_columnconfigure(1, weight=1)
|
|
145
|
+
|
|
146
|
+
self._center_window(target_width=width)
|
|
147
|
+
self.deiconify()
|
|
148
|
+
|
|
149
|
+
# 🔑 REGISTER LIFECYCLE HANDSHAKE HOOK: Unlocks full Pygubu parent support out of the box
|
|
150
|
+
self._finalize_themeable_lifecycle()
|
|
151
|
+
def _center_window(self, target_width: int):
|
|
152
|
+
self.update_idletasks()
|
|
153
|
+
width = target_width
|
|
154
|
+
height = self.winfo_reqheight()
|
|
155
|
+
if self.master and hasattr(self.master, "winfo_x"):
|
|
156
|
+
x = self.master.winfo_x() + (self.master.winfo_width() // 2) - (width // 2)
|
|
157
|
+
y = self.master.winfo_y() + (self.master.winfo_height() // 2) - (height // 2)
|
|
158
|
+
else:
|
|
159
|
+
x = (self.winfo_screenwidth() // 2) - (width // 2)
|
|
160
|
+
y = (self.winfo_screenheight() // 2) - (height // 2)
|
|
161
|
+
self.geometry(f"{width}x{height}+{max(0, x)}+{max(0, y)}")
|
|
162
|
+
|
|
163
|
+
def _validate_theme_keys(self) -> None:
|
|
164
|
+
"""
|
|
165
|
+
Hard-fails at construction on an incomplete theme block, naming the
|
|
166
|
+
missing key.
|
|
167
|
+
|
|
168
|
+
Raises:
|
|
169
|
+
KeyError: naming the first missing key found.
|
|
170
|
+
"""
|
|
171
|
+
# Resolved the way ThemeableWidget resolves it, so the message names
|
|
172
|
+
# the block actually read rather than the subclass's own name.
|
|
173
|
+
name = getattr(self, "_THEME_BLOCK_NAME", None) or self.__class__.__name__
|
|
174
|
+
for key in self._REQUIRED_THEME_KEYS:
|
|
175
|
+
if self._local_defaults.get(key) is None:
|
|
176
|
+
raise KeyError(
|
|
177
|
+
f"'{name}' theme block is missing '{key}' at the top level "
|
|
178
|
+
f"of sCTkThemes.json."
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def configure(self, *args, **kwargs):
|
|
182
|
+
"""
|
|
183
|
+
Standard configuration with positional-argument handling.
|
|
184
|
+
|
|
185
|
+
Three separate bugs were fixed here:
|
|
186
|
+
|
|
187
|
+
1. `super().configure(args)` passed the whole TUPLE as one positional
|
|
188
|
+
argument rather than unwrapping it, so every single-argument query
|
|
189
|
+
forwarded a malformed value.
|
|
190
|
+
2. `if args and isinstance(args, dict)` -- args is ALWAYS a tuple, so
|
|
191
|
+
the dict-merge branch was dead code. Same tautology fixed across
|
|
192
|
+
the batch-one widgets.
|
|
193
|
+
3. No `config = configure` alias existed, so `.config(...)` bypassed
|
|
194
|
+
this override entirely and landed on the native widget.
|
|
195
|
+
"""
|
|
196
|
+
if len(args) == 1:
|
|
197
|
+
if isinstance(args[0], dict):
|
|
198
|
+
kwargs = {**args[0], **kwargs}
|
|
199
|
+
else:
|
|
200
|
+
# FIX: forwarding a property NAME to native configure()
|
|
201
|
+
# passes it as require_redraw -- CustomTkinter declares
|
|
202
|
+
# configure(self, require_redraw=False, **kwargs) -- so the
|
|
203
|
+
# call returns None, which pygubu then hands straight back to
|
|
204
|
+
# _set_property(). Reached whenever a field is blanked in the
|
|
205
|
+
# Designer inspector, which calls configure(name) to read a
|
|
206
|
+
# property's default and expects a Tkinter-style five-tuple.
|
|
207
|
+
#
|
|
208
|
+
# _configure_query() builds one -- see themeable_widget.py.
|
|
209
|
+
return self._configure_query(args[0])
|
|
210
|
+
|
|
211
|
+
self._drop_cleared(kwargs)
|
|
212
|
+
if kwargs: return super().configure(**kwargs)
|
|
213
|
+
return None
|
|
214
|
+
|
|
215
|
+
# Tkinter binds .config to .configure as a SEPARATE class attribute -- it
|
|
216
|
+
# does not track a subclass's override. Without this, .config(...) skips
|
|
217
|
+
# everything above.
|
|
218
|
+
config = configure
|
|
219
|
+
|
|
220
|
+
def _close_dialog(self):
|
|
221
|
+
self.grab_release()
|
|
222
|
+
self.destroy()
|
|
223
|
+
|
|
224
|
+
def on_ok(self, event=None):
|
|
225
|
+
self._result = True
|
|
226
|
+
self._close_dialog()
|
|
227
|
+
|
|
228
|
+
def on_yes(self, event=None):
|
|
229
|
+
self._result = True
|
|
230
|
+
self._close_dialog()
|
|
231
|
+
|
|
232
|
+
def on_no(self, event=None):
|
|
233
|
+
self._result = False
|
|
234
|
+
self._close_dialog()
|
|
235
|
+
|
|
236
|
+
def wait_end(self) -> Optional[bool]:
|
|
237
|
+
if self.master: self.master.wait_window(self)
|
|
238
|
+
else: self.wait_window(self)
|
|
239
|
+
return self._result
|
|
240
|
+
|
|
241
|
+
# =====================================================================
|
|
242
|
+
# ⚡ STANDALONE RE-ROUTING CHANNELS (OUTDENTED TO ZERO INDENT SPACES!)
|
|
243
|
+
# =====================================================================
|
|
244
|
+
def showinfo(title: str, message: str, ok_text: str = "Ok", width: int = 400, master: any = None) -> Optional[bool]:
|
|
245
|
+
return sCTkMessagebox(title, message, "info", master=master, buttons="ok", ok_text=ok_text, width=width).wait_end()
|
|
246
|
+
|
|
247
|
+
def showwarning(title: str, message: str, ok_text: str = "Ok", width: int = 400, master: any = None) -> Optional[bool]:
|
|
248
|
+
return sCTkMessagebox(title, message, "warning", master=master, buttons="ok", ok_text=ok_text, width=width).wait_end()
|
|
249
|
+
|
|
250
|
+
def showerror(title: str, message: str, ok_text: str = "Ok", width: int = 400, master: any = None) -> Optional[bool]:
|
|
251
|
+
return sCTkMessagebox(title, message, "error", master=master, buttons="ok", ok_text=ok_text, width=width).wait_end()
|
|
252
|
+
|
|
253
|
+
def askyesno(title: str, message: str, yes_text: str = "Yes", no_text: str = "No", width: int = 400, master: any = None) -> bool:
|
|
254
|
+
val = sCTkMessagebox(title, message, "info", master=master, buttons="yes_no", yes_text=yes_text, no_text=no_text, width=width).wait_end()
|
|
255
|
+
return True if val is True else False
|
|
256
|
+
|
|
257
|
+
def askwarningyesno(title: str, message: str, yes_text: str = "Yes", no_text: str = "No", width: int = 400, master: any = None) -> bool:
|
|
258
|
+
val = sCTkMessagebox(title, message, "warning", master=master, buttons="yes_no", yes_text=yes_text, no_text=no_text, width=width).wait_end()
|
|
259
|
+
return True if val is True else False
|
|
260
|
+
|
|
261
|
+
def askerroryesno(title: str, message: str, yes_text: str = "Yes", no_text: str = "No", width: int = 400, master: any = None) -> bool:
|
|
262
|
+
val = sCTkMessagebox(title, message, "error", master=master, buttons="yes_no", yes_text=yes_text, no_text=no_text, width=width).wait_end()
|
|
263
|
+
return True if val is True else False
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
# =====================================================================
|
|
267
|
+
# 🔑 THE DYNAMIC BACK-BIND: Injects attributes onto the Class token namespace!
|
|
268
|
+
# =====================================================================
|
|
269
|
+
# This forces sCTkMessage.showinfo(...) to work perfectly everywhere!
|
|
270
|
+
sCTkMessagebox.showinfo = showinfo
|
|
271
|
+
sCTkMessagebox.showwarning = showwarning
|
|
272
|
+
sCTkMessagebox.showerror = showerror
|
|
273
|
+
sCTkMessagebox.askyesno = askyesno
|
|
274
|
+
sCTkMessagebox.askwarningyesno = askwarningyesno
|
|
275
|
+
sCTkMessagebox.askerroryesno = askerroryesno
|