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,795 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
"""
|
|
3
|
+
sCTkScrollableFrame
|
|
4
|
+
|
|
5
|
+
A theme-compliant scrollable viewport container frame. Inherits directly from
|
|
6
|
+
ctk.CTkScrollableFrame so CustomTkinter handles native scrolling and layout;
|
|
7
|
+
this class layers automatic light/dark theme resolution and carefully-tuned
|
|
8
|
+
cross-platform mouse wheel / macOS trackpad scroll handling on top.
|
|
9
|
+
|
|
10
|
+
Base class order matters here: `class sCTkScrollableFrame(
|
|
11
|
+
ctk.CTkScrollableFrame, ThemeableWidget)` puts the native CTk class first, so
|
|
12
|
+
every `super()` call in this file's own methods resolves to
|
|
13
|
+
ctk.CTkScrollableFrame -- and, beneath it, tkinter.Misc -- never to
|
|
14
|
+
ThemeableWidget. ThemeableWidget's own configure()/cget()/_set_appearance_mode()
|
|
15
|
+
overrides have been removed entirely for this reason (see
|
|
16
|
+
themeable_widget.py's docstring); this widget owns all of its own runtime
|
|
17
|
+
color-swapping logic.
|
|
18
|
+
|
|
19
|
+
Unlike sCTkFrame (which has no disabled concept at all), this class DOES
|
|
20
|
+
have a disabled state, via configure(state="disabled")/get_state(). That's
|
|
21
|
+
justified here where it isn't for a plain frame: this widget owns real
|
|
22
|
+
behavior to disable, not just colors. Disabling dims the border and the
|
|
23
|
+
scrollbar and stops all scrolling -- wheel, trackpad, and scrollbar drag.
|
|
24
|
+
|
|
25
|
+
It does NOT cascade to child widgets. Disabling children placed inside this
|
|
26
|
+
frame is entirely the caller's responsibility, exactly as with the labeled
|
|
27
|
+
frame variants -- loop over get_children() and configure(state=...) each one.
|
|
28
|
+
|
|
29
|
+
state and scroll_enabled are independent axes; see _scroll_effective(). A
|
|
30
|
+
frame explicitly set non-scrolling stays non-scrolling across a
|
|
31
|
+
disable/enable round trip rather than being switched on by the state change.
|
|
32
|
+
|
|
33
|
+
SCROLL BINDINGS ARE AUTOMATIC. __init__ binds <Map>, so scroll handling
|
|
34
|
+
activates by itself the first time the widget becomes visible via
|
|
35
|
+
pack()/grid()/place(), and again on any later remap. No separate activation
|
|
36
|
+
call is needed.
|
|
37
|
+
|
|
38
|
+
<Map> is used rather than binding directly in __init__ because the binding
|
|
39
|
+
logic inspects the widget's actual parent hierarchy (via winfo_parent()),
|
|
40
|
+
which isn't fully realized at construction time, before the widget has been
|
|
41
|
+
placed into a layout. <Map> is the earliest point at which it is.
|
|
42
|
+
|
|
43
|
+
To ship a frame that doesn't respond to scroll input, pass
|
|
44
|
+
scroll_enabled=False to the constructor, or call disable_scroll() -- either
|
|
45
|
+
one suppresses the automatic activation rather than being silently
|
|
46
|
+
overridden by it. See disable_scroll()'s docstring for the bulk-update
|
|
47
|
+
workflow this supports, and note that it governs scroll INPUT only; the
|
|
48
|
+
scrollbar itself stays visible, but is made inert -- it can't be dragged
|
|
49
|
+
while scrolling is disabled.
|
|
50
|
+
|
|
51
|
+
THE SCROLL-HANDLING METHODS BELOW (_toggle_scroll_bindings,
|
|
52
|
+
_process_mac_touchpad_scroll, _process_scroll_wheel,
|
|
53
|
+
_decode_mac_touchpad_delta) ARE UNCHANGED FROM THE ORIGINAL, VERIFIED BY THE
|
|
54
|
+
MAINTAINER TO BE WORKING, HARD-WON CODE. They implement cross-platform mouse
|
|
55
|
+
wheel handling (Windows delta scaling, Linux Button-4/Button-5 discrete
|
|
56
|
+
events, macOS Darwin-specific delta scaling) plus raw bit-level decoding of
|
|
57
|
+
macOS's high-precision trackpad gesture event (a packed 32-bit delta split
|
|
58
|
+
into signed 16-bit X/Y components). None of this logic was touched during
|
|
59
|
+
this project's audit; only unrelated bugs elsewhere in this file (argument
|
|
60
|
+
handling, dead code, color-tuple resolution) were fixed. Do not modify the
|
|
61
|
+
scroll-handling methods without extensive real-device testing across all
|
|
62
|
+
three platforms.
|
|
63
|
+
"""
|
|
64
|
+
import sys
|
|
65
|
+
import platform
|
|
66
|
+
import tkinter as tk
|
|
67
|
+
from typing import Any, Optional
|
|
68
|
+
import customtkinter as ctk
|
|
69
|
+
from .themeable_widget import ThemeableWidget
|
|
70
|
+
from .sctk_scroll_mixin import ScrollBindingMixin
|
|
71
|
+
|
|
72
|
+
class sCTkScrollableFrame(ctk.CTkScrollableFrame, ScrollBindingMixin, ThemeableWidget):
|
|
73
|
+
"""Themeable scrollable viewport container.
|
|
74
|
+
|
|
75
|
+
RESOLVED INVESTIGATION -- see _USE_CUSTOM_SCROLL_BINDING below. This
|
|
76
|
+
class inherits directly from ctk.CTkScrollableFrame, so calling
|
|
77
|
+
super().__init__() already triggers native CTk's own scroll-binding
|
|
78
|
+
setup (a global bind_all("<MouseWheel>", ...) combined with a
|
|
79
|
+
_check_if_valid_scroll() walk-up-the-hierarchy check, confirmed directly
|
|
80
|
+
against CustomTkinter's own source). It was suspected this file's own
|
|
81
|
+
additional custom scroll-binding system might be entirely redundant with
|
|
82
|
+
-- and actively competing against -- that already-running native system.
|
|
83
|
+
Confirmed by direct testing NOT to be the case: with this file's custom
|
|
84
|
+
system fully disabled, an external mouse's wheel kept working correctly
|
|
85
|
+
(proving native's inherited handling genuinely works), but the trackpad
|
|
86
|
+
produced zero response at all (not degraded -- completely dead). Trackpad
|
|
87
|
+
gestures on the tested system generate ONLY <TouchpadScroll> events,
|
|
88
|
+
never <MouseWheel> -- these are disjoint event channels for disjoint
|
|
89
|
+
physical inputs, not two systems racing for the same events. This file's
|
|
90
|
+
<TouchpadScroll> handling is the sole channel for trackpad scrolling, not
|
|
91
|
+
a redundant addition alongside native CTk. The real, confirmed bug was
|
|
92
|
+
magnitude loss in _process_mac_touchpad_scroll's scaling -- see that
|
|
93
|
+
method's own docstring.
|
|
94
|
+
|
|
95
|
+
Adds to native ctk.CTkScrollableFrame:
|
|
96
|
+
- Automatic light/dark theme resolution from sCTkThemes.json (via
|
|
97
|
+
ThemeableWidget.__init__ -- see that class's docstring for what it does,
|
|
98
|
+
and just as importantly, what it no longer does).
|
|
99
|
+
- Pygubu Designer property introspection for `fg_color`,
|
|
100
|
+
`label_fg_color`, `scrollbar_button_color`, `border_color`, `state`,
|
|
101
|
+
and `scroll_enabled` via a single-argument configure() call. For the
|
|
102
|
+
color keys the returned tuple's `default` and `current` positions are
|
|
103
|
+
identical; for `state` and `scroll_enabled` they can differ, since
|
|
104
|
+
those two carry live runtime values.
|
|
105
|
+
- A disabled state (see module docstring): dims border and scrollbar
|
|
106
|
+
and stops all scroll input. Does not cascade to children.
|
|
107
|
+
- Manual scrollbar re-theming: `scrollbar_button_color`/
|
|
108
|
+
`scrollbar_button_hover_color` are pushed to the internal scrollbar
|
|
109
|
+
directly, since they aren't automatically covered by a single
|
|
110
|
+
configure() call.
|
|
111
|
+
- winfo_children()/get_children()/get_all_children(): by default,
|
|
112
|
+
winfo_children() filters out children whose class name is exactly
|
|
113
|
+
"CTkScrollbar", "CTkCanvas", or "Canvas" -- internal furniture this
|
|
114
|
+
widget creates for its own scrolling machinery. get_children() is
|
|
115
|
+
this filtered view; get_all_children() returns the raw list. Same
|
|
116
|
+
class-name-based limitation as sCTkFrameLabeledPrimary's identical
|
|
117
|
+
filtering approach: a plain, un-themed widget matching one of these
|
|
118
|
+
exact class names, added directly as a child, would be incorrectly
|
|
119
|
+
filtered out too.
|
|
120
|
+
- Cross-platform scroll wheel and macOS trackpad handling -- see module
|
|
121
|
+
docstring. Wired up automatically on <Map>; no activation call needed.
|
|
122
|
+
- A `scroll_enabled` property (default True), settable at construction
|
|
123
|
+
or at runtime via configure(scroll_enabled=...), enable_scroll(), and
|
|
124
|
+
disable_scroll(), and readable via cget("scroll_enabled"). Governs
|
|
125
|
+
scroll INPUT handling only -- it does not hide the scrollbar.
|
|
126
|
+
|
|
127
|
+
Colors are passed through to configure() as raw (light, dark) tuples rather
|
|
128
|
+
than pre-resolved to a single value, so CustomTkinter's own appearance-mode
|
|
129
|
+
tracking repaints them automatically on a light/dark switch -- the same
|
|
130
|
+
approach validated on sCTkComboBox, sCTkSegmentedButton, and the button
|
|
131
|
+
family. Not separately re-confirmed for this specific widget.
|
|
132
|
+
|
|
133
|
+
WHITELIST GUARD: if a composite widget inherits sCTkScrollableFrame as its
|
|
134
|
+
own base class (e.g. sCTkTableview(sCTkScrollableFrame, ThemeableWidget))
|
|
135
|
+
and explicitly calls ThemeableWidget.__init__ itself before calling
|
|
136
|
+
super().__init__(), that composite's own final_kw -- built from ITS theme
|
|
137
|
+
block -- could contain keys native ctk.CTkScrollableFrame knows nothing
|
|
138
|
+
about. This matters MORE here than for any other container in this
|
|
139
|
+
project: confirmed directly against CustomTkinter's own source,
|
|
140
|
+
CTkScrollableFrame.__init__ has NO **kwargs catch-all at all -- every
|
|
141
|
+
parameter is explicitly named, so ANY unrecognized keyword reaching it
|
|
142
|
+
raises TypeError immediately, at the level of Python's own argument
|
|
143
|
+
binding, before any custom validation even runs.
|
|
144
|
+
_NATIVE_CTKSCROLLABLEFRAME_KWARGS filters final_kw down to only the keys
|
|
145
|
+
the real native constructor accepts before that call. This only matters
|
|
146
|
+
for the Pattern-B composition scenario described above; for direct
|
|
147
|
+
construction of a plain sCTkScrollableFrame, final_kw already only
|
|
148
|
+
contains this widget's own theme keys, so the filter is a no-op.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
# Confirmed directly against CustomTkinter's own ctk_scrollable_frame.py
|
|
152
|
+
# source: every one of CTkScrollableFrame.__init__'s named parameters,
|
|
153
|
+
# excluding "master" (always passed positionally, never part of the
|
|
154
|
+
# filtered kwargs dict).
|
|
155
|
+
_NATIVE_CTKSCROLLABLEFRAME_KWARGS = frozenset({
|
|
156
|
+
"width", "height", "corner_radius", "border_width", "bg_color",
|
|
157
|
+
"fg_color", "border_color", "scrollbar_fg_color",
|
|
158
|
+
"scrollbar_button_color", "scrollbar_button_hover_color",
|
|
159
|
+
"label_fg_color", "label_text_color", "label_text", "label_font",
|
|
160
|
+
"label_anchor", "orientation",
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
# EXPERIMENTAL TOGGLE, under active investigation -- see class docstring.
|
|
164
|
+
# True (default): preserves current behavior -- calling
|
|
165
|
+
# _finalize_split_bindings() sets up this file's own custom scroll
|
|
166
|
+
# system (_toggle_scroll_bindings/_process_scroll_wheel/
|
|
167
|
+
# _process_mac_touchpad_scroll), IN ADDITION to whatever native
|
|
168
|
+
# ctk.CTkScrollableFrame already set up automatically via inheritance.
|
|
169
|
+
# False: _finalize_split_bindings()/_toggle_scroll_bindings() become
|
|
170
|
+
# no-ops. Scrolling relies ENTIRELY on whatever native CTkScrollableFrame
|
|
171
|
+
# already provides automatically via super().__init__() -- no custom
|
|
172
|
+
# binding of any kind. Untested as of this writing; flip this to False
|
|
173
|
+
# to find out whether the reported erratic touchpad behavior disappears
|
|
174
|
+
# when the custom system isn't also running alongside native's own.
|
|
175
|
+
_USE_CUSTOM_SCROLL_BINDING = True
|
|
176
|
+
|
|
177
|
+
# Theme keys that _update_current_visual_state() re-pushes on every
|
|
178
|
+
# repaint. configure() records these into _local_defaults so a runtime
|
|
179
|
+
# override survives the repaint instead of being reverted by it -- see
|
|
180
|
+
# configure()'s docstring.
|
|
181
|
+
_THEME_TRACKED_KEYS = frozenset({
|
|
182
|
+
"fg_color",
|
|
183
|
+
"border_color",
|
|
184
|
+
"label_fg_color",
|
|
185
|
+
"scrollbar_button_color",
|
|
186
|
+
"scrollbar_button_hover_color",
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
def __init__(self, master: Optional[Any] = None, **kwargs: Any) -> None:
|
|
190
|
+
"""
|
|
191
|
+
Args:
|
|
192
|
+
master: Parent container.
|
|
193
|
+
**kwargs: Any native CTkScrollableFrame argument (e.g. `label_text`),
|
|
194
|
+
or a theme-key override (see the "sCTkScrollableFrame" block
|
|
195
|
+
in sCTkThemes.json). Additionally accepts `scroll_enabled`
|
|
196
|
+
(bool, default True) -- pass False to construct a frame that
|
|
197
|
+
doesn't respond to scroll input until enable_scroll() is
|
|
198
|
+
called.
|
|
199
|
+
"""
|
|
200
|
+
# 1. Fire our shared theme logic first. This resolves final_kw
|
|
201
|
+
# (construction-time properties). See ThemeableWidget.__init__ for
|
|
202
|
+
# what actually happens here.
|
|
203
|
+
ThemeableWidget.__init__(self, kwargs)
|
|
204
|
+
|
|
205
|
+
# 2. Store the resolved kwargs onto this instance, so later changes
|
|
206
|
+
# here never leak back into the shared theme registry.
|
|
207
|
+
self._local_defaults = dict(self.final_kw)
|
|
208
|
+
|
|
209
|
+
# 2a. Capture the disabled-state color map. Read from
|
|
210
|
+
# self._widget_disabled_map, NOT from final_kw: ThemeableWidget.__init__
|
|
211
|
+
# deliberately excludes "disabled_map" from final_kw, so reading it
|
|
212
|
+
# there yields an empty dict and every disabled lookup silently falls
|
|
213
|
+
# back -- the confirmed bug pattern fixed in sCTkSwitch, sCTkSpinbox,
|
|
214
|
+
# and sCTkTableview elsewhere in this project.
|
|
215
|
+
# hasattr-guarded for the same reason as _state below: sCTkTableview
|
|
216
|
+
# builds this from the same source before calling super().__init__().
|
|
217
|
+
if not hasattr(self, "_custom_disabled_map"):
|
|
218
|
+
self._custom_disabled_map = dict(self._widget_disabled_map)
|
|
219
|
+
|
|
220
|
+
# 2b. Hard-fail on theme gaps rather than substituting a guessed
|
|
221
|
+
# color, matching the principle established across this project.
|
|
222
|
+
# Only the keys that actually change when disabled are required in
|
|
223
|
+
# disabled_map; fg_color deliberately is NOT among them (the content
|
|
224
|
+
# background stays put when disabled -- only the border and the now-
|
|
225
|
+
# inert scrollbar dim).
|
|
226
|
+
#
|
|
227
|
+
# SCOPED TO DIRECT CONSTRUCTION ONLY. A subclass that inherits this
|
|
228
|
+
# class (e.g. sCTkTableview) reaches here with self.final_kw built
|
|
229
|
+
# from ITS OWN theme block, not this one -- ThemeableWidget's
|
|
230
|
+
# run-once guard means the parent's __init__ never rebuilds it. So
|
|
231
|
+
# validating this widget's theme keys against a subclass's theme
|
|
232
|
+
# block would demand scrollbar colors from, say, the sCTkTableview
|
|
233
|
+
# block, and raise KeyError on every construction. Subclasses own
|
|
234
|
+
# their own theme contract and validate it themselves (sCTkTableview
|
|
235
|
+
# does exactly this); this check is for the concrete class only.
|
|
236
|
+
if type(self) is sCTkScrollableFrame:
|
|
237
|
+
for required_key in ("border_color", "scrollbar_button_color",
|
|
238
|
+
"scrollbar_button_hover_color"):
|
|
239
|
+
if self._local_defaults.get(required_key) is None:
|
|
240
|
+
raise KeyError(
|
|
241
|
+
f"'{(getattr(self, '_THEME_BLOCK_NAME', None) or self.__class__.__name__)}' theme block is missing "
|
|
242
|
+
f"'{required_key}' at the top level of sCTkThemes.json."
|
|
243
|
+
)
|
|
244
|
+
if self._custom_disabled_map.get(required_key) is None:
|
|
245
|
+
raise KeyError(
|
|
246
|
+
f"'{(getattr(self, '_THEME_BLOCK_NAME', None) or self.__class__.__name__)}' theme block is missing "
|
|
247
|
+
f"'{required_key}' in disabled_map."
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# 2c. State and scroll intent. Both must exist before step 4's
|
|
251
|
+
# _update_current_visual_state() call, which reads them.
|
|
252
|
+
#
|
|
253
|
+
# These are two INDEPENDENT axes, deliberately not collapsed into one.
|
|
254
|
+
# _state is the user-facing enabled/disabled presentation;
|
|
255
|
+
# _scroll_enabled is the developer's own intent about whether this
|
|
256
|
+
# frame should scroll at all. Effective scrolling is the AND of the
|
|
257
|
+
# two -- see _scroll_effective(). Because state changes never write
|
|
258
|
+
# to _scroll_enabled, a frame that was constructed or configured
|
|
259
|
+
# non-scrolling stays non-scrolling after a disable/enable round
|
|
260
|
+
# trip, instead of being silently switched on by the state change.
|
|
261
|
+
# A subclass may legitimately establish either of these before calling
|
|
262
|
+
# super().__init__() -- sCTkTableview sets self._state from its own
|
|
263
|
+
# constructor argument, and would otherwise have it silently reset to
|
|
264
|
+
# the default here. hasattr-guarded so the subclass's value wins.
|
|
265
|
+
if not hasattr(self, "_state"):
|
|
266
|
+
self._state = str(self._local_defaults.get("state", "normal"))
|
|
267
|
+
if not hasattr(self, "_scroll_enabled"):
|
|
268
|
+
self._scroll_enabled = bool(self._local_defaults.get("scroll_enabled", True))
|
|
269
|
+
|
|
270
|
+
# 3. Initialize CustomTkinter natively with the clean final kwargs array.
|
|
271
|
+
# 3. Initialize CustomTkinter natively. Only forwards the subset of
|
|
272
|
+
# final_kw that native CTkScrollableFrame actually accepts -- see this
|
|
273
|
+
# class's docstring ("WHITELIST GUARD") for why this filtering exists,
|
|
274
|
+
# and why it's especially important for this specific native class.
|
|
275
|
+
native_kwargs = {k: v for k, v in self.final_kw.items() if k in self._NATIVE_CTKSCROLLABLEFRAME_KWARGS}
|
|
276
|
+
super().__init__(master, **native_kwargs)
|
|
277
|
+
|
|
278
|
+
# 4. Apply initial theming.
|
|
279
|
+
self._update_current_visual_state()
|
|
280
|
+
|
|
281
|
+
# Touchpad accumulator state, owned by ScrollBindingMixin. Must run
|
|
282
|
+
# before any binding happens.
|
|
283
|
+
self._init_scroll_state()
|
|
284
|
+
|
|
285
|
+
# 5. Scroll activation. Bindings come up automatically -- no caller
|
|
286
|
+
# action required. An earlier version needed a manual
|
|
287
|
+
# _finalize_split_bindings() call after placement; a scrollable frame
|
|
288
|
+
# that doesn't scroll by default is a confusing thing to ship, given
|
|
289
|
+
# what the widget's own name promises.
|
|
290
|
+
#
|
|
291
|
+
# _state and _scroll_enabled were both established in step 2c, since
|
|
292
|
+
# step 4 above needs them.
|
|
293
|
+
#
|
|
294
|
+
# ScrollBindingMixin installs all four activation mechanisms -- <Map>
|
|
295
|
+
# on this widget, <Map> on _parent_frame, after_idle(), and the
|
|
296
|
+
# debounced <Configure> content rebind -- and documents why each is
|
|
297
|
+
# needed. _parent_frame is passed because it is the widget the
|
|
298
|
+
# geometry manager actually sees; this widget itself is a canvas-window
|
|
299
|
+
# child and may never receive <Map> at all.
|
|
300
|
+
self._install_scroll_activation(
|
|
301
|
+
extra_map_widget=getattr(self, "_parent_frame", None))
|
|
302
|
+
|
|
303
|
+
# ------------------------------------------------------------------
|
|
304
|
+
# ScrollBindingMixin contract
|
|
305
|
+
# ------------------------------------------------------------------
|
|
306
|
+
def _scroll_target(self):
|
|
307
|
+
"""
|
|
308
|
+
The widget to scroll: this frame's parent canvas, resolved through the
|
|
309
|
+
Tk widget path rather than a CustomTkinter private attribute.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
The canvas, or None if the hierarchy isn't realized yet.
|
|
313
|
+
"""
|
|
314
|
+
try:
|
|
315
|
+
parent_widget = self.nametowidget(self.winfo_parent())
|
|
316
|
+
if parent_widget and parent_widget.__class__.__name__ == "Canvas":
|
|
317
|
+
return parent_widget
|
|
318
|
+
except Exception:
|
|
319
|
+
pass
|
|
320
|
+
return None
|
|
321
|
+
|
|
322
|
+
def _scroll_layers(self):
|
|
323
|
+
"""
|
|
324
|
+
Every widget that should respond to a scroll event over this frame:
|
|
325
|
+
the frame itself, its parent canvas and that canvas's own parent, the
|
|
326
|
+
internal scrollbar, and the full content tree.
|
|
327
|
+
|
|
328
|
+
The content walk stops at nested CTkScrollableFrame boundaries -- see
|
|
329
|
+
ScrollBindingMixin._collect_scroll_descendants().
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
An ordered, deduplicated list of widgets.
|
|
333
|
+
"""
|
|
334
|
+
layers = [self]
|
|
335
|
+
|
|
336
|
+
canvas = self._scroll_target()
|
|
337
|
+
if canvas is not None:
|
|
338
|
+
layers.append(canvas)
|
|
339
|
+
try:
|
|
340
|
+
grandparent = self.nametowidget(canvas.winfo_parent())
|
|
341
|
+
if grandparent:
|
|
342
|
+
layers.append(grandparent)
|
|
343
|
+
except Exception:
|
|
344
|
+
pass
|
|
345
|
+
|
|
346
|
+
# The scrollbar is a SIBLING of the canvas, not a descendant of this
|
|
347
|
+
# frame, so the content walk below would never reach it -- it has to
|
|
348
|
+
# be added explicitly or the wheel does nothing while the pointer is
|
|
349
|
+
# over the scrollbar itself.
|
|
350
|
+
if getattr(self, "_scrollbar", None) is not None:
|
|
351
|
+
self._collect_scroll_descendants(self._scrollbar, layers)
|
|
352
|
+
|
|
353
|
+
self._collect_scroll_descendants(self, layers)
|
|
354
|
+
return layers
|
|
355
|
+
|
|
356
|
+
def _scroll_permitted(self) -> bool:
|
|
357
|
+
"""The AND of scroll_enabled and state. See _scroll_effective()."""
|
|
358
|
+
return self._scroll_effective()
|
|
359
|
+
|
|
360
|
+
def _scroll_drag_targets(self):
|
|
361
|
+
"""The internal scrollbar, whose dragging is blocked when disabled."""
|
|
362
|
+
bar = getattr(self, "_scrollbar", None)
|
|
363
|
+
return [bar] if bar is not None else []
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _scroll_effective(self) -> bool:
|
|
367
|
+
"""
|
|
368
|
+
Whether this widget should actually respond to scroll input right now.
|
|
369
|
+
|
|
370
|
+
The AND of the two independent axes described in __init__ step 2c:
|
|
371
|
+
|
|
372
|
+
scroll_enabled | state | scrolls?
|
|
373
|
+
---------------+-----------+---------
|
|
374
|
+
True | normal | yes
|
|
375
|
+
True | disabled | no
|
|
376
|
+
False | normal | no
|
|
377
|
+
False | disabled | no
|
|
378
|
+
|
|
379
|
+
Every place that binds or unbinds routes through here rather than
|
|
380
|
+
reading either flag directly, so the two can't drift apart.
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
True only if scrolling is both wanted and permitted.
|
|
384
|
+
"""
|
|
385
|
+
return self._scroll_enabled and self._state == "normal"
|
|
386
|
+
|
|
387
|
+
def is_scrolling(self) -> bool:
|
|
388
|
+
"""
|
|
389
|
+
Whether this widget currently responds to scroll input.
|
|
390
|
+
|
|
391
|
+
Distinct from cget("scroll_enabled"), which reports stored INTENT.
|
|
392
|
+
A frame with scroll_enabled=True that has been disabled via
|
|
393
|
+
configure(state="disabled") reports True from cget and False from
|
|
394
|
+
here -- that difference is the point: the intent survives the state
|
|
395
|
+
change so it can be restored when the state goes back to normal.
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
The live effective scroll state.
|
|
399
|
+
"""
|
|
400
|
+
return self._scroll_effective()
|
|
401
|
+
|
|
402
|
+
def get_state(self) -> str:
|
|
403
|
+
"""
|
|
404
|
+
Returns the current state, "normal" or "disabled". Mirrors the same
|
|
405
|
+
accessor on sCTkFrameLabeledPrimary/Secondary.
|
|
406
|
+
"""
|
|
407
|
+
return self._state
|
|
408
|
+
|
|
409
|
+
def enable_scroll(self) -> None:
|
|
410
|
+
"""
|
|
411
|
+
Turns scroll handling back on, rebinding every layer immediately.
|
|
412
|
+
|
|
413
|
+
Public convenience wrapper over configure(scroll_enabled=True); both
|
|
414
|
+
are equivalent and go through the same code path. Safe to call
|
|
415
|
+
repeatedly -- bindings are always torn down before being rebuilt, so
|
|
416
|
+
nothing accumulates.
|
|
417
|
+
"""
|
|
418
|
+
self.configure(scroll_enabled=True)
|
|
419
|
+
|
|
420
|
+
def disable_scroll(self) -> None:
|
|
421
|
+
"""
|
|
422
|
+
Turns scroll handling off, removing every scroll binding this widget
|
|
423
|
+
knows about. The widget stops responding to wheel and trackpad input
|
|
424
|
+
until enable_scroll() is called.
|
|
425
|
+
|
|
426
|
+
Public convenience wrapper over configure(scroll_enabled=False); both
|
|
427
|
+
are equivalent and go through the same code path.
|
|
428
|
+
|
|
429
|
+
This also suppresses the automatic <Map>-triggered binding: calling
|
|
430
|
+
this before the widget is first placed means scroll stays off through
|
|
431
|
+
pack()/grid()/place() rather than being silently re-enabled. That
|
|
432
|
+
makes the following a supported pattern for bulk content updates,
|
|
433
|
+
where rebinding on every widget added would be wasted work:
|
|
434
|
+
|
|
435
|
+
frame = sCTkScrollableFrame(master)
|
|
436
|
+
frame.disable_scroll()
|
|
437
|
+
frame.pack(fill="both", expand=True)
|
|
438
|
+
for item in many_items:
|
|
439
|
+
SomeWidget(frame, text=item).pack()
|
|
440
|
+
frame.enable_scroll()
|
|
441
|
+
|
|
442
|
+
Passing scroll_enabled=False to the constructor achieves the same
|
|
443
|
+
starting state without the separate call.
|
|
444
|
+
|
|
445
|
+
Blocks native CTk's own global mouse-wheel handling as well as this
|
|
446
|
+
file's custom bindings, and blocks click-and-drag on the internal
|
|
447
|
+
scrollbar -- see _toggle_scroll_bindings(bind=False) and
|
|
448
|
+
ScrollBindingMixin._set_scroll_drag_blocked() for the two mechanisms.
|
|
449
|
+
|
|
450
|
+
NOTE: the scrollbar remains VISIBLE, just inert -- it can't be
|
|
451
|
+
dragged, but it isn't hidden. CustomTkinter's scrollbar has no
|
|
452
|
+
disabled state to lock (confirmed by direct investigation earlier in
|
|
453
|
+
this project's audit), so there's no greyed-out appearance to switch
|
|
454
|
+
to either. Hiding it entirely is a separate concern, handled
|
|
455
|
+
elsewhere in this project by color-matching and zero width.
|
|
456
|
+
"""
|
|
457
|
+
self.configure(scroll_enabled=False)
|
|
458
|
+
|
|
459
|
+
def _set_appearance_mode(self, mode_string: str) -> None:
|
|
460
|
+
"""
|
|
461
|
+
Forwards CustomTkinter's internal light/dark mode change notification to
|
|
462
|
+
the native widget.
|
|
463
|
+
|
|
464
|
+
No longer manually re-triggers _update_current_visual_state(). That
|
|
465
|
+
method now passes raw (light, dark) tuples straight through to
|
|
466
|
+
configure() instead of pre-resolving to a single color, so CTk's own
|
|
467
|
+
appearance-mode tracking should repaint correctly on its own -- the
|
|
468
|
+
same approach validated on sCTkComboBox, sCTkSegmentedButton, and the
|
|
469
|
+
button family.
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
mode_string: The new appearance mode ("Light" or "Dark"), as passed
|
|
473
|
+
by CustomTkinter's internal appearance-mode change machinery.
|
|
474
|
+
"""
|
|
475
|
+
if hasattr(super(), "_set_appearance_mode"):
|
|
476
|
+
try:
|
|
477
|
+
super()._set_appearance_mode(mode_string)
|
|
478
|
+
except Exception:
|
|
479
|
+
pass
|
|
480
|
+
|
|
481
|
+
def configure(self, *args: Any, **kwargs: Any) -> Any:
|
|
482
|
+
"""
|
|
483
|
+
Standard widget configuration, with Pygubu/positional-argument handling.
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
*args: At most one positional argument is meaningful:
|
|
487
|
+
- a dict: merged into kwargs and processed normally below.
|
|
488
|
+
- one of "fg_color"/"label_fg_color"/"scrollbar_button_color"/
|
|
489
|
+
"border_color": returns a Tkinter-style
|
|
490
|
+
(name, name, name, default, current) tuple. `default` and
|
|
491
|
+
`current` are always identical, since this widget has no
|
|
492
|
+
disabled state to vary them.
|
|
493
|
+
- anything else: forwarded directly to the native widget's
|
|
494
|
+
configure(), which does not support single-argument property
|
|
495
|
+
queries for arbitrary properties -- a known limitation
|
|
496
|
+
shared with the wider Pygubu-query investigation set aside
|
|
497
|
+
elsewhere in this project.
|
|
498
|
+
**kwargs: Standard CTkScrollableFrame configuration options.
|
|
499
|
+
|
|
500
|
+
Returns:
|
|
501
|
+
The query tuple described above for the single-argument case, or
|
|
502
|
+
None for the keyword-argument case (this method doesn't return
|
|
503
|
+
super().configure()'s result in that branch).
|
|
504
|
+
"""
|
|
505
|
+
# args is always a tuple -- args[0] is the actual value passed, whether
|
|
506
|
+
# that's a string or a dict. An earlier version of this method compared
|
|
507
|
+
# the wrapped tuple directly (`pname = args`), so the query branches
|
|
508
|
+
# below never matched anything. Don't reintroduce that.
|
|
509
|
+
if len(args) == 1:
|
|
510
|
+
if isinstance(args[0], dict):
|
|
511
|
+
kwargs = {**args[0], **kwargs}
|
|
512
|
+
else:
|
|
513
|
+
pname = args[0]
|
|
514
|
+
if pname in ["fg_color", "label_fg_color", "label_text_color",
|
|
515
|
+
"scrollbar_button_color", "border_color"]:
|
|
516
|
+
# FIX: the default came straight from the theme with no
|
|
517
|
+
# fallback, so a key the block does not define reported
|
|
518
|
+
# None -- and pygubu handed that None back to the widget:
|
|
519
|
+
#
|
|
520
|
+
# color is None, for transparency set color='transparent'
|
|
521
|
+
#
|
|
522
|
+
# Falling back to the current value makes clearing such a
|
|
523
|
+
# field a no-op rather than an error. label_text_color is
|
|
524
|
+
# in the list because this widget applies it; it was
|
|
525
|
+
# missing, so clearing it fell through to a branch that did
|
|
526
|
+
# not know about it and returned None.
|
|
527
|
+
theme_value = self._theme_default(pname)
|
|
528
|
+
current = self._local_defaults.get(pname)
|
|
529
|
+
if theme_value is None:
|
|
530
|
+
theme_value = current
|
|
531
|
+
return (pname, pname, pname,
|
|
532
|
+
self._query_value(theme_value),
|
|
533
|
+
self._query_value(current))
|
|
534
|
+
if pname == "state":
|
|
535
|
+
return (pname, pname, pname,
|
|
536
|
+
str(self._local_defaults.get("state", "normal")),
|
|
537
|
+
str(self._state))
|
|
538
|
+
if pname == "scroll_enabled":
|
|
539
|
+
# Unlike the color properties above, this one reports live
|
|
540
|
+
# state in the `current` position: `default` is the value
|
|
541
|
+
# the widget was constructed with, `current` is whatever
|
|
542
|
+
# enable_scroll()/disable_scroll() last set.
|
|
543
|
+
return (pname, pname, pname,
|
|
544
|
+
str(self._local_defaults.get("scroll_enabled", True)),
|
|
545
|
+
str(self._scroll_enabled))
|
|
546
|
+
# FIX: do NOT forward pname to native configure(). Unlike
|
|
547
|
+
# CTkFrame -- whose signature is configure(require_redraw=False,
|
|
548
|
+
# **kwargs) and so silently swallows a positional --
|
|
549
|
+
# CTkScrollableFrame.configure() is declared configure(self,
|
|
550
|
+
# **kwargs) and accepts NO positional argument at all. Passing
|
|
551
|
+
# one raised "CTkScrollableFrame.configure() takes 1 positional
|
|
552
|
+
# argument but 2 were given".
|
|
553
|
+
#
|
|
554
|
+
# This surfaced through Pygubu Designer: blanking a property in
|
|
555
|
+
# the inspector makes it call widget.configure(pname) to
|
|
556
|
+
# discover that property's default value, which reaches this
|
|
557
|
+
# branch for any name not handled above.
|
|
558
|
+
#
|
|
559
|
+
# A Tkinter-style 5-tuple is returned instead, built from
|
|
560
|
+
# cget() so the reported value is real. Returning None is not an
|
|
561
|
+
# option -- callers index into the tuple.
|
|
562
|
+
try:
|
|
563
|
+
current = self.cget(pname)
|
|
564
|
+
except Exception:
|
|
565
|
+
current = None
|
|
566
|
+
return (pname, pname, pname, current, current)
|
|
567
|
+
|
|
568
|
+
# scroll_enabled is this library's own property, not a native CTk one.
|
|
569
|
+
# It MUST be popped before the super() call below: CTkScrollableFrame
|
|
570
|
+
# .configure() raises on any keyword it doesn't recognize, exactly as
|
|
571
|
+
# its __init__ does (see the WHITELIST GUARD note in this class's
|
|
572
|
+
# docstring).
|
|
573
|
+
#
|
|
574
|
+
# Deliberately does NOT write back into self._local_defaults -- that
|
|
575
|
+
# dict holds construction-time defaults, and overwriting it here would
|
|
576
|
+
# make the single-argument query above report the current value in its
|
|
577
|
+
# `default` slot, collapsing the distinction between the two.
|
|
578
|
+
if "scroll_enabled" in kwargs:
|
|
579
|
+
new_state = bool(kwargs.pop("scroll_enabled"))
|
|
580
|
+
self._scroll_enabled = new_state
|
|
581
|
+
# Apply immediately rather than waiting for the next <Map>:
|
|
582
|
+
# enable_scroll() on an already-visible widget has to take effect
|
|
583
|
+
# now, and disable_scroll() has to tear bindings down now.
|
|
584
|
+
# _toggle_scroll_bindings() is idempotent either way. Routed
|
|
585
|
+
# through _scroll_effective() so enable_scroll() on a widget
|
|
586
|
+
# that's currently state="disabled" records the intent without
|
|
587
|
+
# actually re-enabling scrolling behind the disabled presentation.
|
|
588
|
+
self._toggle_scroll_bindings(bind=self._scroll_effective())
|
|
589
|
+
|
|
590
|
+
# state is likewise this library's own property, not a native CTk one,
|
|
591
|
+
# and must be popped before the super() call for the same reason.
|
|
592
|
+
# Runtime overrides have to reach the map a repaint reads, or the
|
|
593
|
+
# repaint puts the theme value straight back -- see
|
|
594
|
+
# ThemeableWidget._record_theme_overrides().
|
|
595
|
+
self._record_theme_overrides(kwargs)
|
|
596
|
+
|
|
597
|
+
if "state" in kwargs:
|
|
598
|
+
self._state = str(kwargs.pop("state"))
|
|
599
|
+
self._toggle_scroll_bindings(bind=self._scroll_effective())
|
|
600
|
+
self._update_current_visual_state()
|
|
601
|
+
|
|
602
|
+
# FIX: record theme overrides BEFORE the repaint below.
|
|
603
|
+
#
|
|
604
|
+
# _update_current_visual_state() re-pushes every tracked color from
|
|
605
|
+
# self._local_defaults. Since configure() never wrote to that dict,
|
|
606
|
+
# the sequence was: super().configure(fg_color="red") turns the widget
|
|
607
|
+
# red, then the repaint immediately overwrites it with the theme value
|
|
608
|
+
# -- so runtime color overrides silently did nothing.
|
|
609
|
+
#
|
|
610
|
+
# The repaint isn't wrong to exist (appearance-mode switches and state
|
|
611
|
+
# changes both need it); it was wrong to treat _local_defaults as
|
|
612
|
+
# authoritative when configure() had just introduced a newer value it
|
|
613
|
+
# never recorded. Writing the override in first makes the repaint
|
|
614
|
+
# reproduce it rather than revert it, and makes the override survive
|
|
615
|
+
# later appearance-mode and state changes -- matching CustomTkinter's
|
|
616
|
+
# own semantics, where configure(fg_color=...) sticks.
|
|
617
|
+
#
|
|
618
|
+
# Note this stores exactly what the caller passed: a single color
|
|
619
|
+
# replaces the theme's (light, dark) tuple for that key, so appearance
|
|
620
|
+
# tracking for it is intentionally given up. That's what asking for
|
|
621
|
+
# one specific color means.
|
|
622
|
+
#
|
|
623
|
+
# disabled_map still wins while disabled -- an override here sets the
|
|
624
|
+
# NORMAL-state color. See _update_current_visual_state()'s themed().
|
|
625
|
+
# The one widget that had NO empty-string rule at all.
|
|
626
|
+
#
|
|
627
|
+
# Every other configure() in the library drops a cleared theme value;
|
|
628
|
+
# this one forwarded it -- to the native widget, and into
|
|
629
|
+
# _local_defaults below, where the next repaint reapplied it. A
|
|
630
|
+
# Designer clear on a colour therefore reached CTkScrollableFrame as
|
|
631
|
+
# fg_color="". Called BEFORE the loop so a cleared value is never
|
|
632
|
+
# stored. label_text="" is kept, as a caption rather than a colour --
|
|
633
|
+
# see ThemeableWidget._CONTENT_KEYS.
|
|
634
|
+
self._drop_cleared(kwargs)
|
|
635
|
+
|
|
636
|
+
for key in self._THEME_TRACKED_KEYS:
|
|
637
|
+
if key in kwargs:
|
|
638
|
+
self._local_defaults[key] = kwargs[key]
|
|
639
|
+
|
|
640
|
+
# Re-checked after the pops above: configure(scroll_enabled=...) or
|
|
641
|
+
# configure(state=...) on its own leaves kwargs empty, and there's no
|
|
642
|
+
# reason to call through to the native widget for it.
|
|
643
|
+
if kwargs:
|
|
644
|
+
super().configure(**kwargs)
|
|
645
|
+
self._update_current_visual_state()
|
|
646
|
+
|
|
647
|
+
# Tkinter/CTk convention binds .config to .configure as a SEPARATE class
|
|
648
|
+
# attribute -- it does not automatically track whichever configure() a
|
|
649
|
+
# subclass defines. An earlier version of this file was MISSING this line
|
|
650
|
+
# entirely: calling .config(...) on an instance silently skipped this
|
|
651
|
+
# entire override and landed on the native widget's configure() directly,
|
|
652
|
+
# bypassing theming completely -- confirmed as a critical bug on
|
|
653
|
+
# sCTkSegmentedButton earlier in this project's audit; the same fix
|
|
654
|
+
# applies here.
|
|
655
|
+
config = configure
|
|
656
|
+
|
|
657
|
+
def cget(self, attribute_name: str) -> Any:
|
|
658
|
+
"""
|
|
659
|
+
Standard property accessor, extended to know about `scroll_enabled`.
|
|
660
|
+
|
|
661
|
+
Native CTkScrollableFrame.cget() raises on any attribute it doesn't
|
|
662
|
+
recognize, so this library's own property has to be intercepted here
|
|
663
|
+
before delegating; everything else passes straight through unchanged.
|
|
664
|
+
|
|
665
|
+
Args:
|
|
666
|
+
attribute_name: The property to read.
|
|
667
|
+
|
|
668
|
+
Returns:
|
|
669
|
+
For "scroll_enabled", the live bool -- not the construction-time
|
|
670
|
+
default. For anything else, whatever the native widget returns.
|
|
671
|
+
"""
|
|
672
|
+
if attribute_name == "scroll_enabled":
|
|
673
|
+
return self._scroll_enabled
|
|
674
|
+
if attribute_name == "state":
|
|
675
|
+
return self._state
|
|
676
|
+
return super().cget(attribute_name)
|
|
677
|
+
|
|
678
|
+
def _update_current_visual_state(self) -> None:
|
|
679
|
+
"""
|
|
680
|
+
Recomputes and applies this widget's colors from the theme file.
|
|
681
|
+
|
|
682
|
+
Called after construction and on every keyword configure() call.
|
|
683
|
+
|
|
684
|
+
Passes raw (light, dark) tuples straight through to configure() instead
|
|
685
|
+
of resolving to a single color first, so CTk's native tracking can
|
|
686
|
+
handle appearance-mode repaints without help from _set_appearance_mode.
|
|
687
|
+
Every value here traces back to sCTkThemes.json; there are no
|
|
688
|
+
hardcoded colors in this method. The .get() calls carry no fallback
|
|
689
|
+
because __init__ already hard-failed on any missing key.
|
|
690
|
+
"""
|
|
691
|
+
is_disabled = self._state == "disabled"
|
|
692
|
+
|
|
693
|
+
def themed(key: str) -> Any:
|
|
694
|
+
"""Disabled-state value when disabled and one exists, else normal."""
|
|
695
|
+
if is_disabled and self._custom_disabled_map.get(key) is not None:
|
|
696
|
+
return self._custom_disabled_map[key]
|
|
697
|
+
return self._local_defaults.get(key)
|
|
698
|
+
|
|
699
|
+
config_payload = {}
|
|
700
|
+
# label_text_color included: this widget can carry a label, and a
|
|
701
|
+
# disabled panel whose heading stays at full contrast reads as enabled.
|
|
702
|
+
# The two labelled-frame variants already dim theirs; this one did not,
|
|
703
|
+
# so its disabled_map.label_text_color had no effect.
|
|
704
|
+
for key in ("fg_color", "border_color", "label_fg_color",
|
|
705
|
+
"label_text_color"):
|
|
706
|
+
val = themed(key)
|
|
707
|
+
if val is not None:
|
|
708
|
+
config_payload[key] = val
|
|
709
|
+
|
|
710
|
+
if config_payload:
|
|
711
|
+
super().configure(**config_payload)
|
|
712
|
+
|
|
713
|
+
# Re-theme the internal scrollbar, which isn't automatically covered
|
|
714
|
+
# by the configure() call above.
|
|
715
|
+
if hasattr(self, "_scrollbar") and self._scrollbar:
|
|
716
|
+
try:
|
|
717
|
+
normal_bar = themed("scrollbar_button_color")
|
|
718
|
+
# When disabled the scrollbar is inert (drag is blocked), so
|
|
719
|
+
# it must not light up on hover either -- a bright hover would
|
|
720
|
+
# falsely advertise it as draggable. Read from disabled_map
|
|
721
|
+
# like any other dimmed color; themed() falls back to the
|
|
722
|
+
# normal hover color automatically when not disabled.
|
|
723
|
+
normal_hover = themed("scrollbar_button_hover_color")
|
|
724
|
+
self._scrollbar.configure(button_color=normal_bar, button_hover_color=normal_hover)
|
|
725
|
+
|
|
726
|
+
if hasattr(self._scrollbar, "_draw"):
|
|
727
|
+
self._scrollbar._draw()
|
|
728
|
+
except Exception:
|
|
729
|
+
pass
|
|
730
|
+
|
|
731
|
+
# =========================================================================
|
|
732
|
+
# SCROLL-HANDLING METHODS BELOW: UNCHANGED FROM THE ORIGINAL. See module
|
|
733
|
+
# docstring. Do not modify without extensive real-device testing.
|
|
734
|
+
# =========================================================================
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def _finalize_split_bindings(self):
|
|
738
|
+
"""
|
|
739
|
+
Standard layout binding connection pass [1.1].
|
|
740
|
+
|
|
741
|
+
RETAINED FOR COMPATIBILITY. Calling this after placing the widget was
|
|
742
|
+
once mandatory; it no longer is, since __init__ binds <Map> and
|
|
743
|
+
activates scrolling automatically. Existing callers that still invoke
|
|
744
|
+
it are harmless -- the underlying toggle is idempotent -- but new code
|
|
745
|
+
shouldn't need it. Composite widgets in this project that rebuild
|
|
746
|
+
their content dynamically (sCTkFileExplorer, sCTkTableview) do still
|
|
747
|
+
call it deliberately, to bind rows created after the initial <Map>.
|
|
748
|
+
|
|
749
|
+
EXPERIMENTAL: the authoritative _USE_CUSTOM_SCROLL_BINDING gate now
|
|
750
|
+
lives inside _toggle_scroll_bindings() itself (see that method's
|
|
751
|
+
docstring for why), so it's enforced regardless of entry point. The
|
|
752
|
+
check here is just an early-exit optimization, not the real gate.
|
|
753
|
+
"""
|
|
754
|
+
if not self._USE_CUSTOM_SCROLL_BINDING:
|
|
755
|
+
return
|
|
756
|
+
# Routed through _scroll_effective() rather than forcing bind=True:
|
|
757
|
+
# composite widgets in this project call this after rebuilding their
|
|
758
|
+
# content, and a rebuild must not silently re-enable scrolling on a
|
|
759
|
+
# frame that is disabled or was explicitly set non-scrolling.
|
|
760
|
+
self._toggle_scroll_bindings(bind=self._scroll_effective())
|
|
761
|
+
|
|
762
|
+
# =========================================================================
|
|
763
|
+
# END UNCHANGED SCROLL-HANDLING METHODS
|
|
764
|
+
# =========================================================================
|
|
765
|
+
|
|
766
|
+
def winfo_children(self, include_private: bool = False) -> list:
|
|
767
|
+
"""
|
|
768
|
+
By default, filters out children whose exact class name is
|
|
769
|
+
"CTkScrollbar", "CTkCanvas", or "Canvas" -- internal furniture this
|
|
770
|
+
widget creates for its own scrolling machinery. See this class's
|
|
771
|
+
docstring for a known limitation of this approach.
|
|
772
|
+
|
|
773
|
+
Args:
|
|
774
|
+
include_private: If True, returns the raw, unfiltered list instead.
|
|
775
|
+
|
|
776
|
+
Returns:
|
|
777
|
+
A list of child widgets.
|
|
778
|
+
"""
|
|
779
|
+
raw_children = super().winfo_children()
|
|
780
|
+
if include_private:
|
|
781
|
+
return raw_children
|
|
782
|
+
|
|
783
|
+
filtered_children = []
|
|
784
|
+
for child in raw_children:
|
|
785
|
+
if child.__class__.__name__ not in ["CTkScrollbar", "CTkCanvas", "Canvas"]:
|
|
786
|
+
filtered_children.append(child)
|
|
787
|
+
return filtered_children
|
|
788
|
+
|
|
789
|
+
def get_children(self) -> list:
|
|
790
|
+
"""Equivalent to winfo_children(include_private=False)."""
|
|
791
|
+
return self.winfo_children(include_private=False)
|
|
792
|
+
|
|
793
|
+
def get_all_children(self) -> list:
|
|
794
|
+
"""Equivalent to winfo_children(include_private=True)."""
|
|
795
|
+
return self.winfo_children(include_private=True)
|