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,424 @@
1
+ #!/usr/bin/python3
2
+ """
3
+ sCTkSegmentedButton
4
+
5
+ A theme-compliant segmented button strip widget: a horizontal row of connected
6
+ text buttons where selecting one automatically unselects the others (similar to
7
+ a row of radio buttons). Inherits directly from ctk.CTkSegmentedButton so that
8
+ CustomTkinter handles native state changes, input locks, and button rendering;
9
+ this class layers automatic light/dark theme resolution and a distinct
10
+ enabled/disabled visual state on top.
11
+
12
+ Base class order matters here: `class sCTkSegmentedButton(ctk.CTkSegmentedButton,
13
+ ThemeableWidget)` puts the native CTk class first. That means every `super()`
14
+ call in this file's own methods (configure(), _set_appearance_mode(), etc.)
15
+ resolves to ctk.CTkSegmentedButton -- and, beneath it, tkinter.Misc -- never to
16
+ ThemeableWidget. ThemeableWidget's own configure()/cget()/_set_appearance_mode()
17
+ overrides have been removed entirely for this reason (see themeable_widget.py's
18
+ docstring); this widget owns all of its own runtime color-swapping logic.
19
+ """
20
+ from typing import Any, Callable, Optional
21
+ import tkinter as tk
22
+ import customtkinter as ctk
23
+ from .themeable_widget import ThemeableWidget, defer
24
+
25
+
26
+ class sCTkSegmentedButton(ctk.CTkSegmentedButton, ThemeableWidget):
27
+ """Themeable segmented button strip.
28
+
29
+ Adds to native ctk.CTkSegmentedButton:
30
+ - Automatic light/dark theme resolution from sCTkThemes.json (via
31
+ ThemeableWidget.__init__ -- see that class's docstring for what it does,
32
+ and just as importantly, what it no longer does).
33
+ - A distinct enabled/disabled visual state, tracked in
34
+ self._custom_current_state and swapped via _apply_custom_theme_colors().
35
+ - Per-segment text-color handling that keeps the currently-selected
36
+ segment visually distinct even while the whole control is disabled.
37
+
38
+ Every color this widget applies comes from sCTkThemes.json; there are no
39
+ hardcoded hex values in this class. Colors are passed through to configure()
40
+ as raw (light, dark) tuples rather than pre-resolved to a single value, so
41
+ CustomTkinter's own appearance-mode tracking repaints them automatically on
42
+ a light/dark switch -- confirmed by direct testing, including while disabled.
43
+ """
44
+
45
+ # What a segmented button shows when `values` has never been set.
46
+ #
47
+ # This is CustomTkinter's own placeholder, a hardcoded literal in
48
+ # CTkSegmentedButton.__init__:
49
+ #
50
+ # self._value_list: List[str] = ["CTkSegmentedButton"]
51
+ #
52
+ # Note it does NOT carry this library's "s" prefix, because the string was
53
+ # written by hand rather than derived from the class -- a subclass shows it
54
+ # unchanged.
55
+ #
56
+ # Clearing the property in the Designer restores exactly this, so a cleared
57
+ # field matches a freshly placed widget. An earlier version invented its
58
+ # own placeholder here, which meant "clear" produced something the widget
59
+ # never shows on its own.
60
+ DEFAULT_VALUES = ("CTkSegmentedButton",)
61
+
62
+ def __init__(self, master: Optional[Any] = None, **kw: Any) -> None:
63
+ """
64
+ Args:
65
+ master: Parent container.
66
+ **kw: `values` (list[str]), `variable` (tkinter.StringVar), and
67
+ `command` (callable) are pulled out explicitly below. Everything
68
+ else is either a native CTkSegmentedButton argument or a
69
+ theme-key override (see the "sCTkSegmentedButton" block in
70
+ sCTkThemes.json).
71
+
72
+ Note: `unselected_color`, `unselected_hover_color`, `border_width`,
73
+ `border_color`, and `selected_color_padding` are silently dropped from
74
+ the constructor kwargs below, regardless of what's passed in here --
75
+ they're only ever sourced from the theme file. See
76
+ _apply_custom_theme_colors() for why `unselected_color` in particular
77
+ is never independently configurable even there.
78
+ """
79
+ # 1. Capture widget-specific attributes early, before the mixin's own
80
+ # configuration pass. These must come out of kw BEFORE ThemeableWidget.__init__
81
+ # runs, since that call treats every remaining key in kw as a theme-overridable
82
+ # property and would otherwise try to merge these into final_kw too.
83
+ values = kw.pop("values", None)
84
+ variable: Optional[tk.StringVar] = kw.pop("variable", None)
85
+ command: Optional[Callable] = kw.pop("command", None)
86
+
87
+ # 2. Fire our shared theme logic to map properties natively out of themes.json.
88
+ # This resolves final_kw (construction-time properties) and the disabled/pressed/
89
+ # alarm color maps. See ThemeableWidget.__init__ for what actually happens here.
90
+ ThemeableWidget.__init__(self, kw)
91
+ self._local_defaults = dict(self.final_kw)
92
+ self._custom_disabled_map = dict(self._widget_disabled_map)
93
+ self._validate_theme_keys()
94
+
95
+ # 3. Remove keys the native CTkSegmentedButton constructor either doesn't
96
+ # accept, or that would break its internal layout if set at construction
97
+ # time (the border/padding options in particular). These are applied later,
98
+ # after construction, exclusively through _apply_custom_theme_colors() --
99
+ # except unselected_color, which is never sourced from the theme at all;
100
+ # it's always derived from fg_color instead, by design (see that method).
101
+ self.final_kw.pop("state", None)
102
+ for key in ["unselected_color", "unselected_hover_color", "border_width", "border_color",
103
+ "selected_color_padding"]:
104
+ self.final_kw.pop(key, None)
105
+
106
+ # 4. Initialize CustomTkinter safely with the pristine final kwargs array.
107
+ super().__init__(master, **self.final_kw)
108
+ self._custom_current_state = "normal"
109
+
110
+ if values is not None: super().configure(values=values)
111
+ if variable is not None: super().configure(variable=variable)
112
+ if command is not None: super().configure(command=command)
113
+
114
+ # Set up a delayed look-sync pass on startup. This delay is necessary,
115
+ # not cosmetic: CTkSegmentedButton builds its internal per-segment buttons
116
+ # (self._buttons_dict) asynchronously, and they don't exist yet at the
117
+ # moment super().__init__() returns above. _apply_custom_theme_colors()
118
+ # bails out early if _buttons_dict isn't populated, so calling it
119
+ # immediately here would silently do nothing.
120
+ defer(self, 15, self._apply_custom_theme_colors)
121
+ self._finalize_themeable_lifecycle()
122
+
123
+ def configure(self, *args: Any, **kwargs: Any) -> None:
124
+ """
125
+ Standard widget configuration, with Pygubu/positional-argument handling.
126
+
127
+ Args:
128
+ *args: At most one positional argument is meaningful:
129
+ - a dict: merged into kwargs and processed normally below.
130
+ - anything else (typically a property-name string): forwarded
131
+ directly to the native widget's configure(). This does NOT
132
+ return a Pygubu-style query tuple the way sCTkComboBox /
133
+ sCTkCheckBox do for a handful of hardcoded property names --
134
+ the wider Pygubu single-argument query behavior across the
135
+ library is a known follow-up item, not specific to this widget.
136
+ **kwargs: Standard CTkSegmentedButton configuration options, plus:
137
+ - values / variable / command: routed through super().configure()
138
+ individually, same as in __init__.
139
+ - state: routed through self.state() rather than forwarded
140
+ as-is (see state() for why that distinction matters).
141
+
142
+ Returns:
143
+ None in the keyword-argument case. Whatever the native
144
+ super().configure() returns in the single-positional-argument case.
145
+ """
146
+ # args is always a tuple -- args[0] is the actual value passed, whether
147
+ # that's a string or a dict. Comparing or forwarding `args` itself
148
+ # (instead of `args[0]`) was a real bug earlier in this project's audit;
149
+ # don't reintroduce it.
150
+ if len(args) == 1:
151
+ if isinstance(args[0], dict):
152
+ kwargs = {**args[0], **kwargs}
153
+ else:
154
+ if args[0] == "values":
155
+ # Without this the fall-through asks cget("values") and
156
+ # uses the answer as its own default, so blanking the
157
+ # field reported "the default is what you already have"
158
+ # and the previous list stayed on screen.
159
+ #
160
+ # Both slots go through _query_value with the property
161
+ # name, which renders a list as the JSON pygubu parses.
162
+ try:
163
+ current = list(super().cget("values"))
164
+ except Exception:
165
+ current = []
166
+ return ("values", "values", "values",
167
+ self._query_value(list(self.DEFAULT_VALUES), "values"),
168
+ self._query_value(current, "values"))
169
+
170
+ # FIX: forwarding a property NAME to a native configure()
171
+ # passes it as require_redraw -- CustomTkinter declares
172
+ # configure(self, require_redraw=False, **kwargs) -- so the call
173
+ # returns None, or raises outright where the signature takes no
174
+ # positional at all:
175
+ #
176
+ # TypeError: CTkSegmentedButton.configure() takes 1
177
+ # positional argument but 2 were given
178
+ #
179
+ # Reached whenever a field is blanked in the Designer
180
+ # inspector, which calls configure(name) to read the property's
181
+ # default and expects a Tkinter-style five-tuple back.
182
+ #
183
+ # _configure_query() builds one -- see themeable_widget.py.
184
+ return self._configure_query(args[0])
185
+
186
+ if "values" in kwargs:
187
+ values_val = kwargs.pop("values")
188
+ # An empty value restores CustomTkinter's own placeholder, so a
189
+ # cleared field matches a freshly placed widget.
190
+ if values_val in ("", None, []):
191
+ values_val = list(self.DEFAULT_VALUES)
192
+ super().configure(values=values_val)
193
+ if "variable" in kwargs: super().configure(variable=kwargs.pop("variable"))
194
+ if "command" in kwargs: super().configure(command=kwargs.pop("command"))
195
+
196
+ # Routing "state" through self.state() (rather than leaving it in kwargs
197
+ # to fall through to super().configure() below) is what keeps
198
+ # self._custom_current_state, the native interactive lock, and the
199
+ # disabled color swap all in sync. Leaving it in kwargs previously
200
+ # locked the widget correctly but left get_state() permanently wrong.
201
+ # Runtime overrides have to reach the map a repaint reads, or the
202
+ # repaint puts the theme value straight back -- see
203
+ # ThemeableWidget._record_theme_overrides().
204
+ self._record_theme_overrides(kwargs)
205
+
206
+ if "state" in kwargs:
207
+ self.state(kwargs.pop("state"))
208
+
209
+ self._drop_cleared(kwargs)
210
+
211
+ if kwargs:
212
+ super().configure(**kwargs)
213
+
214
+ # Tkinter/CTk convention binds .config to .configure as a SEPARATE class
215
+ # attribute -- it does not automatically track whichever configure() a
216
+ # subclass defines. Without this line, calling .config(...) on an instance
217
+ # would silently skip this entire override and land on the native widget's
218
+ # configure() directly, bypassing theming, state handling, and the
219
+ # positional-argument handling above.
220
+ config = configure
221
+
222
+ def _set_appearance_mode(self, mode_string: str) -> None:
223
+ """
224
+ Forwards CustomTkinter's internal light/dark mode change notification to
225
+ the native widget.
226
+
227
+ No longer manually re-triggers _apply_custom_theme_colors(). That method
228
+ now passes raw (light, dark) tuples straight through to configure()
229
+ instead of pre-resolving to a single color, so CTk's own appearance-mode
230
+ tracking repaints correctly on its own -- confirmed by direct testing.
231
+ If colors ever stop following mode changes (especially while disabled),
232
+ that's the signal this no longer holds and the manual re-trigger needs
233
+ to come back.
234
+
235
+ Args:
236
+ mode_string: The new appearance mode ("Light" or "Dark"), as passed
237
+ by CustomTkinter's internal appearance-mode change machinery.
238
+ """
239
+ if hasattr(super(), "_set_appearance_mode"):
240
+ try:
241
+ super()._set_appearance_mode(mode_string)
242
+ except Exception:
243
+ pass
244
+
245
+ def state(self, mode: Optional[str] = None) -> str:
246
+ """
247
+ Gets or sets the widget's enabled/disabled visual state.
248
+
249
+ Uses self._custom_current_state (not self._state) as its source of
250
+ truth, matching every other sCTk widget. An earlier version of this
251
+ method read/wrote self._state instead, which nothing in this class
252
+ actually updated -- the only code that did was ThemeableWidget.configure(),
253
+ which is dead code (see themeable_widget.py's docstring) -- so get_state()
254
+ always silently reported "normal" regardless of the widget's real state.
255
+
256
+ Args:
257
+ mode: If None, returns the current state without changing anything.
258
+ Otherwise, only the literal string "disabled" (case-insensitive)
259
+ is treated as disabled -- any other value, including typos, is
260
+ silently treated as "normal". No exception is raised either way.
261
+
262
+ Returns:
263
+ The resulting state, always exactly "normal" or "disabled" (lowercase).
264
+ """
265
+ if mode is None:
266
+ return str(getattr(self, "_custom_current_state", "normal")).lower()
267
+
268
+ self._custom_current_state = "disabled" if mode.lower() == "disabled" else "normal"
269
+ super().configure(state=self._custom_current_state)
270
+ self._apply_custom_theme_colors()
271
+ return self._custom_current_state
272
+
273
+ def get_state(self) -> str:
274
+ """Equivalent to calling state() with no argument."""
275
+ return self.state()
276
+
277
+ def set(self, *args: Any, **kwargs: Any) -> None:
278
+ """
279
+ Selects a segment programmatically.
280
+
281
+ Wraps the native CTkSegmentedButton.set(), then repaints, since
282
+ selecting a segment changes which one needs the "selected" highlight --
283
+ native set() alone doesn't trigger that repaint on its own.
284
+ """
285
+ super().set(*args, **kwargs)
286
+ self._apply_custom_theme_colors()
287
+
288
+ def _clicked(self, *args: Any, **kwargs: Any) -> None:
289
+ """
290
+ Internal click handler override.
291
+
292
+ Wraps CTkSegmentedButton's own _clicked() (which updates the native
293
+ selection) with a repaint, for the same reason set() needs one: the
294
+ selected segment's color needs to change to reflect the new selection.
295
+ """
296
+ super()._clicked(*args, **kwargs)
297
+ self._apply_custom_theme_colors()
298
+
299
+ # Required at the TOP LEVEL of the theme block.
300
+ #
301
+ # selected_hover_color is deliberately NOT listed: it exists in the theme
302
+ # file but is read by no code path at all -- dead data, the same situation
303
+ # pointer_color was in for the dial family. Requiring it would demand a key
304
+ # that does nothing. Either wire it up or remove it from the theme.
305
+ _REQUIRED_THEME_KEYS = ("fg_color", "selected_color", "text_color",
306
+ "unselected_hover_color")
307
+
308
+ # Required inside disabled_map. selected_text_color lives ONLY here -- there
309
+ # is no top-level equivalent, since a selected segment's normal text colour
310
+ # comes from text_color.
311
+ _REQUIRED_DISABLED_KEYS = ("fg_color", "selected_color", "text_color",
312
+ "selected_text_color")
313
+
314
+ def _validate_theme_keys(self) -> None:
315
+ """
316
+ Hard-fails at construction on an incomplete theme block, naming the
317
+ missing key and where it belongs.
318
+
319
+ Raises:
320
+ KeyError: naming the first missing key found.
321
+ """
322
+ name = self.__class__.__name__
323
+ for key in self._REQUIRED_THEME_KEYS:
324
+ if self._local_defaults.get(key) is None:
325
+ raise KeyError(
326
+ f"'{name}' theme block is missing '{key}' at the top level "
327
+ f"of sCTkThemes.json."
328
+ )
329
+ for key in self._REQUIRED_DISABLED_KEYS:
330
+ if self._custom_disabled_map.get(key) is None:
331
+ raise KeyError(
332
+ f"'{name}' theme block is missing '{key}' in disabled_map."
333
+ )
334
+
335
+ def _apply_custom_theme_colors(self) -> None:
336
+ """
337
+ Recomputes and applies this widget's colors from the theme file, based
338
+ on the current enabled/disabled state and which segment (if any) is
339
+ selected.
340
+
341
+ Called: after construction (via the deferred defer(self, 15, ...) in
342
+ __init__), on every state() change, on every set(), and on every click.
343
+
344
+ Passes raw (light, dark) tuples straight through to configure() instead
345
+ of resolving to a single color first, so CTk's native tracking can
346
+ handle appearance-mode repaints without help from _set_appearance_mode.
347
+ Every value here traces back to sCTkThemes.json -- there are no
348
+ hardcoded colors in this method; the tuples on each `.get(key, ...)`
349
+ Every colour comes from the theme block, which is validated at
350
+ construction -- there are no fallbacks to fall through to.
351
+ """
352
+ # Guards against being called before CTkSegmentedButton has built its
353
+ # internal per-segment buttons (see the __init__ comment on the 15ms delay).
354
+ if not hasattr(self, "_buttons_dict") or not self._buttons_dict:
355
+ return
356
+
357
+ is_disabled = str(getattr(self, "_custom_current_state", "normal")).lower() == "disabled"
358
+ target_map = self._custom_disabled_map if is_disabled else self._local_defaults
359
+
360
+ # No `.get(key, fallback)` anywhere below. Those fallbacks were
361
+ # unreachable given the current theme file -- every key they guarded
362
+ # is present -- but that was a property of the THEME, not the code.
363
+ # Delete a key from the block and the fallback would silently
364
+ # activate, producing a plausible-looking wrong colour instead of the
365
+ # KeyError every other widget now raises. _validate_theme_keys()
366
+ # makes that failure loud.
367
+ fg_tuple = tuple(target_map.get("fg_color"))
368
+ # The theme JSON's disabled_map key is "selected_color", not
369
+ # "disabled_selected_color" -- an earlier version of this line used the
370
+ # wrong key name, so it silently always fell back to a hardcoded default
371
+ # that was subtly wrong in dark mode. Confirm the key name matches
372
+ # sCTkThemes.json's disabled_map block before changing this.
373
+ d_selected = tuple(self._custom_disabled_map.get("selected_color"))
374
+ n_selected = tuple(self._local_defaults.get("selected_color"))
375
+ # NOTE: unselected_hover_color is popped out of final_kw in __init__
376
+ # (it is not a native CTkSegmentedButton constructor argument), but
377
+ # _local_defaults is copied BEFORE that pop, so it survives here.
378
+ # Don't move the copy.
379
+ unselected_hover = tuple(self._local_defaults.get("unselected_hover_color"))
380
+
381
+ # unselected_color is deliberately NOT read from the theme file --
382
+ # unselected segments always mirror fg_color, by design, so they blend
383
+ # into the widget's own background rather than appearing individually
384
+ # distinct or transparent. There is no independent "unselected_color"
385
+ # theme key for this widget at all.
386
+ fg_payload = {
387
+ "fg_color": fg_tuple,
388
+ "selected_color": d_selected if is_disabled else n_selected,
389
+ "unselected_color": fg_tuple,
390
+ "unselected_hover_color": fg_tuple if is_disabled else unselected_hover,
391
+ }
392
+ super().configure(**fg_payload)
393
+
394
+ base_txt_tuple = tuple(target_map.get("text_color"))
395
+ # Reads disabled_map.selected_text_color / disabled_map.text_color.
396
+ # An earlier version of this hardcoded both colors directly, ignoring
397
+ # the theme file entirely -- don't reintroduce literal hex values here.
398
+ selected_disabled_txt = tuple(self._custom_disabled_map.get("selected_text_color"))
399
+ unselected_disabled_txt = tuple(self._custom_disabled_map.get("text_color"))
400
+
401
+ for val_name, button in self._buttons_dict.items():
402
+ # Clear layout padding bounds flush to the container track edge.
403
+ try:
404
+ button.grid_configure(padx=0, pady=0)
405
+ except Exception:
406
+ pass
407
+
408
+ if is_disabled:
409
+ if hasattr(self, "_current_value") and val_name == self._current_value:
410
+ # The selected segment keeps a more prominent text color
411
+ # while disabled, so the user can still tell what was chosen
412
+ # even though the whole control is grayed out.
413
+ # BOTH text options: a disabled CTkSegmentedButton puts its
414
+ # inner CTkButtons into CTk's own disabled state, and a
415
+ # disabled CTkButton draws from text_color_disabled and
416
+ # ignores text_color entirely -- so the theme's disabled
417
+ # colours were discarded for CustomTkinter's default grey.
418
+ button.configure(text_color=selected_disabled_txt,
419
+ text_color_disabled=selected_disabled_txt)
420
+ else:
421
+ button.configure(text_color=unselected_disabled_txt,
422
+ text_color_disabled=unselected_disabled_txt)
423
+ else:
424
+ button.configure(text_color=base_txt_tuple)