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,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Fastattack (https://github.com/fastattackv/MoreCustomTkinterWidgets)
4
+ Originally "Selector" - herein a modified into "sCTkSelector"
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the election
9
+ of the Software, and sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to the
11
+ following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
@@ -0,0 +1,366 @@
1
+ #!/usr/bin/python3
2
+ """
3
+ sCTkButtonPrimary
4
+
5
+ A theme-compliant, high-visibility "primary action" button widget -- the most
6
+ prominent of the library's three button tiers (see sCTkButtonSecondary,
7
+ sCTkButtonTertiary). Inherits directly from ctk.CTkButton so CustomTkinter
8
+ handles native rendering; this class layers automatic light/dark theme
9
+ resolution and a four-state visual model (normal/disabled/pressed/alarm) on top.
10
+
11
+ Base class order matters here: `class sCTkButtonPrimary(ctk.CTkButton,
12
+ ThemeableWidget)` puts the native CTk class first, so every `super()` call in
13
+ this file's own methods resolves to ctk.CTkButton -- 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
+ from typing import Any, Optional
20
+ import customtkinter as ctk
21
+ from .themeable_widget import ThemeableWidget, defer
22
+
23
+
24
+ class sCTkButtonPrimary(ctk.CTkButton, ThemeableWidget):
25
+ """Themeable, high-visibility primary action button.
26
+
27
+ Adds to native ctk.CTkButton:
28
+ - Automatic light/dark theme resolution from sCTkThemes.json (via
29
+ ThemeableWidget.__init__ -- see that class's docstring for what it does,
30
+ and just as importantly, what it no longer does).
31
+ - A four-state visual model, not just enabled/disabled: normal, disabled,
32
+ pressed, and alarm. Precedence when more than one could apply is
33
+ disabled > alarm > pressed > normal (see _update_current_visual_state()
34
+ and the guard clauses in set_pressed()/set_alarm_state()).
35
+ - Pygubu Designer property introspection for a handful of properties via
36
+ a single-argument configure() call.
37
+
38
+ Colors are passed through to configure() as raw (light, dark) tuples rather
39
+ than pre-resolved to a single value, so CustomTkinter's own appearance-mode
40
+ tracking repaints them automatically on a light/dark switch -- the same
41
+ approach validated on sCTkComboBox and sCTkSegmentedButton.
42
+
43
+ Disabling uses CTk's native state="disabled" rather than manually unbinding
44
+ mouse events -- confirmed by direct testing to be the only mechanism that
45
+ actually blocks clicks (see state() for why). The post-transition color
46
+ reapply is deferred via after_idle rather than called immediately, since an
47
+ immediate call was found to lose a race against CTk's own internal
48
+ state-change repaint, leaving buttons visually stuck on the wrong palette
49
+ after repeated disable/enable cycles.
50
+ """
51
+
52
+ def __init__(self, master: Optional[Any] = None, **kw: Any) -> None:
53
+ """
54
+ Args:
55
+ master: Parent container.
56
+ **kw: Any native CTkButton argument, or a theme-key override (see
57
+ the "sCTkButtonPrimary" block in sCTkThemes.json, including its
58
+ disabled_map, pressed_map, and alarm_map sub-blocks).
59
+ """
60
+ # 1. Fire our shared theme logic first. This resolves final_kw
61
+ # (construction-time properties) and the disabled/pressed/alarm color
62
+ # maps. See ThemeableWidget.__init__ for what actually happens here.
63
+ # NO PLACEHOLDER TEXT. CustomTkinter defaults `text` to the class
64
+ # name, so a CTkButton built without text reads "CTkButton". Nothing wants
65
+ # that in a real interface, and the Designer made it worse: clearing
66
+ # the text field removes the property, the canvas rebuilds the widget
67
+ # from what is left, and the placeholder appeared in place of the
68
+ # blank that was asked for.
69
+ kw.setdefault("text", "")
70
+ ThemeableWidget.__init__(self, kw)
71
+
72
+ # 2. Deep-copy each resolved map onto this instance, so later changes
73
+ # here never leak back into the shared theme registry.
74
+ self._local_defaults = dict(self.final_kw)
75
+ self._custom_disabled_map = dict(self._widget_disabled_map)
76
+ self._custom_pressed_map = dict(self._widget_pressed_map)
77
+ self._custom_alarm_map = dict(self._widget_alarm_map)
78
+
79
+ # 3. Initialize CustomTkinter natively with the clean final kwargs array.
80
+ super().__init__(master, **self.final_kw)
81
+
82
+ self.is_pressed = False
83
+ self.is_alarm = False
84
+ self._custom_current_state = "normal"
85
+ self._update_current_visual_state()
86
+
87
+ # 4. Register lifecycle handshake hook, notifying Pygubu-style consumers
88
+ # that construction is complete.
89
+ self._finalize_themeable_lifecycle()
90
+
91
+ def configure(self, *args: Any, **kwargs: Any) -> Any:
92
+ """
93
+ Standard widget configuration, with Pygubu/positional-argument handling.
94
+
95
+ Args:
96
+ *args: At most one positional argument is meaningful:
97
+ - a dict: merged into kwargs and processed normally below.
98
+ - the literal string "state": returns a Tkinter-style
99
+ (name, name, name, default, current) tuple.
100
+ - one of "fg_color"/"border_color"/"text_color"/"hover_color":
101
+ returns the same style of tuple, with `current` reflecting
102
+ whichever of disabled/alarm/pressed/normal is currently
103
+ active. Note the returned value is str(value), where value
104
+ may itself be a (light, dark) tuple rather than a single
105
+ resolved color -- a known limitation shared with the wider
106
+ Pygubu-query investigation set aside elsewhere in this
107
+ project, not fixed here.
108
+ - anything else: forwarded directly to the native widget's
109
+ configure(), which does not support single-argument property
110
+ queries for arbitrary properties (same limitation).
111
+ **kwargs: Standard CTkButton configuration options, plus: passing
112
+ `state=...` routes through self.state() rather than being
113
+ forwarded as-is.
114
+
115
+ Returns:
116
+ The query tuple described above for the single-argument case, or
117
+ whatever super().configure() returns for the keyword-argument case
118
+ (typically None).
119
+ """
120
+ # args is always a tuple -- args[0] is the actual value passed, whether
121
+ # that's a string or a dict. An earlier version of this method compared
122
+ # the wrapped tuple directly (`pname = args`) and forwarded it unwrapped
123
+ # to super().configure(), which is not a valid call shape for the
124
+ # native widget. Don't reintroduce that.
125
+ if len(args) == 1:
126
+ if isinstance(args[0], dict):
127
+ kwargs = {**args[0], **kwargs}
128
+ else:
129
+ pname = args[0]
130
+ if pname == "state":
131
+ return ("state", "state", "state", "normal", str(self.state()))
132
+
133
+ if pname in ["fg_color", "border_color", "text_color", "hover_color"]:
134
+ current_state = str(self.state()).lower()
135
+ if current_state == "disabled" and self._custom_disabled_map:
136
+ val = self._custom_disabled_map.get(pname)
137
+ elif getattr(self, "is_alarm", False) and self._custom_alarm_map:
138
+ val = self._custom_alarm_map.get(pname)
139
+ elif getattr(self, "is_pressed", False) and self._custom_pressed_map:
140
+ val = self._custom_pressed_map.get(pname)
141
+ else:
142
+ val = self._local_defaults.get(pname)
143
+ return (pname, pname, pname, self._query_value(self._theme_default(pname)), self._query_value(val))
144
+
145
+ return self._configure_query(pname)
146
+
147
+ # Runtime overrides have to reach the map a repaint reads, or
148
+ # _update_current_visual_state() puts the theme value straight back --
149
+ # see ThemeableWidget._record_theme_overrides().
150
+ self._record_theme_overrides(kwargs)
151
+
152
+ if "state" in kwargs:
153
+ target_state = kwargs.pop("state")
154
+ self.state(target_state)
155
+
156
+ # Theme values cleared to "" are dropped; `text=""` is kept, so a
157
+ # button's caption can actually be blanked. See
158
+ # ThemeableWidget._CONTENT_KEYS.
159
+ self._drop_cleared(kwargs)
160
+ if kwargs:
161
+ return super().configure(**kwargs)
162
+ return None
163
+
164
+ # Tkinter/CTk convention binds .config to .configure as a SEPARATE class
165
+ # attribute -- it does not automatically track whichever configure() a
166
+ # subclass defines. Without this line, calling .config(...) on an instance
167
+ # would silently skip this entire override and land on the native widget's
168
+ # configure() directly, bypassing theming and state handling entirely.
169
+ config = configure
170
+
171
+ def _set_appearance_mode(self, mode_string: str) -> None:
172
+ """
173
+ Forwards CustomTkinter's internal light/dark mode change notification to
174
+ the native widget.
175
+
176
+ No longer manually re-triggers _update_current_visual_state(). That
177
+ method now passes raw (light, dark) tuples straight through to
178
+ configure() instead of pre-resolving to a single color, so CTk's own
179
+ appearance-mode tracking should repaint correctly on its own -- the same
180
+ approach validated on sCTkComboBox and sCTkSegmentedButton. If colors
181
+ ever stop following mode changes (especially while disabled, pressed,
182
+ or in alarm), that's the signal this doesn't hold here and the manual
183
+ re-trigger needs to come back.
184
+
185
+ Args:
186
+ mode_string: The new appearance mode ("Light" or "Dark"), as passed
187
+ by CustomTkinter's internal appearance-mode change machinery.
188
+ """
189
+ if hasattr(super(), "_set_appearance_mode"):
190
+ try:
191
+ super()._set_appearance_mode(mode_string)
192
+ except Exception:
193
+ pass
194
+
195
+ def get_state(self) -> str:
196
+ """Equivalent to calling state() with no argument."""
197
+ return self.state()
198
+
199
+ def state(self, mode: Optional[str] = None) -> str:
200
+ """
201
+ Gets or sets the widget's enabled/disabled state.
202
+
203
+ Args:
204
+ mode: If None, returns the current state without changing anything.
205
+ Otherwise, only the literal string "disabled" (case-insensitive)
206
+ is treated as disabled; anything in ("normal", "enabled",
207
+ "active") is treated as enabled. Any other value falls through
208
+ without matching either branch, leaving the state unchanged.
209
+
210
+ Returns:
211
+ The resulting state ("normal" or "disabled", lowercase).
212
+ """
213
+ if mode is None:
214
+ return str(getattr(self, "_custom_current_state", "normal")).lower()
215
+
216
+ mode = mode.lower()
217
+ if mode in ("normal", "enabled", "active"):
218
+ self._custom_current_state = "normal"
219
+ super().configure(state="normal", hover=True)
220
+ # Re-establish the mouse bindings that disabling below may have
221
+ # torn down.
222
+ if hasattr(self, "_create_bindings"):
223
+ try: self._create_bindings()
224
+ except Exception: pass
225
+ # Deferred via after_idle rather than called immediately: CTk's own
226
+ # state="normal" transition appears to schedule its own internal
227
+ # repaint, which can run AFTER an immediate call here and silently
228
+ # overwrite our colors back to something stale (observed directly:
229
+ # buttons stayed visually "disabled" after re-enabling). Queuing
230
+ # our reapply via after_idle lets CTk's own pending repaint settle
231
+ # first, so ours is the one that actually wins the final paint.
232
+ defer(self, "idle", self._update_current_visual_state)
233
+
234
+ elif mode == "disabled":
235
+ self._custom_current_state = "disabled"
236
+ # Uses CTk's native state="disabled" -- confirmed by direct testing
237
+ # to be necessary, not optional. An earlier version instead
238
+ # manually unbound mouse events on the canvas while keeping native
239
+ # state at "normal", to avoid CTk's own disabled rendering
240
+ # potentially fighting with this widget's disabled/pressed/alarm
241
+ # color system. That approach was confirmed broken: clicks still
242
+ # fired the button's command while "disabled", almost certainly
243
+ # because CTkButton binds its click handler at the canvas-ITEM
244
+ # level (tag_bind on the drawn rectangle/text), not the
245
+ # canvas-WIDGET level -- so canvas.unbind() never reached it.
246
+ # Native state="disabled" is what actually blocks interaction;
247
+ # don't replace this with manual event unbinding again.
248
+ super().configure(state="disabled", hover=False)
249
+ defer(self, "idle", self._update_current_visual_state)
250
+
251
+ return self._custom_current_state
252
+
253
+ def set_pressed(self, pressed: bool) -> None:
254
+ """
255
+ Toggles the visual "pressed" state.
256
+
257
+ No-op while disabled or in alarm state -- see the precedence order
258
+ documented on _update_current_visual_state().
259
+
260
+ Args:
261
+ pressed: True to show the pressed-state colors, False to return to
262
+ normal (assuming not disabled or in alarm).
263
+ """
264
+ if getattr(self, "_custom_current_state", "normal") == "disabled" or self.is_alarm:
265
+ return
266
+ self.is_pressed = pressed
267
+ self._update_current_visual_state()
268
+
269
+ def set_alarm_state(self, active: bool) -> None:
270
+ """
271
+ Forces the button into (or out of) a high-visibility alarm/warning state.
272
+
273
+ No-op while disabled. Activating alarm state clears any current
274
+ "pressed" state, since alarm takes visual precedence over pressed (see
275
+ _update_current_visual_state()).
276
+
277
+ Args:
278
+ active: True to enter alarm state, False to leave it.
279
+ """
280
+ if getattr(self, "_custom_current_state", "normal") == "disabled":
281
+ return
282
+ self.is_alarm = active
283
+ if self.is_alarm:
284
+ self.is_pressed = False
285
+ self._update_current_visual_state()
286
+
287
+ def _update_current_visual_state(self) -> None:
288
+ """
289
+ Recomputes and applies this widget's colors from the theme file, based
290
+ on the current state. Precedence, checked in this order: disabled >
291
+ alarm > pressed > normal -- only the first matching branch applies.
292
+
293
+ Called after construction, on every state()/set_pressed()/
294
+ set_alarm_state() change, and (indirectly) on appearance-mode changes
295
+ via _set_appearance_mode().
296
+
297
+ Passes raw (light, dark) tuples straight through to configure() instead
298
+ of resolving to a single color first, so CTk's native tracking can
299
+ handle appearance-mode repaints without help from _set_appearance_mode.
300
+ Every value here traces back to sCTkThemes.json; there are no
301
+ hardcoded colors in this method.
302
+
303
+ Only the normal (final else) branch also reapplies border_width,
304
+ corner_radius, and font -- these don't vary between
305
+ disabled/alarm/pressed/normal, so there's no need to repeat them in
306
+ every branch; they're set once here and otherwise left alone.
307
+ `border_color` is checked in every branch for consistency with the
308
+ other themed widgets, but sCTkButtonPrimary's theme block doesn't
309
+ currently define one at all -- this button style has no themed border
310
+ by design, so that lookup always resolves to None and is skipped.
311
+ """
312
+ if getattr(self, "_custom_current_state", "normal") == "disabled":
313
+ config_payload = {}
314
+ for key in ("fg_color", "hover_color", "border_color"):
315
+ val = self._custom_disabled_map.get(key)
316
+ if val is not None:
317
+ config_payload[key] = val
318
+
319
+ # disabled_map's text_color goes to the NATIVE text_color_disabled.
320
+ #
321
+ # CTkButton has its own text_color_disabled option and its draw
322
+ # code uses that whenever state is "disabled", overriding whatever
323
+ # text_color was set to. Assigning the disabled value to text_color
324
+ # here was therefore discarded at draw time and the theme key had
325
+ # no visible effect at all.
326
+ #
327
+ # text_color keeps the NORMAL value, so the two options do not
328
+ # fight and CTk chooses between them as it is designed to.
329
+ disabled_text = self._custom_disabled_map.get("text_color")
330
+ if disabled_text is not None:
331
+ config_payload["text_color_disabled"] = disabled_text
332
+ normal_text = self._local_defaults.get("text_color")
333
+ if normal_text is not None:
334
+ config_payload["text_color"] = normal_text
335
+
336
+ if config_payload:
337
+ super().configure(**config_payload)
338
+ return
339
+
340
+ if self.is_alarm:
341
+ config_payload = {}
342
+ for key in ("fg_color", "hover_color", "border_color", "text_color"):
343
+ val = self._custom_alarm_map.get(key)
344
+ if val is not None:
345
+ config_payload[key] = val
346
+ config_payload["hover"] = False
347
+ super().configure(**config_payload)
348
+
349
+ elif self.is_pressed:
350
+ config_payload = {}
351
+ for key in ("fg_color", "hover_color", "border_color", "text_color"):
352
+ val = self._custom_pressed_map.get(key)
353
+ if val is not None:
354
+ config_payload[key] = val
355
+ config_payload["hover"] = False
356
+ super().configure(**config_payload)
357
+
358
+ else:
359
+ config_payload = {}
360
+ for key in ("fg_color", "hover_color", "border_color", "text_color", "border_width", "corner_radius", "font"):
361
+ val = self._local_defaults.get(key)
362
+ if val is not None:
363
+ config_payload[key] = val
364
+ config_payload["hover"] = True
365
+ if config_payload:
366
+ super().configure(**config_payload)