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,463 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
"""
|
|
3
|
+
sCTkSwitch
|
|
4
|
+
|
|
5
|
+
A theme-compliant toggle switch widget. Inherits directly from ctk.CTkSwitch
|
|
6
|
+
so CustomTkinter handles native rendering and drag/click behavior; this class
|
|
7
|
+
layers automatic light/dark theme resolution and a distinct enabled/disabled
|
|
8
|
+
visual state on top.
|
|
9
|
+
|
|
10
|
+
Base class order matters here: `class sCTkSwitch(ctk.CTkSwitch,
|
|
11
|
+
ThemeableWidget)` puts the native CTk class first, so every `super()` call in
|
|
12
|
+
this file's own methods resolves to ctk.CTkSwitch -- 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
|
+
Two things about this file are architecturally different from every other
|
|
19
|
+
widget in this project, worth understanding before editing it:
|
|
20
|
+
|
|
21
|
+
1. configure()'s signature is `configure(self, require_redraw=None, **kwargs)`
|
|
22
|
+
rather than `configure(self, *args, **kwargs)`. This mirrors real CTk's own
|
|
23
|
+
configure() signature convention (confirmed via ThemeableWidget's own
|
|
24
|
+
handling of it) rather than using *args. Because of this, the single-arg
|
|
25
|
+
query bug that affected almost every other widget in this project (a
|
|
26
|
+
wrapped tuple never matching a string comparison) never existed here in
|
|
27
|
+
the first place -- `require_redraw` receives the actual passed value
|
|
28
|
+
directly (a string, a dict, whatever), with no unwrapping needed. No fix
|
|
29
|
+
was required for this file's argument handling.
|
|
30
|
+
|
|
31
|
+
2. Disabling uses a different, more robust mechanism than the button family's
|
|
32
|
+
original (confirmed-broken) manual canvas.unbind() approach: it prepends a
|
|
33
|
+
dedicated bindtag (self._block_tag) to the canvas's and text label's
|
|
34
|
+
bindtags, with a class-level binding that returns "break" on <Button-1>.
|
|
35
|
+
Since Tkinter processes bindtags in order and "break" halts further
|
|
36
|
+
propagation, this intercepts clicks regardless of which underlying level
|
|
37
|
+
the real click handler is bound at -- unlike plain unbind(), which only
|
|
38
|
+
removes widget-level bindings and was confirmed to miss the actual handler
|
|
39
|
+
on CTkButton. This is combined with native state="disabled" as well
|
|
40
|
+
(belt-and-suspenders). Left as-is; not confirmed broken, and the mechanism
|
|
41
|
+
itself is sound, but not independently re-tested as part of this project's
|
|
42
|
+
audit.
|
|
43
|
+
"""
|
|
44
|
+
from typing import Any, Callable, Optional
|
|
45
|
+
import customtkinter as ctk
|
|
46
|
+
from .themeable_widget import ThemeableWidget
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class sCTkSwitch(ctk.CTkSwitch, ThemeableWidget):
|
|
50
|
+
"""Themeable toggle switch.
|
|
51
|
+
|
|
52
|
+
Adds to native ctk.CTkSwitch:
|
|
53
|
+
- Automatic light/dark theme resolution from sCTkThemes.json (via
|
|
54
|
+
ThemeableWidget.__init__ -- see that class's docstring for what it does,
|
|
55
|
+
and just as importantly, what it no longer does).
|
|
56
|
+
- A distinct enabled/disabled visual state, using both native
|
|
57
|
+
state="disabled" and a bindtag-based click interceptor (see module
|
|
58
|
+
docstring).
|
|
59
|
+
- Pygubu Designer property introspection for "state" via a single-
|
|
60
|
+
argument configure() call, plus a cget() override for both "state"
|
|
61
|
+
and "command".
|
|
62
|
+
- A command-forwarding wrapper that tries calling the user's command
|
|
63
|
+
with the switch's current value, falling back to calling it with no
|
|
64
|
+
arguments if that raises TypeError -- accommodating both
|
|
65
|
+
`command=lambda val: ...` and `command=lambda: ...` styles.
|
|
66
|
+
|
|
67
|
+
Colors are passed through to configure() as raw (light, dark) tuples rather
|
|
68
|
+
than pre-resolved to a single value, so CustomTkinter's own appearance-mode
|
|
69
|
+
tracking repaints them automatically on a light/dark switch -- the same
|
|
70
|
+
approach validated on sCTkComboBox, sCTkSegmentedButton, and the button
|
|
71
|
+
family. Not separately re-confirmed for this specific widget.
|
|
72
|
+
|
|
73
|
+
HISTORY: an earlier version of this file only ever varied text_color by
|
|
74
|
+
state, leaving fg_color/progress_color/button_color/button_hover_color
|
|
75
|
+
fixed regardless of disabled state, with a comment claiming this was
|
|
76
|
+
deliberate. It wasn't -- full dimming had previously been attempted and
|
|
77
|
+
abandoned after repeated failures, which is also why a separate widget
|
|
78
|
+
(sCTkSwitchAlt) exists, built specifically to work around this and a
|
|
79
|
+
related light-mode visibility problem (the button/handle, white in the
|
|
80
|
+
stock theme, visually fades into a light background). Full dimming is now
|
|
81
|
+
confirmed working by direct testing, once several infrastructure bugs
|
|
82
|
+
elsewhere in this file were fixed (the tuple-vs-resolved-color pattern,
|
|
83
|
+
an always-empty _custom_disabled_map caused by reading "disabled_map" out
|
|
84
|
+
of final_kw -- where ThemeableWidget deliberately never puts it -- instead
|
|
85
|
+
of the correctly-populated _widget_disabled_map, and hardcoded fallback
|
|
86
|
+
colors masking the real theme values).
|
|
87
|
+
|
|
88
|
+
The handle-fading-into-background problem itself was investigated
|
|
89
|
+
separately and RULED OUT as a bg_color issue, by direct, objective
|
|
90
|
+
testing (comparing sCTkSwitch.cget("bg_color") against the real parent
|
|
91
|
+
color, not just eyeballing screenshots): CTkSwitch already resolves
|
|
92
|
+
bg_color to match its real parent correctly on its own, including through
|
|
93
|
+
a transparent intermediate frame, with no help needed from this widget at
|
|
94
|
+
all. An experimental fix that explicitly set bg_color (adapted from
|
|
95
|
+
sCTkSwitchAlt's _apply_parent_bg_handshake()) was tested and found to
|
|
96
|
+
actively make one scenario WORSE -- overriding CTk's already-correct
|
|
97
|
+
native resolution with a hardcoded fallback that didn't match the real
|
|
98
|
+
background -- and removed entirely as a result.
|
|
99
|
+
|
|
100
|
+
The reason the port didn't apply: sCTkSwitchAlt needed that logic because
|
|
101
|
+
it draws on a raw tk.Canvas, which has no built-in awareness of parent
|
|
102
|
+
colors or transparency at all. sCTkSwitch subclasses the real
|
|
103
|
+
ctk.CTkSwitch, which already has this exact "find my real background
|
|
104
|
+
through any transparent chain" logic built in natively. The actual
|
|
105
|
+
handle-fading complaint is most likely a genuine low-contrast theme
|
|
106
|
+
color choice (white button_color against a pale fg_color in light mode),
|
|
107
|
+
not a code defect -- the same category of issue as the label family's
|
|
108
|
+
disabled-color contrast work earlier in this project, not the button
|
|
109
|
+
family's disable-mechanism bug.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
def __init__(
|
|
113
|
+
self,
|
|
114
|
+
master: Optional[Any] = None,
|
|
115
|
+
onvalue: Any = 1,
|
|
116
|
+
offvalue: Any = 0,
|
|
117
|
+
command: Optional[Callable] = None,
|
|
118
|
+
**kw: Any,
|
|
119
|
+
) -> None:
|
|
120
|
+
"""
|
|
121
|
+
Args:
|
|
122
|
+
master: Parent container.
|
|
123
|
+
onvalue: Value reported when the switch is on.
|
|
124
|
+
offvalue: Value reported when the switch is off.
|
|
125
|
+
command: Called on toggle. May accept the new value as a single
|
|
126
|
+
argument, or no arguments at all -- see this class's docstring.
|
|
127
|
+
**kw: `state` is pulled out explicitly below. Everything else is
|
|
128
|
+
either a native CTkSwitch argument or a theme-key override
|
|
129
|
+
(see the "sCTkSwitch" block in sCTkThemes.json). Color-related
|
|
130
|
+
theme keys and "disabled_map" itself are stripped out of the
|
|
131
|
+
kwargs sent to the native constructor -- they're applied
|
|
132
|
+
separately via _apply_custom_theme_colors() below.
|
|
133
|
+
"""
|
|
134
|
+
# 1. Fire our shared theme logic. This resolves final_kw
|
|
135
|
+
# (construction-time properties). See ThemeableWidget.__init__ for
|
|
136
|
+
# what actually happens here.
|
|
137
|
+
# NO PLACEHOLDER TEXT. CustomTkinter defaults `text` to the class
|
|
138
|
+
# name, so a CTkSwitch built without text reads "CTkSwitch". Nothing wants
|
|
139
|
+
# that in a real interface, and the Designer made it worse: clearing
|
|
140
|
+
# the text field removes the property, the canvas rebuilds the widget
|
|
141
|
+
# from what is left, and the placeholder appeared in place of the
|
|
142
|
+
# blank that was asked for.
|
|
143
|
+
kw.setdefault("text", "")
|
|
144
|
+
ThemeableWidget.__init__(self, kw)
|
|
145
|
+
|
|
146
|
+
# 2. Store the resolved maps onto this instance.
|
|
147
|
+
self._local_defaults = dict(self.final_kw)
|
|
148
|
+
self._custom_disabled_map = dict(self._widget_disabled_map)
|
|
149
|
+
|
|
150
|
+
# 3. Extract the initial state and value-related config early.
|
|
151
|
+
state_val = kw.pop("state", "normal")
|
|
152
|
+
self._custom_current_state = "normal" if str(state_val).lower() == "normal" else "disabled"
|
|
153
|
+
self._onvalue_payload = onvalue
|
|
154
|
+
self._offvalue_payload = offvalue
|
|
155
|
+
self._user_command = command
|
|
156
|
+
|
|
157
|
+
# 4. Strip color-related theme keys (and disabled_map itself) out of
|
|
158
|
+
# final_kw before construction -- these are applied afterward via
|
|
159
|
+
# _apply_custom_theme_colors(), not passed to the native constructor.
|
|
160
|
+
# bg_color is deliberately NOT set here at all -- confirmed by direct
|
|
161
|
+
# testing that CTkSwitch already resolves it correctly on its own,
|
|
162
|
+
# including through a transparent intermediate parent. See class
|
|
163
|
+
# docstring.
|
|
164
|
+
for pop_key in ["fg_color", "progress_color", "button_color", "button_hover_color",
|
|
165
|
+
"text_color", "font", "disabled_map"]:
|
|
166
|
+
self.final_kw.pop(pop_key, None)
|
|
167
|
+
|
|
168
|
+
# 5. Wrap the user's command so it can be called with or without an
|
|
169
|
+
# argument -- see this class's docstring.
|
|
170
|
+
wrapped_command = None
|
|
171
|
+
if self._user_command:
|
|
172
|
+
wrapped_command = self._execute_safe_command_forwarding
|
|
173
|
+
|
|
174
|
+
# 6. Initialize CustomTkinter natively with the clean final kwargs array.
|
|
175
|
+
# bg_color is intentionally never set here -- see class docstring for
|
|
176
|
+
# why (confirmed by direct testing that CTkSwitch already resolves it
|
|
177
|
+
# correctly on its own).
|
|
178
|
+
super().__init__(master, onvalue=onvalue, offvalue=offvalue, command=wrapped_command, **self.final_kw)
|
|
179
|
+
|
|
180
|
+
# 7. Set up the bindtag-based click interceptor -- see module docstring.
|
|
181
|
+
self._block_tag = f"sCTkSwitchBlock_{id(self)}"
|
|
182
|
+
if hasattr(self, "_canvas") and self._canvas.winfo_exists():
|
|
183
|
+
self._canvas.bind_class(self._block_tag, "<Button-1>", lambda e: "break")
|
|
184
|
+
self._canvas.bindtags((self._block_tag,) + self._canvas.bindtags())
|
|
185
|
+
if hasattr(self, "_text_label") and self._text_label.winfo_exists():
|
|
186
|
+
self._text_label.bind_class(self._block_tag, "<Button-1>", lambda e: "break")
|
|
187
|
+
self._text_label.bindtags((self._block_tag,) + self._text_label.bindtags())
|
|
188
|
+
|
|
189
|
+
# 8. Apply initial theming and complete lifecycle registration.
|
|
190
|
+
self._apply_custom_theme_colors()
|
|
191
|
+
if self._custom_current_state == "disabled":
|
|
192
|
+
self.configure(state="disabled")
|
|
193
|
+
self._finalize_themeable_lifecycle()
|
|
194
|
+
|
|
195
|
+
def _execute_safe_command_forwarding(self) -> None:
|
|
196
|
+
"""
|
|
197
|
+
Calls the user's command, first trying with the switch's current
|
|
198
|
+
value as a single argument, falling back to calling with no arguments
|
|
199
|
+
if that raises TypeError.
|
|
200
|
+
|
|
201
|
+
Confirmed by direct testing: exceptions from the user's command
|
|
202
|
+
propagate normally (an earlier version swallowed all of them
|
|
203
|
+
silently -- no error, no traceback, nothing, which hid real bugs
|
|
204
|
+
completely). Tkinter's own default callback-exception handling
|
|
205
|
+
reports propagated exceptions to the console without crashing the
|
|
206
|
+
running application.
|
|
207
|
+
|
|
208
|
+
KNOWN, UNFIXED ISSUE: the inner `except TypeError` can't distinguish
|
|
209
|
+
"your command doesn't accept an argument" from "your command raised a
|
|
210
|
+
TypeError for some unrelated reason inside its own body." Either one
|
|
211
|
+
triggers the same fallback -- calling the command AGAIN, with no
|
|
212
|
+
arguments. A genuine TypeError bug in a one-argument command will
|
|
213
|
+
therefore raise a second, confusing "missing required argument" error
|
|
214
|
+
on top of the real one -- confirmed by direct testing. Python's
|
|
215
|
+
exception chaining keeps both tracebacks visible (the real bug prints
|
|
216
|
+
first, followed by "During handling of the above exception..."), so
|
|
217
|
+
the bug isn't hidden, just noisier than necessary. Fixing this
|
|
218
|
+
properly would mean checking the command's actual signature (e.g. via
|
|
219
|
+
inspect.signature(...).bind(...)) before calling it, rather than
|
|
220
|
+
inferring compatibility from a caught exception -- not yet
|
|
221
|
+
implemented.
|
|
222
|
+
"""
|
|
223
|
+
if not self._user_command:
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
active_val = self.get()
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
self._user_command(active_val)
|
|
230
|
+
except TypeError:
|
|
231
|
+
self._user_command()
|
|
232
|
+
|
|
233
|
+
# Native constructor defaults, for the single-argument query below.
|
|
234
|
+
#
|
|
235
|
+
# Deliberately EMPTY. This widget declares no width or height of its own --
|
|
236
|
+
# it forwards both to native CTkSwitch -- so there is no default here that
|
|
237
|
+
# can be stated from this file's own source. Every property therefore
|
|
238
|
+
# reports its current value as its default, which makes blanking a field in
|
|
239
|
+
# a designer a no-op rather than a jump to a guessed value. Guessing would
|
|
240
|
+
# be worse than not answering: the guess gets applied.
|
|
241
|
+
_NATIVE_QUERY_DEFAULTS = {}
|
|
242
|
+
|
|
243
|
+
def configure(self, require_redraw: Any = None, **kwargs: Any) -> Any:
|
|
244
|
+
"""
|
|
245
|
+
Standard widget configuration, with Pygubu/positional-argument handling.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
require_redraw: Despite the name (matching real CTk's own
|
|
249
|
+
configure() signature), this parameter doubles as the
|
|
250
|
+
Pygubu single-argument query slot when called positionally:
|
|
251
|
+
- the literal string "state": returns a Tkinter-style
|
|
252
|
+
(name, name, name, default, current) tuple.
|
|
253
|
+
- a dict: merged into kwargs and processed normally below.
|
|
254
|
+
- anything else (a string): forwarded directly to the native
|
|
255
|
+
widget's configure().
|
|
256
|
+
When called as an actual keyword (require_redraw=True/False),
|
|
257
|
+
behaves as CTk's native redraw-control flag.
|
|
258
|
+
**kwargs: Standard CTkSwitch configuration options, plus:
|
|
259
|
+
`state` updates self._custom_current_state and is also
|
|
260
|
+
forwarded to the native configure(); `command` is re-wrapped
|
|
261
|
+
through _execute_safe_command_forwarding(); `onvalue`/
|
|
262
|
+
`offvalue` update the cached payload values used by that
|
|
263
|
+
wrapper.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
The query tuple described above for the single-string-argument
|
|
267
|
+
case, or None otherwise (colors are always reapplied via
|
|
268
|
+
_apply_custom_theme_colors() before returning).
|
|
269
|
+
"""
|
|
270
|
+
if require_redraw is not None and not kwargs and isinstance(require_redraw, str):
|
|
271
|
+
if require_redraw == "state":
|
|
272
|
+
return ('state', 'state', 'State', 'normal', str(getattr(self, "_custom_current_state", "normal")))
|
|
273
|
+
# FIX: this used to be `return super().configure(require_redraw)`.
|
|
274
|
+
#
|
|
275
|
+
# The native configure() is declared
|
|
276
|
+
# configure(self, require_redraw=False, **kwargs), so the property
|
|
277
|
+
# NAME was passed as require_redraw and the call returned None.
|
|
278
|
+
# Pygubu's _get_default_value() expects a five-element tuple, got
|
|
279
|
+
# None, and handed that None back to _set_property() -- where
|
|
280
|
+
# CustomTkinter raised "float() argument must be a string or a real
|
|
281
|
+
# number, not 'NoneType'".
|
|
282
|
+
#
|
|
283
|
+
# Reached whenever a field is blanked in the Designer inspector,
|
|
284
|
+
# which calls configure(name) to read the property's default.
|
|
285
|
+
#
|
|
286
|
+
# A proper Tkinter-style tuple is returned instead, built from
|
|
287
|
+
# cget() so the reported current value is real. Properties with no
|
|
288
|
+
# known native default report their current value as the default,
|
|
289
|
+
# making a blank a no-op rather than a jump to a guessed value.
|
|
290
|
+
try:
|
|
291
|
+
_current = self.cget(require_redraw)
|
|
292
|
+
except Exception:
|
|
293
|
+
_current = None
|
|
294
|
+
|
|
295
|
+
# FIX: the default came from _NATIVE_QUERY_DEFAULTS or, failing
|
|
296
|
+
# that, from cget() -- the CURRENT value. This widget's table is
|
|
297
|
+
# empty, so clearing a colour in the Designer reported the
|
|
298
|
+
# override as its own default and put it straight back: setting
|
|
299
|
+
# fg_color red and then clearing it left the switch red.
|
|
300
|
+
#
|
|
301
|
+
# The THEME's value is the right answer, taken from an untouched
|
|
302
|
+
# copy so a runtime override cannot corrupt it -- see
|
|
303
|
+
# ThemeableWidget._theme_default(). The native table still wins
|
|
304
|
+
# where it has an entry, for properties the theme says nothing
|
|
305
|
+
# about.
|
|
306
|
+
_default = self._NATIVE_QUERY_DEFAULTS.get(require_redraw)
|
|
307
|
+
if _default is None:
|
|
308
|
+
_default = self._theme_default(require_redraw)
|
|
309
|
+
if _default is None:
|
|
310
|
+
_default = _current
|
|
311
|
+
return (require_redraw, require_redraw, require_redraw,
|
|
312
|
+
self._query_value(_default, require_redraw),
|
|
313
|
+
self._query_value(_current, require_redraw))
|
|
314
|
+
|
|
315
|
+
if isinstance(require_redraw, dict):
|
|
316
|
+
kwargs = require_redraw | kwargs
|
|
317
|
+
|
|
318
|
+
# Runtime overrides have to reach the map a repaint reads, or the
|
|
319
|
+
# repaint puts the theme value straight back -- see
|
|
320
|
+
# ThemeableWidget._record_theme_overrides().
|
|
321
|
+
self._record_theme_overrides(kwargs)
|
|
322
|
+
|
|
323
|
+
if "state" in kwargs:
|
|
324
|
+
self._custom_current_state = str(kwargs.pop("state")).lower()
|
|
325
|
+
super().configure(state=self._custom_current_state)
|
|
326
|
+
|
|
327
|
+
if "command" in kwargs:
|
|
328
|
+
self._user_command = kwargs.pop("command")
|
|
329
|
+
super().configure(command=self._execute_safe_command_forwarding if self._user_command else None)
|
|
330
|
+
|
|
331
|
+
if "onvalue" in kwargs: self._onvalue_payload = kwargs["onvalue"]
|
|
332
|
+
if "offvalue" in kwargs: self._offvalue_payload = kwargs["offvalue"]
|
|
333
|
+
|
|
334
|
+
self._drop_cleared(kwargs)
|
|
335
|
+
|
|
336
|
+
if kwargs: super().configure(**kwargs)
|
|
337
|
+
self._apply_custom_theme_colors()
|
|
338
|
+
|
|
339
|
+
# Tkinter/CTk convention binds .config to .configure as a SEPARATE class
|
|
340
|
+
# attribute -- it does not automatically track whichever configure() a
|
|
341
|
+
# subclass defines. Without this line, calling .config(...) on an instance
|
|
342
|
+
# would silently skip this entire override and land on the native widget's
|
|
343
|
+
# configure() directly, bypassing theming and state handling entirely.
|
|
344
|
+
config = configure
|
|
345
|
+
|
|
346
|
+
def cget(self, attribute_name: str) -> Any:
|
|
347
|
+
"""
|
|
348
|
+
Intercepts "state" and "command" queries, since both are tracked on
|
|
349
|
+
this instance rather than delegated to the native widget for these
|
|
350
|
+
two properties specifically.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
attribute_name: The property name being queried.
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
self._custom_current_state if querying "state"; self._user_command
|
|
357
|
+
if querying "command"; otherwise whatever the native
|
|
358
|
+
CTkSwitch.cget() returns.
|
|
359
|
+
"""
|
|
360
|
+
pname = str(attribute_name).lower()
|
|
361
|
+
if pname == "state": return getattr(self, "_custom_current_state", "normal")
|
|
362
|
+
if pname == "command": return self._user_command
|
|
363
|
+
return super().cget(attribute_name)
|
|
364
|
+
|
|
365
|
+
def _set_appearance_mode(self, mode_string: str) -> None:
|
|
366
|
+
"""
|
|
367
|
+
Forwards CustomTkinter's internal light/dark mode change notification to
|
|
368
|
+
the native widget, then reapplies theme colors.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
mode_string: The new appearance mode ("Light" or "Dark"), as passed
|
|
372
|
+
by CustomTkinter's internal appearance-mode change machinery.
|
|
373
|
+
"""
|
|
374
|
+
if hasattr(super(), "_set_appearance_mode"):
|
|
375
|
+
try:
|
|
376
|
+
super()._set_appearance_mode(mode_string)
|
|
377
|
+
except Exception:
|
|
378
|
+
pass
|
|
379
|
+
self._apply_custom_theme_colors()
|
|
380
|
+
|
|
381
|
+
def state(self, mode: Optional[str] = None) -> str:
|
|
382
|
+
"""
|
|
383
|
+
Gets or sets the widget's enabled/disabled state.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
mode: If None, returns the current state without changing
|
|
387
|
+
anything. Otherwise forwarded to configure(state=mode).
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
If querying: the current state string. If setting: echoes back
|
|
391
|
+
`mode` exactly as given (not re-queried afterward).
|
|
392
|
+
"""
|
|
393
|
+
if mode is None: return str(getattr(self, "_custom_current_state", "normal")).lower()
|
|
394
|
+
self.configure(state=mode)
|
|
395
|
+
return mode
|
|
396
|
+
|
|
397
|
+
def get_state(self) -> str:
|
|
398
|
+
"""Equivalent to calling state() with no argument."""
|
|
399
|
+
return self.state()
|
|
400
|
+
|
|
401
|
+
def _apply_custom_theme_colors(self) -> None:
|
|
402
|
+
"""
|
|
403
|
+
Recomputes and applies this widget's colors from the theme file, based
|
|
404
|
+
on the current state, and updates the bindtag-based click interceptor
|
|
405
|
+
to match.
|
|
406
|
+
|
|
407
|
+
Called after construction, on every configure()/state() call, and
|
|
408
|
+
(via _set_appearance_mode) on appearance-mode changes.
|
|
409
|
+
|
|
410
|
+
Passes raw (light, dark) tuples straight through to configure() instead
|
|
411
|
+
of resolving to a single color first, so CTk's native tracking can
|
|
412
|
+
handle appearance-mode repaints without help from _set_appearance_mode.
|
|
413
|
+
|
|
414
|
+
All five properties (fg_color, progress_color, button_color,
|
|
415
|
+
button_hover_color, text_color) are required to be present in both
|
|
416
|
+
the top-level theme block and disabled_map -- missing any raises
|
|
417
|
+
immediately rather than substituting a hardcoded color. This full
|
|
418
|
+
dimming is confirmed working by direct testing; see this class's
|
|
419
|
+
docstring for the history of why it wasn't always the behavior here.
|
|
420
|
+
"""
|
|
421
|
+
is_disabled = self._custom_current_state == "disabled"
|
|
422
|
+
normal_map = self._local_defaults
|
|
423
|
+
disabled_map = self._custom_disabled_map
|
|
424
|
+
|
|
425
|
+
for required_key in ("fg_color", "progress_color", "button_color", "button_hover_color", "text_color"):
|
|
426
|
+
if normal_map.get(required_key) is None:
|
|
427
|
+
raise KeyError(
|
|
428
|
+
f"'{self.__class__.__name__}' theme block is missing '{required_key}' "
|
|
429
|
+
f"at the top level of sCTkThemes.json."
|
|
430
|
+
)
|
|
431
|
+
if disabled_map.get(required_key) is None:
|
|
432
|
+
raise KeyError(
|
|
433
|
+
f"'{getattr(self, '_THEME_BLOCK_NAME', None) or self.__class__.__name__}' "
|
|
434
|
+
f"theme block is missing '{required_key}' in disabled_map."
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
color_map = disabled_map if is_disabled else normal_map
|
|
438
|
+
|
|
439
|
+
theme_payload = {
|
|
440
|
+
"text_color": color_map.get("text_color"),
|
|
441
|
+
"text_color_disabled": disabled_map.get("text_color"),
|
|
442
|
+
"fg_color": color_map.get("fg_color"),
|
|
443
|
+
"progress_color": color_map.get("progress_color"),
|
|
444
|
+
"button_color": color_map.get("button_color"),
|
|
445
|
+
"button_hover_color": color_map.get("button_hover_color"),
|
|
446
|
+
}
|
|
447
|
+
super().configure(**theme_payload)
|
|
448
|
+
|
|
449
|
+
# Toggle the click-interceptor bindtag to match the current state.
|
|
450
|
+
if is_disabled:
|
|
451
|
+
if self._block_tag not in self._canvas.bindtags():
|
|
452
|
+
self._canvas.bindtags((self._block_tag,) + self._canvas.bindtags())
|
|
453
|
+
if hasattr(self, "_text_label") and self._block_tag not in self._text_label.bindtags():
|
|
454
|
+
self._text_label.bindtags((self._block_tag,) + self._text_label.bindtags())
|
|
455
|
+
else:
|
|
456
|
+
if self._block_tag in self._canvas.bindtags():
|
|
457
|
+
tags = list(self._canvas.bindtags())
|
|
458
|
+
tags.remove(self._block_tag)
|
|
459
|
+
self._canvas.bindtags(tuple(tags))
|
|
460
|
+
if hasattr(self, "_text_label") and self._block_tag in self._text_label.bindtags():
|
|
461
|
+
tags = list(self._text_label.bindtags())
|
|
462
|
+
tags.remove(self._block_tag)
|
|
463
|
+
self._text_label.bindtags(tuple(tags))
|