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,291 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
"""
|
|
3
|
+
sCTkTextboxPrimary
|
|
4
|
+
|
|
5
|
+
A theme-compliant, high-emphasis multi-line text area widget -- the more
|
|
6
|
+
prominent of the library's two textbox tiers (see also sCTkTextboxSecondary).
|
|
7
|
+
Inherits directly from ctk.CTkTextbox so CustomTkinter handles native
|
|
8
|
+
rendering and text editing; 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 sCTkTextboxPrimary(ctk.CTkTextbox,
|
|
12
|
+
ThemeableWidget)` puts the native CTk class first, so every `super()` call in
|
|
13
|
+
this file's own methods resolves to ctk.CTkTextbox -- 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
|
+
Disabling uses CTk's native state="disabled", consistent with every other
|
|
20
|
+
widget in this library confirmed to correctly block interaction this way.
|
|
21
|
+
"""
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
import platform
|
|
25
|
+
from typing import Any, Optional
|
|
26
|
+
import tkinter as tk
|
|
27
|
+
import customtkinter as ctk
|
|
28
|
+
from .themeable_widget import ThemeableWidget
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class sCTkTextboxPrimary(ctk.CTkTextbox, ThemeableWidget):
|
|
32
|
+
"""Themeable, high-emphasis multi-line text area.
|
|
33
|
+
|
|
34
|
+
Adds to native ctk.CTkTextbox:
|
|
35
|
+
- Automatic light/dark theme resolution from sCTkThemes.json (via
|
|
36
|
+
ThemeableWidget.__init__ -- see that class's docstring for what it does,
|
|
37
|
+
and just as importantly, what it no longer does).
|
|
38
|
+
- A distinct enabled/disabled visual state, using CTk's native
|
|
39
|
+
state="disabled".
|
|
40
|
+
- Pygubu Designer property introspection for `state`, `fg_color`,
|
|
41
|
+
`text_color`, `border_color`, `scrollbar_button_color`, and
|
|
42
|
+
`scrollbar_button_hover_color` via a single-argument configure() call.
|
|
43
|
+
- A transparency fallback: if fg_color resolves to "transparent" or
|
|
44
|
+
empty at construction, the widget copies its parent's fg_color
|
|
45
|
+
instead. Presumably a workaround for CTkTextbox not rendering true
|
|
46
|
+
transparency the way canvas-based widgets can; preserved as-is, not
|
|
47
|
+
independently re-verified as part of this project's audit.
|
|
48
|
+
- Manual scrollbar re-theming: CTkTextbox's internal scrollbar isn't
|
|
49
|
+
automatically covered by this widget's own theme keys, so
|
|
50
|
+
scrollbar_button_color/scrollbar_button_hover_color are pushed to it
|
|
51
|
+
directly whenever colors are recomputed.
|
|
52
|
+
|
|
53
|
+
Colors are passed through to configure() as raw (light, dark) tuples rather
|
|
54
|
+
than pre-resolved to a single value, so CustomTkinter's own appearance-mode
|
|
55
|
+
tracking repaints them automatically on a light/dark switch -- the same
|
|
56
|
+
approach validated on sCTkComboBox, sCTkSegmentedButton, and the button
|
|
57
|
+
family. Not separately re-confirmed for this specific widget.
|
|
58
|
+
|
|
59
|
+
scrollbar_button_color and scrollbar_button_hover_color are required to be
|
|
60
|
+
present in whichever map is active for the scrollbar re-theming step below
|
|
61
|
+
-- if either is missing, this raises immediately rather than substituting
|
|
62
|
+
a hardcoded color. An earlier version used hardcoded hex fallbacks here,
|
|
63
|
+
several of which didn't even match the real theme file's actual values;
|
|
64
|
+
per this project's design, ThemeableWidget-based widgets are meant to fail
|
|
65
|
+
hard on incomplete theme data (see sCTkLabelPrimary/Secondary/Tertiary for
|
|
66
|
+
the precedent this follows).
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(self, master: Optional[Any] = None, **kw: Any) -> None:
|
|
70
|
+
"""
|
|
71
|
+
Args:
|
|
72
|
+
master: Parent container.
|
|
73
|
+
**kw: `state` is pulled out explicitly below (applied after
|
|
74
|
+
construction, via self.state(), rather than passed to the
|
|
75
|
+
native constructor). Everything else is either a native
|
|
76
|
+
CTkTextbox argument or a theme-key override (see the
|
|
77
|
+
"sCTkTextboxPrimary" block in sCTkThemes.json).
|
|
78
|
+
"""
|
|
79
|
+
# 1. Fire our shared theme logic. This resolves final_kw
|
|
80
|
+
# (construction-time properties) and the disabled color map. See
|
|
81
|
+
# ThemeableWidget.__init__ for what actually happens here.
|
|
82
|
+
ThemeableWidget.__init__(self, kw)
|
|
83
|
+
|
|
84
|
+
# 2. Store the resolved maps onto this instance. Note: unlike most
|
|
85
|
+
# other widgets in this project, these are NOT deep-copied (dict(...))
|
|
86
|
+
# here -- they reference the same dict objects final_kw/
|
|
87
|
+
# _widget_disabled_map point to. Preserved as-is from the original;
|
|
88
|
+
# this only matters if something elsewhere mutates those dicts after
|
|
89
|
+
# construction, which doesn't currently happen anywhere in this file.
|
|
90
|
+
self._local_defaults = self.final_kw
|
|
91
|
+
self._custom_disabled_map = self._widget_disabled_map
|
|
92
|
+
|
|
93
|
+
# Extract "state" from final_kw after ThemeableWidget's merge -- an
|
|
94
|
+
# explicit state= kwarg correctly overrides any "state" the theme
|
|
95
|
+
# JSON might define at the top level.
|
|
96
|
+
state_init = self.final_kw.pop("state", "normal")
|
|
97
|
+
|
|
98
|
+
# 3. Initialize CustomTkinter natively with the clean final kwargs array.
|
|
99
|
+
super().__init__(master, **self.final_kw)
|
|
100
|
+
|
|
101
|
+
# Transparency fallback -- see this class's docstring.
|
|
102
|
+
try:
|
|
103
|
+
current_fg = super().cget("fg_color")
|
|
104
|
+
if current_fg in ("transparent", ""):
|
|
105
|
+
parent_bg = self.master.cget("fg_color")
|
|
106
|
+
if parent_bg not in ("transparent", ""):
|
|
107
|
+
super().configure(fg_color=parent_bg)
|
|
108
|
+
except Exception:
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
# 4. Apply the requested initial state now that the native widget exists.
|
|
112
|
+
self._custom_current_state = "normal"
|
|
113
|
+
self.state(state_init)
|
|
114
|
+
|
|
115
|
+
# 5. Register lifecycle handshake hook, notifying Pygubu-style consumers
|
|
116
|
+
# that construction is complete.
|
|
117
|
+
self._finalize_themeable_lifecycle()
|
|
118
|
+
|
|
119
|
+
def _set_appearance_mode(self, mode_string: str) -> None:
|
|
120
|
+
"""
|
|
121
|
+
Forwards CustomTkinter's internal light/dark mode change notification to
|
|
122
|
+
the native widget.
|
|
123
|
+
|
|
124
|
+
No longer manually re-triggers _update_current_visual_state(). That
|
|
125
|
+
method now passes raw (light, dark) tuples straight through to
|
|
126
|
+
configure() instead of pre-resolving to a single color, so CTk's own
|
|
127
|
+
appearance-mode tracking should repaint correctly on its own -- the
|
|
128
|
+
same approach validated on sCTkComboBox, sCTkSegmentedButton, and the
|
|
129
|
+
button family.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
mode_string: The new appearance mode ("Light" or "Dark"), as passed
|
|
133
|
+
by CustomTkinter's internal appearance-mode change machinery.
|
|
134
|
+
"""
|
|
135
|
+
if hasattr(super(), "_set_appearance_mode"):
|
|
136
|
+
try:
|
|
137
|
+
super()._set_appearance_mode(mode_string)
|
|
138
|
+
except Exception:
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
def configure(self, *args: Any, **kwargs: Any) -> Any:
|
|
142
|
+
"""
|
|
143
|
+
Standard widget configuration, with Pygubu/positional-argument handling.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
*args: At most one positional argument is meaningful:
|
|
147
|
+
- a dict: merged into kwargs and processed normally below.
|
|
148
|
+
- the literal string "state": returns a Tkinter-style
|
|
149
|
+
(name, name, name, default, current) tuple.
|
|
150
|
+
- one of "fg_color"/"text_color"/"border_color"/
|
|
151
|
+
"scrollbar_button_color"/"scrollbar_button_hover_color":
|
|
152
|
+
returns the same style of tuple. Note the returned value is
|
|
153
|
+
str(value), where value may itself be a (light, dark) tuple
|
|
154
|
+
rather than a single resolved color -- a known limitation
|
|
155
|
+
shared with the wider Pygubu-query investigation set aside
|
|
156
|
+
elsewhere in this project, not fixed here.
|
|
157
|
+
- anything else: forwarded directly to the native widget's
|
|
158
|
+
configure(), which does not support single-argument property
|
|
159
|
+
queries for arbitrary properties (same limitation).
|
|
160
|
+
**kwargs: Standard CTkTextbox configuration options, plus: passing
|
|
161
|
+
`state=...` updates self._custom_current_state and triggers a
|
|
162
|
+
repaint, rather than being forwarded as-is.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
The query tuple described above for the single-argument case, or
|
|
166
|
+
whatever super().configure() returns for the keyword-argument case
|
|
167
|
+
(typically None).
|
|
168
|
+
"""
|
|
169
|
+
# args is always a tuple -- args[0] is the actual value passed, whether
|
|
170
|
+
# that's a string or a dict. An earlier version of this method compared
|
|
171
|
+
# the wrapped tuple directly (`pname = args`) and forwarded it unwrapped
|
|
172
|
+
# to super().configure(), which is not a valid call shape for the
|
|
173
|
+
# native widget. Don't reintroduce that.
|
|
174
|
+
if len(args) == 1:
|
|
175
|
+
if isinstance(args[0], dict):
|
|
176
|
+
kwargs = {**args[0], **kwargs}
|
|
177
|
+
else:
|
|
178
|
+
pname = args[0]
|
|
179
|
+
if pname == "state":
|
|
180
|
+
return ("state", "state", "state", "normal", str(self.state()))
|
|
181
|
+
|
|
182
|
+
if pname in ["fg_color", "text_color", "border_color", "scrollbar_button_color",
|
|
183
|
+
"scrollbar_button_hover_color"]:
|
|
184
|
+
val = self._custom_disabled_map.get(pname) if self._custom_current_state == "disabled" else self._local_defaults.get(pname)
|
|
185
|
+
return (pname, pname, pname, self._query_value(self._theme_default(pname)), self._query_value(val))
|
|
186
|
+
|
|
187
|
+
return self._configure_query(pname)
|
|
188
|
+
|
|
189
|
+
# Runtime overrides have to reach the map a repaint reads, or the
|
|
190
|
+
# repaint puts the theme value straight back -- see
|
|
191
|
+
# ThemeableWidget._record_theme_overrides().
|
|
192
|
+
self._record_theme_overrides(kwargs)
|
|
193
|
+
|
|
194
|
+
if "state" in kwargs:
|
|
195
|
+
self._custom_current_state = str(kwargs.pop("state")).lower()
|
|
196
|
+
self._update_current_visual_state()
|
|
197
|
+
|
|
198
|
+
if kwargs:
|
|
199
|
+
return super().configure(**kwargs)
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
# Tkinter/CTk convention binds .config to .configure as a SEPARATE class
|
|
203
|
+
# attribute -- it does not automatically track whichever configure() a
|
|
204
|
+
# subclass defines. Without this line, calling .config(...) on an instance
|
|
205
|
+
# would silently skip this entire override and land on the native widget's
|
|
206
|
+
# configure() directly, bypassing theming and state handling entirely.
|
|
207
|
+
config = configure
|
|
208
|
+
|
|
209
|
+
def get_state(self) -> str:
|
|
210
|
+
"""Equivalent to calling state() with no argument."""
|
|
211
|
+
return self.state()
|
|
212
|
+
|
|
213
|
+
def state(self, state_string: Optional[str] = None) -> str:
|
|
214
|
+
"""
|
|
215
|
+
Gets or sets the widget's enabled/disabled visual state.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
state_string: If given, forwarded to configure(state=state_string).
|
|
219
|
+
If None, no change is made.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
The current state string (whatever was last set, or "normal" by
|
|
223
|
+
default).
|
|
224
|
+
"""
|
|
225
|
+
if state_string is not None:
|
|
226
|
+
self.configure(state=state_string)
|
|
227
|
+
return getattr(self, "_custom_current_state", "normal")
|
|
228
|
+
|
|
229
|
+
def _update_current_visual_state(self) -> None:
|
|
230
|
+
"""
|
|
231
|
+
Recomputes and applies this widget's colors from the theme file, based
|
|
232
|
+
on the current state, then sets the native interactive lock, then
|
|
233
|
+
re-themes the internal scrollbar.
|
|
234
|
+
|
|
235
|
+
Called on every state()/configure(state=...) change.
|
|
236
|
+
|
|
237
|
+
Passes raw (light, dark) tuples straight through to configure() instead
|
|
238
|
+
of resolving to a single color first, so CTk's native tracking can
|
|
239
|
+
handle appearance-mode repaints without help from _set_appearance_mode.
|
|
240
|
+
|
|
241
|
+
scrollbar_button_color and scrollbar_button_hover_color are required
|
|
242
|
+
to be present in whichever map is active -- see this class's
|
|
243
|
+
docstring.
|
|
244
|
+
"""
|
|
245
|
+
is_disabled = getattr(self, "_custom_current_state", "normal") == "disabled"
|
|
246
|
+
target_map = self._custom_disabled_map if is_disabled else self._local_defaults
|
|
247
|
+
|
|
248
|
+
config_payload = {}
|
|
249
|
+
for key in ("fg_color", "border_color", "text_color", "scrollbar_button_color", "scrollbar_button_hover_color"):
|
|
250
|
+
val = target_map.get(key)
|
|
251
|
+
if val is not None:
|
|
252
|
+
config_payload[key] = val
|
|
253
|
+
|
|
254
|
+
if config_payload:
|
|
255
|
+
super().configure(**config_payload)
|
|
256
|
+
|
|
257
|
+
if is_disabled:
|
|
258
|
+
super().configure(state="disabled")
|
|
259
|
+
else:
|
|
260
|
+
super().configure(state="normal")
|
|
261
|
+
|
|
262
|
+
# Re-theme the internal scrollbar, which isn't automatically covered
|
|
263
|
+
# by the configure() call above.
|
|
264
|
+
if hasattr(self, "_scrollbar") and self._scrollbar:
|
|
265
|
+
try:
|
|
266
|
+
if target_map.get("scrollbar_button_color") is None:
|
|
267
|
+
raise KeyError(
|
|
268
|
+
f"'{self.__class__.__name__}' theme block is missing "
|
|
269
|
+
f"'scrollbar_button_color' in its "
|
|
270
|
+
f"{'disabled_map' if is_disabled else 'top-level'} section."
|
|
271
|
+
)
|
|
272
|
+
if is_disabled:
|
|
273
|
+
bar_color = target_map.get("scrollbar_button_color")
|
|
274
|
+
self._scrollbar.configure(button_color=bar_color, button_hover_color=bar_color)
|
|
275
|
+
else:
|
|
276
|
+
if target_map.get("scrollbar_button_hover_color") is None:
|
|
277
|
+
raise KeyError(
|
|
278
|
+
f"'{self.__class__.__name__}' theme block is missing "
|
|
279
|
+
f"'scrollbar_button_hover_color' at the top level."
|
|
280
|
+
)
|
|
281
|
+
self._scrollbar.configure(
|
|
282
|
+
button_color=target_map.get("scrollbar_button_color"),
|
|
283
|
+
button_hover_color=target_map.get("scrollbar_button_hover_color"),
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
if hasattr(self._scrollbar, "_draw"):
|
|
287
|
+
self._scrollbar._draw()
|
|
288
|
+
except KeyError:
|
|
289
|
+
raise
|
|
290
|
+
except Exception:
|
|
291
|
+
pass
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
"""
|
|
3
|
+
sCTkTextboxSecondary
|
|
4
|
+
|
|
5
|
+
A theme-compliant, lower-emphasis multi-line text area widget (see also
|
|
6
|
+
sCTkTextboxPrimary). Inherits directly from ctk.CTkTextbox so CustomTkinter
|
|
7
|
+
handles native rendering and text editing; this class layers automatic
|
|
8
|
+
light/dark theme resolution and a distinct enabled/disabled visual state on
|
|
9
|
+
top.
|
|
10
|
+
|
|
11
|
+
Base class order matters here: `class sCTkTextboxSecondary(ctk.CTkTextbox,
|
|
12
|
+
ThemeableWidget)` puts the native CTk class first, so every `super()` call in
|
|
13
|
+
this file's own methods resolves to ctk.CTkTextbox -- 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
|
+
Disabling uses CTk's native state="disabled", consistent with every other
|
|
20
|
+
widget in this library confirmed to correctly block interaction this way.
|
|
21
|
+
"""
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
import platform
|
|
25
|
+
from typing import Any, Optional
|
|
26
|
+
import tkinter as tk
|
|
27
|
+
import customtkinter as ctk
|
|
28
|
+
from .themeable_widget import ThemeableWidget
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class sCTkTextboxSecondary(ctk.CTkTextbox, ThemeableWidget):
|
|
32
|
+
"""Themeable, lower-emphasis multi-line text area.
|
|
33
|
+
|
|
34
|
+
Adds to native ctk.CTkTextbox:
|
|
35
|
+
- Automatic light/dark theme resolution from sCTkThemes.json (via
|
|
36
|
+
ThemeableWidget.__init__ -- see that class's docstring for what it does,
|
|
37
|
+
and just as importantly, what it no longer does).
|
|
38
|
+
- A distinct enabled/disabled visual state, using CTk's native
|
|
39
|
+
state="disabled".
|
|
40
|
+
- Pygubu Designer property introspection for `state`, `fg_color`,
|
|
41
|
+
`text_color`, `border_color`, `scrollbar_button_color`, and
|
|
42
|
+
`scrollbar_button_hover_color` via a single-argument configure() call.
|
|
43
|
+
- A transparency fallback: if fg_color resolves to "transparent" or
|
|
44
|
+
empty at construction, the widget copies its parent's fg_color
|
|
45
|
+
instead. Same rationale as sCTkTextboxPrimary's identical fallback.
|
|
46
|
+
- Manual scrollbar re-theming, same as sCTkTextboxPrimary.
|
|
47
|
+
|
|
48
|
+
Colors are passed through to configure() as raw (light, dark) tuples rather
|
|
49
|
+
than pre-resolved to a single value, so CustomTkinter's own appearance-mode
|
|
50
|
+
tracking repaints them automatically on a light/dark switch -- the same
|
|
51
|
+
approach validated on sCTkComboBox, sCTkSegmentedButton, and the button
|
|
52
|
+
family. Not separately re-confirmed for this specific widget.
|
|
53
|
+
|
|
54
|
+
scrollbar_button_color and scrollbar_button_hover_color are required to be
|
|
55
|
+
present in whichever map is active for the scrollbar re-theming step below
|
|
56
|
+
-- if either is missing, this raises immediately rather than substituting
|
|
57
|
+
a hardcoded color. See sCTkTextboxPrimary's docstring for the full
|
|
58
|
+
rationale.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(self, master: Optional[Any] = None, **kw: Any) -> None:
|
|
62
|
+
"""
|
|
63
|
+
Args:
|
|
64
|
+
master: Parent container.
|
|
65
|
+
**kw: `state` is pulled out explicitly below. Everything else is
|
|
66
|
+
either a native CTkTextbox argument or a theme-key override
|
|
67
|
+
(see the "sCTkTextboxSecondary" block in sCTkThemes.json).
|
|
68
|
+
"""
|
|
69
|
+
# 1. Fire our shared theme logic. This resolves final_kw
|
|
70
|
+
# (construction-time properties) and the disabled color map.
|
|
71
|
+
ThemeableWidget.__init__(self, kw)
|
|
72
|
+
|
|
73
|
+
# 2. Store the resolved maps onto this instance (not deep-copied --
|
|
74
|
+
# see sCTkTextboxPrimary's identical note).
|
|
75
|
+
self._local_defaults = self.final_kw
|
|
76
|
+
self._custom_disabled_map = self._widget_disabled_map
|
|
77
|
+
|
|
78
|
+
# Extract "state" from final_kw after ThemeableWidget's merge.
|
|
79
|
+
state_init = self.final_kw.pop("state", "normal")
|
|
80
|
+
|
|
81
|
+
# 3. Initialize CustomTkinter natively with the clean final kwargs array.
|
|
82
|
+
super().__init__(master, **self.final_kw)
|
|
83
|
+
|
|
84
|
+
# Transparency fallback -- see this class's docstring.
|
|
85
|
+
try:
|
|
86
|
+
current_fg = super().cget("fg_color")
|
|
87
|
+
if current_fg in ("transparent", ""):
|
|
88
|
+
parent_bg = self.master.cget("fg_color")
|
|
89
|
+
if parent_bg not in ("transparent", ""):
|
|
90
|
+
super().configure(fg_color=parent_bg)
|
|
91
|
+
except Exception:
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
# 4. Apply the requested initial state now that the native widget exists.
|
|
95
|
+
self._custom_current_state = "normal"
|
|
96
|
+
self.state(state_init)
|
|
97
|
+
|
|
98
|
+
# 5. Register lifecycle handshake hook, notifying Pygubu-style consumers
|
|
99
|
+
# that construction is complete.
|
|
100
|
+
self._finalize_themeable_lifecycle()
|
|
101
|
+
|
|
102
|
+
def _set_appearance_mode(self, mode_string: str) -> None:
|
|
103
|
+
"""
|
|
104
|
+
Forwards CustomTkinter's internal light/dark mode change notification to
|
|
105
|
+
the native widget.
|
|
106
|
+
|
|
107
|
+
No longer manually re-triggers _update_current_visual_state() -- see
|
|
108
|
+
sCTkTextboxPrimary's identical method for the full reasoning.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
mode_string: The new appearance mode ("Light" or "Dark"), as passed
|
|
112
|
+
by CustomTkinter's internal appearance-mode change machinery.
|
|
113
|
+
"""
|
|
114
|
+
if hasattr(super(), "_set_appearance_mode"):
|
|
115
|
+
try:
|
|
116
|
+
super()._set_appearance_mode(mode_string)
|
|
117
|
+
except Exception:
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
def configure(self, *args: Any, **kwargs: Any) -> Any:
|
|
121
|
+
"""
|
|
122
|
+
Standard widget configuration, with Pygubu/positional-argument handling.
|
|
123
|
+
See sCTkTextboxPrimary's identical method for full Args/Returns detail.
|
|
124
|
+
"""
|
|
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", "text_color", "border_color", "scrollbar_button_color",
|
|
134
|
+
"scrollbar_button_hover_color"]:
|
|
135
|
+
val = self._custom_disabled_map.get(pname) if self._custom_current_state == "disabled" else self._local_defaults.get(pname)
|
|
136
|
+
return (pname, pname, pname, self._query_value(self._theme_default(pname)), self._query_value(val))
|
|
137
|
+
|
|
138
|
+
return self._configure_query(pname)
|
|
139
|
+
|
|
140
|
+
# Runtime overrides have to reach the map a repaint reads, or the
|
|
141
|
+
# repaint puts the theme value straight back -- see
|
|
142
|
+
# ThemeableWidget._record_theme_overrides().
|
|
143
|
+
self._record_theme_overrides(kwargs)
|
|
144
|
+
|
|
145
|
+
if "state" in kwargs:
|
|
146
|
+
self._custom_current_state = str(kwargs.pop("state")).lower()
|
|
147
|
+
self._update_current_visual_state()
|
|
148
|
+
|
|
149
|
+
if kwargs:
|
|
150
|
+
return super().configure(**kwargs)
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
# Tkinter/CTk convention binds .config to .configure as a SEPARATE class
|
|
154
|
+
# attribute -- it does not automatically track whichever configure() a
|
|
155
|
+
# subclass defines. Without this line, calling .config(...) on an instance
|
|
156
|
+
# would silently skip this entire override and land on the native widget's
|
|
157
|
+
# configure() directly, bypassing theming and state handling entirely.
|
|
158
|
+
config = configure
|
|
159
|
+
|
|
160
|
+
def get_state(self) -> str:
|
|
161
|
+
"""Equivalent to calling state() with no argument."""
|
|
162
|
+
return self.state()
|
|
163
|
+
|
|
164
|
+
def state(self, state_string: Optional[str] = None) -> str:
|
|
165
|
+
"""
|
|
166
|
+
Gets or sets the widget's enabled/disabled visual state. See
|
|
167
|
+
sCTkTextboxPrimary's identical method for full Args/Returns detail.
|
|
168
|
+
"""
|
|
169
|
+
if state_string is not None:
|
|
170
|
+
self.configure(state=state_string)
|
|
171
|
+
return getattr(self, "_custom_current_state", "normal")
|
|
172
|
+
|
|
173
|
+
def _update_current_visual_state(self) -> None:
|
|
174
|
+
"""
|
|
175
|
+
Recomputes and applies this widget's colors from the theme file, based
|
|
176
|
+
on the current state, then sets the native interactive lock, then
|
|
177
|
+
re-themes the internal scrollbar. See sCTkTextboxPrimary's identical
|
|
178
|
+
method for the full reasoning, including the required-key checks for
|
|
179
|
+
scrollbar_button_color/scrollbar_button_hover_color.
|
|
180
|
+
"""
|
|
181
|
+
is_disabled = getattr(self, "_custom_current_state", "normal") == "disabled"
|
|
182
|
+
target_map = self._custom_disabled_map if is_disabled else self._local_defaults
|
|
183
|
+
|
|
184
|
+
config_payload = {}
|
|
185
|
+
for key in ("fg_color", "border_color", "text_color", "scrollbar_button_color", "scrollbar_button_hover_color"):
|
|
186
|
+
val = target_map.get(key)
|
|
187
|
+
if val is not None:
|
|
188
|
+
config_payload[key] = val
|
|
189
|
+
|
|
190
|
+
if config_payload:
|
|
191
|
+
super().configure(**config_payload)
|
|
192
|
+
|
|
193
|
+
if is_disabled:
|
|
194
|
+
super().configure(state="disabled")
|
|
195
|
+
else:
|
|
196
|
+
super().configure(state="normal")
|
|
197
|
+
|
|
198
|
+
if hasattr(self, "_scrollbar") and self._scrollbar:
|
|
199
|
+
try:
|
|
200
|
+
if target_map.get("scrollbar_button_color") is None:
|
|
201
|
+
raise KeyError(
|
|
202
|
+
f"'{self.__class__.__name__}' theme block is missing "
|
|
203
|
+
f"'scrollbar_button_color' in its "
|
|
204
|
+
f"{'disabled_map' if is_disabled else 'top-level'} section."
|
|
205
|
+
)
|
|
206
|
+
if is_disabled:
|
|
207
|
+
bar_color = target_map.get("scrollbar_button_color")
|
|
208
|
+
self._scrollbar.configure(button_color=bar_color, button_hover_color=bar_color)
|
|
209
|
+
else:
|
|
210
|
+
if target_map.get("scrollbar_button_hover_color") is None:
|
|
211
|
+
raise KeyError(
|
|
212
|
+
f"'{self.__class__.__name__}' theme block is missing "
|
|
213
|
+
f"'scrollbar_button_hover_color' at the top level."
|
|
214
|
+
)
|
|
215
|
+
self._scrollbar.configure(
|
|
216
|
+
button_color=target_map.get("scrollbar_button_color"),
|
|
217
|
+
button_hover_color=target_map.get("scrollbar_button_hover_color"),
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
if hasattr(self._scrollbar, "_draw"):
|
|
221
|
+
self._scrollbar._draw()
|
|
222
|
+
except KeyError:
|
|
223
|
+
raise
|
|
224
|
+
except Exception:
|
|
225
|
+
pass
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
"""
|
|
3
|
+
sCTkToplevel
|
|
4
|
+
|
|
5
|
+
A theme-compliant top-level window wrapper, for secondary windows, modal
|
|
6
|
+
dialogs, and popups. Inherits directly from ctk.CTkToplevel so CustomTkinter
|
|
7
|
+
handles native window management; this class layers automatic light/dark
|
|
8
|
+
theme resolution on top.
|
|
9
|
+
|
|
10
|
+
Base class order matters here: `class sCTkToplevel(ctk.CTkToplevel,
|
|
11
|
+
ThemeableWidget)` puts the native CTk class first, so every `super()` call in
|
|
12
|
+
this file's own methods resolves to ctk.CTkToplevel -- and, beneath it,
|
|
13
|
+
tkinter.Misc -- never to ThemeableWidget. ThemeableWidget's own
|
|
14
|
+
configure()/cget()/_set_appearance_mode() overrides have been removed entirely
|
|
15
|
+
for this reason (see themeable_widget.py's docstring).
|
|
16
|
+
|
|
17
|
+
This is the simplest widget in this project's audit so far: no disabled
|
|
18
|
+
state, no state()/get_state() at all, and no per-state color-swapping logic.
|
|
19
|
+
A top-level window has no interactive "enabled/disabled" concept the way a
|
|
20
|
+
control does, and its theme block (see sCTkThemes.json) defines only
|
|
21
|
+
fg_color -- nothing else.
|
|
22
|
+
"""
|
|
23
|
+
from typing import Any, Optional
|
|
24
|
+
import customtkinter as ctk
|
|
25
|
+
from .themeable_widget import ThemeableWidget
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class sCTkToplevel(ctk.CTkToplevel, ThemeableWidget):
|
|
29
|
+
"""Themeable top-level window.
|
|
30
|
+
|
|
31
|
+
Adds to native ctk.CTkToplevel:
|
|
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
|
+
- Positional-dict support in configure(), e.g.
|
|
36
|
+
toplevel.configure({"fg_color": "red"}) -- merged into keyword
|
|
37
|
+
arguments and applied normally.
|
|
38
|
+
|
|
39
|
+
Single-argument property queries go to ThemeableWidget's shared
|
|
40
|
+
_configure_query(), like every other widget. This class ignored them
|
|
41
|
+
until recently, which mattered as soon as sCTkToplevel was registered
|
|
42
|
+
with the Designer: blanking a field there reads the property's default
|
|
43
|
+
first, and an unanswered query returns None, which pygubu then applies.
|
|
44
|
+
|
|
45
|
+
There is still no state()/get_state() and no per-state colour swapping.
|
|
46
|
+
A window has no enabled/disabled concept, and nothing in this class
|
|
47
|
+
reapplies colours after construction -- which is also why it needs no
|
|
48
|
+
override recording: with no repaint to revert them, a runtime change
|
|
49
|
+
simply stays.
|
|
50
|
+
|
|
51
|
+
WHITELIST GUARD: if a composite widget inherits sCTkToplevel as its own
|
|
52
|
+
base class and explicitly calls ThemeableWidget.__init__ itself before
|
|
53
|
+
calling super().__init__(), that composite's own final_kw could contain
|
|
54
|
+
keys native ctk.CTkToplevel knows nothing about. This matters MORE here
|
|
55
|
+
than for most widgets: CTkToplevel's own __init__ explicitly calls
|
|
56
|
+
check_kwargs_empty(kwargs, raise_error=True) after popping its known-valid
|
|
57
|
+
keys, confirmed directly against CustomTkinter's own source -- meaning ANY
|
|
58
|
+
unrecognized keyword reaching it is GUARANTEED to raise, not just
|
|
59
|
+
incidentally likely to. _NATIVE_CTKTOPLEVEL_KWARGS filters final_kw down
|
|
60
|
+
to only the keys the real native constructor accepts before that call.
|
|
61
|
+
This only matters for the Pattern-B composition scenario described above;
|
|
62
|
+
for direct construction of a plain sCTkToplevel, final_kw already only
|
|
63
|
+
contains this widget's own theme keys (just fg_color), so the filter is a
|
|
64
|
+
no-op.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
# Confirmed directly against CustomTkinter's own ctk_toplevel.py source:
|
|
68
|
+
# CTkToplevel's own _valid_tk_toplevel_arguments set, plus fg_color as the
|
|
69
|
+
# one CTk-specific parameter defined separately in its __init__ signature.
|
|
70
|
+
# "master" is excluded here since it's always passed positionally, never
|
|
71
|
+
# part of the filtered kwargs dict.
|
|
72
|
+
_NATIVE_CTKTOPLEVEL_KWARGS = frozenset({
|
|
73
|
+
"bd", "borderwidth", "class", "container", "cursor", "height",
|
|
74
|
+
"highlightbackground", "highlightthickness", "menu", "relief",
|
|
75
|
+
"screen", "takefocus", "use", "visual", "width", "fg_color",
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
def __init__(self, master: Optional[Any] = None, **kwargs: Any) -> None:
|
|
79
|
+
"""
|
|
80
|
+
Args:
|
|
81
|
+
master: Parent window.
|
|
82
|
+
**kwargs: Any native CTkToplevel argument, or an fg_color override
|
|
83
|
+
(see the "sCTkToplevel" block in sCTkThemes.json, which
|
|
84
|
+
currently defines only fg_color).
|
|
85
|
+
"""
|
|
86
|
+
# 1. Fire our shared theme logic first. This resolves final_kw
|
|
87
|
+
# (construction-time properties). See ThemeableWidget.__init__ for
|
|
88
|
+
# what actually happens here.
|
|
89
|
+
ThemeableWidget.__init__(self, kwargs)
|
|
90
|
+
self._local_defaults = dict(self.final_kw)
|
|
91
|
+
|
|
92
|
+
# 2. Initialize CustomTkinter's native top-level window. Only forwards
|
|
93
|
+
# the subset of final_kw that native CTkToplevel actually accepts --
|
|
94
|
+
# see this class's docstring ("WHITELIST GUARD") for why this
|
|
95
|
+
# filtering exists.
|
|
96
|
+
native_kwargs = {k: v for k, v in self.final_kw.items() if k in self._NATIVE_CTKTOPLEVEL_KWARGS}
|
|
97
|
+
super().__init__(master, **native_kwargs)
|
|
98
|
+
|
|
99
|
+
# 3. Register lifecycle handshake hook, notifying Pygubu-style consumers
|
|
100
|
+
# that construction is complete.
|
|
101
|
+
self._finalize_themeable_lifecycle()
|
|
102
|
+
|
|
103
|
+
def configure(self, *args: Any, **kwargs: Any) -> Any:
|
|
104
|
+
"""
|
|
105
|
+
Standard widget configuration, with positional-dict support.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
*args: At most one positional argument is meaningful:
|
|
109
|
+
- a dict: merged into kwargs and processed normally.
|
|
110
|
+
- a property name: returns a Tkinter-style
|
|
111
|
+
(name, name, name, default, current) tuple from the shared
|
|
112
|
+
query helper.
|
|
113
|
+
**kwargs: Standard CTkToplevel configuration options.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
The query tuple for the single-argument case, otherwise whatever
|
|
117
|
+
super().configure() returns.
|
|
118
|
+
"""
|
|
119
|
+
# args is always a tuple, never a dict itself -- args[0] is the actual
|
|
120
|
+
# dict if one was passed. An earlier version checked
|
|
121
|
+
# `isinstance(args, dict)` directly on the tuple, which can never be
|
|
122
|
+
# true; that dict-merge path was unreachable. Fixed here to check
|
|
123
|
+
# args[0] instead.
|
|
124
|
+
if len(args) == 1:
|
|
125
|
+
if isinstance(args[0], dict):
|
|
126
|
+
kwargs = {**args[0], **kwargs}
|
|
127
|
+
else:
|
|
128
|
+
# The single-argument query, which this widget used to ignore.
|
|
129
|
+
#
|
|
130
|
+
# Pygubu calls configure(name) to read a property's default
|
|
131
|
+
# whenever a field is blanked in the inspector. Falling through
|
|
132
|
+
# to the native configure() does not answer that: CustomTkinter
|
|
133
|
+
# declares configure(self, require_redraw=False, **kwargs), so
|
|
134
|
+
# the property NAME arrives as require_redraw and the call
|
|
135
|
+
# returns None -- which pygubu then applies.
|
|
136
|
+
#
|
|
137
|
+
# _configure_query() is the shared helper every other widget
|
|
138
|
+
# uses; this one simply never called it.
|
|
139
|
+
return self._configure_query(args[0])
|
|
140
|
+
|
|
141
|
+
if kwargs:
|
|
142
|
+
# RETURNED, not discarded. The docstring noted this was preserved
|
|
143
|
+
# as-is from an earlier version; a caller reading the result got
|
|
144
|
+
# None whatever the native widget said.
|
|
145
|
+
return super().configure(**kwargs)
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
# Tkinter/CTk convention binds .config to .configure as a SEPARATE class
|
|
149
|
+
# attribute -- it does not automatically track whichever configure() a
|
|
150
|
+
# subclass defines. Without this line, calling .config(...) on an instance
|
|
151
|
+
# would silently skip this entire override and land on the native widget's
|
|
152
|
+
# configure() directly.
|
|
153
|
+
config = configure
|