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,283 @@
1
+ #!/usr/bin/python3
2
+ """
3
+ sCTkLabelPrimary
4
+
5
+ A theme-compliant, high-emphasis header label widget -- the most prominent of
6
+ the library's three label tiers (see also sCTkLabelSecondary,
7
+ sCTkLabelTertiary). Inherits directly from ctk.CTkLabel so CustomTkinter
8
+ handles native rendering; this class layers automatic light/dark theme
9
+ resolution and a distinct enabled/disabled visual state on top.
10
+
11
+ Base class order matters here: `class sCTkLabelPrimary(ctk.CTkLabel,
12
+ ThemeableWidget)` puts the native CTk class first, so every `super()` call in
13
+ this file's own methods resolves to ctk.CTkLabel -- and, beneath it,
14
+ tkinter.Misc -- never to ThemeableWidget. ThemeableWidget's own
15
+ configure()/cget()/_set_appearance_mode() overrides have been removed entirely
16
+ for this reason (see themeable_widget.py's docstring); this widget owns all of
17
+ its own runtime color-swapping logic.
18
+
19
+ Unlike the button/checkbox family, "disabled" here is purely visual (dimmed
20
+ text color) -- CTkLabel has no native click handling to block in the first
21
+ place, so there's no equivalent of the click-passthrough bug found and fixed
22
+ on the button widgets.
23
+
24
+ Internal state attribute is named self._custom_current_state, matching the
25
+ convention used across the rest of the library (sCTkComboBox, the button
26
+ family, sCTkCheckBox, etc.) -- an earlier version of this file used
27
+ self._current_state instead, which was inconsistent both within the label
28
+ family (sCTkLabelSecondary already used _custom_current_state) and against
29
+ every other themed widget. Purely an internal rename; doesn't affect the
30
+ public state()/get_state() API.
31
+ """
32
+ from typing import Any, Optional
33
+ import customtkinter as ctk
34
+ from .themeable_widget import ThemeableWidget
35
+
36
+
37
+ class sCTkLabelPrimary(ctk.CTkLabel, ThemeableWidget):
38
+ """Themeable, high-emphasis header label.
39
+
40
+ Adds to native ctk.CTkLabel:
41
+ - Automatic light/dark theme resolution from sCTkThemes.json (via
42
+ ThemeableWidget.__init__ -- see that class's docstring for what it does,
43
+ and just as importantly, what it no longer does).
44
+ - A distinct enabled/disabled visual state -- purely a text-color dim,
45
+ since labels have no native interactivity to block.
46
+ - Pygubu Designer property introspection for `state`, `fg_color`, and
47
+ `text_color` via a single-argument configure() call, and a cget()
48
+ override so querying "state" doesn't raise (native CTkLabel has no
49
+ real "state" option to cget in the first place).
50
+
51
+ Colors are passed through to configure() as raw (light, dark) tuples rather
52
+ than pre-resolved to a single value, so CustomTkinter's own appearance-mode
53
+ tracking repaints them automatically on a light/dark switch -- the same
54
+ approach validated on sCTkComboBox, sCTkSegmentedButton, and the button
55
+ family. Not separately re-confirmed for this specific widget.
56
+ """
57
+
58
+ def __init__(self, master: Optional[Any] = None, **kwargs: Any) -> None:
59
+ """
60
+ Args:
61
+ master: Parent container.
62
+ **kwargs: `state` is pulled out explicitly below (applied after
63
+ construction, via self.configure(), rather than passed to the
64
+ native constructor). Everything else is either a native
65
+ CTkLabel argument (e.g. `text`, `font`) or a theme-key override
66
+ (see the "sCTkLabelPrimary" block in sCTkThemes.json).
67
+ """
68
+ # 1. Capture the initial state before ThemeableWidget's own kwargs pass,
69
+ # so it doesn't get merged into final_kw and sent to the native
70
+ # constructor (which doesn't accept a "state" argument the way this
71
+ # widget's own state() method expects).
72
+ # NO PLACEHOLDER TEXT. CustomTkinter defaults `text` to the class
73
+ # name, so a CTkLabel built without text reads "CTkLabel". Nothing wants
74
+ # that in a real interface, and the Designer made it worse: clearing
75
+ # the text field removes the property, the canvas rebuilds the widget
76
+ # from what is left, and the placeholder appeared in place of the
77
+ # blank that was asked for.
78
+ kwargs.setdefault("text", "")
79
+ state_init = kwargs.pop("state", "normal")
80
+
81
+ # 2. Fire our shared theme logic. This resolves final_kw
82
+ # (construction-time properties) and the disabled color map. See
83
+ # ThemeableWidget.__init__ for what actually happens here.
84
+ ThemeableWidget.__init__(self, kwargs)
85
+
86
+ # 3. Deep-copy the resolved map onto this instance, so later changes
87
+ # here never leak back into the shared theme registry.
88
+ self._local_defaults = dict(self.final_kw)
89
+ self._custom_disabled_map = dict(self._widget_disabled_map)
90
+
91
+ # Remove any stray "state" key from final_kw -- it was already
92
+ # extracted above, but this guards against it being reintroduced by
93
+ # a theme override with the same name.
94
+ self.final_kw.pop("state", None)
95
+
96
+ # 4. Initialize CustomTkinter natively with the clean final kwargs array.
97
+ super().__init__(master, **self.final_kw)
98
+
99
+ # 5. Apply the requested initial state now that the native widget exists.
100
+ self._custom_current_state = "normal"
101
+ self.configure(state=state_init)
102
+
103
+ # 6. Register lifecycle handshake hook, notifying Pygubu-style consumers
104
+ # that construction is complete.
105
+ self._finalize_themeable_lifecycle()
106
+
107
+ def _set_appearance_mode(self, mode_string: str) -> None:
108
+ """
109
+ Forwards CustomTkinter's internal light/dark mode change notification to
110
+ the native widget.
111
+
112
+ No longer manually re-triggers _update_current_visual_state(). That
113
+ method now passes raw (light, dark) tuples straight through to
114
+ configure() instead of pre-resolving to a single color, so CTk's own
115
+ appearance-mode tracking should repaint correctly on its own -- the
116
+ same approach validated on sCTkComboBox, sCTkSegmentedButton, and the
117
+ button family.
118
+
119
+ Args:
120
+ mode_string: The new appearance mode ("Light" or "Dark"), as passed
121
+ by CustomTkinter's internal appearance-mode change machinery.
122
+ """
123
+ if hasattr(super(), "_set_appearance_mode"):
124
+ try:
125
+ super()._set_appearance_mode(mode_string)
126
+ except Exception:
127
+ pass
128
+
129
+ def configure(self, *args: Any, **kwargs: Any) -> Any:
130
+ """
131
+ Standard widget configuration, with Pygubu/positional-argument handling.
132
+
133
+ Args:
134
+ *args: At most one positional argument is meaningful:
135
+ - a dict: merged into kwargs and processed normally below.
136
+ - the literal string "state": returns a Tkinter-style
137
+ (name, name, name, default, current) tuple.
138
+ - "fg_color" or "text_color": returns the same style of tuple,
139
+ with `current` reflecting the disabled or normal value as
140
+ appropriate. Note the returned value is str(value), where
141
+ value may itself be a (light, dark) tuple rather than a
142
+ single resolved color -- a known limitation shared with the
143
+ wider Pygubu-query investigation set aside elsewhere in this
144
+ project, not fixed here.
145
+ - anything else: forwarded directly to the native widget's
146
+ configure(), which does not support single-argument property
147
+ queries for arbitrary properties (same limitation).
148
+ **kwargs: Standard CTkLabel configuration options, plus: passing
149
+ `state=...` routes through self.state() rather than being
150
+ forwarded as-is.
151
+
152
+ Returns:
153
+ The query tuple described above for the single-argument case, or
154
+ whatever super().configure() returns for the keyword-argument case
155
+ (typically None).
156
+ """
157
+ # args is always a tuple -- args[0] is the actual value passed, whether
158
+ # that's a string or a dict. An earlier version of this method compared
159
+ # the wrapped tuple directly (`pname = args`), so the query branches
160
+ # below never matched anything; every single-argument call silently
161
+ # fell through to native configure() instead. Don't reintroduce that.
162
+ if len(args) == 1:
163
+ if isinstance(args[0], dict):
164
+ kwargs = {**args[0], **kwargs}
165
+ else:
166
+ pname = args[0]
167
+ if pname == "state":
168
+ return ("state", "state", "state", "normal", str(self.state()))
169
+
170
+ if pname in ["fg_color", "text_color"]:
171
+ current_state = str(self.state()).lower()
172
+ val = self._custom_disabled_map.get(pname) if current_state == "disabled" else self._local_defaults.get(pname)
173
+ return (pname, pname, pname, self._query_value(self._theme_default(pname)), self._query_value(val))
174
+
175
+ return self._configure_query(pname)
176
+
177
+ # Runtime overrides have to reach the map a repaint reads, or
178
+ # _update_current_visual_state() puts the theme value straight back --
179
+ # see ThemeableWidget._record_theme_overrides().
180
+ self._record_theme_overrides(kwargs)
181
+
182
+ if "state" in kwargs:
183
+ self.state(kwargs.pop("state"))
184
+
185
+ # Theme values cleared to "" are dropped; `text=""` is kept, so a
186
+ # label can actually be blanked. See ThemeableWidget._CONTENT_KEYS.
187
+ self._drop_cleared(kwargs)
188
+
189
+ if kwargs:
190
+ return super().configure(**kwargs)
191
+ return None
192
+
193
+ # Tkinter/CTk convention binds .config to .configure as a SEPARATE class
194
+ # attribute -- it does not automatically track whichever configure() a
195
+ # subclass defines. Without this line, calling .config(...) on an instance
196
+ # would silently skip this entire override and land on the native widget's
197
+ # configure() directly, bypassing theming and state handling entirely.
198
+ config = configure
199
+
200
+ def cget(self, attribute_name: str) -> Any:
201
+ """
202
+ Intercepts "state" queries so they route through state() instead of
203
+ being forwarded to the native widget, which has no real "state" option
204
+ to cget in the first place and would otherwise raise.
205
+
206
+ Args:
207
+ attribute_name: The property name being queried.
208
+
209
+ Returns:
210
+ self.state() if attribute_name is "state" (case-insensitive),
211
+ otherwise whatever the native CTkLabel.cget() returns.
212
+ """
213
+ if str(attribute_name).lower() == "state":
214
+ return self.state()
215
+ return super().cget(attribute_name)
216
+
217
+ def get_state(self) -> str:
218
+ """Equivalent to calling state() with no argument."""
219
+ return self.state()
220
+
221
+ def state(self, mode: Optional[str] = None) -> str:
222
+ """
223
+ Gets or sets the widget's enabled/disabled visual state.
224
+
225
+ Args:
226
+ mode: If None, returns the current state without changing anything.
227
+ Otherwise, only the literal string "disabled" (case-insensitive)
228
+ is treated as disabled; anything in ("normal", "enabled",
229
+ "active") is treated as enabled. Any other value matches
230
+ neither branch and leaves the state unchanged.
231
+
232
+ Returns:
233
+ The resulting state ("normal" or "disabled", lowercase).
234
+ """
235
+ if mode is None:
236
+ return str(getattr(self, "_custom_current_state", "normal")).lower()
237
+
238
+ mode = mode.lower()
239
+ if mode in ("normal", "enabled", "active"):
240
+ self._custom_current_state = "normal"
241
+ self._update_current_visual_state()
242
+ elif mode == "disabled":
243
+ self._custom_current_state = "disabled"
244
+ self._update_current_visual_state()
245
+
246
+ return self._custom_current_state
247
+
248
+ def _update_current_visual_state(self) -> None:
249
+ """
250
+ Recomputes and applies this widget's colors from the theme file, based
251
+ on the current state.
252
+
253
+ Called after construction and on every state() change.
254
+
255
+ Passes raw (light, dark) tuples straight through to configure() instead
256
+ of resolving to a single color first, so CTk's native tracking can
257
+ handle appearance-mode repaints without help from _set_appearance_mode.
258
+
259
+ text_color is required to be present in whichever map is active (the
260
+ top-level block for normal state, disabled_map for disabled) -- if
261
+ it's missing, this raises immediately rather than silently falling
262
+ back to a default or silently omitting the property. An earlier
263
+ version fell back to CTk's own global ThemeManager default; per this
264
+ project's design, ThemeableWidget-based widgets are meant to fail hard
265
+ on incomplete theme data, not paper over it.
266
+ """
267
+ is_disabled = self._custom_current_state == "disabled"
268
+ target_map = self._custom_disabled_map if is_disabled else self._local_defaults
269
+
270
+ if target_map.get("text_color") is None:
271
+ raise KeyError(
272
+ f"'{getattr(self, '_THEME_BLOCK_NAME', None) or self.__class__.__name__}' "
273
+ f"theme block is missing 'text_color' in its "
274
+ f"{'disabled_map' if is_disabled else 'top-level'} section of sCTkThemes.json."
275
+ )
276
+
277
+ config_payload = {}
278
+ for key in ("fg_color", "text_color", "font"):
279
+ val = target_map.get(key)
280
+ if val is not None:
281
+ config_payload[key] = val
282
+
283
+ super().configure(**config_payload)
@@ -0,0 +1,275 @@
1
+ #!/usr/bin/python3
2
+ """
3
+ sCTkLabelSecondary
4
+
5
+ A theme-compliant, mid-emphasis interface label widget -- sits between
6
+ sCTkLabelPrimary and sCTkLabelTertiary in the library's three label tiers.
7
+ Inherits directly from ctk.CTkLabel so CustomTkinter handles native rendering;
8
+ this class layers automatic light/dark theme resolution and a distinct
9
+ enabled/disabled visual state on top.
10
+
11
+ Base class order matters here: `class sCTkLabelSecondary(ctk.CTkLabel,
12
+ ThemeableWidget)` puts the native CTk class first, so every `super()` call in
13
+ this file's own methods resolves to ctk.CTkLabel -- and, beneath it,
14
+ tkinter.Misc -- never to ThemeableWidget. ThemeableWidget's own
15
+ configure()/cget()/_set_appearance_mode() overrides have been removed entirely
16
+ for this reason (see themeable_widget.py's docstring); this widget owns all of
17
+ its own runtime color-swapping logic.
18
+
19
+ Unlike the button/checkbox family, "disabled" here is purely visual (dimmed
20
+ text color) -- CTkLabel has no native click handling to block in the first
21
+ place.
22
+ """
23
+ from typing import Any, Optional
24
+ import customtkinter as ctk
25
+ from .themeable_widget import ThemeableWidget
26
+
27
+
28
+ class sCTkLabelSecondary(ctk.CTkLabel, ThemeableWidget):
29
+ """Themeable, mid-emphasis interface label.
30
+
31
+ Adds to native ctk.CTkLabel:
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 distinct enabled/disabled visual state -- purely a text-color dim,
36
+ since labels have no native interactivity to block.
37
+ - Pygubu Designer property introspection for `state`, `fg_color`, and
38
+ `text_color` via a single-argument configure() call, plus a cget()
39
+ override so querying "state" doesn't raise (native CTkLabel has no
40
+ real "state" option to cget in the first place).
41
+
42
+ Colors are passed through to configure() as raw (light, dark) tuples rather
43
+ than pre-resolved to a single value, so CustomTkinter's own appearance-mode
44
+ tracking repaints them automatically on a light/dark switch -- the same
45
+ approach validated on sCTkComboBox, sCTkSegmentedButton, and the button
46
+ family. Not separately re-confirmed for this specific widget.
47
+ """
48
+
49
+ def __init__(self, master: Optional[Any] = None, **kwargs: Any) -> None:
50
+ """
51
+ Args:
52
+ master: Parent container.
53
+ **kwargs: `state` is pulled out explicitly below (applied after
54
+ construction, via self.configure(), rather than passed to the
55
+ native constructor). Everything else is either a native
56
+ CTkLabel argument (e.g. `text`, `font`) or a theme-key override
57
+ (see the "sCTkLabelSecondary" block in sCTkThemes.json).
58
+ """
59
+ # 1. Capture the initial state before ThemeableWidget's own kwargs pass,
60
+ # so it doesn't get merged into final_kw and sent to the native
61
+ # constructor.
62
+ # NO PLACEHOLDER TEXT. CustomTkinter defaults `text` to the class
63
+ # name, so a CTkLabel built without text reads "CTkLabel". Nothing wants
64
+ # that in a real interface, and the Designer made it worse: clearing
65
+ # the text field removes the property, the canvas rebuilds the widget
66
+ # from what is left, and the placeholder appeared in place of the
67
+ # blank that was asked for.
68
+ kwargs.setdefault("text", "")
69
+ state_init = kwargs.pop("state", "normal")
70
+
71
+ # 2. Fire our shared theme logic. This resolves final_kw
72
+ # (construction-time properties) and the disabled color map. See
73
+ # ThemeableWidget.__init__ for what actually happens here.
74
+ ThemeableWidget.__init__(self, kwargs)
75
+
76
+ # 3. Deep-copy the resolved map onto this instance, so later changes
77
+ # here never leak back into the shared theme registry.
78
+ self._local_defaults = dict(self.final_kw)
79
+ self._custom_disabled_map = dict(self._widget_disabled_map)
80
+
81
+ # Remove any stray "state" key from final_kw -- already extracted
82
+ # above, but guards against it being reintroduced by a theme override.
83
+ self.final_kw.pop("state", None)
84
+
85
+ # 4. Initialize CustomTkinter natively with the clean final kwargs array.
86
+ super().__init__(master, **self.final_kw)
87
+
88
+ # 5. Apply the requested initial state now that the native widget exists.
89
+ self._custom_current_state = "normal"
90
+ self.configure(state=state_init)
91
+
92
+ # 6. Register lifecycle handshake hook, notifying Pygubu-style consumers
93
+ # that construction is complete.
94
+ self._finalize_themeable_lifecycle()
95
+
96
+ def _set_appearance_mode(self, mode_string: str) -> None:
97
+ """
98
+ Forwards CustomTkinter's internal light/dark mode change notification to
99
+ the native widget.
100
+
101
+ No longer manually re-triggers _update_current_visual_state(). That
102
+ method now passes raw (light, dark) tuples straight through to
103
+ configure() instead of pre-resolving to a single color, so CTk's own
104
+ appearance-mode tracking should repaint correctly on its own -- the
105
+ same approach validated on sCTkComboBox, sCTkSegmentedButton, and the
106
+ button family.
107
+
108
+ Args:
109
+ mode_string: The new appearance mode ("Light" or "Dark"), as passed
110
+ by CustomTkinter's internal appearance-mode change machinery.
111
+ """
112
+ if hasattr(super(), "_set_appearance_mode"):
113
+ try:
114
+ super()._set_appearance_mode(mode_string)
115
+ except Exception:
116
+ pass
117
+
118
+ def configure(self, *args: Any, **kwargs: Any) -> Any:
119
+ """
120
+ Standard widget configuration, with Pygubu/positional-argument handling.
121
+
122
+ Args:
123
+ *args: At most one positional argument is meaningful:
124
+ - a dict: merged into kwargs and processed normally below.
125
+ - the literal string "state": returns a Tkinter-style
126
+ (name, name, name, default, current) tuple.
127
+ - "fg_color" or "text_color": returns the same style of tuple,
128
+ with `current` reflecting the disabled or normal value as
129
+ appropriate. Note the returned value is str(value), where
130
+ value may itself be a (light, dark) tuple rather than a
131
+ single resolved color -- a known limitation shared with the
132
+ wider Pygubu-query investigation set aside elsewhere in this
133
+ project, not fixed here.
134
+ - anything else: forwarded directly to the native widget's
135
+ configure(), which does not support single-argument property
136
+ queries for arbitrary properties (same limitation).
137
+ **kwargs: Standard CTkLabel configuration options, plus: passing
138
+ `state=...` routes through self.state() rather than being
139
+ forwarded as-is.
140
+
141
+ Returns:
142
+ The query tuple described above for the single-argument case, or
143
+ whatever super().configure() returns for the keyword-argument case
144
+ (typically None).
145
+ """
146
+ # args is always a tuple -- args[0] is the actual value passed, whether
147
+ # that's a string or a dict. An earlier version of this method compared
148
+ # the wrapped tuple directly (`pname = args`), so the query branches
149
+ # below never matched anything. Don't reintroduce that.
150
+ if len(args) == 1:
151
+ if isinstance(args[0], dict):
152
+ kwargs = {**args[0], **kwargs}
153
+ else:
154
+ pname = args[0]
155
+ if pname == "state":
156
+ return ("state", "state", "state", "normal", str(self.state()))
157
+
158
+ if pname in ["fg_color", "text_color"]:
159
+ current_state = str(self.state()).lower()
160
+ val = self._custom_disabled_map.get(pname) if current_state == "disabled" else self._local_defaults.get(pname)
161
+ return (pname, pname, pname, self._query_value(self._theme_default(pname)), self._query_value(val))
162
+
163
+ return self._configure_query(pname)
164
+
165
+ # Runtime overrides have to reach the map a repaint reads, or
166
+ # _update_current_visual_state() puts the theme value straight back --
167
+ # see ThemeableWidget._record_theme_overrides().
168
+ self._record_theme_overrides(kwargs)
169
+
170
+ if "state" in kwargs:
171
+ self.state(kwargs.pop("state"))
172
+
173
+ # Theme values cleared to "" are dropped; `text=""` is kept, so a
174
+ # label can actually be blanked. See ThemeableWidget._CONTENT_KEYS.
175
+ self._drop_cleared(kwargs)
176
+
177
+ if kwargs:
178
+ return super().configure(**kwargs)
179
+ return None
180
+
181
+ # Tkinter/CTk convention binds .config to .configure as a SEPARATE class
182
+ # attribute -- it does not automatically track whichever configure() a
183
+ # subclass defines. An earlier version of this file was MISSING this line
184
+ # entirely: calling .config(...) on an instance silently skipped this
185
+ # entire override and landed on the native widget's configure() directly,
186
+ # bypassing theming and state handling completely -- confirmed as a
187
+ # critical bug on sCTkSegmentedButton earlier in this project's audit; the
188
+ # same fix applies here.
189
+ config = configure
190
+
191
+ def cget(self, attribute_name: str) -> Any:
192
+ """
193
+ Intercepts "state" queries so they route through state() instead of
194
+ being forwarded to the native widget, which has no real "state" option
195
+ to cget in the first place and would otherwise raise.
196
+
197
+ Args:
198
+ attribute_name: The property name being queried.
199
+
200
+ Returns:
201
+ self.state() if attribute_name is "state" (case-insensitive),
202
+ otherwise whatever the native CTkLabel.cget() returns.
203
+ """
204
+ if str(attribute_name).lower() == "state":
205
+ return self.state()
206
+ return super().cget(attribute_name)
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 enabled/disabled visual state.
215
+
216
+ Args:
217
+ mode: If None, returns the current state without changing anything.
218
+ Otherwise, only the literal string "disabled" (case-insensitive)
219
+ is treated as disabled; anything in ("normal", "enabled",
220
+ "active") is treated as enabled. Any other value matches
221
+ neither branch and leaves the state unchanged.
222
+
223
+ Returns:
224
+ The resulting state ("normal" or "disabled", lowercase).
225
+ """
226
+ if mode is None:
227
+ return str(getattr(self, "_custom_current_state", "normal")).lower()
228
+
229
+ mode = mode.lower()
230
+ if mode in ("normal", "enabled", "active"):
231
+ self._custom_current_state = "normal"
232
+ self._update_current_visual_state()
233
+
234
+ elif mode == "disabled":
235
+ self._custom_current_state = "disabled"
236
+ self._update_current_visual_state()
237
+
238
+ return self._custom_current_state
239
+
240
+ def _update_current_visual_state(self) -> None:
241
+ """
242
+ Recomputes and applies this widget's colors from the theme file, based
243
+ on the current state.
244
+
245
+ Called after construction and on every state() change.
246
+
247
+ Passes raw (light, dark) tuples straight through to configure() instead
248
+ of resolving to a single color first, so CTk's native tracking can
249
+ handle appearance-mode repaints without help from _set_appearance_mode.
250
+
251
+ text_color is required to be present in whichever map is active (the
252
+ top-level block for normal state, disabled_map for disabled) -- if
253
+ it's missing, this raises immediately rather than silently omitting
254
+ the property, which is what an earlier version of this method did (no
255
+ fallback and no error -- text_color would simply never get set). Per
256
+ this project's design, ThemeableWidget-based widgets are meant to fail
257
+ hard on incomplete theme data, not silently do nothing.
258
+ """
259
+ is_disabled = self._custom_current_state == "disabled"
260
+ target_map = self._custom_disabled_map if is_disabled else self._local_defaults
261
+
262
+ if target_map.get("text_color") is None:
263
+ raise KeyError(
264
+ f"'{getattr(self, '_THEME_BLOCK_NAME', None) or self.__class__.__name__}' "
265
+ f"theme block is missing 'text_color' in its "
266
+ f"{'disabled_map' if is_disabled else 'top-level'} section of sCTkThemes.json."
267
+ )
268
+
269
+ config_payload = {}
270
+ for key in ("fg_color", "text_color", "font"):
271
+ val = target_map.get(key)
272
+ if val is not None:
273
+ config_payload[key] = val
274
+
275
+ super().configure(**config_payload)