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