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,274 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
"""
|
|
3
|
+
sCTkProgressBar
|
|
4
|
+
|
|
5
|
+
A theme-compliant progress indicator bar widget. Inherits directly from
|
|
6
|
+
ctk.CTkProgressBar so CustomTkinter handles native rendering; this class
|
|
7
|
+
layers automatic light/dark theme resolution and a distinct "disabled" visual
|
|
8
|
+
state on top.
|
|
9
|
+
|
|
10
|
+
Base class order matters here: `class sCTkProgressBar(ctk.CTkProgressBar,
|
|
11
|
+
ThemeableWidget)` puts the native CTk class first, so every `super()` call in
|
|
12
|
+
this file's own methods resolves to ctk.CTkProgressBar -- and, beneath it,
|
|
13
|
+
tkinter.Misc -- never to ThemeableWidget. ThemeableWidget's own
|
|
14
|
+
configure()/cget()/_set_appearance_mode() overrides have been removed entirely
|
|
15
|
+
for this reason (see themeable_widget.py's docstring); this widget owns all of
|
|
16
|
+
its own runtime color-swapping logic.
|
|
17
|
+
|
|
18
|
+
Progress bars have no native interactivity to block (no click handling at
|
|
19
|
+
all), so "disabled" here is purely a visual "looks inactive" state -- it never
|
|
20
|
+
touches CTk's native state machinery, unlike interactive widgets.
|
|
21
|
+
"""
|
|
22
|
+
import os
|
|
23
|
+
from typing import Any, Optional
|
|
24
|
+
import customtkinter as ctk
|
|
25
|
+
from .themeable_widget import ThemeableWidget
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class sCTkProgressBar(ctk.CTkProgressBar, ThemeableWidget):
|
|
29
|
+
"""Themeable progress bar.
|
|
30
|
+
|
|
31
|
+
Adds to native ctk.CTkProgressBar:
|
|
32
|
+
- Automatic light/dark theme resolution from sCTkThemes.json (via
|
|
33
|
+
ThemeableWidget.__init__ -- see that class's docstring for what it does,
|
|
34
|
+
and just as importantly, what it no longer does).
|
|
35
|
+
- A purely visual "disabled" state, tracked in self._custom_current_state.
|
|
36
|
+
Since progress bars have no click behavior to block, this only swaps
|
|
37
|
+
colors -- it never calls the native configure(state=...).
|
|
38
|
+
- Pygubu Designer property introspection for `state`, `fg_color`,
|
|
39
|
+
`progress_color`, and `border_color` via a single-argument configure()
|
|
40
|
+
call, plus a cget() override so querying "state" doesn't raise (native
|
|
41
|
+
CTkProgressBar has no real "state" option to cget in the first place).
|
|
42
|
+
- A bind() override that no-ops while running inside Pygubu Designer
|
|
43
|
+
(detected via the PYGUBU_DESIGNER_RUNNING environment variable) --
|
|
44
|
+
the same pattern used by sCTkRadioButton, presumably to avoid event
|
|
45
|
+
bindings misbehaving during design-time layout.
|
|
46
|
+
|
|
47
|
+
Colors are passed through to configure() as raw (light, dark) tuples rather
|
|
48
|
+
than pre-resolved to a single value, so CustomTkinter's own appearance-mode
|
|
49
|
+
tracking repaints them automatically on a light/dark switch -- the same
|
|
50
|
+
approach validated on sCTkComboBox, sCTkSegmentedButton, and the button
|
|
51
|
+
family. Not separately re-confirmed for this specific widget.
|
|
52
|
+
|
|
53
|
+
There's no `border_color` anywhere in this widget's theme block (not even
|
|
54
|
+
at the top level), even though the repaint loop checks for one. This
|
|
55
|
+
style simply has no themed border; the lookup always resolves to nothing
|
|
56
|
+
and is skipped, the same situation as sCTkButtonPrimary's border_color.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(self, master: Optional[Any] = None, **kwargs: Any) -> None:
|
|
60
|
+
"""
|
|
61
|
+
Args:
|
|
62
|
+
master: Parent container.
|
|
63
|
+
**kwargs: `state` is pulled out explicitly below (applied after
|
|
64
|
+
construction, via self.state(), rather than passed to the
|
|
65
|
+
native constructor). Everything else is either a native
|
|
66
|
+
CTkProgressBar argument or a theme-key override (see the
|
|
67
|
+
"sCTkProgressBar" block in sCTkThemes.json).
|
|
68
|
+
"""
|
|
69
|
+
# 1. Capture the initial state before ThemeableWidget's own kwargs
|
|
70
|
+
# pass, so it doesn't get merged into final_kw and sent to the native
|
|
71
|
+
# constructor.
|
|
72
|
+
state_init = kwargs.pop("state", "normal")
|
|
73
|
+
|
|
74
|
+
# 2. Fire our shared theme logic. This resolves final_kw
|
|
75
|
+
# (construction-time properties) and the disabled color map. See
|
|
76
|
+
# ThemeableWidget.__init__ for what actually happens here.
|
|
77
|
+
ThemeableWidget.__init__(self, kwargs)
|
|
78
|
+
|
|
79
|
+
# 3. Deep-copy the resolved map onto this instance, so later changes
|
|
80
|
+
# here never leak back into the shared theme registry.
|
|
81
|
+
self._local_defaults = dict(self.final_kw)
|
|
82
|
+
self._custom_disabled_map = dict(self._widget_disabled_map)
|
|
83
|
+
|
|
84
|
+
# Remove any stray "state" key from final_kw -- already extracted
|
|
85
|
+
# above, but guards against it being reintroduced by a theme override.
|
|
86
|
+
self.final_kw.pop("state", None)
|
|
87
|
+
|
|
88
|
+
# 4. Initialize CustomTkinter natively with the clean final kwargs array.
|
|
89
|
+
super().__init__(master, **self.final_kw)
|
|
90
|
+
|
|
91
|
+
# 5. Apply the requested initial state now that the native widget exists.
|
|
92
|
+
self._custom_current_state = "normal"
|
|
93
|
+
self.state(state_init)
|
|
94
|
+
|
|
95
|
+
# 6. Register lifecycle handshake hook, notifying Pygubu-style consumers
|
|
96
|
+
# that construction is complete.
|
|
97
|
+
self._finalize_themeable_lifecycle()
|
|
98
|
+
|
|
99
|
+
def configure(self, *args: Any, **kwargs: Any) -> Any:
|
|
100
|
+
"""
|
|
101
|
+
Standard widget configuration, with Pygubu/positional-argument handling.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
*args: At most one positional argument is meaningful:
|
|
105
|
+
- a dict: merged into kwargs and processed normally below.
|
|
106
|
+
- the literal string "state": returns a Tkinter-style
|
|
107
|
+
(name, name, name, default, current) tuple.
|
|
108
|
+
- one of "fg_color"/"progress_color"/"border_color": returns
|
|
109
|
+
the same style of tuple, with `current` reflecting the
|
|
110
|
+
disabled or normal value as appropriate. Note the returned
|
|
111
|
+
value is str(value), where value may itself be a
|
|
112
|
+
(light, dark) tuple rather than a single resolved color --
|
|
113
|
+
a known limitation shared with the wider Pygubu-query
|
|
114
|
+
investigation set aside elsewhere in this project, not
|
|
115
|
+
fixed here.
|
|
116
|
+
- anything else: forwarded directly to the native widget's
|
|
117
|
+
configure(), which does not support single-argument property
|
|
118
|
+
queries for arbitrary properties (same limitation).
|
|
119
|
+
**kwargs: Standard CTkProgressBar configuration options, plus:
|
|
120
|
+
passing `state=...` routes through self.state() rather than
|
|
121
|
+
being forwarded as-is.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
The query tuple described above for the single-argument case, or
|
|
125
|
+
None for the keyword-argument case.
|
|
126
|
+
"""
|
|
127
|
+
# args is always a tuple -- args[0] is the actual value passed, whether
|
|
128
|
+
# that's a string or a dict. An earlier version of this method compared
|
|
129
|
+
# the wrapped tuple directly (`pname = args`); its fallback used *args
|
|
130
|
+
# unpacking so it didn't crash, but the query branches below never
|
|
131
|
+
# matched anything. Don't reintroduce the tuple comparison.
|
|
132
|
+
if len(args) == 1:
|
|
133
|
+
if isinstance(args[0], dict):
|
|
134
|
+
kwargs = {**args[0], **kwargs}
|
|
135
|
+
else:
|
|
136
|
+
pname = args[0]
|
|
137
|
+
if pname == "state":
|
|
138
|
+
return ("state", "state", "state", "normal", str(self.state()))
|
|
139
|
+
|
|
140
|
+
if pname in ["fg_color", "progress_color", "border_color"]:
|
|
141
|
+
current_state = str(self.state()).lower()
|
|
142
|
+
val = self._custom_disabled_map.get(pname) if current_state == "disabled" else self._local_defaults.get(pname)
|
|
143
|
+
return (pname, pname, pname, self._query_value(self._theme_default(pname)), self._query_value(val))
|
|
144
|
+
|
|
145
|
+
return self._configure_query(pname)
|
|
146
|
+
|
|
147
|
+
# Runtime overrides have to reach the map a repaint reads, or the
|
|
148
|
+
# repaint puts the theme value straight back -- see
|
|
149
|
+
# ThemeableWidget._record_theme_overrides().
|
|
150
|
+
self._record_theme_overrides(kwargs)
|
|
151
|
+
|
|
152
|
+
if "state" in kwargs:
|
|
153
|
+
self.state(kwargs.pop("state"))
|
|
154
|
+
|
|
155
|
+
if kwargs:
|
|
156
|
+
super().configure(**kwargs)
|
|
157
|
+
self._update_current_visual_state()
|
|
158
|
+
|
|
159
|
+
# Tkinter/CTk convention binds .config to .configure as a SEPARATE class
|
|
160
|
+
# attribute -- it does not automatically track whichever configure() a
|
|
161
|
+
# subclass defines. An earlier version of this file was MISSING this line
|
|
162
|
+
# entirely: calling .config(...) on an instance silently skipped this
|
|
163
|
+
# entire override and landed on the native widget's configure() directly,
|
|
164
|
+
# bypassing theming and state handling completely -- confirmed as a
|
|
165
|
+
# critical bug on sCTkSegmentedButton earlier in this project's audit; the
|
|
166
|
+
# same fix applies here.
|
|
167
|
+
config = configure
|
|
168
|
+
|
|
169
|
+
def cget(self, attribute_name: str) -> Any:
|
|
170
|
+
"""
|
|
171
|
+
Intercepts "state" queries so they route through state() instead of
|
|
172
|
+
being forwarded to the native widget, which has no real "state" option
|
|
173
|
+
to cget in the first place and would otherwise raise.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
attribute_name: The property name being queried.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
self.state() if attribute_name is "state" (case-insensitive),
|
|
180
|
+
otherwise whatever the native CTkProgressBar.cget() returns.
|
|
181
|
+
"""
|
|
182
|
+
if str(attribute_name).lower() == "state":
|
|
183
|
+
return self.state()
|
|
184
|
+
return super().cget(attribute_name)
|
|
185
|
+
|
|
186
|
+
def _set_appearance_mode(self, mode_string: str) -> None:
|
|
187
|
+
"""
|
|
188
|
+
Forwards CustomTkinter's internal light/dark mode change notification to
|
|
189
|
+
the native widget.
|
|
190
|
+
|
|
191
|
+
No longer manually re-triggers _update_current_visual_state(). That
|
|
192
|
+
method now passes raw (light, dark) tuples straight through to
|
|
193
|
+
configure() instead of pre-resolving to a single color, so CTk's own
|
|
194
|
+
appearance-mode tracking should repaint correctly on its own -- the
|
|
195
|
+
same approach validated on sCTkComboBox, sCTkSegmentedButton, and the
|
|
196
|
+
button family.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
mode_string: The new appearance mode ("Light" or "Dark"), as passed
|
|
200
|
+
by CustomTkinter's internal appearance-mode change machinery.
|
|
201
|
+
"""
|
|
202
|
+
if hasattr(super(), "_set_appearance_mode"):
|
|
203
|
+
try:
|
|
204
|
+
super()._set_appearance_mode(mode_string)
|
|
205
|
+
except Exception:
|
|
206
|
+
pass
|
|
207
|
+
|
|
208
|
+
def get_state(self) -> str:
|
|
209
|
+
"""Equivalent to calling state() with no argument."""
|
|
210
|
+
return self.state()
|
|
211
|
+
|
|
212
|
+
def state(self, mode: Optional[str] = None) -> str:
|
|
213
|
+
"""
|
|
214
|
+
Gets or sets the widget's visual "disabled" state.
|
|
215
|
+
|
|
216
|
+
Unlike most widgets in this library, any string is accepted and
|
|
217
|
+
stored verbatim (lowercased) -- there's no validation against a fixed
|
|
218
|
+
set of recognized values, since progress bars don't have a real
|
|
219
|
+
native "state" concept to validate against in the first place. Only
|
|
220
|
+
the literal value "disabled" actually changes the rendered colors;
|
|
221
|
+
anything else is treated as "not disabled" by _update_current_visual_state().
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
mode: If None, returns the current state without changing anything.
|
|
225
|
+
Otherwise, stored directly (lowercased) as the new state.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
The resulting state string (lowercase).
|
|
229
|
+
"""
|
|
230
|
+
if mode is None:
|
|
231
|
+
return str(getattr(self, "_custom_current_state", "normal")).lower()
|
|
232
|
+
|
|
233
|
+
target_state = mode.lower()
|
|
234
|
+
self._custom_current_state = target_state
|
|
235
|
+
self._update_current_visual_state()
|
|
236
|
+
return self._custom_current_state
|
|
237
|
+
|
|
238
|
+
def _update_current_visual_state(self) -> None:
|
|
239
|
+
"""
|
|
240
|
+
Recomputes and applies this widget's colors from the theme file, based
|
|
241
|
+
on the current state.
|
|
242
|
+
|
|
243
|
+
Called after construction, on every state() change, and after any
|
|
244
|
+
keyword configure() call.
|
|
245
|
+
|
|
246
|
+
Passes raw (light, dark) tuples straight through to configure() instead
|
|
247
|
+
of resolving to a single color first, so CTk's native tracking can
|
|
248
|
+
handle appearance-mode repaints without help from _set_appearance_mode.
|
|
249
|
+
Every value here traces back to sCTkThemes.json; there are no
|
|
250
|
+
hardcoded colors in this method.
|
|
251
|
+
"""
|
|
252
|
+
is_disabled = getattr(self, "_custom_current_state", "normal") == "disabled"
|
|
253
|
+
target_map = self._custom_disabled_map if is_disabled else self._local_defaults
|
|
254
|
+
|
|
255
|
+
config_payload = {}
|
|
256
|
+
for key in ("fg_color", "progress_color", "border_color", "border_width", "corner_radius"):
|
|
257
|
+
val = target_map.get(key)
|
|
258
|
+
if val is not None:
|
|
259
|
+
config_payload[key] = val
|
|
260
|
+
|
|
261
|
+
if config_payload:
|
|
262
|
+
super().configure(**config_payload)
|
|
263
|
+
|
|
264
|
+
def bind(self, sequence=None, command=None, add=None):
|
|
265
|
+
"""
|
|
266
|
+
No-ops while running inside Pygubu Designer (detected via the
|
|
267
|
+
PYGUBU_DESIGNER_RUNNING environment variable), otherwise forwards to
|
|
268
|
+
the native widget's bind(). Presumably guards against event bindings
|
|
269
|
+
misbehaving during design-time layout, since progress bars have no
|
|
270
|
+
real interactive purpose for the Designer to preview anyway.
|
|
271
|
+
"""
|
|
272
|
+
if "PYGUBU_DESIGNER_RUNNING" in os.environ:
|
|
273
|
+
return None
|
|
274
|
+
return super().bind(sequence, command, add)
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
"""
|
|
3
|
+
sCTkRadioButton
|
|
4
|
+
|
|
5
|
+
A theme-compliant radio selection button widget. Inherits directly from
|
|
6
|
+
ctk.CTkRadioButton so CustomTkinter handles native rendering, mutual
|
|
7
|
+
exclusion via shared variables, and click behavior; this class layers
|
|
8
|
+
automatic light/dark theme resolution and a distinct enabled/disabled visual
|
|
9
|
+
state on top.
|
|
10
|
+
|
|
11
|
+
Base class order matters here: `class sCTkRadioButton(ctk.CTkRadioButton,
|
|
12
|
+
ThemeableWidget)` puts the native CTk class first, so every `super()` call in
|
|
13
|
+
this file's own methods resolves to ctk.CTkRadioButton -- 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
|
+
Disabling uses CTk's native state="disabled" -- an earlier version used the
|
|
20
|
+
same manual-unbind approach confirmed broken on the button family (clicks
|
|
21
|
+
still fired while "disabled" there), and additionally called
|
|
22
|
+
super().configure(state="normal") even while disabling, never actually
|
|
23
|
+
setting native state to "disabled" at all. Fixed to match the button family's
|
|
24
|
+
confirmed-correct approach; not independently re-tested on this specific
|
|
25
|
+
widget.
|
|
26
|
+
|
|
27
|
+
CONFIRMED by direct testing: configure(variable=...) and configure(value=...)
|
|
28
|
+
now route through the native widget's own super().configure(), rather than
|
|
29
|
+
writing directly to CTkRadioButton's private _variable/_value attributes as
|
|
30
|
+
an earlier version did. That private-attribute approach (see its own "CRASH
|
|
31
|
+
SHIELD PASS" comment, apparently written to work around a ValueError) was
|
|
32
|
+
tested directly and found to be a real, confirmed bug: rebinding a button to
|
|
33
|
+
a new group's variable left the OLD variable's trace active (so the button
|
|
34
|
+
stayed incorrectly coupled to its original group) and corrupted the new
|
|
35
|
+
variable's value during the rebind. The native super().configure() approach
|
|
36
|
+
does not raise, correctly tears down the old binding, and correctly
|
|
37
|
+
establishes real two-way mutual exclusion in the new group.
|
|
38
|
+
"""
|
|
39
|
+
import os
|
|
40
|
+
from typing import Any, Callable, Optional
|
|
41
|
+
import tkinter as tk
|
|
42
|
+
import customtkinter as ctk
|
|
43
|
+
from .themeable_widget import ThemeableWidget
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class sCTkRadioButton(ctk.CTkRadioButton, ThemeableWidget):
|
|
47
|
+
"""Themeable radio selection button.
|
|
48
|
+
|
|
49
|
+
Adds to native ctk.CTkRadioButton:
|
|
50
|
+
- Automatic light/dark theme resolution from sCTkThemes.json (via
|
|
51
|
+
ThemeableWidget.__init__ -- see that class's docstring for what it does,
|
|
52
|
+
and just as importantly, what it no longer does).
|
|
53
|
+
- A distinct enabled/disabled visual state, using CTk's native
|
|
54
|
+
state="disabled" (see module docstring for the fix history).
|
|
55
|
+
- Pygubu Designer property introspection for `state`, `fg_color`,
|
|
56
|
+
`border_color`, `text_color`, and `hover_color` via a single-argument
|
|
57
|
+
configure() call.
|
|
58
|
+
- A bind() override that no-ops while running inside Pygubu Designer
|
|
59
|
+
(detected via the PYGUBU_DESIGNER_RUNNING environment variable) --
|
|
60
|
+
the same pattern used by sCTkProgressBar.
|
|
61
|
+
|
|
62
|
+
Colors are passed through to configure() as raw (light, dark) tuples rather
|
|
63
|
+
than pre-resolved to a single value, so CustomTkinter's own appearance-mode
|
|
64
|
+
tracking repaints them automatically on a light/dark switch -- the same
|
|
65
|
+
approach validated on sCTkComboBox, sCTkSegmentedButton, and the button
|
|
66
|
+
family. Not separately re-confirmed for this specific widget.
|
|
67
|
+
|
|
68
|
+
CONFIRMED by direct testing: rebinding variable/value via
|
|
69
|
+
super().configure() (the "proper" approach) works correctly -- it does
|
|
70
|
+
not raise, correctly tears down the old variable's trace (a button
|
|
71
|
+
rebound to a new group no longer responds to the old group's other
|
|
72
|
+
buttons), and correctly establishes real two-way mutual exclusion in the
|
|
73
|
+
new group. The private-attribute approach previously used here was
|
|
74
|
+
confirmed WORSE than just "might not work": it left the old variable's
|
|
75
|
+
trace active (so the rebound button stayed incorrectly coupled to its
|
|
76
|
+
original group) and corrupted the new variable's value during the
|
|
77
|
+
rebind. See configure() for the finalized implementation.
|
|
78
|
+
|
|
79
|
+
The theme's border_width_unchecked and border_width_checked control this
|
|
80
|
+
widget's border thickness based on whether it's the currently-selected
|
|
81
|
+
button in its group (thicker when checked, to show the filled dot) --
|
|
82
|
+
these are real, top-level-only theme keys (not in disabled_map), applied
|
|
83
|
+
once at construction via final_kw and left alone afterward, the same way
|
|
84
|
+
font or corner_radius are elsewhere in this library. The native widget
|
|
85
|
+
handles switching between checked/unchecked appearance internally; no
|
|
86
|
+
repaint-time logic is needed for these two keys. The repaint loop below
|
|
87
|
+
no longer checks for "border_width", "radiobutton_width", or
|
|
88
|
+
"radiobutton_height" -- an earlier version did, but none of those three
|
|
89
|
+
correspond to anything in the real theme file; they were removed rather
|
|
90
|
+
than renamed, since border_width_unchecked/checked don't need to be
|
|
91
|
+
re-applied on disable/enable in the first place.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(self, master: Optional[Any] = None, **kw: Any) -> None:
|
|
95
|
+
"""
|
|
96
|
+
Args:
|
|
97
|
+
master: Parent container.
|
|
98
|
+
**kw: `variable` (tkinter.Variable), `value` (any), and `command`
|
|
99
|
+
(callable) are read (not popped -- see below) and explicitly
|
|
100
|
+
re-applied to final_kw before construction. Everything else is
|
|
101
|
+
either a native CTkRadioButton argument or a theme-key
|
|
102
|
+
override (see the "sCTkRadioButton" block in sCTkThemes.json).
|
|
103
|
+
"""
|
|
104
|
+
# 1. Capture references to pass to the native constructor. Uses .get()
|
|
105
|
+
# rather than .pop() -- these keys are read here AND still present in
|
|
106
|
+
# kw when it's passed to ThemeableWidget.__init__ below, which will
|
|
107
|
+
# also copy them into final_kw as ordinary (non-color) values. The
|
|
108
|
+
# explicit re-assignment in step 3 is what guarantees they survive
|
|
109
|
+
# regardless of whether ThemeableWidget's own merge logic keeps them.
|
|
110
|
+
variable = kw.get("variable", None)
|
|
111
|
+
value = kw.get("value", None)
|
|
112
|
+
command = kw.get("command", None)
|
|
113
|
+
|
|
114
|
+
# 2. Fire our shared theme logic. This resolves final_kw
|
|
115
|
+
# (construction-time properties) and the disabled color map. See
|
|
116
|
+
# ThemeableWidget.__init__ for what actually happens here.
|
|
117
|
+
# NO PLACEHOLDER TEXT. CustomTkinter defaults `text` to the class
|
|
118
|
+
# name, so a CTkRadioButton built without text reads "CTkRadioButton". Nothing wants
|
|
119
|
+
# that in a real interface, and the Designer made it worse: clearing
|
|
120
|
+
# the text field removes the property, the canvas rebuilds the widget
|
|
121
|
+
# from what is left, and the placeholder appeared in place of the
|
|
122
|
+
# blank that was asked for.
|
|
123
|
+
kw.setdefault("text", "")
|
|
124
|
+
ThemeableWidget.__init__(self, kw)
|
|
125
|
+
|
|
126
|
+
# 3. Deep-copy the resolved map onto this instance, so later changes
|
|
127
|
+
# here never leak back into the shared theme registry.
|
|
128
|
+
self._local_defaults = dict(self.final_kw)
|
|
129
|
+
self._custom_disabled_map = dict(self._widget_disabled_map)
|
|
130
|
+
|
|
131
|
+
# Remove any stray "state" key from final_kw -- prevents native
|
|
132
|
+
# constructor validation collisions.
|
|
133
|
+
self.final_kw.pop("state", None)
|
|
134
|
+
|
|
135
|
+
# Explicitly ensure these survive into the native constructor call,
|
|
136
|
+
# regardless of how ThemeableWidget's merge handled them.
|
|
137
|
+
if variable is not None: self.final_kw["variable"] = variable
|
|
138
|
+
if value is not None: self.final_kw["value"] = value
|
|
139
|
+
if command is not None: self.final_kw["command"] = command
|
|
140
|
+
|
|
141
|
+
# 4. Initialize CustomTkinter natively, letting it wire up its own
|
|
142
|
+
# StringVar/mutual-exclusion tracking bindings.
|
|
143
|
+
super().__init__(master, **self.final_kw)
|
|
144
|
+
|
|
145
|
+
self._custom_current_state = "normal"
|
|
146
|
+
self._update_current_visual_state()
|
|
147
|
+
|
|
148
|
+
# 5. Register lifecycle handshake hook, notifying Pygubu-style consumers
|
|
149
|
+
# that construction is complete.
|
|
150
|
+
self._finalize_themeable_lifecycle()
|
|
151
|
+
|
|
152
|
+
def configure(self, *args: Any, **kwargs: Any) -> Any:
|
|
153
|
+
"""
|
|
154
|
+
Standard widget configuration, with Pygubu/positional-argument handling.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
*args: At most one positional argument is meaningful:
|
|
158
|
+
- a dict: merged into kwargs and processed normally below.
|
|
159
|
+
- the literal string "state": returns a Tkinter-style
|
|
160
|
+
(name, name, name, default, current) tuple.
|
|
161
|
+
- one of "fg_color"/"border_color"/"text_color"/"hover_color":
|
|
162
|
+
returns the same style of tuple, with `current` reflecting
|
|
163
|
+
the disabled or normal value as appropriate. Note the
|
|
164
|
+
returned value is str(value), where value may itself be a
|
|
165
|
+
(light, dark) tuple rather than a single resolved color --
|
|
166
|
+
a known limitation shared with the wider Pygubu-query
|
|
167
|
+
investigation set aside elsewhere in this project, not
|
|
168
|
+
fixed here.
|
|
169
|
+
- anything else: forwarded directly to the native widget's
|
|
170
|
+
configure(), which does not support single-argument property
|
|
171
|
+
queries for arbitrary properties (same limitation).
|
|
172
|
+
**kwargs: Standard CTkRadioButton configuration options, plus:
|
|
173
|
+
`variable`/`value` are each routed through super().configure()
|
|
174
|
+
individually -- confirmed correct for rebinding to a new
|
|
175
|
+
group after construction, see module docstring; `command`
|
|
176
|
+
and `state` are each routed individually too.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
The query tuple described above for the single-argument case, or
|
|
180
|
+
whatever super().configure() returns for the keyword-argument case
|
|
181
|
+
(typically None).
|
|
182
|
+
"""
|
|
183
|
+
# args is always a tuple -- args[0] is the actual value passed, whether
|
|
184
|
+
# that's a string or a dict. An earlier version of this method compared
|
|
185
|
+
# the wrapped tuple directly (`pname = args`) and forwarded it unwrapped
|
|
186
|
+
# to super().configure(), which is not a valid call shape for the
|
|
187
|
+
# native widget. Don't reintroduce that.
|
|
188
|
+
if len(args) == 1:
|
|
189
|
+
if isinstance(args[0], dict):
|
|
190
|
+
kwargs = {**args[0], **kwargs}
|
|
191
|
+
else:
|
|
192
|
+
pname = args[0]
|
|
193
|
+
if pname == "state":
|
|
194
|
+
return ("state", "state", "state", "normal", str(self.state()))
|
|
195
|
+
|
|
196
|
+
if pname in ["fg_color", "border_color", "text_color", "hover_color"]:
|
|
197
|
+
current_state = str(self.state()).lower()
|
|
198
|
+
val = self._custom_disabled_map.get(pname) if current_state == "disabled" else self._local_defaults.get(pname)
|
|
199
|
+
return (pname, pname, pname, self._query_value(self._theme_default(pname)), self._query_value(val))
|
|
200
|
+
|
|
201
|
+
return self._configure_query(pname)
|
|
202
|
+
|
|
203
|
+
# CONFIRMED by direct testing: routing variable/value through
|
|
204
|
+
# super().configure() (rather than writing directly to CTkRadioButton's
|
|
205
|
+
# private _variable/_value attributes) does not raise, and correctly
|
|
206
|
+
# rebinds mutual exclusion to the new group -- see class docstring
|
|
207
|
+
# for the full confirmation and what was wrong with the private-
|
|
208
|
+
# attribute approach this replaces.
|
|
209
|
+
if "variable" in kwargs:
|
|
210
|
+
super().configure(variable=kwargs.pop("variable"))
|
|
211
|
+
if "value" in kwargs:
|
|
212
|
+
super().configure(value=kwargs.pop("value"))
|
|
213
|
+
if "command" in kwargs:
|
|
214
|
+
super().configure(command=kwargs.pop("command"))
|
|
215
|
+
|
|
216
|
+
# Runtime overrides have to reach the map a repaint reads, or the
|
|
217
|
+
# repaint puts the theme value straight back -- see
|
|
218
|
+
# ThemeableWidget._record_theme_overrides().
|
|
219
|
+
self._record_theme_overrides(kwargs)
|
|
220
|
+
|
|
221
|
+
if "state" in kwargs:
|
|
222
|
+
self.state(kwargs.pop("state"))
|
|
223
|
+
|
|
224
|
+
self._drop_cleared(kwargs)
|
|
225
|
+
|
|
226
|
+
if kwargs:
|
|
227
|
+
return super().configure(**kwargs)
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
# Tkinter/CTk convention binds .config to .configure as a SEPARATE class
|
|
231
|
+
# attribute -- it does not automatically track whichever configure() a
|
|
232
|
+
# subclass defines. Without this line, calling .config(...) on an instance
|
|
233
|
+
# would silently skip this entire override and land on the native widget's
|
|
234
|
+
# configure() directly, bypassing theming and state handling entirely.
|
|
235
|
+
config = configure
|
|
236
|
+
|
|
237
|
+
def _set_appearance_mode(self, mode_string: str) -> None:
|
|
238
|
+
"""
|
|
239
|
+
Forwards CustomTkinter's internal light/dark mode change notification to
|
|
240
|
+
the native widget.
|
|
241
|
+
|
|
242
|
+
No longer manually re-triggers _update_current_visual_state(). That
|
|
243
|
+
method now passes raw (light, dark) tuples straight through to
|
|
244
|
+
configure() instead of pre-resolving to a single color, so CTk's own
|
|
245
|
+
appearance-mode tracking should repaint correctly on its own -- the
|
|
246
|
+
same approach validated on sCTkComboBox, sCTkSegmentedButton, and the
|
|
247
|
+
button family.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
mode_string: The new appearance mode ("Light" or "Dark"), as passed
|
|
251
|
+
by CustomTkinter's internal appearance-mode change machinery.
|
|
252
|
+
"""
|
|
253
|
+
if hasattr(super(), "_set_appearance_mode"):
|
|
254
|
+
try:
|
|
255
|
+
super()._set_appearance_mode(mode_string)
|
|
256
|
+
except Exception:
|
|
257
|
+
pass
|
|
258
|
+
|
|
259
|
+
def get_state(self) -> str:
|
|
260
|
+
"""Equivalent to calling state() with no argument."""
|
|
261
|
+
return self.state()
|
|
262
|
+
|
|
263
|
+
def state(self, mode: Optional[str] = None) -> str:
|
|
264
|
+
"""
|
|
265
|
+
Gets or sets the widget's enabled/disabled state.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
mode: If None, returns the current state without changing anything.
|
|
269
|
+
Otherwise, only the literal string "disabled" (case-insensitive)
|
|
270
|
+
is treated as disabled; anything in ("normal", "enabled",
|
|
271
|
+
"active") is treated as enabled. Any other value matches
|
|
272
|
+
neither branch, though colors are still harmlessly re-applied.
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
The resulting state ("normal" or "disabled", lowercase).
|
|
276
|
+
"""
|
|
277
|
+
if mode is None:
|
|
278
|
+
return str(getattr(self, "_custom_current_state", "normal")).lower()
|
|
279
|
+
|
|
280
|
+
mode = mode.lower()
|
|
281
|
+
if mode in ("normal", "enabled", "active"):
|
|
282
|
+
self._custom_current_state = "normal"
|
|
283
|
+
super().configure(state="normal")
|
|
284
|
+
# Re-establish the mouse bindings that disabling below may have
|
|
285
|
+
# torn down.
|
|
286
|
+
if hasattr(self, "_create_bindings"):
|
|
287
|
+
try:
|
|
288
|
+
self._create_bindings()
|
|
289
|
+
except Exception:
|
|
290
|
+
pass
|
|
291
|
+
self._update_current_visual_state()
|
|
292
|
+
|
|
293
|
+
elif mode == "disabled":
|
|
294
|
+
self._custom_current_state = "disabled"
|
|
295
|
+
# Uses CTk's native state="disabled" -- confirmed necessary (not
|
|
296
|
+
# optional) on the button family, which originally used the same
|
|
297
|
+
# manual-unbind code this method used to have, AND (unlike this
|
|
298
|
+
# widget's original version) at least called state="normal" for
|
|
299
|
+
# both branches -- meaning native state was NEVER actually set to
|
|
300
|
+
# "disabled" at all before this fix. See module docstring.
|
|
301
|
+
super().configure(state="disabled")
|
|
302
|
+
self._update_current_visual_state()
|
|
303
|
+
|
|
304
|
+
return self._custom_current_state
|
|
305
|
+
|
|
306
|
+
def _update_current_visual_state(self) -> None:
|
|
307
|
+
"""
|
|
308
|
+
Recomputes and applies this widget's colors from the theme file, based
|
|
309
|
+
on the current state.
|
|
310
|
+
|
|
311
|
+
Called after construction and on every state() change.
|
|
312
|
+
|
|
313
|
+
Passes raw (light, dark) tuples straight through to configure() instead
|
|
314
|
+
of resolving to a single color first, so CTk's native tracking can
|
|
315
|
+
handle appearance-mode repaints without help from _set_appearance_mode.
|
|
316
|
+
Every value here traces back to sCTkThemes.json; there are no
|
|
317
|
+
hardcoded colors in this method.
|
|
318
|
+
|
|
319
|
+
See this class's docstring regarding "border_width" /
|
|
320
|
+
"radiobutton_width" / "radiobutton_height" -- those keys have been
|
|
321
|
+
removed from this loop entirely; they never matched anything in the
|
|
322
|
+
real theme (see this class's docstring for what the actual keys are
|
|
323
|
+
and why they don't need repaint-time handling).
|
|
324
|
+
"""
|
|
325
|
+
is_disabled = getattr(self, "_custom_current_state", "normal") == "disabled"
|
|
326
|
+
target_map = self._custom_disabled_map if is_disabled else self._local_defaults
|
|
327
|
+
|
|
328
|
+
config_payload = {}
|
|
329
|
+
for key in ("fg_color", "border_color", "hover_color", "font"):
|
|
330
|
+
val = target_map.get(key)
|
|
331
|
+
if val is not None:
|
|
332
|
+
config_payload[key] = val
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
# text_color is handled separately, because this widget sets CTk's own
|
|
336
|
+
# state="disabled" -- and the native draw code then uses its
|
|
337
|
+
# text_color_disabled option, overriding whatever text_color was set
|
|
338
|
+
# to. Swapping text_color out of disabled_map was therefore discarded
|
|
339
|
+
# at draw time and the theme key had no visible effect.
|
|
340
|
+
#
|
|
341
|
+
# Both options are set, so CTk chooses between them as designed.
|
|
342
|
+
_normal_text = self._local_defaults.get("text_color")
|
|
343
|
+
if _normal_text is not None:
|
|
344
|
+
config_payload["text_color"] = _normal_text
|
|
345
|
+
_disabled_text = self._custom_disabled_map.get("text_color")
|
|
346
|
+
if _disabled_text is not None:
|
|
347
|
+
config_payload["text_color_disabled"] = _disabled_text
|
|
348
|
+
|
|
349
|
+
if config_payload:
|
|
350
|
+
super().configure(**config_payload)
|
|
351
|
+
|
|
352
|
+
def bind(self, sequence=None, command=None, add=None):
|
|
353
|
+
"""
|
|
354
|
+
No-ops while running inside Pygubu Designer (detected via the
|
|
355
|
+
PYGUBU_DESIGNER_RUNNING environment variable), otherwise forwards to
|
|
356
|
+
the native widget's bind(). Same pattern used by sCTkProgressBar.
|
|
357
|
+
"""
|
|
358
|
+
if "PYGUBU_DESIGNER_RUNNING" in os.environ:
|
|
359
|
+
return None
|
|
360
|
+
return super().bind(sequence, command, add)
|