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,278 @@
1
+ #!/usr/bin/python3
2
+ """
3
+ sCTkLabelTertiary
4
+
5
+ A theme-compliant, low-emphasis inline description label widget -- the
6
+ lowest-emphasis of the library's three label tiers (see also sCTkLabelPrimary,
7
+ sCTkLabelSecondary). Inherits directly from ctk.CTkLabel so CustomTkinter
8
+ handles native rendering; this class layers automatic light/dark theme
9
+ resolution and a distinct enabled/disabled visual state on top.
10
+
11
+ Base class order matters here: `class sCTkLabelTertiary(ctk.CTkLabel,
12
+ ThemeableWidget)` puts the native CTk class first, so every `super()` call in
13
+ this file's own methods resolves to ctk.CTkLabel -- and, beneath it,
14
+ tkinter.Misc -- never to ThemeableWidget. ThemeableWidget's own
15
+ configure()/cget()/_set_appearance_mode() overrides have been removed entirely
16
+ for this reason (see themeable_widget.py's docstring); this widget owns all of
17
+ its own runtime color-swapping logic.
18
+
19
+ Unlike the button/checkbox family, "disabled" here is purely visual (dimmed
20
+ text color) -- CTkLabel has no native click handling to block in the first
21
+ place.
22
+
23
+ Internal state attribute is named self._custom_current_state, matching the
24
+ convention used across the rest of the library -- an earlier version of this
25
+ file used self._current_state instead, inconsistent both within the label
26
+ family (sCTkLabelSecondary already used _custom_current_state) and against
27
+ every other themed widget. Purely an internal rename; doesn't affect the
28
+ public state()/get_state() API.
29
+ """
30
+ from typing import Any, Optional
31
+ import customtkinter as ctk
32
+ from .themeable_widget import ThemeableWidget
33
+
34
+
35
+ class sCTkLabelTertiary(ctk.CTkLabel, ThemeableWidget):
36
+ """Themeable, low-emphasis inline description label.
37
+
38
+ Adds to native ctk.CTkLabel:
39
+ - Automatic light/dark theme resolution from sCTkThemes.json (via
40
+ ThemeableWidget.__init__ -- see that class's docstring for what it does,
41
+ and just as importantly, what it no longer does).
42
+ - A distinct enabled/disabled visual state -- purely a text-color dim,
43
+ since labels have no native interactivity to block.
44
+ - Pygubu Designer property introspection for `state`, `fg_color`, and
45
+ `text_color` via a single-argument configure() call, plus a cget()
46
+ override so querying "state" doesn't raise (native CTkLabel has no
47
+ real "state" option to cget in the first place).
48
+
49
+ Colors are passed through to configure() as raw (light, dark) tuples rather
50
+ than pre-resolved to a single value, so CustomTkinter's own appearance-mode
51
+ tracking repaints them automatically on a light/dark switch -- the same
52
+ approach validated on sCTkComboBox, sCTkSegmentedButton, and the button
53
+ family. Not separately re-confirmed for this specific widget.
54
+ """
55
+
56
+ def __init__(self, master: Optional[Any] = None, **kwargs: Any) -> None:
57
+ """
58
+ Args:
59
+ master: Parent container.
60
+ **kwargs: `state` is pulled out explicitly below (applied after
61
+ construction, via self.configure(), rather than passed to the
62
+ native constructor). Everything else is either a native
63
+ CTkLabel argument (e.g. `text`, `font`) or a theme-key override
64
+ (see the "sCTkLabelTertiary" block in sCTkThemes.json).
65
+ """
66
+ # 1. Capture the initial state before ThemeableWidget's own kwargs pass,
67
+ # so it doesn't get merged into final_kw and sent to the native
68
+ # constructor.
69
+ # NO PLACEHOLDER TEXT. CustomTkinter defaults `text` to the class
70
+ # name, so a CTkLabel built without text reads "CTkLabel". Nothing wants
71
+ # that in a real interface, and the Designer made it worse: clearing
72
+ # the text field removes the property, the canvas rebuilds the widget
73
+ # from what is left, and the placeholder appeared in place of the
74
+ # blank that was asked for.
75
+ kwargs.setdefault("text", "")
76
+ state_init = kwargs.pop("state", "normal")
77
+
78
+ # 2. Fire our shared theme logic. This resolves final_kw
79
+ # (construction-time properties) and the disabled color map. See
80
+ # ThemeableWidget.__init__ for what actually happens here.
81
+ ThemeableWidget.__init__(self, kwargs)
82
+
83
+ # 3. Deep-copy the resolved map onto this instance, so later changes
84
+ # here never leak back into the shared theme registry.
85
+ self._local_defaults = dict(self.final_kw)
86
+ self._custom_disabled_map = dict(self._widget_disabled_map)
87
+
88
+ # Remove any stray "state" key from final_kw -- already extracted
89
+ # above, but guards against it being reintroduced by a theme override.
90
+ self.final_kw.pop("state", None)
91
+
92
+ # 4. Initialize CustomTkinter natively with the clean final kwargs array.
93
+ super().__init__(master, **self.final_kw)
94
+
95
+ # 5. Apply the requested initial state now that the native widget exists.
96
+ self._custom_current_state = "normal"
97
+ self.configure(state=state_init)
98
+
99
+ # 6. Register lifecycle handshake hook, notifying Pygubu-style consumers
100
+ # that construction is complete.
101
+ self._finalize_themeable_lifecycle()
102
+
103
+ def _set_appearance_mode(self, mode_string: str) -> None:
104
+ """
105
+ Forwards CustomTkinter's internal light/dark mode change notification to
106
+ the native widget.
107
+
108
+ No longer manually re-triggers _update_current_visual_state(). That
109
+ method now passes raw (light, dark) tuples straight through to
110
+ configure() instead of pre-resolving to a single color, so CTk's own
111
+ appearance-mode tracking should repaint correctly on its own -- the
112
+ same approach validated on sCTkComboBox, sCTkSegmentedButton, and the
113
+ button family.
114
+
115
+ Args:
116
+ mode_string: The new appearance mode ("Light" or "Dark"), as passed
117
+ by CustomTkinter's internal appearance-mode change machinery.
118
+ """
119
+ if hasattr(super(), "_set_appearance_mode"):
120
+ try:
121
+ super()._set_appearance_mode(mode_string)
122
+ except Exception:
123
+ pass
124
+
125
+ def configure(self, *args: Any, **kwargs: Any) -> Any:
126
+ """
127
+ Standard widget configuration, with Pygubu/positional-argument handling.
128
+
129
+ Args:
130
+ *args: At most one positional argument is meaningful:
131
+ - a dict: merged into kwargs and processed normally below.
132
+ - the literal string "state": returns a Tkinter-style
133
+ (name, name, name, default, current) tuple.
134
+ - "fg_color" or "text_color": returns the same style of tuple,
135
+ with `current` reflecting the disabled or normal value as
136
+ appropriate. Note the returned value is str(value), where
137
+ value may itself be a (light, dark) tuple rather than a
138
+ single resolved color -- a known limitation shared with the
139
+ wider Pygubu-query investigation set aside elsewhere in this
140
+ project, not fixed here.
141
+ - anything else: forwarded directly to the native widget's
142
+ configure(), which does not support single-argument property
143
+ queries for arbitrary properties (same limitation).
144
+ **kwargs: Standard CTkLabel configuration options, plus: passing
145
+ `state=...` routes through self.state() rather than being
146
+ forwarded as-is.
147
+
148
+ Returns:
149
+ The query tuple described above for the single-argument case, or
150
+ whatever super().configure() returns for the keyword-argument case
151
+ (typically None).
152
+ """
153
+ # args is always a tuple -- args[0] is the actual value passed, whether
154
+ # that's a string or a dict. An earlier version of this method compared
155
+ # the wrapped tuple directly (`pname = args`), so the query branches
156
+ # below never matched anything. Don't reintroduce that.
157
+ if len(args) == 1:
158
+ if isinstance(args[0], dict):
159
+ kwargs = {**args[0], **kwargs}
160
+ else:
161
+ pname = args[0]
162
+ if pname == "state":
163
+ return ("state", "state", "state", "normal", str(self.state()))
164
+
165
+ if pname in ["fg_color", "text_color"]:
166
+ current_state = str(self.state()).lower()
167
+ val = self._custom_disabled_map.get(pname) if current_state == "disabled" else self._local_defaults.get(pname)
168
+ return (pname, pname, pname, self._query_value(self._theme_default(pname)), self._query_value(val))
169
+
170
+ return self._configure_query(pname)
171
+
172
+ # Runtime overrides have to reach the map a repaint reads, or
173
+ # _update_current_visual_state() puts the theme value straight back --
174
+ # see ThemeableWidget._record_theme_overrides().
175
+ self._record_theme_overrides(kwargs)
176
+
177
+ if "state" in kwargs:
178
+ self.state(kwargs.pop("state"))
179
+
180
+ # Theme values cleared to "" are dropped; `text=""` is kept, so a
181
+ # label can actually be blanked. See ThemeableWidget._CONTENT_KEYS.
182
+ self._drop_cleared(kwargs)
183
+
184
+ if kwargs:
185
+ return super().configure(**kwargs)
186
+ return None
187
+
188
+ # Tkinter/CTk convention binds .config to .configure as a SEPARATE class
189
+ # attribute -- it does not automatically track whichever configure() a
190
+ # subclass defines. Without this line, calling .config(...) on an instance
191
+ # would silently skip this entire override and land on the native widget's
192
+ # configure() directly, bypassing theming and state handling entirely.
193
+ config = configure
194
+
195
+ def cget(self, attribute_name: str) -> Any:
196
+ """
197
+ Intercepts "state" queries so they route through state() instead of
198
+ being forwarded to the native widget, which has no real "state" option
199
+ to cget in the first place and would otherwise raise.
200
+
201
+ Args:
202
+ attribute_name: The property name being queried.
203
+
204
+ Returns:
205
+ self.state() if attribute_name is "state" (case-insensitive),
206
+ otherwise whatever the native CTkLabel.cget() returns.
207
+ """
208
+ if str(attribute_name).lower() == "state":
209
+ return self.state()
210
+ return super().cget(attribute_name)
211
+
212
+ def get_state(self) -> str:
213
+ """Equivalent to calling state() with no argument."""
214
+ return self.state()
215
+
216
+ def state(self, mode: Optional[str] = None) -> str:
217
+ """
218
+ Gets or sets the widget's enabled/disabled visual state.
219
+
220
+ Args:
221
+ mode: If None, returns the current state without changing anything.
222
+ Otherwise, only the literal string "disabled" (case-insensitive)
223
+ is treated as disabled; anything in ("normal", "enabled",
224
+ "active") is treated as enabled. Any other value matches
225
+ neither branch and leaves the state unchanged.
226
+
227
+ Returns:
228
+ The resulting state ("normal" or "disabled", lowercase).
229
+ """
230
+ if mode is None:
231
+ return str(getattr(self, "_custom_current_state", "normal")).lower()
232
+
233
+ mode = mode.lower()
234
+ if mode in ("normal", "enabled", "active"):
235
+ self._custom_current_state = "normal"
236
+ self._update_current_visual_state()
237
+ elif mode == "disabled":
238
+ self._custom_current_state = "disabled"
239
+ self._update_current_visual_state()
240
+
241
+ return self._custom_current_state
242
+
243
+ def _update_current_visual_state(self) -> None:
244
+ """
245
+ Recomputes and applies this widget's colors from the theme file, based
246
+ on the current state.
247
+
248
+ Called after construction and on every state() change.
249
+
250
+ Passes raw (light, dark) tuples straight through to configure() instead
251
+ of resolving to a single color first, so CTk's native tracking can
252
+ handle appearance-mode repaints without help from _set_appearance_mode.
253
+
254
+ text_color is required to be present in whichever map is active (the
255
+ top-level block for normal state, disabled_map for disabled) -- if
256
+ it's missing, this raises immediately rather than silently falling
257
+ back to a default or silently omitting the property. An earlier
258
+ version fell back to CTk's own global ThemeManager default; per this
259
+ project's design, ThemeableWidget-based widgets are meant to fail hard
260
+ on incomplete theme data, not paper over it.
261
+ """
262
+ is_disabled = self._custom_current_state == "disabled"
263
+ target_map = self._custom_disabled_map if is_disabled else self._local_defaults
264
+
265
+ if target_map.get("text_color") is None:
266
+ raise KeyError(
267
+ f"'{getattr(self, '_THEME_BLOCK_NAME', None) or self.__class__.__name__}' "
268
+ f"theme block is missing 'text_color' in its "
269
+ f"{'disabled_map' if is_disabled else 'top-level'} section of sCTkThemes.json."
270
+ )
271
+
272
+ config_payload = {}
273
+ for key in ("fg_color", "text_color", "font"):
274
+ val = target_map.get(key)
275
+ if val is not None:
276
+ config_payload[key] = val
277
+
278
+ super().configure(**config_payload)
@@ -0,0 +1,275 @@
1
+ #!/usr/bin/python3
2
+ """
3
+ sCTkMessage
4
+
5
+ An advanced, themeable dialog window system subclassed from ctk.CTkToplevel.
6
+ Supports customizable single prompt text or dual choice prompts returning boolean states.
7
+ Derived from Message class by Fastattack, 2024.
8
+ https://github.com/fastattackv/MoreCustomTkinterWidget
9
+ """
10
+ import os
11
+ import textwrap
12
+
13
+ import customtkinter as ctk
14
+ from .themeable_widget import ThemeableWidget
15
+
16
+ from PIL import Image
17
+ from typing import Literal, Union, Tuple, Optional
18
+
19
+ from .sctk_button_primary import sCTkButtonPrimary
20
+ from .sctk_button_secondary import sCTkButtonSecondary
21
+ from .sctk_label_primary import sCTkLabelPrimary
22
+
23
+ class sCTkMessagebox(ctk.CTkToplevel, ThemeableWidget):
24
+ """Advanced themeable message dialog window supporting single or dual prompt states."""
25
+
26
+ # Required at the TOP LEVEL of the theme block.
27
+ # type_font styles the "[ERROR]" / "[INFO]" marker, which is deliberately
28
+ # distinct from the message body's `font`. It was hardcoded as
29
+ # ("Arial", 12, "bold") at both sites below, so the theme file had no way
30
+ # to reach it while every other value on those lines was themed.
31
+ _REQUIRED_THEME_KEYS = ("font", "text_color", "fg_color", "type_font")
32
+
33
+ # WHITELIST GUARD. Native CTkToplevel names fg_color explicitly and passes
34
+ # everything else through to tkinter.Toplevel, which raises TclError on any
35
+ # option it doesn't know. Theme keys like `font` and `text_color` are for
36
+ # this widget's own label, NOT for the window, so final_kw is filtered down
37
+ # to this set before the native constructor sees it.
38
+ _NATIVE_CTKTOPLEVEL_KWARGS = frozenset({"fg_color"})
39
+
40
+ # There is no disabled_map and no state(): this is a modal dialog. It grabs
41
+ # input on construction and destroys itself on dismissal, so there is no
42
+ # window of time in which a disabled appearance would mean anything.
43
+
44
+ def __init__(self,
45
+ title: str,
46
+ message: str,
47
+ typ: Literal["info", "warning", "error"],
48
+ master: any = None,
49
+ buttons: Literal["ok", "yes_no"] = "ok",
50
+ ok_text: str = "Ok",
51
+ yes_text: str = "Yes",
52
+ no_text: str = "No",
53
+ width: int = 400,
54
+ *args, **kwargs):
55
+
56
+ # 1. Fire our shared theme logic first to extract registry configurations safely
57
+ ThemeableWidget.__init__(self, kwargs)
58
+
59
+ # 2. 🛠️ THE MUTATION SAFEGUARD DEEP COPY:
60
+ self._local_defaults = dict(self.final_kw)
61
+ self._validate_theme_keys()
62
+
63
+ # 3. Initialize CTkToplevel natively.
64
+ #
65
+ # FIX: this previously forwarded the RAW kwargs dict rather than the
66
+ # resolved final_kw, so the theme block never reached the native
67
+ # constructor at all -- the dialog window itself was unthemed, and
68
+ # ThemeableWidget's resolution work was discarded for everything
69
+ # except the two keys read back manually below. Passing final_kw
70
+ # through the whitelist above fixes that and simultaneously protects
71
+ # the native constructor: a caller passing font= to this widget would
72
+ # previously have had it forwarded to tkinter.Toplevel, which raises
73
+ # TclError on an option it doesn't recognize.
74
+ native_kwargs = {k: v for k, v in self.final_kw.items()
75
+ if k in self._NATIVE_CTKTOPLEVEL_KWARGS}
76
+ super().__init__(master=master, *args, **native_kwargs)
77
+ self.withdraw()
78
+
79
+ self._result = None
80
+ self.lift()
81
+ self.attributes("-topmost", True)
82
+ self.resizable(False, False)
83
+ self.grab_set()
84
+ self.title(title)
85
+
86
+ # No fallbacks: validated at construction, so both lookups resolve.
87
+ font_config = self._local_defaults.get("font")
88
+ text_color_config = self._local_defaults.get("text_color")
89
+
90
+ # 4. Custom Local Icon Asset Extraction Loops
91
+ # 4. 🔑 DYNAMIC ASSET PATH ANCHOR: Safely navigates down into assets/images/
92
+ current_script_dir = os.path.dirname(os.path.abspath(__file__))
93
+ images_dir = os.path.normpath(os.path.join(current_script_dir, "assets", "images"))
94
+
95
+ light_icon_path = os.path.join(images_dir, f"{typ}.png")
96
+ dark_icon_path = os.path.join(images_dir, f"{typ}_dark.png")
97
+
98
+ # Fallback to standard light version if an explicit dark-theme variant is missing
99
+ if not os.path.exists(dark_icon_path):
100
+ dark_icon_path = light_icon_path
101
+
102
+ if os.path.exists(light_icon_path):
103
+ try:
104
+ pil_light = Image.open(light_icon_path)
105
+ pil_dark = Image.open(dark_icon_path)
106
+ ctk_image = ctk.CTkImage(light_image=pil_light, dark_image=pil_dark, size=(85, 85))
107
+ self.image_label = sCTkLabelPrimary(self, text="", image=ctk_image, width=85, height=85)
108
+ self.image_label.grid(row=0, column=0, padx=(15, 5), pady=20, sticky="n")
109
+ except Exception as image_err:
110
+ # Graceful extraction safety recovery trace
111
+ print(f"⚠️ sCustomTkinter Warning -> Image format initialization error: {image_err}")
112
+ self.image_label = sCTkLabelPrimary(
113
+ self, text=f"[{typ.upper()}]",
114
+ font=self._local_defaults.get("type_font"))
115
+ self.image_label.grid(row=0, column=0, padx=(15, 5), pady=20, sticky="n")
116
+ else:
117
+ # Safe text-based interface fallback if filesystem layout tracks vanish
118
+ print(f"⚠️ sCustomTkinter Warning -> Missing visual alert asset from target path: {light_icon_path}")
119
+ self.image_label = sCTkLabelPrimary(
120
+ self, text=f"[{typ.upper()}]",
121
+ font=self._local_defaults.get("type_font"))
122
+ self.image_label.grid(row=0, column=0, padx=(15, 5), pady=20, sticky="n")
123
+
124
+ max_text_width_pixels = width - 180
125
+ char_limit_per_line = max(20, int(max_text_width_pixels / 11.5))
126
+ wrapped_message = "\n".join(textwrap.wrap(message, width=char_limit_per_line))
127
+
128
+ self.label = sCTkLabelPrimary(self, text=wrapped_message, font=font_config, text_color=text_color_config, justify="left", anchor="w", wraplength=max_text_width_pixels)
129
+ self.label.grid(row=0, column=1, padx=(10, 35), pady=20, sticky="w")
130
+
131
+ if buttons == "yes_no":
132
+ self.yes_button = sCTkButtonPrimary(self, text=yes_text, command=self.on_yes)
133
+ self.yes_button.grid(row=1, column=0, padx=(15, 5), pady=15, sticky="ew")
134
+ self.no_button = sCTkButtonSecondary(self, text=no_text, command=self.on_no)
135
+ self.no_button.grid(row=1, column=1, padx=(5, 15), pady=15, sticky="ew")
136
+ self.bind("<Return>", self.on_yes)
137
+ self.grid_columnconfigure(0, weight=1, uniform="dialog_buttons")
138
+ self.grid_columnconfigure(1, weight=1, uniform="dialog_buttons")
139
+ else:
140
+ self.ok_button = sCTkButtonPrimary(self, text=ok_text, command=self.on_ok)
141
+ self.ok_button.grid(row=1, column=0, columnspan=2, padx=15, pady=15)
142
+ self.bind("<Return>", self.on_ok)
143
+ self.grid_columnconfigure(0, weight=1)
144
+ self.grid_columnconfigure(1, weight=1)
145
+
146
+ self._center_window(target_width=width)
147
+ self.deiconify()
148
+
149
+ # 🔑 REGISTER LIFECYCLE HANDSHAKE HOOK: Unlocks full Pygubu parent support out of the box
150
+ self._finalize_themeable_lifecycle()
151
+ def _center_window(self, target_width: int):
152
+ self.update_idletasks()
153
+ width = target_width
154
+ height = self.winfo_reqheight()
155
+ if self.master and hasattr(self.master, "winfo_x"):
156
+ x = self.master.winfo_x() + (self.master.winfo_width() // 2) - (width // 2)
157
+ y = self.master.winfo_y() + (self.master.winfo_height() // 2) - (height // 2)
158
+ else:
159
+ x = (self.winfo_screenwidth() // 2) - (width // 2)
160
+ y = (self.winfo_screenheight() // 2) - (height // 2)
161
+ self.geometry(f"{width}x{height}+{max(0, x)}+{max(0, y)}")
162
+
163
+ def _validate_theme_keys(self) -> None:
164
+ """
165
+ Hard-fails at construction on an incomplete theme block, naming the
166
+ missing key.
167
+
168
+ Raises:
169
+ KeyError: naming the first missing key found.
170
+ """
171
+ # Resolved the way ThemeableWidget resolves it, so the message names
172
+ # the block actually read rather than the subclass's own name.
173
+ name = getattr(self, "_THEME_BLOCK_NAME", None) or self.__class__.__name__
174
+ for key in self._REQUIRED_THEME_KEYS:
175
+ if self._local_defaults.get(key) is None:
176
+ raise KeyError(
177
+ f"'{name}' theme block is missing '{key}' at the top level "
178
+ f"of sCTkThemes.json."
179
+ )
180
+
181
+ def configure(self, *args, **kwargs):
182
+ """
183
+ Standard configuration with positional-argument handling.
184
+
185
+ Three separate bugs were fixed here:
186
+
187
+ 1. `super().configure(args)` passed the whole TUPLE as one positional
188
+ argument rather than unwrapping it, so every single-argument query
189
+ forwarded a malformed value.
190
+ 2. `if args and isinstance(args, dict)` -- args is ALWAYS a tuple, so
191
+ the dict-merge branch was dead code. Same tautology fixed across
192
+ the batch-one widgets.
193
+ 3. No `config = configure` alias existed, so `.config(...)` bypassed
194
+ this override entirely and landed on the native widget.
195
+ """
196
+ if len(args) == 1:
197
+ if isinstance(args[0], dict):
198
+ kwargs = {**args[0], **kwargs}
199
+ else:
200
+ # FIX: forwarding a property NAME to native configure()
201
+ # passes it as require_redraw -- CustomTkinter declares
202
+ # configure(self, require_redraw=False, **kwargs) -- so the
203
+ # call returns None, which pygubu then hands straight back to
204
+ # _set_property(). Reached whenever a field is blanked in the
205
+ # Designer inspector, which calls configure(name) to read a
206
+ # property's default and expects a Tkinter-style five-tuple.
207
+ #
208
+ # _configure_query() builds one -- see themeable_widget.py.
209
+ return self._configure_query(args[0])
210
+
211
+ self._drop_cleared(kwargs)
212
+ if kwargs: return super().configure(**kwargs)
213
+ return None
214
+
215
+ # Tkinter binds .config to .configure as a SEPARATE class attribute -- it
216
+ # does not track a subclass's override. Without this, .config(...) skips
217
+ # everything above.
218
+ config = configure
219
+
220
+ def _close_dialog(self):
221
+ self.grab_release()
222
+ self.destroy()
223
+
224
+ def on_ok(self, event=None):
225
+ self._result = True
226
+ self._close_dialog()
227
+
228
+ def on_yes(self, event=None):
229
+ self._result = True
230
+ self._close_dialog()
231
+
232
+ def on_no(self, event=None):
233
+ self._result = False
234
+ self._close_dialog()
235
+
236
+ def wait_end(self) -> Optional[bool]:
237
+ if self.master: self.master.wait_window(self)
238
+ else: self.wait_window(self)
239
+ return self._result
240
+
241
+ # =====================================================================
242
+ # ⚡ STANDALONE RE-ROUTING CHANNELS (OUTDENTED TO ZERO INDENT SPACES!)
243
+ # =====================================================================
244
+ def showinfo(title: str, message: str, ok_text: str = "Ok", width: int = 400, master: any = None) -> Optional[bool]:
245
+ return sCTkMessagebox(title, message, "info", master=master, buttons="ok", ok_text=ok_text, width=width).wait_end()
246
+
247
+ def showwarning(title: str, message: str, ok_text: str = "Ok", width: int = 400, master: any = None) -> Optional[bool]:
248
+ return sCTkMessagebox(title, message, "warning", master=master, buttons="ok", ok_text=ok_text, width=width).wait_end()
249
+
250
+ def showerror(title: str, message: str, ok_text: str = "Ok", width: int = 400, master: any = None) -> Optional[bool]:
251
+ return sCTkMessagebox(title, message, "error", master=master, buttons="ok", ok_text=ok_text, width=width).wait_end()
252
+
253
+ def askyesno(title: str, message: str, yes_text: str = "Yes", no_text: str = "No", width: int = 400, master: any = None) -> bool:
254
+ val = sCTkMessagebox(title, message, "info", master=master, buttons="yes_no", yes_text=yes_text, no_text=no_text, width=width).wait_end()
255
+ return True if val is True else False
256
+
257
+ def askwarningyesno(title: str, message: str, yes_text: str = "Yes", no_text: str = "No", width: int = 400, master: any = None) -> bool:
258
+ val = sCTkMessagebox(title, message, "warning", master=master, buttons="yes_no", yes_text=yes_text, no_text=no_text, width=width).wait_end()
259
+ return True if val is True else False
260
+
261
+ def askerroryesno(title: str, message: str, yes_text: str = "Yes", no_text: str = "No", width: int = 400, master: any = None) -> bool:
262
+ val = sCTkMessagebox(title, message, "error", master=master, buttons="yes_no", yes_text=yes_text, no_text=no_text, width=width).wait_end()
263
+ return True if val is True else False
264
+
265
+
266
+ # =====================================================================
267
+ # 🔑 THE DYNAMIC BACK-BIND: Injects attributes onto the Class token namespace!
268
+ # =====================================================================
269
+ # This forces sCTkMessage.showinfo(...) to work perfectly everywhere!
270
+ sCTkMessagebox.showinfo = showinfo
271
+ sCTkMessagebox.showwarning = showwarning
272
+ sCTkMessagebox.showerror = showerror
273
+ sCTkMessagebox.askyesno = askyesno
274
+ sCTkMessagebox.askwarningyesno = askwarningyesno
275
+ sCTkMessagebox.askerroryesno = askerroryesno