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.
Files changed (101) hide show
  1. scustomtkinter/__init__.py +81 -0
  2. scustomtkinter/assets/images/error.png +0 -0
  3. scustomtkinter/assets/images/info.png +0 -0
  4. scustomtkinter/assets/images/warning.png +0 -0
  5. scustomtkinter/assets/sCTkThemes.json +1963 -0
  6. scustomtkinter/license_morecustomtkinterwidgets.txt +22 -0
  7. scustomtkinter/sctk_button_primary.py +366 -0
  8. scustomtkinter/sctk_button_secondary.py +354 -0
  9. scustomtkinter/sctk_button_tertiary.py +375 -0
  10. scustomtkinter/sctk_checkbox.py +270 -0
  11. scustomtkinter/sctk_combobox.py +155 -0
  12. scustomtkinter/sctk_core.py +15 -0
  13. scustomtkinter/sctk_dial.py +1472 -0
  14. scustomtkinter/sctk_dialog.py +701 -0
  15. scustomtkinter/sctk_dialog_toplevel.py +268 -0
  16. scustomtkinter/sctk_entry_primary.py +400 -0
  17. scustomtkinter/sctk_entry_secondary.py +342 -0
  18. scustomtkinter/sctk_file_explorer.py +879 -0
  19. scustomtkinter/sctk_frame.py +210 -0
  20. scustomtkinter/sctk_frame_labeled_primary.py +667 -0
  21. scustomtkinter/sctk_frame_labeled_secondary.py +598 -0
  22. scustomtkinter/sctk_frequency_display.py +636 -0
  23. scustomtkinter/sctk_label_primary.py +283 -0
  24. scustomtkinter/sctk_label_secondary.py +275 -0
  25. scustomtkinter/sctk_label_tertiary.py +278 -0
  26. scustomtkinter/sctk_messagebox.py +275 -0
  27. scustomtkinter/sctk_notebook.py +1103 -0
  28. scustomtkinter/sctk_optionmenu_border_mixin.py +203 -0
  29. scustomtkinter/sctk_optionmenu_primary.py +378 -0
  30. scustomtkinter/sctk_optionmenu_secondary.py +346 -0
  31. scustomtkinter/sctk_path_chooser.py +516 -0
  32. scustomtkinter/sctk_progress_bar.py +274 -0
  33. scustomtkinter/sctk_radiobutton.py +360 -0
  34. scustomtkinter/sctk_scroll_mixin.py +546 -0
  35. scustomtkinter/sctk_scrollable_frame.py +795 -0
  36. scustomtkinter/sctk_scrollbar.py +313 -0
  37. scustomtkinter/sctk_segmentedbutton.py +424 -0
  38. scustomtkinter/sctk_selector.py +581 -0
  39. scustomtkinter/sctk_separator.py +308 -0
  40. scustomtkinter/sctk_slider.py +264 -0
  41. scustomtkinter/sctk_smeter.py +405 -0
  42. scustomtkinter/sctk_smeter_bar.py +557 -0
  43. scustomtkinter/sctk_spinbox.py +664 -0
  44. scustomtkinter/sctk_switch.py +463 -0
  45. scustomtkinter/sctk_tableview.py +869 -0
  46. scustomtkinter/sctk_tabview.py +409 -0
  47. scustomtkinter/sctk_textbox_primary.py +291 -0
  48. scustomtkinter/sctk_textbox_secondary.py +225 -0
  49. scustomtkinter/sctk_toplevel.py +153 -0
  50. scustomtkinter/themeable_widget.py +938 -0
  51. scustomtkinter-0.2.1.dist-info/METADATA +336 -0
  52. scustomtkinter-0.2.1.dist-info/RECORD +101 -0
  53. scustomtkinter-0.2.1.dist-info/WHEEL +5 -0
  54. scustomtkinter-0.2.1.dist-info/entry_points.txt +2 -0
  55. scustomtkinter-0.2.1.dist-info/licenses/LICENSE +21 -0
  56. scustomtkinter-0.2.1.dist-info/licenses/scustomtkinter/license_morecustomtkinterwidgets.txt +22 -0
  57. scustomtkinter-0.2.1.dist-info/top_level.txt +2 -0
  58. scustomtkinter_pygubu/__init__.py +1 -0
  59. scustomtkinter_pygubu/designer/__init__.py +0 -0
  60. scustomtkinter_pygubu/designer/plugin.py +1854 -0
  61. scustomtkinter_pygubu/designer/properties.py +247 -0
  62. scustomtkinter_pygubu/sCTkButtonPrimarybo.py +46 -0
  63. scustomtkinter_pygubu/sCTkButtonSecondarybo.py +45 -0
  64. scustomtkinter_pygubu/sCTkButtonTertiarybo.py +44 -0
  65. scustomtkinter_pygubu/sCTkCheckBoxbo.py +45 -0
  66. scustomtkinter_pygubu/sCTkComboBoxbo.py +45 -0
  67. scustomtkinter_pygubu/sCTkCorebo.py +120 -0
  68. scustomtkinter_pygubu/sCTkDialbo.py +371 -0
  69. scustomtkinter_pygubu/sCTkDialogbo.py +397 -0
  70. scustomtkinter_pygubu/sCTkEntryPrimarybo.py +100 -0
  71. scustomtkinter_pygubu/sCTkEntrySecondarybo.py +98 -0
  72. scustomtkinter_pygubu/sCTkFileExplorerbo.py +189 -0
  73. scustomtkinter_pygubu/sCTkFrameLabeledPrimarybo.py +69 -0
  74. scustomtkinter_pygubu/sCTkFrameLabeledSecondarybo.py +71 -0
  75. scustomtkinter_pygubu/sCTkFramebo.py +46 -0
  76. scustomtkinter_pygubu/sCTkFrequencyDisplaybo.py +175 -0
  77. scustomtkinter_pygubu/sCTkLabelPrimarybo.py +45 -0
  78. scustomtkinter_pygubu/sCTkLabelSecondarybo.py +44 -0
  79. scustomtkinter_pygubu/sCTkLabelTertiarybo.py +45 -0
  80. scustomtkinter_pygubu/sCTkNotebookbo.py +392 -0
  81. scustomtkinter_pygubu/sCTkOptionMenuPrimarybo.py +45 -0
  82. scustomtkinter_pygubu/sCTkOptionMenuSecondarybo.py +39 -0
  83. scustomtkinter_pygubu/sCTkPathChooserbo.py +204 -0
  84. scustomtkinter_pygubu/sCTkProgressBarbo.py +45 -0
  85. scustomtkinter_pygubu/sCTkRadioButtonbo.py +45 -0
  86. scustomtkinter_pygubu/sCTkSMeterBarbo.py +153 -0
  87. scustomtkinter_pygubu/sCTkSMeterbo.py +101 -0
  88. scustomtkinter_pygubu/sCTkScrollableFramebo.py +66 -0
  89. scustomtkinter_pygubu/sCTkScrollbarbo.py +48 -0
  90. scustomtkinter_pygubu/sCTkSegmentedButtonbo.py +47 -0
  91. scustomtkinter_pygubu/sCTkSelectorbo.py +167 -0
  92. scustomtkinter_pygubu/sCTkSeparatorbo.py +141 -0
  93. scustomtkinter_pygubu/sCTkSliderbo.py +45 -0
  94. scustomtkinter_pygubu/sCTkSpinboxbo.py +200 -0
  95. scustomtkinter_pygubu/sCTkSwitchbo.py +45 -0
  96. scustomtkinter_pygubu/sCTkTableviewbo.py +188 -0
  97. scustomtkinter_pygubu/sCTkTabviewbo.py +226 -0
  98. scustomtkinter_pygubu/sCTkTextboxPrimarybo.py +45 -0
  99. scustomtkinter_pygubu/sCTkTextboxSecondarybo.py +45 -0
  100. scustomtkinter_pygubu/sCTkToplevelbo.py +55 -0
  101. 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
+