scustomtkinter 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scustomtkinter/__init__.py +81 -0
- scustomtkinter/assets/images/error.png +0 -0
- scustomtkinter/assets/images/info.png +0 -0
- scustomtkinter/assets/images/warning.png +0 -0
- scustomtkinter/assets/sCTkThemes.json +1963 -0
- scustomtkinter/license_morecustomtkinterwidgets.txt +22 -0
- scustomtkinter/sctk_button_primary.py +366 -0
- scustomtkinter/sctk_button_secondary.py +354 -0
- scustomtkinter/sctk_button_tertiary.py +375 -0
- scustomtkinter/sctk_checkbox.py +270 -0
- scustomtkinter/sctk_combobox.py +155 -0
- scustomtkinter/sctk_core.py +15 -0
- scustomtkinter/sctk_dial.py +1472 -0
- scustomtkinter/sctk_dialog.py +701 -0
- scustomtkinter/sctk_dialog_toplevel.py +268 -0
- scustomtkinter/sctk_entry_primary.py +400 -0
- scustomtkinter/sctk_entry_secondary.py +342 -0
- scustomtkinter/sctk_file_explorer.py +879 -0
- scustomtkinter/sctk_frame.py +210 -0
- scustomtkinter/sctk_frame_labeled_primary.py +667 -0
- scustomtkinter/sctk_frame_labeled_secondary.py +598 -0
- scustomtkinter/sctk_frequency_display.py +636 -0
- scustomtkinter/sctk_label_primary.py +283 -0
- scustomtkinter/sctk_label_secondary.py +275 -0
- scustomtkinter/sctk_label_tertiary.py +278 -0
- scustomtkinter/sctk_messagebox.py +275 -0
- scustomtkinter/sctk_notebook.py +1103 -0
- scustomtkinter/sctk_optionmenu_border_mixin.py +203 -0
- scustomtkinter/sctk_optionmenu_primary.py +378 -0
- scustomtkinter/sctk_optionmenu_secondary.py +346 -0
- scustomtkinter/sctk_path_chooser.py +516 -0
- scustomtkinter/sctk_progress_bar.py +274 -0
- scustomtkinter/sctk_radiobutton.py +360 -0
- scustomtkinter/sctk_scroll_mixin.py +546 -0
- scustomtkinter/sctk_scrollable_frame.py +795 -0
- scustomtkinter/sctk_scrollbar.py +313 -0
- scustomtkinter/sctk_segmentedbutton.py +424 -0
- scustomtkinter/sctk_selector.py +581 -0
- scustomtkinter/sctk_separator.py +308 -0
- scustomtkinter/sctk_slider.py +264 -0
- scustomtkinter/sctk_smeter.py +405 -0
- scustomtkinter/sctk_smeter_bar.py +557 -0
- scustomtkinter/sctk_spinbox.py +664 -0
- scustomtkinter/sctk_switch.py +463 -0
- scustomtkinter/sctk_tableview.py +869 -0
- scustomtkinter/sctk_tabview.py +409 -0
- scustomtkinter/sctk_textbox_primary.py +291 -0
- scustomtkinter/sctk_textbox_secondary.py +225 -0
- scustomtkinter/sctk_toplevel.py +153 -0
- scustomtkinter/themeable_widget.py +938 -0
- scustomtkinter-0.2.1.dist-info/METADATA +336 -0
- scustomtkinter-0.2.1.dist-info/RECORD +101 -0
- scustomtkinter-0.2.1.dist-info/WHEEL +5 -0
- scustomtkinter-0.2.1.dist-info/entry_points.txt +2 -0
- scustomtkinter-0.2.1.dist-info/licenses/LICENSE +21 -0
- scustomtkinter-0.2.1.dist-info/licenses/scustomtkinter/license_morecustomtkinterwidgets.txt +22 -0
- scustomtkinter-0.2.1.dist-info/top_level.txt +2 -0
- scustomtkinter_pygubu/__init__.py +1 -0
- scustomtkinter_pygubu/designer/__init__.py +0 -0
- scustomtkinter_pygubu/designer/plugin.py +1854 -0
- scustomtkinter_pygubu/designer/properties.py +247 -0
- scustomtkinter_pygubu/sCTkButtonPrimarybo.py +46 -0
- scustomtkinter_pygubu/sCTkButtonSecondarybo.py +45 -0
- scustomtkinter_pygubu/sCTkButtonTertiarybo.py +44 -0
- scustomtkinter_pygubu/sCTkCheckBoxbo.py +45 -0
- scustomtkinter_pygubu/sCTkComboBoxbo.py +45 -0
- scustomtkinter_pygubu/sCTkCorebo.py +120 -0
- scustomtkinter_pygubu/sCTkDialbo.py +371 -0
- scustomtkinter_pygubu/sCTkDialogbo.py +397 -0
- scustomtkinter_pygubu/sCTkEntryPrimarybo.py +100 -0
- scustomtkinter_pygubu/sCTkEntrySecondarybo.py +98 -0
- scustomtkinter_pygubu/sCTkFileExplorerbo.py +189 -0
- scustomtkinter_pygubu/sCTkFrameLabeledPrimarybo.py +69 -0
- scustomtkinter_pygubu/sCTkFrameLabeledSecondarybo.py +71 -0
- scustomtkinter_pygubu/sCTkFramebo.py +46 -0
- scustomtkinter_pygubu/sCTkFrequencyDisplaybo.py +175 -0
- scustomtkinter_pygubu/sCTkLabelPrimarybo.py +45 -0
- scustomtkinter_pygubu/sCTkLabelSecondarybo.py +44 -0
- scustomtkinter_pygubu/sCTkLabelTertiarybo.py +45 -0
- scustomtkinter_pygubu/sCTkNotebookbo.py +392 -0
- scustomtkinter_pygubu/sCTkOptionMenuPrimarybo.py +45 -0
- scustomtkinter_pygubu/sCTkOptionMenuSecondarybo.py +39 -0
- scustomtkinter_pygubu/sCTkPathChooserbo.py +204 -0
- scustomtkinter_pygubu/sCTkProgressBarbo.py +45 -0
- scustomtkinter_pygubu/sCTkRadioButtonbo.py +45 -0
- scustomtkinter_pygubu/sCTkSMeterBarbo.py +153 -0
- scustomtkinter_pygubu/sCTkSMeterbo.py +101 -0
- scustomtkinter_pygubu/sCTkScrollableFramebo.py +66 -0
- scustomtkinter_pygubu/sCTkScrollbarbo.py +48 -0
- scustomtkinter_pygubu/sCTkSegmentedButtonbo.py +47 -0
- scustomtkinter_pygubu/sCTkSelectorbo.py +167 -0
- scustomtkinter_pygubu/sCTkSeparatorbo.py +141 -0
- scustomtkinter_pygubu/sCTkSliderbo.py +45 -0
- scustomtkinter_pygubu/sCTkSpinboxbo.py +200 -0
- scustomtkinter_pygubu/sCTkSwitchbo.py +45 -0
- scustomtkinter_pygubu/sCTkTableviewbo.py +188 -0
- scustomtkinter_pygubu/sCTkTabviewbo.py +226 -0
- scustomtkinter_pygubu/sCTkTextboxPrimarybo.py +45 -0
- scustomtkinter_pygubu/sCTkTextboxSecondarybo.py +45 -0
- scustomtkinter_pygubu/sCTkToplevelbo.py +55 -0
- scustomtkinter_pygubu/sCTkWidgetSetForPygubuDesigner.py +141 -0
|
@@ -0,0 +1,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)
|