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,581 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
"""
|
|
3
|
+
sCTkSelector
|
|
4
|
+
|
|
5
|
+
An advanced theme-compliant, scrollable option tree selector widget.
|
|
6
|
+
Pairs an optional high-contrast search field lane with a dynamic listing frame card
|
|
7
|
+
to manage multi-state checkbox row configurations natively.
|
|
8
|
+
"""
|
|
9
|
+
import customtkinter as ctk
|
|
10
|
+
from .themeable_widget import ThemeableWidget, parse_list_property
|
|
11
|
+
|
|
12
|
+
from typing import Optional, Union, Tuple
|
|
13
|
+
|
|
14
|
+
from .sctk_frame import sCTkFrame
|
|
15
|
+
from .sctk_checkbox import sCTkCheckBox
|
|
16
|
+
from .sctk_entry_primary import sCTkEntryPrimary
|
|
17
|
+
from .sctk_scrollable_frame import sCTkScrollableFrame
|
|
18
|
+
|
|
19
|
+
class sCTkSelector(sCTkFrame, ThemeableWidget):
|
|
20
|
+
# The placeholder list a cleared `items` property falls back to.
|
|
21
|
+
#
|
|
22
|
+
# NOT the constructor's default -- sCTkSelector(parent) with no items is
|
|
23
|
+
# legitimately empty, and stays that way. This is what the Designer's
|
|
24
|
+
# inspector shows on a fresh widget, and what its builder object passes
|
|
25
|
+
# when the field is blank, so restoring it here keeps the design view,
|
|
26
|
+
# the preview and the generated code agreeing.
|
|
27
|
+
#
|
|
28
|
+
# Previously a cleared field reported "[]" as its default and every
|
|
29
|
+
# checkbox vanished, which looked like the widget had broken.
|
|
30
|
+
DEFAULT_ITEMS = ("Item 1", "Item 2")
|
|
31
|
+
|
|
32
|
+
def __init__(self, master, items: Optional[list[str]] = None, multiple_choices=True, searchBox=True, **kwargs):
|
|
33
|
+
# 1. SANITIZE RUNTIME ARGUMENTS: Strip unmanaged properties out immediately
|
|
34
|
+
state_init = kwargs.pop("state", "normal")
|
|
35
|
+
pack_prop_init = kwargs.pop("pack_propagate", None)
|
|
36
|
+
|
|
37
|
+
# 2. ENFORCE SYSTEM REGISTRY INTERACTION:
|
|
38
|
+
ThemeableWidget.__init__(self, kwargs)
|
|
39
|
+
|
|
40
|
+
# 🛠️ THE MUTATION SAFEGUARD DEEP COPY SHIELD:
|
|
41
|
+
self._local_defaults = dict(self.final_kw)
|
|
42
|
+
self._custom_disabled_map = dict(self._widget_disabled_map)
|
|
43
|
+
|
|
44
|
+
# FIX: required-key validation moved here, to construction time,
|
|
45
|
+
# rather than living inside _update_current_visual_state() (called
|
|
46
|
+
# only later, and repeatedly on every state change). Moved so that
|
|
47
|
+
# border_color -- now also passed to both sub-widget constructors
|
|
48
|
+
# below, to keep their normal-state border visually consistent with
|
|
49
|
+
# this widget's own theme -- is guaranteed to exist before anything
|
|
50
|
+
# tries to use it, rather than risking a confusing native error at
|
|
51
|
+
# construction if a key were missing.
|
|
52
|
+
for required_key in ("text_color", "checkbox_fg_color", "checkbox_hover_color", "border_color", "checkmark_color"):
|
|
53
|
+
if self._local_defaults.get(required_key) is None:
|
|
54
|
+
raise KeyError(
|
|
55
|
+
f"'{(getattr(self, '_THEME_BLOCK_NAME', None) or self.__class__.__name__)}' theme block is missing '{required_key}' "
|
|
56
|
+
f"at the top level of sCTkThemes.json."
|
|
57
|
+
)
|
|
58
|
+
for required_key in ("text_color", "checkbox_fg_color", "border_color", "checkmark_color"):
|
|
59
|
+
if self._custom_disabled_map.get(required_key) is None:
|
|
60
|
+
raise KeyError(
|
|
61
|
+
f"'{(getattr(self, '_THEME_BLOCK_NAME', None) or self.__class__.__name__)}' theme block is missing '{required_key}' in disabled_map."
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
fg_color = self._local_defaults.get("fg_color", "transparent")
|
|
65
|
+
# FIX: an earlier version never coordinated border_color between this
|
|
66
|
+
# widget and its two internal sub-widgets (search_bar,
|
|
67
|
+
# checkboxes_frame) -- each independently used its own standalone
|
|
68
|
+
# theme's border_color, which could visibly mismatch (confirmed by
|
|
69
|
+
# direct testing: sCTkEntryPrimary and sCTkScrollableFrame's own
|
|
70
|
+
# default border colors differ in dark mode). Passed here, once, at
|
|
71
|
+
# construction, rather than forced on every _update_current_visual_state()
|
|
72
|
+
# call -- that would fight with sCTkEntryPrimary's own correct
|
|
73
|
+
# readonly/disabled border-color changes, undoing the whole point of
|
|
74
|
+
# its three-state model. This only establishes the shared NORMAL-state
|
|
75
|
+
# border; each sub-widget's own state-driven color changes afterward
|
|
76
|
+
# are left completely alone.
|
|
77
|
+
selector_border_color = self._local_defaults.get("border_color")
|
|
78
|
+
|
|
79
|
+
# 3. Call the parent sCTkFrame constructor safely
|
|
80
|
+
super().__init__(master, **self.final_kw)
|
|
81
|
+
|
|
82
|
+
self._state = "normal"
|
|
83
|
+
self.search_var = ctk.StringVar(self)
|
|
84
|
+
self.search_var.trace_add("write", self._search_modified)
|
|
85
|
+
|
|
86
|
+
self.search_bar = None
|
|
87
|
+
self._search_box_visible = bool(searchBox)
|
|
88
|
+
|
|
89
|
+
self.checkboxes_frame = sCTkScrollableFrame(self, fg_color=fg_color, border_color=selector_border_color)
|
|
90
|
+
self.checkboxes_frame.pack(expand=True, fill="both", side="bottom")
|
|
91
|
+
|
|
92
|
+
if hasattr(self.checkboxes_frame, "_parent_frame") and self.checkboxes_frame._parent_frame is not None:
|
|
93
|
+
self.checkboxes_frame._parent_frame.pack_propagate(False)
|
|
94
|
+
|
|
95
|
+
self.checkboxes = []
|
|
96
|
+
self.selected_indexes = []
|
|
97
|
+
self.multiple_choices = multiple_choices
|
|
98
|
+
|
|
99
|
+
if items is None:
|
|
100
|
+
items = []
|
|
101
|
+
|
|
102
|
+
# 4. Route variables into the configure parser loop for execution mapping
|
|
103
|
+
self.configure(
|
|
104
|
+
items=items,
|
|
105
|
+
multiple_choices=multiple_choices,
|
|
106
|
+
searchBox=self._search_box_visible,
|
|
107
|
+
pack_propagate=pack_prop_init,
|
|
108
|
+
state=state_init
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# 🔑 REGISTER LIFECYCLE HANDSHAKE HOOK: Pushes notifications up to Pygubu systems cleanly.
|
|
112
|
+
self._finalize_themeable_lifecycle()
|
|
113
|
+
|
|
114
|
+
def _selection(self, index: int):
|
|
115
|
+
if index in self.selected_indexes:
|
|
116
|
+
self.selected_indexes.remove(index)
|
|
117
|
+
else:
|
|
118
|
+
if self.multiple_choices:
|
|
119
|
+
self.selected_indexes.append(index)
|
|
120
|
+
else:
|
|
121
|
+
if self.selected_indexes:
|
|
122
|
+
for i in self.selected_indexes:
|
|
123
|
+
if i < len(self.checkboxes):
|
|
124
|
+
self.checkboxes[i].deselect()
|
|
125
|
+
self.selected_indexes.clear()
|
|
126
|
+
self.selected_indexes.append(index)
|
|
127
|
+
else:
|
|
128
|
+
self.selected_indexes.append(index)
|
|
129
|
+
|
|
130
|
+
def _reset_scroll(self):
|
|
131
|
+
if hasattr(self.checkboxes_frame, "_parent_canvas") and self.checkboxes_frame._parent_canvas is not None:
|
|
132
|
+
self.checkboxes_frame._parent_canvas.yview_moveto(0)
|
|
133
|
+
|
|
134
|
+
def _search_modified(self, *args):
|
|
135
|
+
value = self.search_var.get()
|
|
136
|
+
row = 0
|
|
137
|
+
for x in range(len(self.checkboxes)):
|
|
138
|
+
if self.checkboxes[x].cget("text").startswith(value):
|
|
139
|
+
self.checkboxes[x].grid(row=row, column=0, padx=3, pady=3, sticky="w")
|
|
140
|
+
row += 1
|
|
141
|
+
else:
|
|
142
|
+
self.checkboxes[x].grid_forget()
|
|
143
|
+
self._reset_scroll()
|
|
144
|
+
|
|
145
|
+
def get_all_items(self) -> list:
|
|
146
|
+
return [checkbox.cget("text") for checkbox in self.checkboxes]
|
|
147
|
+
def configure(self, cnf=None, **kwargs):
|
|
148
|
+
if cnf is not None and not kwargs and isinstance(cnf, str):
|
|
149
|
+
pname = cnf
|
|
150
|
+
# width and height report this widget's OWN defaults.
|
|
151
|
+
#
|
|
152
|
+
# Neither is in the theme block, so the shared query fell back to
|
|
153
|
+
# the current value -- and pygubu uses that as the default when a
|
|
154
|
+
# field is cleared. Clearing a height of 100 therefore set it to
|
|
155
|
+
# 100 again, which looked like the clear doing nothing.
|
|
156
|
+
#
|
|
157
|
+
# 200x150 is what the constructor falls back to when no size is
|
|
158
|
+
# given; see the else branch in configure().
|
|
159
|
+
# The reported default is 0, meaning "no size asked for".
|
|
160
|
+
#
|
|
161
|
+
# Pygubu does not send an empty string when a field is cleared --
|
|
162
|
+
# it asks the widget for the default and sets THAT. Reporting 150
|
|
163
|
+
# therefore sent height=150, which is indistinguishable from the
|
|
164
|
+
# user typing 150, so the widget honoured it: propagate off, height
|
|
165
|
+
# exactly 150. A freshly placed selector sends no height at all,
|
|
166
|
+
# takes the no-size branch, and fits its contents instead -- so the
|
|
167
|
+
# same logical state gave two different sizes.
|
|
168
|
+
#
|
|
169
|
+
# Zero is treated as "not asked for" below, which makes clearing
|
|
170
|
+
# land in the same branch a fresh load does.
|
|
171
|
+
if pname == "width":
|
|
172
|
+
return ("width", "width", "Width", 0,
|
|
173
|
+
int(self.final_kw.get("width", 0) or 0))
|
|
174
|
+
if pname == "height":
|
|
175
|
+
return ("height", "height", "Height", 0,
|
|
176
|
+
int(self.final_kw.get("height", 0) or 0))
|
|
177
|
+
|
|
178
|
+
if pname == "state": return ("state", "state", "state", "normal", str(self.state()))
|
|
179
|
+
if pname == "multiple_choices": return ("multiple_choices", "multiple_choices", "multiple_choices", "True", str(self.multiple_choices))
|
|
180
|
+
if pname == "searchBox": return ("searchBox", "searchBox", "searchBox", "True", str(self._search_box_visible))
|
|
181
|
+
if pname == "items":
|
|
182
|
+
# FIX: the default slot used to be the literal "[]", so
|
|
183
|
+
# clearing the field in the Designer reported "the default is
|
|
184
|
+
# an empty list" and every checkbox vanished -- the widget did
|
|
185
|
+
# exactly what it was told, but the answer was wrong. The
|
|
186
|
+
# constructor's default is DEFAULT_ITEMS, which is also what
|
|
187
|
+
# the inspector shows on a fresh widget.
|
|
188
|
+
#
|
|
189
|
+
# Both slots go through _query_value with the property name,
|
|
190
|
+
# so a list is rendered as JSON -- the form pygubu parses and
|
|
191
|
+
# the form a user types. str() of a Python list is neither.
|
|
192
|
+
current_items = ([cb.cget("text") for cb in self.checkboxes]
|
|
193
|
+
if hasattr(self, "checkboxes") else [])
|
|
194
|
+
return ("items", "items", "items",
|
|
195
|
+
self._query_value(list(self.DEFAULT_ITEMS), "items"),
|
|
196
|
+
self._query_value(current_items, "items"))
|
|
197
|
+
if pname == "pack_propagate": return (pname, pname, pname, "None", str(getattr(self, "_pack_propagate_val", None)))
|
|
198
|
+
if pname in ["fg_color", "border_color", "text_color"]:
|
|
199
|
+
current_state = str(self.state()).lower()
|
|
200
|
+
val = self._custom_disabled_map.get(pname) if current_state == "disabled" else self._local_defaults.get(pname)
|
|
201
|
+
return (pname, pname, pname,
|
|
202
|
+
self._query_value(self._theme_default(pname)),
|
|
203
|
+
self._query_value(val))
|
|
204
|
+
# FIX: forwarding a property NAME to native configure() passes it
|
|
205
|
+
# as require_redraw and returns None, where pygubu expects a
|
|
206
|
+
# Tkinter-style five-tuple -- it then hands that None straight back
|
|
207
|
+
# to _set_property(). Reached whenever a field is blanked in the
|
|
208
|
+
# Designer inspector.
|
|
209
|
+
#
|
|
210
|
+
# This file uses the older (self, cnf=None, **kwargs) signature, so
|
|
211
|
+
# the query arrives as `cnf`. The batch pass that fixed this across
|
|
212
|
+
# the library matched on `pname` and `require_redraw` and missed it.
|
|
213
|
+
return self._configure_query(cnf)
|
|
214
|
+
|
|
215
|
+
if isinstance(cnf, dict): kwargs = cnf | kwargs
|
|
216
|
+
|
|
217
|
+
if "items" in kwargs:
|
|
218
|
+
items_val = kwargs.pop("items")
|
|
219
|
+
# An empty value means "use the placeholder list", matching the
|
|
220
|
+
# query above and what the builder object passes when the
|
|
221
|
+
# inspector field is blank.
|
|
222
|
+
if items_val == "" or items_val is None:
|
|
223
|
+
items_val = list(self.DEFAULT_ITEMS)
|
|
224
|
+
elif isinstance(items_val, str):
|
|
225
|
+
# Shared parser: accepts the Python-literal form this
|
|
226
|
+
# widget's inspector default uses AND the bare
|
|
227
|
+
# comma-separated form every other widget expects.
|
|
228
|
+
try: items_val = parse_list_property(items_val)
|
|
229
|
+
except Exception: items_val = []
|
|
230
|
+
if items_val is not None:
|
|
231
|
+
if len(set(items_val)) == len(items_val):
|
|
232
|
+
for checkbox in self.checkboxes: checkbox.destroy()
|
|
233
|
+
self.checkboxes.clear()
|
|
234
|
+
self.selected_indexes.clear()
|
|
235
|
+
for index in range(len(items_val)):
|
|
236
|
+
self.checkboxes.append(sCTkCheckBox(self.checkboxes_frame, text=items_val[index], command=lambda a=index: self._selection(a)))
|
|
237
|
+
self._search_modified()
|
|
238
|
+
else: raise ValueError("There is two times or more the same item in the given items list")
|
|
239
|
+
|
|
240
|
+
if "searchBox" in kwargs:
|
|
241
|
+
sb_val = kwargs.pop("searchBox")
|
|
242
|
+
if sb_val == "" or sb_val is None: sb_val = True
|
|
243
|
+
elif isinstance(sb_val, str): sb_val = str(sb_val).lower() in ['true', '1', 'yes']
|
|
244
|
+
self._search_box_visible = sb_val
|
|
245
|
+
if self._search_box_visible:
|
|
246
|
+
# FIX: search_bar's border_color now aligned with this
|
|
247
|
+
# widget's own theme at creation time, matching the same
|
|
248
|
+
# fix applied to checkboxes_frame in __init__ -- see that
|
|
249
|
+
# constructor call's comment for the full reasoning.
|
|
250
|
+
if not hasattr(self, "search_bar") or self.search_bar is None: self.search_bar = sCTkEntryPrimary(self, textvariable=self.search_var, border_color=self._local_defaults.get("border_color"))
|
|
251
|
+
self.search_bar.pack(anchor="n", fill="x")
|
|
252
|
+
if hasattr(self, "checkboxes_frame") and self.checkboxes_frame is not None:
|
|
253
|
+
self.checkboxes_frame.pack_forget()
|
|
254
|
+
self.checkboxes_frame.pack(expand=True, fill="both", side="bottom")
|
|
255
|
+
if self._state == "disabled" and self.search_bar is not None: self.search_bar.configure(state="readonly")
|
|
256
|
+
else:
|
|
257
|
+
if hasattr(self, "search_bar") and self.search_bar is not None: self.search_bar.pack_forget()
|
|
258
|
+
|
|
259
|
+
if "multiple_choices" in kwargs:
|
|
260
|
+
mult_val = kwargs.pop("multiple_choices")
|
|
261
|
+
if mult_val == "" or mult_val is None: mult_val = True
|
|
262
|
+
elif isinstance(mult_val, str): mult_val = str(mult_val).lower() in ['true', '1', 'yes']
|
|
263
|
+
self.multiple_choices = mult_val
|
|
264
|
+
|
|
265
|
+
# Runtime overrides have to reach the map a repaint reads, or the
|
|
266
|
+
# repaint puts the theme value straight back -- see
|
|
267
|
+
# ThemeableWidget._record_theme_overrides().
|
|
268
|
+
self._record_theme_overrides(kwargs)
|
|
269
|
+
|
|
270
|
+
if "state" in kwargs: self.state(kwargs.pop("state"))
|
|
271
|
+
|
|
272
|
+
# grid_propagate is deliberately GONE.
|
|
273
|
+
#
|
|
274
|
+
# pack_propagate() and grid_propagate() control whether a container
|
|
275
|
+
# resizes to fit its children, and which one applies depends on how
|
|
276
|
+
# THE CHILDREN are managed -- not on how this widget is managed by its
|
|
277
|
+
# own parent. This widget packs its children, so pack_propagate is the
|
|
278
|
+
# meaningful call and grid_propagate could never do anything.
|
|
279
|
+
#
|
|
280
|
+
# It was offered anyway, so setting it looked like a knob that did
|
|
281
|
+
# nothing, while pack_propagate appeared to work "regardless of
|
|
282
|
+
# geometry management" -- which is simply what it does. A property that
|
|
283
|
+
# provably cannot have an effect is worse than an absent one: someone
|
|
284
|
+
# sets it, sees nothing, and goes looking for a bug in their layout.
|
|
285
|
+
pack_prop_val = kwargs.pop("pack_propagate", None)
|
|
286
|
+
# Accepted and discarded, so existing code passing it does not raise.
|
|
287
|
+
kwargs.pop("grid_propagate", None)
|
|
288
|
+
if pack_prop_val is not None and pack_prop_val != "": setattr(self, "_pack_propagate_val", str(pack_prop_val).lower() in ['true', '1', 'yes'])
|
|
289
|
+
|
|
290
|
+
for k, v in list(kwargs.items()):
|
|
291
|
+
if k in self._local_defaults: self.final_kw[k] = kwargs.pop(k)
|
|
292
|
+
|
|
293
|
+
if "fg_color" in self.final_kw:
|
|
294
|
+
new_fg = self.final_kw.get("fg_color")
|
|
295
|
+
if hasattr(self, "checkboxes_frame"): self.checkboxes_frame.configure(fg_color=new_fg)
|
|
296
|
+
|
|
297
|
+
# A CLEARED width or height means "go back to the default size".
|
|
298
|
+
#
|
|
299
|
+
# The Designer sends an empty string, and the blank-value filter at the
|
|
300
|
+
# end of this method simply drops it -- so nothing told the widget to
|
|
301
|
+
# revert and the last size stayed on the frame. Clearing the field
|
|
302
|
+
# looked like it did nothing until some other change forced a repaint.
|
|
303
|
+
#
|
|
304
|
+
# Removing it from final_kw as well is what matters: the lookup below
|
|
305
|
+
# falls back to that dict, so a stale entry there would keep the old
|
|
306
|
+
# size alive.
|
|
307
|
+
# Clearing is decided PER KEY, not for both at once.
|
|
308
|
+
#
|
|
309
|
+
# The Designer sends every property when any one of them changes, so
|
|
310
|
+
# editing the width also delivers height=0. Treating that as "the size
|
|
311
|
+
# was cleared" turned propagate back on and pushed the default width
|
|
312
|
+
# over the 400 just asked for -- setting a width made the height
|
|
313
|
+
# collapse to its contents and the width not take at all.
|
|
314
|
+
#
|
|
315
|
+
# Blank or zero both mean "no size asked for" here; see the query
|
|
316
|
+
# branch above for why a cleared field arrives as 0 rather than "".
|
|
317
|
+
_SIZE_DEFAULTS = {"width": 200, "height": 150}
|
|
318
|
+
_cleared = set()
|
|
319
|
+
for _size_key in ("width", "height"):
|
|
320
|
+
if _size_key in kwargs and str(kwargs[_size_key]).strip() in ("", "0"):
|
|
321
|
+
kwargs.pop(_size_key)
|
|
322
|
+
self.final_kw.pop(_size_key, None)
|
|
323
|
+
_cleared.add(_size_key)
|
|
324
|
+
# Reset the frame's stored value too. It remembers whatever it
|
|
325
|
+
# was last told, and turning propagate on only masks that --
|
|
326
|
+
# the next explicit size would let the old number reappear.
|
|
327
|
+
kwargs[_size_key] = _SIZE_DEFAULTS[_size_key]
|
|
328
|
+
|
|
329
|
+
# FIX: these read final_kw only, and width/height never arrive there.
|
|
330
|
+
#
|
|
331
|
+
# The loop above moves a keyword into final_kw only if it is a key this
|
|
332
|
+
# widget's THEME block defines, and the sCTkSelector block has no width
|
|
333
|
+
# or height. So an explicit height stayed in kwargs, h_val came back 0,
|
|
334
|
+
# and the else branch below ran -- setting 200x150 and, worse, turning
|
|
335
|
+
# ON pack_propagate. The height was then applied to the frame and
|
|
336
|
+
# immediately overridden by the frame shrinking to fit its children.
|
|
337
|
+
#
|
|
338
|
+
# The symptom was a height property that appeared to do nothing, in the
|
|
339
|
+
# Designer and in generated code alike.
|
|
340
|
+
w_val = int(kwargs.get("width", self.final_kw.get("width", 0)) or 0)
|
|
341
|
+
h_val = int(kwargs.get("height", self.final_kw.get("height", 0)) or 0)
|
|
342
|
+
|
|
343
|
+
# And put the COERCED values back, because the Designer sends strings.
|
|
344
|
+
#
|
|
345
|
+
# h_val above is an int and is used for the propagate decision, but the
|
|
346
|
+
# raw kwargs entry -- '100', with quotes -- is what reaches
|
|
347
|
+
# CTkFrame.configure() at the end of this method, which wants a number.
|
|
348
|
+
# The height was computed correctly, used correctly, and then forwarded
|
|
349
|
+
# in a form the native widget could not act on.
|
|
350
|
+
if "width" in kwargs:
|
|
351
|
+
kwargs["width"] = w_val
|
|
352
|
+
if "height" in kwargs:
|
|
353
|
+
kwargs["height"] = h_val
|
|
354
|
+
# Propagate goes back on only when NEITHER size was asked for, which
|
|
355
|
+
# is the state a freshly placed selector is in.
|
|
356
|
+
_explicit = {k for k in ("width", "height")
|
|
357
|
+
if k not in _cleared and (k in kwargs or k in self.final_kw)}
|
|
358
|
+
|
|
359
|
+
if _explicit:
|
|
360
|
+
# An explicit size means propagate OFF, unless this very call says
|
|
361
|
+
# otherwise.
|
|
362
|
+
#
|
|
363
|
+
# FIX: this used to fall back to self._pack_propagate_val, which is
|
|
364
|
+
# set to True at construction when no size was given -- and then
|
|
365
|
+
# outlives that situation. So a height set later was computed
|
|
366
|
+
# correctly, applied correctly, and then ignored, because the frame
|
|
367
|
+
# was still sizing itself to its children. winfo_reqheight() stayed
|
|
368
|
+
# at the content height no matter what was asked for.
|
|
369
|
+
#
|
|
370
|
+
# The `False` default in the old getattr never applied: the
|
|
371
|
+
# attribute always exists by this point.
|
|
372
|
+
#
|
|
373
|
+
# Asking for a size and asking the widget to size itself to its
|
|
374
|
+
# contents are contradictory. The explicit number is the more
|
|
375
|
+
# specific instruction, so it wins.
|
|
376
|
+
use_pack_p = pack_prop_val if pack_prop_val is not None else False
|
|
377
|
+
else:
|
|
378
|
+
self.final_kw["width"] = 200
|
|
379
|
+
self.final_kw["height"] = 150
|
|
380
|
+
if _cleared:
|
|
381
|
+
# Reset the frame's stored size as well as the propagate flag.
|
|
382
|
+
#
|
|
383
|
+
# The frame remembers whatever it was last told. Turning
|
|
384
|
+
# propagate back on only MASKS that: the widget fits its
|
|
385
|
+
# contents and looks right, but the old number is still there,
|
|
386
|
+
# and the next explicit size turns propagate off and lets it
|
|
387
|
+
# reappear. Setting width 400, clearing it, then setting a
|
|
388
|
+
# height brought the 400 back.
|
|
389
|
+
#
|
|
390
|
+
# Safe to forward here because propagate is on in this branch,
|
|
391
|
+
# so these do not fight the content sizing -- which is why a
|
|
392
|
+
# fresh load and a cleared field still agree.
|
|
393
|
+
kwargs["width"] = 200
|
|
394
|
+
kwargs["height"] = 150
|
|
395
|
+
use_pack_p = pack_prop_val if pack_prop_val is not None else getattr(self, "_pack_propagate_val", True)
|
|
396
|
+
|
|
397
|
+
if isinstance(use_pack_p, str): use_pack_p = use_pack_p.lower() in ['true', '1', 'yes']
|
|
398
|
+
if use_pack_p is not None: self.pack_propagate(use_pack_p)
|
|
399
|
+
|
|
400
|
+
if hasattr(self, "checkboxes_frame") and hasattr(self.checkboxes_frame, "_parent_frame"):
|
|
401
|
+
# The INNER frame stays non-propagating, always.
|
|
402
|
+
#
|
|
403
|
+
# FIX: this used to follow use_pack_p, the outer frame's setting.
|
|
404
|
+
# Construction deliberately pins it to False (see __init__), which
|
|
405
|
+
# is what gives a fresh selector its full scroll area however few
|
|
406
|
+
# checkboxes it holds. Setting it True made the inner frame shrink
|
|
407
|
+
# to its contents, so clearing a height produced a selector smaller
|
|
408
|
+
# than a freshly placed one -- the same logical state, two sizes.
|
|
409
|
+
#
|
|
410
|
+
# The outer frame's propagate is the one that means anything to a
|
|
411
|
+
# caller: it decides whether an explicit height is honoured.
|
|
412
|
+
self.checkboxes_frame._parent_frame.pack_propagate(False)
|
|
413
|
+
|
|
414
|
+
self.final_kw.pop("pack_propagate", None)
|
|
415
|
+
self.final_kw.pop("grid_propagate", None) # harmless if it was passed
|
|
416
|
+
self.final_kw.pop("state", None)
|
|
417
|
+
|
|
418
|
+
self._drop_cleared(kwargs)
|
|
419
|
+
if kwargs: return super().configure(**kwargs)
|
|
420
|
+
return None
|
|
421
|
+
|
|
422
|
+
# Tkinter/CTk convention binds .config to .configure as a SEPARATE class
|
|
423
|
+
# attribute -- it does not automatically track whichever configure() a
|
|
424
|
+
# subclass defines. Without this line, calling .config(...) silently skips
|
|
425
|
+
# this entire override and lands on sCTkFrame's configure() instead,
|
|
426
|
+
# bypassing the items/searchBox/multiple_choices/state handling above.
|
|
427
|
+
# Confirmed as a critical bug on sCTkSegmentedButton earlier in this
|
|
428
|
+
# project's audit; this was the last widget in the library still missing
|
|
429
|
+
# the alias.
|
|
430
|
+
#
|
|
431
|
+
# Note this class uses the older Tkinter `(self, cnf=None, **kwargs)`
|
|
432
|
+
# signature rather than `*args`. That's correct here and not the source of
|
|
433
|
+
# the tuple-comparison bugs found elsewhere: cnf is a real parameter
|
|
434
|
+
# holding the value itself, so `isinstance(cnf, dict)` and `pname = cnf`
|
|
435
|
+
# both behave as intended.
|
|
436
|
+
config = configure
|
|
437
|
+
|
|
438
|
+
def _set_appearance_mode(self, mode_string: str):
|
|
439
|
+
if hasattr(super(), "_set_appearance_mode"):
|
|
440
|
+
try:
|
|
441
|
+
super()._set_appearance_mode(mode_string)
|
|
442
|
+
except Exception:
|
|
443
|
+
pass
|
|
444
|
+
self._update_current_visual_state()
|
|
445
|
+
|
|
446
|
+
def get_state(self) -> str:
|
|
447
|
+
return self.state()
|
|
448
|
+
|
|
449
|
+
def state(self, mode: str = None) -> str:
|
|
450
|
+
"""Dedicated state manager controlling human inputs programmatically via canvas intercept shields."""
|
|
451
|
+
if mode is None: return str(getattr(self, "_state", "normal")).lower()
|
|
452
|
+
mode = mode.lower()
|
|
453
|
+
if mode in ("normal", "enabled", "active"):
|
|
454
|
+
self._state = "normal"
|
|
455
|
+
if hasattr(self, "search_bar") and self.search_bar is not None:
|
|
456
|
+
self.search_bar.configure(state="normal")
|
|
457
|
+
if hasattr(self, "checkboxes"):
|
|
458
|
+
for cb in self.checkboxes:
|
|
459
|
+
cb.configure(state="normal")
|
|
460
|
+
if hasattr(cb, "_create_bindings"):
|
|
461
|
+
try:
|
|
462
|
+
cb._create_bindings()
|
|
463
|
+
except Exception:
|
|
464
|
+
pass
|
|
465
|
+
elif mode == "disabled":
|
|
466
|
+
self._state = "disabled"
|
|
467
|
+
if hasattr(self, "search_bar") and self.search_bar is not None:
|
|
468
|
+
self.search_bar.configure(state="readonly")
|
|
469
|
+
if hasattr(self, "checkboxes"):
|
|
470
|
+
for cb in self.checkboxes:
|
|
471
|
+
# 🔑 HARD INTERCEPT UNBIND MATRIX: Paralyzes mouse clicking tracks completely
|
|
472
|
+
try:
|
|
473
|
+
if hasattr(cb, "_canvas") and cb._canvas:
|
|
474
|
+
cb._canvas.unbind("<Enter>")
|
|
475
|
+
cb._canvas.unbind("<Leave>")
|
|
476
|
+
cb._canvas.unbind("<Button-1>")
|
|
477
|
+
cb._canvas.unbind("<ButtonRelease>")
|
|
478
|
+
if hasattr(cb, "_text_label") and cb._text_label:
|
|
479
|
+
cb._text_label.unbind("<Enter>")
|
|
480
|
+
cb._text_label.unbind("<Leave>")
|
|
481
|
+
cb._text_label.unbind("<Button-1>")
|
|
482
|
+
cb._text_label.unbind("<ButtonRelease>")
|
|
483
|
+
except Exception:
|
|
484
|
+
pass
|
|
485
|
+
self._update_current_visual_state()
|
|
486
|
+
return self._state
|
|
487
|
+
|
|
488
|
+
def _update_current_visual_state(self):
|
|
489
|
+
"""
|
|
490
|
+
Applies checkbox and search-bar colors based on the current state.
|
|
491
|
+
|
|
492
|
+
Required-key validation for the theme keys this method (and
|
|
493
|
+
__init__'s sub-widget construction) depends on happens once, in
|
|
494
|
+
__init__ -- not repeated here on every call.
|
|
495
|
+
|
|
496
|
+
FIX: an earlier version's disabled branch used 100% hardcoded
|
|
497
|
+
literals -- self._custom_disabled_map was set up in __init__ but
|
|
498
|
+
never actually consulted here, meaning a correctly-populated
|
|
499
|
+
disabled_map in sCTkThemes.json had zero effect on what users
|
|
500
|
+
actually saw. Now reads from self._custom_disabled_map like every
|
|
501
|
+
other widget in this project, with hard-fail validation for
|
|
502
|
+
required keys.
|
|
503
|
+
|
|
504
|
+
FIX: an earlier version derived the checkbox's fill/hover color from
|
|
505
|
+
this widget's OWN fg_color/hover_color theme keys -- the same keys
|
|
506
|
+
that control the surrounding frame's own background -- falling back
|
|
507
|
+
to a hardcoded accent color pair whenever fg_color was "transparent"
|
|
508
|
+
(a common, legitimate choice for frame-type widgets, not a theme
|
|
509
|
+
gap). Reusing fg_color for two different purposes doesn't work when
|
|
510
|
+
the frame is meant to be transparent. Now uses dedicated
|
|
511
|
+
"checkbox_fg_color"/"checkbox_hover_color" theme keys instead, with
|
|
512
|
+
hard-fail validation, rather than overloading fg_color or silently
|
|
513
|
+
substituting a hardcoded guess.
|
|
514
|
+
|
|
515
|
+
FIX: an earlier version also wrote cb._inner_fg_color and cb._hover
|
|
516
|
+
directly onto each checkbox instance, in both branches -- confirmed
|
|
517
|
+
against sctk_checkbox.py's actual source that neither attribute is
|
|
518
|
+
ever read by CheckBox's own code anywhere. These were writes to
|
|
519
|
+
private attributes CheckBox never defined, with no effect. Removed
|
|
520
|
+
entirely, along with the now-unused color computation that only ever
|
|
521
|
+
fed into the dead cb._inner_fg_color write (which itself included a
|
|
522
|
+
cross-widget reach into the checkbox's own private _local_defaults
|
|
523
|
+
for a theme key, "inner_fg_color", that isn't part of CheckBox's
|
|
524
|
+
documented theme key set at all).
|
|
525
|
+
|
|
526
|
+
Passes raw (light, dark) tuples straight through to each checkbox's
|
|
527
|
+
configure() instead of resolving to a single color first, matching
|
|
528
|
+
the tuple-based approach validated elsewhere in this project. An
|
|
529
|
+
earlier version resolved everything to a single string first, which
|
|
530
|
+
still worked correctly here specifically because _set_appearance_mode
|
|
531
|
+
already manually re-triggers this whole method on every light/dark
|
|
532
|
+
switch -- but that's inconsistent with the more robust pattern used
|
|
533
|
+
elsewhere, which doesn't depend on a manual re-trigger at all.
|
|
534
|
+
"""
|
|
535
|
+
is_disabled = getattr(self, "_state", "normal") == "disabled"
|
|
536
|
+
|
|
537
|
+
if hasattr(self, "search_bar") and self.search_bar is not None:
|
|
538
|
+
self.search_bar._update_current_visual_state()
|
|
539
|
+
|
|
540
|
+
if not hasattr(self, "checkboxes"):
|
|
541
|
+
return
|
|
542
|
+
|
|
543
|
+
for cb in self.checkboxes:
|
|
544
|
+
if is_disabled:
|
|
545
|
+
cb.configure(state="disabled")
|
|
546
|
+
d_map = self._custom_disabled_map
|
|
547
|
+
# FIX: an earlier version never referenced self._custom_disabled_map
|
|
548
|
+
# here at all -- these four lines were 100% hardcoded literals
|
|
549
|
+
# with zero theme connection. hover_color intentionally reuses
|
|
550
|
+
# checkbox_fg_color, not a separate disabled hover key -- hover
|
|
551
|
+
# can't meaningfully trigger while disabled anyway, matching the
|
|
552
|
+
# same "no distinct disabled hover" convention used elsewhere
|
|
553
|
+
# in this project (e.g. sCTkSlider).
|
|
554
|
+
cb.configure(
|
|
555
|
+
text_color=d_map.get("text_color"),
|
|
556
|
+
fg_color=d_map.get("checkbox_fg_color"),
|
|
557
|
+
border_color=d_map.get("border_color"),
|
|
558
|
+
hover_color=d_map.get("checkbox_fg_color"),
|
|
559
|
+
checkmark_color=d_map.get("checkmark_color"),
|
|
560
|
+
)
|
|
561
|
+
else:
|
|
562
|
+
cb.configure(state="normal")
|
|
563
|
+
m = self._local_defaults
|
|
564
|
+
# FIX: an earlier version read fg_color/hover_color here --
|
|
565
|
+
# the SAME keys that control the surrounding frame's own
|
|
566
|
+
# background -- with a hardcoded accent-color fallback for
|
|
567
|
+
# whenever fg_color was "transparent" (the frame's own
|
|
568
|
+
# default). Now uses dedicated checkbox_fg_color/
|
|
569
|
+
# checkbox_hover_color keys instead, so the checkbox's accent
|
|
570
|
+
# color no longer depends on what the frame's background
|
|
571
|
+
# happens to be set to.
|
|
572
|
+
cb.configure(
|
|
573
|
+
text_color=m.get("text_color"),
|
|
574
|
+
fg_color=m.get("checkbox_fg_color"),
|
|
575
|
+
border_color=m.get("border_color"),
|
|
576
|
+
hover_color=m.get("checkbox_hover_color"),
|
|
577
|
+
checkmark_color=m.get("checkmark_color"),
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
# Force an explicit redrawing pass on the inner elements safely
|
|
581
|
+
if hasattr(cb, "_draw"): cb._draw()
|