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.
Files changed (101) hide show
  1. scustomtkinter/__init__.py +81 -0
  2. scustomtkinter/assets/images/error.png +0 -0
  3. scustomtkinter/assets/images/info.png +0 -0
  4. scustomtkinter/assets/images/warning.png +0 -0
  5. scustomtkinter/assets/sCTkThemes.json +1963 -0
  6. scustomtkinter/license_morecustomtkinterwidgets.txt +22 -0
  7. scustomtkinter/sctk_button_primary.py +366 -0
  8. scustomtkinter/sctk_button_secondary.py +354 -0
  9. scustomtkinter/sctk_button_tertiary.py +375 -0
  10. scustomtkinter/sctk_checkbox.py +270 -0
  11. scustomtkinter/sctk_combobox.py +155 -0
  12. scustomtkinter/sctk_core.py +15 -0
  13. scustomtkinter/sctk_dial.py +1472 -0
  14. scustomtkinter/sctk_dialog.py +701 -0
  15. scustomtkinter/sctk_dialog_toplevel.py +268 -0
  16. scustomtkinter/sctk_entry_primary.py +400 -0
  17. scustomtkinter/sctk_entry_secondary.py +342 -0
  18. scustomtkinter/sctk_file_explorer.py +879 -0
  19. scustomtkinter/sctk_frame.py +210 -0
  20. scustomtkinter/sctk_frame_labeled_primary.py +667 -0
  21. scustomtkinter/sctk_frame_labeled_secondary.py +598 -0
  22. scustomtkinter/sctk_frequency_display.py +636 -0
  23. scustomtkinter/sctk_label_primary.py +283 -0
  24. scustomtkinter/sctk_label_secondary.py +275 -0
  25. scustomtkinter/sctk_label_tertiary.py +278 -0
  26. scustomtkinter/sctk_messagebox.py +275 -0
  27. scustomtkinter/sctk_notebook.py +1103 -0
  28. scustomtkinter/sctk_optionmenu_border_mixin.py +203 -0
  29. scustomtkinter/sctk_optionmenu_primary.py +378 -0
  30. scustomtkinter/sctk_optionmenu_secondary.py +346 -0
  31. scustomtkinter/sctk_path_chooser.py +516 -0
  32. scustomtkinter/sctk_progress_bar.py +274 -0
  33. scustomtkinter/sctk_radiobutton.py +360 -0
  34. scustomtkinter/sctk_scroll_mixin.py +546 -0
  35. scustomtkinter/sctk_scrollable_frame.py +795 -0
  36. scustomtkinter/sctk_scrollbar.py +313 -0
  37. scustomtkinter/sctk_segmentedbutton.py +424 -0
  38. scustomtkinter/sctk_selector.py +581 -0
  39. scustomtkinter/sctk_separator.py +308 -0
  40. scustomtkinter/sctk_slider.py +264 -0
  41. scustomtkinter/sctk_smeter.py +405 -0
  42. scustomtkinter/sctk_smeter_bar.py +557 -0
  43. scustomtkinter/sctk_spinbox.py +664 -0
  44. scustomtkinter/sctk_switch.py +463 -0
  45. scustomtkinter/sctk_tableview.py +869 -0
  46. scustomtkinter/sctk_tabview.py +409 -0
  47. scustomtkinter/sctk_textbox_primary.py +291 -0
  48. scustomtkinter/sctk_textbox_secondary.py +225 -0
  49. scustomtkinter/sctk_toplevel.py +153 -0
  50. scustomtkinter/themeable_widget.py +938 -0
  51. scustomtkinter-0.2.1.dist-info/METADATA +336 -0
  52. scustomtkinter-0.2.1.dist-info/RECORD +101 -0
  53. scustomtkinter-0.2.1.dist-info/WHEEL +5 -0
  54. scustomtkinter-0.2.1.dist-info/entry_points.txt +2 -0
  55. scustomtkinter-0.2.1.dist-info/licenses/LICENSE +21 -0
  56. scustomtkinter-0.2.1.dist-info/licenses/scustomtkinter/license_morecustomtkinterwidgets.txt +22 -0
  57. scustomtkinter-0.2.1.dist-info/top_level.txt +2 -0
  58. scustomtkinter_pygubu/__init__.py +1 -0
  59. scustomtkinter_pygubu/designer/__init__.py +0 -0
  60. scustomtkinter_pygubu/designer/plugin.py +1854 -0
  61. scustomtkinter_pygubu/designer/properties.py +247 -0
  62. scustomtkinter_pygubu/sCTkButtonPrimarybo.py +46 -0
  63. scustomtkinter_pygubu/sCTkButtonSecondarybo.py +45 -0
  64. scustomtkinter_pygubu/sCTkButtonTertiarybo.py +44 -0
  65. scustomtkinter_pygubu/sCTkCheckBoxbo.py +45 -0
  66. scustomtkinter_pygubu/sCTkComboBoxbo.py +45 -0
  67. scustomtkinter_pygubu/sCTkCorebo.py +120 -0
  68. scustomtkinter_pygubu/sCTkDialbo.py +371 -0
  69. scustomtkinter_pygubu/sCTkDialogbo.py +397 -0
  70. scustomtkinter_pygubu/sCTkEntryPrimarybo.py +100 -0
  71. scustomtkinter_pygubu/sCTkEntrySecondarybo.py +98 -0
  72. scustomtkinter_pygubu/sCTkFileExplorerbo.py +189 -0
  73. scustomtkinter_pygubu/sCTkFrameLabeledPrimarybo.py +69 -0
  74. scustomtkinter_pygubu/sCTkFrameLabeledSecondarybo.py +71 -0
  75. scustomtkinter_pygubu/sCTkFramebo.py +46 -0
  76. scustomtkinter_pygubu/sCTkFrequencyDisplaybo.py +175 -0
  77. scustomtkinter_pygubu/sCTkLabelPrimarybo.py +45 -0
  78. scustomtkinter_pygubu/sCTkLabelSecondarybo.py +44 -0
  79. scustomtkinter_pygubu/sCTkLabelTertiarybo.py +45 -0
  80. scustomtkinter_pygubu/sCTkNotebookbo.py +392 -0
  81. scustomtkinter_pygubu/sCTkOptionMenuPrimarybo.py +45 -0
  82. scustomtkinter_pygubu/sCTkOptionMenuSecondarybo.py +39 -0
  83. scustomtkinter_pygubu/sCTkPathChooserbo.py +204 -0
  84. scustomtkinter_pygubu/sCTkProgressBarbo.py +45 -0
  85. scustomtkinter_pygubu/sCTkRadioButtonbo.py +45 -0
  86. scustomtkinter_pygubu/sCTkSMeterBarbo.py +153 -0
  87. scustomtkinter_pygubu/sCTkSMeterbo.py +101 -0
  88. scustomtkinter_pygubu/sCTkScrollableFramebo.py +66 -0
  89. scustomtkinter_pygubu/sCTkScrollbarbo.py +48 -0
  90. scustomtkinter_pygubu/sCTkSegmentedButtonbo.py +47 -0
  91. scustomtkinter_pygubu/sCTkSelectorbo.py +167 -0
  92. scustomtkinter_pygubu/sCTkSeparatorbo.py +141 -0
  93. scustomtkinter_pygubu/sCTkSliderbo.py +45 -0
  94. scustomtkinter_pygubu/sCTkSpinboxbo.py +200 -0
  95. scustomtkinter_pygubu/sCTkSwitchbo.py +45 -0
  96. scustomtkinter_pygubu/sCTkTableviewbo.py +188 -0
  97. scustomtkinter_pygubu/sCTkTabviewbo.py +226 -0
  98. scustomtkinter_pygubu/sCTkTextboxPrimarybo.py +45 -0
  99. scustomtkinter_pygubu/sCTkTextboxSecondarybo.py +45 -0
  100. scustomtkinter_pygubu/sCTkToplevelbo.py +55 -0
  101. 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))