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,308 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
"""
|
|
3
|
+
sCTkSeparator
|
|
4
|
+
|
|
5
|
+
An advanced Separator widget supporting custom section header text,
|
|
6
|
+
dashed line patterns, corner roundness, and responsive orientation modes.
|
|
7
|
+
Inherits cleanly and directly from ctk.CTkBaseClass to preserve native canvas draw engines.
|
|
8
|
+
|
|
9
|
+
Derived from Selector class by Fastattack, 2024.
|
|
10
|
+
https://github.com
|
|
11
|
+
"""
|
|
12
|
+
import customtkinter as ctk
|
|
13
|
+
from .themeable_widget import ThemeableWidget
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class sCTkSeparator(ctk.CTkBaseClass, ThemeableWidget):
|
|
17
|
+
"""Advanced Separator supporting headers, dashed lines, and themes.json matrices."""
|
|
18
|
+
|
|
19
|
+
# Required at the TOP LEVEL of the theme block. Structural parameters
|
|
20
|
+
# (orientation, length, width, text, dash) are deliberately absent: they
|
|
21
|
+
# are constructor arguments with sensible defaults, and although the theme
|
|
22
|
+
# CAN supply them, requiring them would force layout decisions into the
|
|
23
|
+
# stylesheet.
|
|
24
|
+
_REQUIRED_THEME_KEYS = ("fg_color", "text_color", "font", "corner_radius")
|
|
25
|
+
|
|
26
|
+
# Required inside disabled_map.
|
|
27
|
+
_REQUIRED_DISABLED_KEYS = ("fg_color", "text_color")
|
|
28
|
+
|
|
29
|
+
def __init__(self, master=None, **kwargs):
|
|
30
|
+
# 1. Fire our shared theme logic first. It automatically finds "sCTkSeparator" in themes.json
|
|
31
|
+
ThemeableWidget.__init__(self, kwargs)
|
|
32
|
+
|
|
33
|
+
# 2. 🛠️ THE MUTATION SAFEGUARD DEEP COPY SHIELD:
|
|
34
|
+
self._local_defaults = dict(self.final_kw)
|
|
35
|
+
self._custom_disabled_map = dict(self._widget_disabled_map)
|
|
36
|
+
self._validate_theme_keys()
|
|
37
|
+
|
|
38
|
+
# Extract structural parameters safely out of the resolved theme dictionary
|
|
39
|
+
self._orientation = str(self.final_kw.pop("orientation", "vertical")).lower()
|
|
40
|
+
length = int(self.final_kw.pop("length", 100))
|
|
41
|
+
width = float(self.final_kw.pop("width", 4))
|
|
42
|
+
|
|
43
|
+
self._text = str(self.final_kw.pop("text", ""))
|
|
44
|
+
self._dash = self.final_kw.pop("dash", None)
|
|
45
|
+
# No fallback: validated above.
|
|
46
|
+
self._font = self.final_kw.pop("font")
|
|
47
|
+
|
|
48
|
+
# 🔑 VERTICAL HOUSING REMAP: Programmatically upscale horizontal bounds to fit text
|
|
49
|
+
if self._text and width <= 4:
|
|
50
|
+
width = 28
|
|
51
|
+
|
|
52
|
+
if self._orientation == "vertical":
|
|
53
|
+
height = length
|
|
54
|
+
elif self._orientation == "horizontal":
|
|
55
|
+
height = width
|
|
56
|
+
width = length
|
|
57
|
+
else:
|
|
58
|
+
raise ValueError(
|
|
59
|
+
f"The value for orientation is incorrect: \"{self._orientation}\". Should be \"vertical\" or \"horizontal\"")
|
|
60
|
+
|
|
61
|
+
# 3. Initialize CustomTkinter's base structure using finalized parameters
|
|
62
|
+
ctk.CTkBaseClass.__init__(
|
|
63
|
+
self,
|
|
64
|
+
master=master,
|
|
65
|
+
width=width,
|
|
66
|
+
height=height,
|
|
67
|
+
bg_color=self.final_kw.get("bg_color", "transparent")
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
self._custom_current_state = "normal"
|
|
71
|
+
self._corner_radius = self.final_kw.get("corner_radius")
|
|
72
|
+
self._fg_color = self._check_color_type(self.final_kw.get("fg_color"))
|
|
73
|
+
|
|
74
|
+
# Map text color vectors safely out of the extracted dictionary layout layers
|
|
75
|
+
# No CTkLabel fallback: text_color is validated above, so borrowing another
|
|
76
|
+
# widget class's colour would only mask a theme gap.
|
|
77
|
+
self._text_color = self._check_color_type(self.final_kw.get("text_color"))
|
|
78
|
+
|
|
79
|
+
# 4. Canvas and render configurations
|
|
80
|
+
self._canvas = ctk.CTkCanvas(self, highlightthickness=0)
|
|
81
|
+
self._canvas.place(x=0, y=0, relwidth=1, relheight=1)
|
|
82
|
+
self._draw_engine = ctk.DrawEngine(self._canvas)
|
|
83
|
+
|
|
84
|
+
# 5. Bind layout adjustments to bypass CTkBaseClass strict bind filters safely
|
|
85
|
+
super(ctk.CTkBaseClass, self).bind("<Configure>", lambda e: self._draw(), add="+")
|
|
86
|
+
|
|
87
|
+
# 6. Trigger the initial render loop pass
|
|
88
|
+
self._draw(no_color_updates=True)
|
|
89
|
+
|
|
90
|
+
# 🔑 7. REGISTER LIFECYCLE HANDSHAKE HOOK: Pushes notifications up to Pygubu systems cleanly.
|
|
91
|
+
self._finalize_themeable_lifecycle()
|
|
92
|
+
|
|
93
|
+
def _validate_theme_keys(self) -> None:
|
|
94
|
+
"""
|
|
95
|
+
Hard-fails at construction on an incomplete theme block, naming the
|
|
96
|
+
missing key and where it belongs.
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
KeyError: naming the first missing key found.
|
|
100
|
+
"""
|
|
101
|
+
name = self.__class__.__name__
|
|
102
|
+
for key in self._REQUIRED_THEME_KEYS:
|
|
103
|
+
if self._local_defaults.get(key) is None:
|
|
104
|
+
raise KeyError(
|
|
105
|
+
f"'{name}' theme block is missing '{key}' at the top level "
|
|
106
|
+
f"of sCTkThemes.json."
|
|
107
|
+
)
|
|
108
|
+
for key in self._REQUIRED_DISABLED_KEYS:
|
|
109
|
+
if self._custom_disabled_map.get(key) is None:
|
|
110
|
+
raise KeyError(
|
|
111
|
+
f"'{name}' theme block is missing '{key}' in disabled_map."
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def _set_appearance_mode(self, mode_string: str):
|
|
115
|
+
"""Native look catcher ensuring active or disabled tracks repaint fluidly on theme shifts."""
|
|
116
|
+
if hasattr(super(), "_set_appearance_mode"):
|
|
117
|
+
try:
|
|
118
|
+
super()._set_appearance_mode(mode_string)
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
self._update_current_visual_state()
|
|
122
|
+
def _update_current_visual_state(self):
|
|
123
|
+
"""Forwards global theme preference swipes directly to our core draw layout routine."""
|
|
124
|
+
self._draw()
|
|
125
|
+
|
|
126
|
+
def _draw(self, no_color_updates=False):
|
|
127
|
+
if hasattr(super(), "_draw"):
|
|
128
|
+
try: super()._draw(no_color_updates)
|
|
129
|
+
except Exception: pass
|
|
130
|
+
current_w = self.winfo_width() if self.winfo_width() > 1 else self._current_width
|
|
131
|
+
current_h = self.winfo_height() if self.winfo_height() > 1 else self._current_height
|
|
132
|
+
self._canvas.delete("all")
|
|
133
|
+
|
|
134
|
+
detected_bg = self._detect_color_of_master()
|
|
135
|
+
if detected_bg == "transparent" or detected_bg is None:
|
|
136
|
+
detected_bg = ctk.ThemeManager.theme["CTk"]["fg_color"]
|
|
137
|
+
|
|
138
|
+
is_disabled = getattr(self, "_custom_current_state", "normal") == "disabled"
|
|
139
|
+
# FIX: these previously carried hardcoded fallbacks, and since this
|
|
140
|
+
# widget's theme block had no disabled_map at all, the fallbacks were
|
|
141
|
+
# ALWAYS taken -- a disabled separator never used the configured
|
|
142
|
+
# theme. disabled_map is now required, so no fallback is needed. (The
|
|
143
|
+
# old text fallback also used the Tk colour name "gray50" rather than
|
|
144
|
+
# a hex pair, the only such value in the library.)
|
|
145
|
+
target_fg = self._custom_disabled_map.get("fg_color") if is_disabled else self._fg_color
|
|
146
|
+
target_txt = self._custom_disabled_map.get("text_color") if is_disabled else self._text_color
|
|
147
|
+
|
|
148
|
+
fg_rendered = self._apply_appearance_mode(self._check_color_type(target_fg))
|
|
149
|
+
txt_rendered = self._apply_appearance_mode(self._check_color_type(target_txt))
|
|
150
|
+
self._canvas.configure(bg=self._apply_appearance_mode(detected_bg))
|
|
151
|
+
|
|
152
|
+
if self._orientation == "horizontal":
|
|
153
|
+
line_thickness = self._current_height if self._current_height < current_h else 4
|
|
154
|
+
if line_thickness > 10: line_thickness = 4
|
|
155
|
+
else:
|
|
156
|
+
line_thickness = self._current_width if self._current_width < current_w else 4
|
|
157
|
+
if line_thickness > 10: line_thickness = 4
|
|
158
|
+
|
|
159
|
+
if self._text:
|
|
160
|
+
t_id = self._canvas.create_text(current_w / 2, current_h / 2, text=self._text, font=self._font, fill=txt_rendered)
|
|
161
|
+
bbox = self._canvas.bbox(t_id)
|
|
162
|
+
if bbox:
|
|
163
|
+
text_width = bbox[2] - bbox[0]
|
|
164
|
+
text_height = bbox[3] - bbox[1]
|
|
165
|
+
tw, th = text_width + 16, text_height + 8
|
|
166
|
+
x1 = (current_w / 2) - (tw / 2)
|
|
167
|
+
x2 = (current_w / 2) + (tw / 2)
|
|
168
|
+
|
|
169
|
+
if self._orientation == "horizontal":
|
|
170
|
+
y1, y2 = 1, current_h - 1
|
|
171
|
+
self._canvas.create_line(x1, y1, x1, y2, fill=fg_rendered, width=2)
|
|
172
|
+
self._canvas.create_line(x2, y1, x2, y2, fill=fg_rendered, width=2)
|
|
173
|
+
mid_y = current_h / 2
|
|
174
|
+
self._canvas.create_line(0, mid_y, x1, mid_y, fill=fg_rendered, width=line_thickness, dash=self._dash)
|
|
175
|
+
self._canvas.create_line(x2, mid_y, current_w, mid_y, fill=fg_rendered, width=line_thickness, dash=self._dash)
|
|
176
|
+
else:
|
|
177
|
+
x1_v, x2_v = 1, current_w - 1
|
|
178
|
+
y1 = (current_h / 2) - (th / 2)
|
|
179
|
+
y2 = (current_h / 2) + (th / 2)
|
|
180
|
+
self._canvas.create_line(x1_v, y1, x2_v, y1, fill=fg_rendered, width=2)
|
|
181
|
+
self._canvas.create_line(x1_v, y2, x2_v, y2, fill=fg_rendered, width=2)
|
|
182
|
+
mid_x = current_w / 2
|
|
183
|
+
self._canvas.create_line(mid_x, 0, mid_x, y1, fill=fg_rendered, width=line_thickness, dash=self._dash)
|
|
184
|
+
self._canvas.create_line(mid_x, y2, mid_x, current_h, fill=fg_rendered, width=line_thickness, dash=self._dash)
|
|
185
|
+
else:
|
|
186
|
+
if self._dash:
|
|
187
|
+
if self._orientation == "horizontal":
|
|
188
|
+
self._canvas.create_line(0, current_h / 2, current_w, current_h / 2, fill=fg_rendered, width=line_thickness, dash=self._dash)
|
|
189
|
+
else:
|
|
190
|
+
self._canvas.create_line(current_w / 2, 0, current_w / 2, current_h, fill=fg_rendered, width=line_thickness, dash=self._dash)
|
|
191
|
+
else:
|
|
192
|
+
self._draw_engine.draw_rounded_rect_with_border(current_w, current_h, self._apply_widget_scaling(self._corner_radius), 0)
|
|
193
|
+
self._canvas.itemconfig("inner_parts", outline=fg_rendered, fill=fg_rendered)
|
|
194
|
+
|
|
195
|
+
def configure(self, *args, **kwargs):
|
|
196
|
+
"""Processes Pygubu designer workspace queries and manages theme state updates cleanly."""
|
|
197
|
+
if args and len(args) == 1:
|
|
198
|
+
pname = args[0]
|
|
199
|
+
if pname == "state": return ("state", "state", "state", "normal", self.get_state())
|
|
200
|
+
if pname in ["fg_color", "text_color"]:
|
|
201
|
+
val = self._custom_disabled_map.get(pname) if self.get_state() == "disabled" else self._local_defaults.get(pname)
|
|
202
|
+
return (pname, pname, pname, self._query_value(self._theme_default(pname)), self._query_value(val))
|
|
203
|
+
return self._configure_query(pname)
|
|
204
|
+
|
|
205
|
+
# FIX: was `if args and isinstance(args, dict)`. args is ALWAYS a
|
|
206
|
+
# tuple, so this never fired and the dict form of configure() was
|
|
207
|
+
# dead code. Same tautology fixed across the batch-one widgets.
|
|
208
|
+
if len(args) == 1 and isinstance(args[0], dict): kwargs = {**args[0], **kwargs}
|
|
209
|
+
# Runtime overrides have to reach the map a repaint reads, or the
|
|
210
|
+
# repaint puts the theme value straight back -- see
|
|
211
|
+
# ThemeableWidget._record_theme_overrides().
|
|
212
|
+
self._record_theme_overrides(kwargs)
|
|
213
|
+
|
|
214
|
+
if "state" in kwargs: self.state(kwargs.pop("state"))
|
|
215
|
+
if "text" in kwargs: self._text = str(kwargs.pop("text"))
|
|
216
|
+
if "dash" in kwargs: self._dash = kwargs.pop("dash")
|
|
217
|
+
|
|
218
|
+
# FIX: orientation was accepted by __init__ and registered as a Pygubu
|
|
219
|
+
# property, but configure() never popped it -- so changing it at
|
|
220
|
+
# runtime fell through to native CTkBaseClass.configure(), whose
|
|
221
|
+
# check_kwargs_empty() raised "['orientation'] are not supported
|
|
222
|
+
# arguments". Construction worked; reconfiguration did not, which is
|
|
223
|
+
# exactly what the Designer does when the dropdown is changed.
|
|
224
|
+
#
|
|
225
|
+
# Unlike the other properties here, orientation has a GEOMETRIC
|
|
226
|
+
# consequence: __init__ assigns height=length for a vertical
|
|
227
|
+
# separator and width=length for a horizontal one. Switching
|
|
228
|
+
# orientation therefore has to swap the two dimensions, or a vertical
|
|
229
|
+
# separator turned horizontal stays tall and thin.
|
|
230
|
+
if "orientation" in kwargs:
|
|
231
|
+
new_orientation = str(kwargs.pop("orientation")).lower()
|
|
232
|
+
if new_orientation not in ("vertical", "horizontal"):
|
|
233
|
+
raise ValueError(
|
|
234
|
+
f'The value for orientation is incorrect: "{new_orientation}". '
|
|
235
|
+
f'Should be "vertical" or "horizontal"'
|
|
236
|
+
)
|
|
237
|
+
if new_orientation != self._orientation:
|
|
238
|
+
self._orientation = new_orientation
|
|
239
|
+
# Swap, rather than recompute from length/width: those were
|
|
240
|
+
# consumed at construction and aren't retained, and the current
|
|
241
|
+
# dimensions already reflect any resizing since.
|
|
242
|
+
kwargs.setdefault("width", self._current_height)
|
|
243
|
+
kwargs.setdefault("height", self._current_width)
|
|
244
|
+
|
|
245
|
+
# length is a construction-time alias for whichever dimension the
|
|
246
|
+
# orientation makes the long one. Translated here so the Designer can
|
|
247
|
+
# edit it after the fact.
|
|
248
|
+
if "length" in kwargs:
|
|
249
|
+
new_length = int(kwargs.pop("length"))
|
|
250
|
+
if self._orientation == "vertical":
|
|
251
|
+
kwargs["height"] = new_length
|
|
252
|
+
else:
|
|
253
|
+
kwargs["width"] = new_length
|
|
254
|
+
|
|
255
|
+
self._drop_cleared(kwargs)
|
|
256
|
+
if kwargs: super().configure(**kwargs)
|
|
257
|
+
self._draw()
|
|
258
|
+
|
|
259
|
+
config = configure
|
|
260
|
+
def get_state(self) -> str: return str(getattr(self, "_custom_current_state", "normal")).lower()
|
|
261
|
+
def state(self, mode: str = None) -> str:
|
|
262
|
+
if mode is None: return self.get_state()
|
|
263
|
+
self._custom_current_state = mode.lower()
|
|
264
|
+
self._draw()
|
|
265
|
+
return self._custom_current_state
|
|
266
|
+
|
|
267
|
+
def cget(self, attribute_name: str):
|
|
268
|
+
if attribute_name == "height": raise ValueError("Use length and width arguments instead.")
|
|
269
|
+
if attribute_name == "state": return self.get_state()
|
|
270
|
+
mapping = {"corner_radius": self._corner_radius, "fg_color": self._fg_color,
|
|
271
|
+
"text_color": self._text_color, "orientation": self._orientation,
|
|
272
|
+
"text": self._text, "dash": self._dash}
|
|
273
|
+
# CHECKED FIRST, not passed as get()'s default.
|
|
274
|
+
#
|
|
275
|
+
# This read `mapping.get(name, super().cget(name))`, and Python works
|
|
276
|
+
# out the default BEFORE calling get() -- so super().cget(name) ran on
|
|
277
|
+
# every call and raised for anything the native base class does not
|
|
278
|
+
# know, including every property listed above. cget("text") and
|
|
279
|
+
# cget("fg_color") both failed on a separator that knew their values
|
|
280
|
+
# perfectly well. text_color was also missing from the list.
|
|
281
|
+
if attribute_name in mapping:
|
|
282
|
+
return mapping[attribute_name]
|
|
283
|
+
return super().cget(attribute_name)
|
|
284
|
+
|
|
285
|
+
def bind(self, sequence=None, command=None, add=True):
|
|
286
|
+
"""
|
|
287
|
+
Routes bindings to the internal canvas, which is what actually
|
|
288
|
+
receives events -- CTkBaseClass filters direct binds on the widget.
|
|
289
|
+
|
|
290
|
+
FIX: the `add` argument was previously accepted and then ignored,
|
|
291
|
+
with add=True hardcoded in the forwarded call. A caller passing
|
|
292
|
+
add=False expecting to REPLACE existing bindings would silently
|
|
293
|
+
accumulate them instead.
|
|
294
|
+
"""
|
|
295
|
+
self._canvas.bind(sequence, command, add=add)
|
|
296
|
+
|
|
297
|
+
def unbind(self, sequence=None, funcid=None):
|
|
298
|
+
"""
|
|
299
|
+
Removes a binding from the internal canvas.
|
|
300
|
+
|
|
301
|
+
FIX: funcid was previously accepted and then discarded, so this always
|
|
302
|
+
removed EVERY binding for the sequence rather than the single one the
|
|
303
|
+
caller identified. That is the same destructive behaviour that made
|
|
304
|
+
unbind() unusable for blocking scrollbar drags in
|
|
305
|
+
sCTkScrollableFrame -- Tk's unbind() with no funcid wipes bindings
|
|
306
|
+
this widget never installed, with no way to restore them.
|
|
307
|
+
"""
|
|
308
|
+
self._canvas.unbind(sequence, funcid)
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
"""
|
|
3
|
+
sCTkSlider
|
|
4
|
+
|
|
5
|
+
A theme-compliant linear adjustment slider widget. Inherits directly from
|
|
6
|
+
ctk.CTkSlider so CustomTkinter handles native mouse-drag handling, scaling,
|
|
7
|
+
and coordinate snapping; this class layers automatic light/dark theme
|
|
8
|
+
resolution and a distinct enabled/disabled visual state on top.
|
|
9
|
+
|
|
10
|
+
Base class order matters here: `class sCTkSlider(ctk.CTkSlider,
|
|
11
|
+
ThemeableWidget)` puts the native CTk class first, so every `super()` call in
|
|
12
|
+
this file's own methods resolves to ctk.CTkSlider -- 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); this widget owns all of
|
|
16
|
+
its own runtime color-swapping logic.
|
|
17
|
+
|
|
18
|
+
Unlike every other widget in this project, state is NOT tracked in a parallel
|
|
19
|
+
self._custom_current_state attribute -- state() and _apply_custom_theme_colors()
|
|
20
|
+
both read the NATIVE widget's own "state" property directly via
|
|
21
|
+
super().cget("state"), treating it as the single source of truth rather than
|
|
22
|
+
duplicating it. Disabling routes "state" straight through to the native
|
|
23
|
+
widget's own configure(), which is the same underlying mechanism confirmed
|
|
24
|
+
correct elsewhere in this library (native state="disabled" blocks
|
|
25
|
+
interaction; manual event-unbinding does not).
|
|
26
|
+
"""
|
|
27
|
+
from typing import Any, Callable, Optional
|
|
28
|
+
import customtkinter as ctk
|
|
29
|
+
from .themeable_widget import ThemeableWidget, defer
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class sCTkSlider(ctk.CTkSlider, ThemeableWidget):
|
|
33
|
+
"""Themeable linear adjustment slider.
|
|
34
|
+
|
|
35
|
+
Adds to native ctk.CTkSlider:
|
|
36
|
+
- Automatic light/dark theme resolution from sCTkThemes.json (via
|
|
37
|
+
ThemeableWidget.__init__ -- see that class's docstring for what it does,
|
|
38
|
+
and just as importantly, what it no longer does).
|
|
39
|
+
- A distinct enabled/disabled visual state, read directly from the
|
|
40
|
+
native widget's own state (see module docstring) rather than a
|
|
41
|
+
parallel instance attribute.
|
|
42
|
+
- Pygubu Designer property introspection for `state`, `fg_color`,
|
|
43
|
+
`progress_color`, `button_color`, and `button_hover_color` via a
|
|
44
|
+
single-argument configure() call.
|
|
45
|
+
|
|
46
|
+
Colors are passed through to configure() as raw (light, dark) tuples rather
|
|
47
|
+
than pre-resolved to a single value, so CustomTkinter's own appearance-mode
|
|
48
|
+
tracking repaints them automatically on a light/dark switch -- the same
|
|
49
|
+
approach validated on sCTkComboBox, sCTkSegmentedButton, and the button
|
|
50
|
+
family. Not separately re-confirmed for this specific widget.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(self, master: Optional[Any] = None, **kw: Any) -> None:
|
|
54
|
+
"""
|
|
55
|
+
Args:
|
|
56
|
+
master: Parent container.
|
|
57
|
+
**kw: `command` (callable) and `variable` (tkinter.Variable) are
|
|
58
|
+
pulled out explicitly below. Everything else is either a
|
|
59
|
+
native CTkSlider argument or a theme-key override (see the
|
|
60
|
+
"sCTkSlider" block in sCTkThemes.json).
|
|
61
|
+
"""
|
|
62
|
+
# 1. Capture widget-specific attributes early, before the mixin's own
|
|
63
|
+
# configuration pass.
|
|
64
|
+
command = kw.pop("command", None)
|
|
65
|
+
variable = kw.pop("variable", None)
|
|
66
|
+
|
|
67
|
+
# 2. Fire our shared theme logic. This resolves final_kw
|
|
68
|
+
# (construction-time properties) and the disabled color map. See
|
|
69
|
+
# ThemeableWidget.__init__ for what actually happens here.
|
|
70
|
+
ThemeableWidget.__init__(self, kw)
|
|
71
|
+
self._local_defaults = dict(self.final_kw)
|
|
72
|
+
self._custom_disabled_map = dict(self._widget_disabled_map)
|
|
73
|
+
|
|
74
|
+
# Remove any stray "state" key from final_kw -- prevents native
|
|
75
|
+
# constructor validation collisions.
|
|
76
|
+
self.final_kw.pop("state", None)
|
|
77
|
+
|
|
78
|
+
# 3. Initialize CustomTkinter natively with the clean final kwargs array.
|
|
79
|
+
super().__init__(master, **self.final_kw)
|
|
80
|
+
|
|
81
|
+
if command is not None: super().configure(command=command)
|
|
82
|
+
if variable is not None: super().configure(variable=variable)
|
|
83
|
+
|
|
84
|
+
# Deferred slightly, matching this file's original startup pattern --
|
|
85
|
+
# applies the initial theme colors shortly after construction.
|
|
86
|
+
defer(self, 10, self._apply_custom_theme_colors)
|
|
87
|
+
self._finalize_themeable_lifecycle()
|
|
88
|
+
|
|
89
|
+
def configure(self, *args: Any, **kwargs: Any) -> Any:
|
|
90
|
+
"""
|
|
91
|
+
Standard widget configuration, with Pygubu/positional-argument handling.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
*args: At most one positional argument is meaningful:
|
|
95
|
+
- a dict: merged into kwargs and processed normally below.
|
|
96
|
+
- the literal string "state": returns a Tkinter-style
|
|
97
|
+
(name, name, name, default, current) tuple.
|
|
98
|
+
- one of "fg_color"/"progress_color"/"button_color"/
|
|
99
|
+
"button_hover_color": returns the same style of tuple, with
|
|
100
|
+
`current` reflecting the disabled or normal value as
|
|
101
|
+
appropriate. Note the returned value is str(value), where
|
|
102
|
+
value may itself be a (light, dark) tuple rather than a
|
|
103
|
+
single resolved color -- a known limitation shared with the
|
|
104
|
+
wider Pygubu-query investigation set aside elsewhere in this
|
|
105
|
+
project, not fixed here.
|
|
106
|
+
- anything else: forwarded directly to the native widget's
|
|
107
|
+
configure(), which does not support single-argument property
|
|
108
|
+
queries for arbitrary properties (same limitation).
|
|
109
|
+
**kwargs: Standard CTkSlider configuration options, plus:
|
|
110
|
+
`command`/`variable` are routed through super().configure()
|
|
111
|
+
individually. Note `state` is NOT specially intercepted here
|
|
112
|
+
-- it flows straight through to the native widget's own
|
|
113
|
+
configure(state=...), which is what makes this widget's
|
|
114
|
+
disable mechanism correct (see module docstring).
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
The query tuple described above for the single-argument case, or
|
|
118
|
+
whatever super().configure() returns for the keyword-argument case
|
|
119
|
+
(typically None).
|
|
120
|
+
"""
|
|
121
|
+
# args[0] was already correctly unwrapped here in the original file --
|
|
122
|
+
# unlike most other widgets in this library, this one never had the
|
|
123
|
+
# `pname = args` tuple-comparison bug. Preserved as-is.
|
|
124
|
+
if len(args) == 1:
|
|
125
|
+
if isinstance(args[0], dict):
|
|
126
|
+
kwargs = {**args[0], **kwargs}
|
|
127
|
+
else:
|
|
128
|
+
pname = args[0]
|
|
129
|
+
if pname == "state":
|
|
130
|
+
return ("state", "state", "state", "normal", str(self.state()))
|
|
131
|
+
|
|
132
|
+
if pname in ["fg_color", "progress_color", "button_color", "button_hover_color"]:
|
|
133
|
+
current_state = str(self.state()).lower()
|
|
134
|
+
val = self._custom_disabled_map.get(pname) if current_state == "disabled" else self._local_defaults.get(pname)
|
|
135
|
+
|
|
136
|
+
# The default falls back to the CURRENT value when the
|
|
137
|
+
# theme has nothing to offer. sCTkThemes.json has no
|
|
138
|
+
# sCTkSlider block at all, so every one of these lookups
|
|
139
|
+
# returns None -- and pygubu hands that None straight back
|
|
140
|
+
# to the widget:
|
|
141
|
+
#
|
|
142
|
+
# color is None, for transparency set color='transparent'
|
|
143
|
+
#
|
|
144
|
+
# With the fallback, clearing such a field is a no-op
|
|
145
|
+
# rather than an error. Adding the theme block is the real
|
|
146
|
+
# fix; this stops the Designer breaking in the meantime.
|
|
147
|
+
theme_value = self._theme_default(pname)
|
|
148
|
+
if theme_value is None:
|
|
149
|
+
theme_value = val if val is not None else super().cget(pname)
|
|
150
|
+
return (pname, pname, pname,
|
|
151
|
+
self._query_value(theme_value),
|
|
152
|
+
self._query_value(val if val is not None else super().cget(pname)))
|
|
153
|
+
|
|
154
|
+
return self._configure_query(pname)
|
|
155
|
+
|
|
156
|
+
# Runtime overrides have to reach the map a repaint reads, or
|
|
157
|
+
# _apply_custom_theme_colors() puts the theme value straight back --
|
|
158
|
+
# see ThemeableWidget._record_theme_overrides().
|
|
159
|
+
self._record_theme_overrides(kwargs)
|
|
160
|
+
|
|
161
|
+
if "command" in kwargs: super().configure(command=kwargs.pop("command"))
|
|
162
|
+
if "variable" in kwargs: super().configure(variable=kwargs.pop("variable"))
|
|
163
|
+
|
|
164
|
+
# "state" is deliberately left in kwargs here, not popped -- it flows
|
|
165
|
+
# through to super().configure(**kwargs) below, reaching the native
|
|
166
|
+
# widget directly. has_state only tracks whether a repaint is needed
|
|
167
|
+
# afterward.
|
|
168
|
+
has_state = "state" in kwargs
|
|
169
|
+
self._drop_cleared(kwargs)
|
|
170
|
+
|
|
171
|
+
if kwargs:
|
|
172
|
+
super().configure(**kwargs)
|
|
173
|
+
|
|
174
|
+
if has_state:
|
|
175
|
+
self._apply_custom_theme_colors()
|
|
176
|
+
|
|
177
|
+
# Tkinter/CTk convention binds .config to .configure as a SEPARATE class
|
|
178
|
+
# attribute -- it does not automatically track whichever configure() a
|
|
179
|
+
# subclass defines. Without this line, calling .config(...) on an instance
|
|
180
|
+
# would silently skip this entire override and land on the native widget's
|
|
181
|
+
# configure() directly, bypassing theming entirely.
|
|
182
|
+
config = configure
|
|
183
|
+
|
|
184
|
+
def _set_appearance_mode(self, mode_string: str) -> None:
|
|
185
|
+
"""
|
|
186
|
+
Forwards CustomTkinter's internal light/dark mode change notification to
|
|
187
|
+
the native widget.
|
|
188
|
+
|
|
189
|
+
No longer manually re-triggers _apply_custom_theme_colors(). That
|
|
190
|
+
method now passes raw (light, dark) tuples straight through to
|
|
191
|
+
configure() instead of pre-resolving to a single color, so CTk's own
|
|
192
|
+
appearance-mode tracking should repaint correctly on its own -- the
|
|
193
|
+
same approach validated on sCTkComboBox, sCTkSegmentedButton, and the
|
|
194
|
+
button family.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
mode_string: The new appearance mode ("Light" or "Dark"), as passed
|
|
198
|
+
by CustomTkinter's internal appearance-mode change machinery.
|
|
199
|
+
"""
|
|
200
|
+
if hasattr(super(), "_set_appearance_mode"):
|
|
201
|
+
try: super()._set_appearance_mode(mode_string)
|
|
202
|
+
except Exception: pass
|
|
203
|
+
|
|
204
|
+
def state(self, mode: Optional[str] = None) -> str:
|
|
205
|
+
"""
|
|
206
|
+
Gets or sets the widget's enabled/disabled state.
|
|
207
|
+
|
|
208
|
+
Unlike every other widget in this library, this reads the query
|
|
209
|
+
directly from the native widget's own cget("state") rather than a
|
|
210
|
+
parallel self._custom_current_state -- see module docstring.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
mode: If None, returns the native widget's current "state" value
|
|
214
|
+
directly. Otherwise, forwarded to configure(state=mode),
|
|
215
|
+
which reaches the native widget's own state handling.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
If querying: whatever the native widget's "state" cget returns,
|
|
219
|
+
lowercased. If setting: echoes back the `mode` argument exactly as
|
|
220
|
+
given (not re-queried from the native widget afterward).
|
|
221
|
+
"""
|
|
222
|
+
if mode is None:
|
|
223
|
+
return str(super().cget("state")).lower()
|
|
224
|
+
self.configure(state=mode)
|
|
225
|
+
return mode
|
|
226
|
+
|
|
227
|
+
def get_state(self) -> str:
|
|
228
|
+
"""Equivalent to calling state() with no argument."""
|
|
229
|
+
return self.state()
|
|
230
|
+
|
|
231
|
+
def _apply_custom_theme_colors(self) -> None:
|
|
232
|
+
"""
|
|
233
|
+
Recomputes and applies this widget's colors from the theme file, based
|
|
234
|
+
on the current state (read directly from the native widget -- see
|
|
235
|
+
module docstring).
|
|
236
|
+
|
|
237
|
+
Called shortly after construction (via after(10, ...)), on every
|
|
238
|
+
state()/configure(state=...) change, and (indirectly, via
|
|
239
|
+
_set_appearance_mode) on appearance-mode changes.
|
|
240
|
+
|
|
241
|
+
Passes raw (light, dark) tuples straight through to configure() instead
|
|
242
|
+
of resolving to a single color first, so CTk's native tracking can
|
|
243
|
+
handle appearance-mode repaints without help from _set_appearance_mode.
|
|
244
|
+
Every value here traces back to sCTkThemes.json; there are no
|
|
245
|
+
hardcoded colors in this method.
|
|
246
|
+
|
|
247
|
+
While disabled, button_hover_color is forced to match button_color --
|
|
248
|
+
since hover can't trigger once natively disabled, this just ensures
|
|
249
|
+
nothing else could show a stale hover color if queried directly.
|
|
250
|
+
"""
|
|
251
|
+
is_disabled = str(super().cget("state")).lower() == "disabled"
|
|
252
|
+
target_map = self._custom_disabled_map if is_disabled else self._local_defaults
|
|
253
|
+
|
|
254
|
+
fg_payload = {}
|
|
255
|
+
for key in ("fg_color", "progress_color", "button_color", "button_hover_color"):
|
|
256
|
+
val = target_map.get(key)
|
|
257
|
+
if val is not None:
|
|
258
|
+
fg_payload[key] = val
|
|
259
|
+
|
|
260
|
+
if is_disabled and "button_color" in fg_payload:
|
|
261
|
+
fg_payload["button_hover_color"] = fg_payload["button_color"]
|
|
262
|
+
|
|
263
|
+
if fg_payload:
|
|
264
|
+
super().configure(**fg_payload)
|