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,409 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
"""
|
|
3
|
+
sCTkTabview
|
|
4
|
+
|
|
5
|
+
A theme-compliant custom multi-page tab container layout.
|
|
6
|
+
Inherits from ctk.CTkTabview and ThemeableWidget to manage dense cockpit dashboard
|
|
7
|
+
panels safely with full live theme repaint loops out of sCTkThemes.json.
|
|
8
|
+
"""
|
|
9
|
+
import tkinter as tk
|
|
10
|
+
|
|
11
|
+
import customtkinter as ctk
|
|
12
|
+
from .themeable_widget import ThemeableWidget
|
|
13
|
+
from .sctk_frame import sCTkFrame
|
|
14
|
+
|
|
15
|
+
class sCTkTabview(ctk.CTkTabview, ThemeableWidget):
|
|
16
|
+
def __init__(self, master=None, **kw):
|
|
17
|
+
|
|
18
|
+
# 1. Run shared mixin logic first to parse master themes.json safely
|
|
19
|
+
ThemeableWidget.__init__(self, kw)
|
|
20
|
+
|
|
21
|
+
# 2. Store your custom memory deep-copy maps cleanly onto instance structures
|
|
22
|
+
self._local_defaults = dict(self.final_kw)
|
|
23
|
+
|
|
24
|
+
# FIX: an earlier version read "disabled_map" out of self._local_defaults
|
|
25
|
+
# (== dict(self.final_kw)), but ThemeableWidget.__init__ deliberately
|
|
26
|
+
# excludes "disabled_map" from final_kw -- this always evaluated to the
|
|
27
|
+
# empty-dict default, meaning EVERY disabled-state color lookup in
|
|
28
|
+
# _apply_custom_theme_colors() silently fell back to its hardcoded
|
|
29
|
+
# literal instead of the real theme. The disabled colors still LOOKED
|
|
30
|
+
# plausible, because those fallbacks are reasonable values, so nothing
|
|
31
|
+
# flagged it visually. Confirmed identical bug, same fix, as sCTkSwitch,
|
|
32
|
+
# sCTkSpinbox, and sCTkTableview elsewhere in this project.
|
|
33
|
+
self._custom_disabled_map = dict(self._widget_disabled_map)
|
|
34
|
+
self._custom_current_state = "normal"
|
|
35
|
+
|
|
36
|
+
# 2a. Hard-fail on theme gaps rather than substituting a guessed value.
|
|
37
|
+
#
|
|
38
|
+
# FIX: an earlier version used .get(key, hardcoded_literal) for every
|
|
39
|
+
# color below and for the font above, silently substituting a plausible
|
|
40
|
+
# guess whenever the real theme was incomplete rather than surfacing the
|
|
41
|
+
# gap. Because those guesses looked reasonable, a broken or partial
|
|
42
|
+
# theme block was invisible. Matches the hard-fail principle established
|
|
43
|
+
# for sCTkSwitch, sCTkTableview, sCTkScrollableFrame, and the label
|
|
44
|
+
# family elsewhere in this project.
|
|
45
|
+
#
|
|
46
|
+
# SCOPED TO DIRECT CONSTRUCTION. A subclass reaches here with final_kw
|
|
47
|
+
# built from ITS OWN theme block -- ThemeableWidget's run-once guard
|
|
48
|
+
# means this constructor never rebuilds it -- so validating this
|
|
49
|
+
# widget's keys against a subclass's block would raise on every
|
|
50
|
+
# construction. Same guard, same reason, as sCTkScrollableFrame.
|
|
51
|
+
if type(self) is sCTkTabview:
|
|
52
|
+
# Colors that visually change when disabled: required in BOTH the
|
|
53
|
+
# top-level block and disabled_map.
|
|
54
|
+
for required_key in ("text_color",
|
|
55
|
+
"segmented_button_fg_color",
|
|
56
|
+
"segmented_button_selected_color",
|
|
57
|
+
"segmented_button_unselected_color"):
|
|
58
|
+
if self._local_defaults.get(required_key) is None:
|
|
59
|
+
raise KeyError(
|
|
60
|
+
f"'{(getattr(self, '_THEME_BLOCK_NAME', None) or self.__class__.__name__)}' theme block is missing "
|
|
61
|
+
f"'{required_key}' at the top level of sCTkThemes.json."
|
|
62
|
+
)
|
|
63
|
+
if self._custom_disabled_map.get(required_key) is None:
|
|
64
|
+
raise KeyError(
|
|
65
|
+
f"'{(getattr(self, '_THEME_BLOCK_NAME', None) or self.__class__.__name__)}' theme block is missing "
|
|
66
|
+
f"'{required_key}' in disabled_map."
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Hover colors are top-level only. They deliberately have NO
|
|
70
|
+
# disabled_map entry: a disabled tab bar must not light up under
|
|
71
|
+
# the cursor, so _apply_custom_theme_colors() collapses hover to
|
|
72
|
+
# the corresponding non-hover disabled color instead of reading a
|
|
73
|
+
# separate one. Same reasoning as sCTkScrollableFrame's inert
|
|
74
|
+
# scrollbar, resolved differently here because there's no
|
|
75
|
+
# meaningful "dimmed hover" distinct from "dimmed".
|
|
76
|
+
#
|
|
77
|
+
# font and segmented_button_height are likewise top-level only --
|
|
78
|
+
# no widget in this project has a disabled-state font, and the
|
|
79
|
+
# tab bar's height doesn't change when disabled.
|
|
80
|
+
for required_key in ("segmented_button_selected_hover_color",
|
|
81
|
+
"segmented_button_unselected_hover_color",
|
|
82
|
+
"font",
|
|
83
|
+
"segmented_button_height"):
|
|
84
|
+
if self._local_defaults.get(required_key) is None:
|
|
85
|
+
raise KeyError(
|
|
86
|
+
f"'{(getattr(self, '_THEME_BLOCK_NAME', None) or self.__class__.__name__)}' theme block is missing "
|
|
87
|
+
f"'{required_key}' at the top level of sCTkThemes.json."
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# 3. Intercept the non-standard font key locally before initialization
|
|
91
|
+
# pass. Popped from final_kw because native CTkTabview doesn't accept
|
|
92
|
+
# a `font` keyword -- it goes to the segmented button instead, in
|
|
93
|
+
# step 5 below. No fallback: validated above.
|
|
94
|
+
target_font = self.final_kw.pop("font", self._local_defaults.get("font"))
|
|
95
|
+
|
|
96
|
+
# Same treatment for the tab bar height. Native CTkTabview names every
|
|
97
|
+
# parameter explicitly with no **kwargs catch-all, so ANY key it
|
|
98
|
+
# doesn't recognize raises ValueError from super().__init__() below --
|
|
99
|
+
# this must be popped, not merely ignored. Forwarded to the segmented
|
|
100
|
+
# button in step 5, which is the widget that actually has a height.
|
|
101
|
+
target_button_height = self.final_kw.pop(
|
|
102
|
+
"segmented_button_height", self._local_defaults.get("segmented_button_height"))
|
|
103
|
+
|
|
104
|
+
# Registry of sCTkFrame page wrappers, keyed by tab name -- see
|
|
105
|
+
# _ensure_sctk_page(). Established BEFORE super().__init__() so it
|
|
106
|
+
# exists no matter when the first tab gets created.
|
|
107
|
+
self._sctk_pages = {}
|
|
108
|
+
|
|
109
|
+
# 4. Initialize parent class natively with clean frame-compliant attributes safely
|
|
110
|
+
super().__init__(master, **self.final_kw)
|
|
111
|
+
|
|
112
|
+
# 5. Forward the extracted font and height parameters down to the
|
|
113
|
+
# native layout bar safely. Applied once here rather than in
|
|
114
|
+
# _apply_custom_theme_colors(), since neither changes with state --
|
|
115
|
+
# re-pushing them on every repaint would be wasted work.
|
|
116
|
+
#
|
|
117
|
+
# NOTE: segmented_button_height is currently a NO-OP visually, and
|
|
118
|
+
# that is expected, not a bug. The height is genuinely applied --
|
|
119
|
+
# _segmented_button.cget("height") reports it back correctly -- but
|
|
120
|
+
# CTkTabview grids the segmented button into a row whose minsize comes
|
|
121
|
+
# from its own private spacing constants, and overlaps it with the
|
|
122
|
+
# page frame below to get the connected-tab look. A taller button is
|
|
123
|
+
# clipped by that row rather than expanding it; confirmed by direct
|
|
124
|
+
# testing with a height of 128, which reported back correctly and
|
|
125
|
+
# changed nothing on screen.
|
|
126
|
+
#
|
|
127
|
+
# Kept, and kept required in the theme, deliberately: it costs
|
|
128
|
+
# nothing, keeps the theme contract stable, and is already wired end
|
|
129
|
+
# to end, so if a future CustomTkinter release exposes the strip
|
|
130
|
+
# height only the application step changes. Making it work today would
|
|
131
|
+
# mean writing CTkTabview's private _top_spacing /
|
|
132
|
+
# _top_button_overhang and re-running _configure_grid(), a CTk
|
|
133
|
+
# internals dependency that could break on any upstream release.
|
|
134
|
+
#
|
|
135
|
+
# This is a CTkTabview layout constraint, NOT a segmented button
|
|
136
|
+
# limitation -- a standalone sCTkSegmentedButton honors height fine.
|
|
137
|
+
if hasattr(self, "_segmented_button") and self._segmented_button:
|
|
138
|
+
try:
|
|
139
|
+
self._segmented_button.configure(font=target_font, height=target_button_height)
|
|
140
|
+
except Exception:
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
# 6. Apply styles and complete lifecycle
|
|
144
|
+
self._apply_custom_theme_colors()
|
|
145
|
+
self._finalize_themeable_lifecycle()
|
|
146
|
+
|
|
147
|
+
def _apply_custom_theme_colors(self):
|
|
148
|
+
"""Cascades structural look profiles directly out of your centralized stylesheet json."""
|
|
149
|
+
if not hasattr(self, "_segmented_button") or not self._segmented_button:
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
is_disabled = (self._custom_current_state == "disabled")
|
|
153
|
+
m = self._custom_disabled_map if is_disabled else self._local_defaults
|
|
154
|
+
|
|
155
|
+
# No .get() fallbacks anywhere below: __init__ hard-fails on any
|
|
156
|
+
# missing key, so every lookup here is guaranteed to resolve. A
|
|
157
|
+
# fallback would only reintroduce the silent-substitution behavior
|
|
158
|
+
# that made a broken theme block invisible.
|
|
159
|
+
resolved_txt = self._resolve_color(m.get("text_color"))
|
|
160
|
+
|
|
161
|
+
if is_disabled:
|
|
162
|
+
# Hover deliberately collapses to the corresponding non-hover
|
|
163
|
+
# disabled color: a disabled tab bar must not light up under the
|
|
164
|
+
# cursor. This is why disabled_map has no hover keys of its own --
|
|
165
|
+
# see the validation note in __init__.
|
|
166
|
+
disabled_selected = self._resolve_color(m.get("segmented_button_selected_color"))
|
|
167
|
+
disabled_unselected = self._resolve_color(m.get("segmented_button_unselected_color"))
|
|
168
|
+
updates = {
|
|
169
|
+
"fg_color": self._resolve_color(m.get("segmented_button_fg_color")),
|
|
170
|
+
"selected_color": disabled_selected,
|
|
171
|
+
"selected_hover_color": disabled_selected,
|
|
172
|
+
"unselected_color": disabled_unselected,
|
|
173
|
+
"unselected_hover_color": disabled_unselected,
|
|
174
|
+
}
|
|
175
|
+
else:
|
|
176
|
+
updates = {
|
|
177
|
+
"fg_color": self._resolve_color(m.get("segmented_button_fg_color")),
|
|
178
|
+
"selected_color": self._resolve_color(m.get("segmented_button_selected_color")),
|
|
179
|
+
"selected_hover_color": self._resolve_color(m.get("segmented_button_selected_hover_color")),
|
|
180
|
+
"unselected_color": self._resolve_color(m.get("segmented_button_unselected_color")),
|
|
181
|
+
"unselected_hover_color": self._resolve_color(m.get("segmented_button_unselected_hover_color")),
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
self._segmented_button.configure(**updates)
|
|
186
|
+
except Exception:
|
|
187
|
+
pass
|
|
188
|
+
|
|
189
|
+
# Intercept child buttons array dictionary to forcefully assign text color mappings cleanly
|
|
190
|
+
#
|
|
191
|
+
# BOTH options are set, because state() puts the segmented button into
|
|
192
|
+
# CTk's own disabled mode -- which puts its inner CTkButtons there too,
|
|
193
|
+
# and a disabled CTkButton draws from text_color_disabled and ignores
|
|
194
|
+
# text_color entirely. Setting only text_color meant the theme's
|
|
195
|
+
# disabled tab colour was discarded at draw time in favour of
|
|
196
|
+
# CustomTkinter's default grey.
|
|
197
|
+
#
|
|
198
|
+
# resolved_txt already comes from the ACTIVE map, so it is the disabled
|
|
199
|
+
# colour when disabled and the normal one otherwise; assigning it to
|
|
200
|
+
# both makes the widget show the theme's value either way.
|
|
201
|
+
if hasattr(self._segmented_button, "_buttons_dict") and self._segmented_button._buttons_dict:
|
|
202
|
+
for button in self._segmented_button._buttons_dict.values():
|
|
203
|
+
try:
|
|
204
|
+
button.configure(text_color=resolved_txt,
|
|
205
|
+
text_color_disabled=resolved_txt)
|
|
206
|
+
except Exception:
|
|
207
|
+
pass
|
|
208
|
+
def configure(self, *args, **kwargs):
|
|
209
|
+
"""
|
|
210
|
+
Standard widget configuration, with Pygubu/positional-argument handling.
|
|
211
|
+
|
|
212
|
+
FIX: an earlier version of this class had no configure() override at
|
|
213
|
+
all. Two consequences, both confirmed against how every other widget
|
|
214
|
+
in this project behaves: configure(state="disabled") silently did
|
|
215
|
+
nothing (state was reachable only via the state() method), and Pygubu
|
|
216
|
+
Designer had no way to query or set any managed property.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
*args: At most one positional argument is meaningful:
|
|
220
|
+
- a dict: merged into kwargs and processed normally.
|
|
221
|
+
- "state": returns a Tkinter-style
|
|
222
|
+
(name, name, name, default, current) tuple.
|
|
223
|
+
- anything else: forwarded to the native widget's configure().
|
|
224
|
+
**kwargs: `state`, plus any native CTkTabview option.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
The query tuple described above for the single-argument case,
|
|
228
|
+
otherwise None.
|
|
229
|
+
"""
|
|
230
|
+
# args is always a tuple -- args[0] is the actual value passed, whether
|
|
231
|
+
# a string or a dict. Comparing the wrapped tuple directly (the
|
|
232
|
+
# `if args and isinstance(args, dict)` pattern found elsewhere in this
|
|
233
|
+
# project) is always False and makes the query branches dead code.
|
|
234
|
+
if len(args) == 1:
|
|
235
|
+
if isinstance(args[0], dict):
|
|
236
|
+
kwargs = {**args[0], **kwargs}
|
|
237
|
+
else:
|
|
238
|
+
pname = args[0]
|
|
239
|
+
if pname == "state":
|
|
240
|
+
return ("state", "state", "state", "normal",
|
|
241
|
+
str(getattr(self, "_custom_current_state", "normal")))
|
|
242
|
+
return self._configure_query(pname)
|
|
243
|
+
|
|
244
|
+
# state is this library's own property, not a native CTkTabview one,
|
|
245
|
+
# and must be removed before the super() call below.
|
|
246
|
+
# Runtime overrides have to reach the map a repaint reads, or the
|
|
247
|
+
# repaint puts the theme value straight back -- see
|
|
248
|
+
# ThemeableWidget._record_theme_overrides().
|
|
249
|
+
self._record_theme_overrides(kwargs)
|
|
250
|
+
|
|
251
|
+
if "state" in kwargs:
|
|
252
|
+
self.state(kwargs.pop("state"))
|
|
253
|
+
|
|
254
|
+
if kwargs:
|
|
255
|
+
super().configure(**kwargs)
|
|
256
|
+
self._apply_custom_theme_colors()
|
|
257
|
+
|
|
258
|
+
# Tkinter/CTk convention binds .config to .configure as a SEPARATE class
|
|
259
|
+
# attribute -- it does not automatically track whichever configure() a
|
|
260
|
+
# subclass defines. Without this line, calling .config(...) skips the
|
|
261
|
+
# override entirely and lands on the native widget's configure(),
|
|
262
|
+
# bypassing theming completely -- confirmed as a critical bug on
|
|
263
|
+
# sCTkSegmentedButton earlier in this project's audit.
|
|
264
|
+
config = configure
|
|
265
|
+
|
|
266
|
+
def cget(self, attribute_name):
|
|
267
|
+
"""
|
|
268
|
+
Standard property accessor, extended to know about `state`.
|
|
269
|
+
|
|
270
|
+
Native CTkTabview.cget() raises on any attribute it doesn't
|
|
271
|
+
recognize, so this library's own property is intercepted here before
|
|
272
|
+
delegating; everything else passes through unchanged.
|
|
273
|
+
"""
|
|
274
|
+
if attribute_name == "state":
|
|
275
|
+
return str(getattr(self, "_custom_current_state", "normal"))
|
|
276
|
+
return super().cget(attribute_name)
|
|
277
|
+
|
|
278
|
+
def _ensure_sctk_page(self, name: str) -> sCTkFrame:
|
|
279
|
+
"""
|
|
280
|
+
Returns the sCTkFrame page wrapper for a tab, creating it on first use.
|
|
281
|
+
|
|
282
|
+
Native CTkTabview.add() constructs a plain ctk.CTkFrame for each tab
|
|
283
|
+
and grids it inline, with no hook to substitute a different class.
|
|
284
|
+
Rather than reimplement add() against CustomTkinter's internals -- or
|
|
285
|
+
mutate the created frame's __class__ at runtime, the fragile pattern
|
|
286
|
+
deliberately retired from sCTkTableview elsewhere in this project --
|
|
287
|
+
this embeds an sCTkFrame INSIDE the native tab frame and hands that
|
|
288
|
+
back to the caller instead. The native frame stays exactly where
|
|
289
|
+
CTkTabview put it and keeps doing its own show/hide/grid work
|
|
290
|
+
untouched; it just becomes an invisible outer shell.
|
|
291
|
+
|
|
292
|
+
The wrapper is transparent with no border of its own, so this is
|
|
293
|
+
purely structural -- the tab looks identical, but everything placed
|
|
294
|
+
in it now has an sCTk widget as its parent.
|
|
295
|
+
|
|
296
|
+
Wrapping happens lazily here rather than only in add(), so a tab
|
|
297
|
+
created by any other path (insert(), or directly by CTkTabview's own
|
|
298
|
+
machinery) still comes back correctly wrapped the first time it's
|
|
299
|
+
asked for.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
name: The tab name, as passed to add().
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
The sCTkFrame to place content into.
|
|
306
|
+
"""
|
|
307
|
+
existing = self._sctk_pages.get(name)
|
|
308
|
+
if existing is not None:
|
|
309
|
+
try:
|
|
310
|
+
if existing.winfo_exists():
|
|
311
|
+
return existing
|
|
312
|
+
except Exception:
|
|
313
|
+
pass
|
|
314
|
+
# Stale entry -- the wrapper was destroyed out from under us.
|
|
315
|
+
self._sctk_pages.pop(name, None)
|
|
316
|
+
|
|
317
|
+
native_tab = super().tab(name)
|
|
318
|
+
page = sCTkFrame(native_tab, fg_color="transparent", border_width=0)
|
|
319
|
+
page.pack(fill="both", expand=True)
|
|
320
|
+
self._sctk_pages[name] = page
|
|
321
|
+
return page
|
|
322
|
+
|
|
323
|
+
def add(self, name: str) -> sCTkFrame:
|
|
324
|
+
"""
|
|
325
|
+
Creates a new tab and returns its sCTkFrame content page.
|
|
326
|
+
|
|
327
|
+
Note the return type differs from native CTkTabview.add(), which
|
|
328
|
+
hands back a ctk.CTkFrame. See _ensure_sctk_page() for why.
|
|
329
|
+
"""
|
|
330
|
+
super().add(name)
|
|
331
|
+
return self._ensure_sctk_page(name)
|
|
332
|
+
|
|
333
|
+
def tab(self, name: str) -> sCTkFrame:
|
|
334
|
+
"""
|
|
335
|
+
Returns a tab's sCTkFrame content page, creating the wrapper if this
|
|
336
|
+
tab hasn't been asked for before. Native CTkTabview.tab() returns the
|
|
337
|
+
underlying ctk.CTkFrame; use super().tab(name) if you specifically
|
|
338
|
+
need that outer shell.
|
|
339
|
+
"""
|
|
340
|
+
return self._ensure_sctk_page(name)
|
|
341
|
+
|
|
342
|
+
def rename(self, old_name: str, new_name: str):
|
|
343
|
+
"""
|
|
344
|
+
Renames a tab, keeping the page registry in step.
|
|
345
|
+
|
|
346
|
+
Native CTkTabview.rename() updates its own _name_list and _tab_dict,
|
|
347
|
+
but knows nothing about this class's _sctk_pages. Without re-keying,
|
|
348
|
+
the wrapper would still be filed under the OLD name -- so tab() and
|
|
349
|
+
delete() would miss it, and _ensure_sctk_page() would build a second
|
|
350
|
+
wrapper inside the same native tab frame.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
old_name: The tab's current name.
|
|
354
|
+
new_name: The name to give it.
|
|
355
|
+
"""
|
|
356
|
+
super().rename(old_name, new_name)
|
|
357
|
+
if old_name in self._sctk_pages:
|
|
358
|
+
self._sctk_pages[new_name] = self._sctk_pages.pop(old_name)
|
|
359
|
+
|
|
360
|
+
def delete(self, name: str):
|
|
361
|
+
"""
|
|
362
|
+
Deletes a tab, tearing down its sCTkFrame wrapper first so no stale
|
|
363
|
+
entry is left in the page registry.
|
|
364
|
+
"""
|
|
365
|
+
page = self._sctk_pages.pop(name, None)
|
|
366
|
+
if page is not None:
|
|
367
|
+
try:
|
|
368
|
+
page.destroy()
|
|
369
|
+
except Exception:
|
|
370
|
+
pass
|
|
371
|
+
return super().delete(name)
|
|
372
|
+
|
|
373
|
+
def state(self, mode: str = None) -> str:
|
|
374
|
+
"""🔑 THE CORE DESIGN PATTERN GATEWAY: Aligns fully with all other repository widgets!"""
|
|
375
|
+
if mode is None:
|
|
376
|
+
return str(getattr(self, "_custom_current_state", "normal")).lower()
|
|
377
|
+
|
|
378
|
+
target_mode = str(mode).lower()
|
|
379
|
+
if target_mode in ("normal", "enabled", "active"):
|
|
380
|
+
self._custom_current_state = "normal"
|
|
381
|
+
if hasattr(self, "_segmented_button") and self._segmented_button:
|
|
382
|
+
try: self._segmented_button.configure(state="normal")
|
|
383
|
+
except Exception: pass
|
|
384
|
+
elif target_mode == "disabled":
|
|
385
|
+
self._custom_current_state = "disabled"
|
|
386
|
+
if hasattr(self, "_segmented_button") and self._segmented_button:
|
|
387
|
+
try: self._segmented_button.configure(state="disabled")
|
|
388
|
+
except Exception: pass
|
|
389
|
+
|
|
390
|
+
self._apply_custom_theme_colors()
|
|
391
|
+
return self._custom_current_state
|
|
392
|
+
|
|
393
|
+
# 🔑 API CONTINUITY PASS-THROUGHS
|
|
394
|
+
def get_state(self) -> str:
|
|
395
|
+
return self.state()
|
|
396
|
+
|
|
397
|
+
def bind(self, sequence=None, func=None, add=None):
|
|
398
|
+
"""Intercepts designer click loops to prevent unexpected NotImplementedError crashes."""
|
|
399
|
+
try:
|
|
400
|
+
return tk.Frame.bind(self, sequence, func, add)
|
|
401
|
+
except Exception:
|
|
402
|
+
return ""
|
|
403
|
+
|
|
404
|
+
def _set_appearance_mode(self, mode_string: str):
|
|
405
|
+
if hasattr(super(), "_set_appearance_mode"):
|
|
406
|
+
try: super()._set_appearance_mode(mode_string)
|
|
407
|
+
except Exception: pass
|
|
408
|
+
self._apply_custom_theme_colors()
|
|
409
|
+
|