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,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)
|