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,308 @@
1
+ #!/usr/bin/python3
2
+ """
3
+ sCTkSeparator
4
+
5
+ An advanced Separator widget supporting custom section header text,
6
+ dashed line patterns, corner roundness, and responsive orientation modes.
7
+ Inherits cleanly and directly from ctk.CTkBaseClass to preserve native canvas draw engines.
8
+
9
+ Derived from Selector class by Fastattack, 2024.
10
+ https://github.com
11
+ """
12
+ import customtkinter as ctk
13
+ from .themeable_widget import ThemeableWidget
14
+
15
+
16
+ class sCTkSeparator(ctk.CTkBaseClass, ThemeableWidget):
17
+ """Advanced Separator supporting headers, dashed lines, and themes.json matrices."""
18
+
19
+ # Required at the TOP LEVEL of the theme block. Structural parameters
20
+ # (orientation, length, width, text, dash) are deliberately absent: they
21
+ # are constructor arguments with sensible defaults, and although the theme
22
+ # CAN supply them, requiring them would force layout decisions into the
23
+ # stylesheet.
24
+ _REQUIRED_THEME_KEYS = ("fg_color", "text_color", "font", "corner_radius")
25
+
26
+ # Required inside disabled_map.
27
+ _REQUIRED_DISABLED_KEYS = ("fg_color", "text_color")
28
+
29
+ def __init__(self, master=None, **kwargs):
30
+ # 1. Fire our shared theme logic first. It automatically finds "sCTkSeparator" in themes.json
31
+ ThemeableWidget.__init__(self, kwargs)
32
+
33
+ # 2. 🛠️ THE MUTATION SAFEGUARD DEEP COPY SHIELD:
34
+ self._local_defaults = dict(self.final_kw)
35
+ self._custom_disabled_map = dict(self._widget_disabled_map)
36
+ self._validate_theme_keys()
37
+
38
+ # Extract structural parameters safely out of the resolved theme dictionary
39
+ self._orientation = str(self.final_kw.pop("orientation", "vertical")).lower()
40
+ length = int(self.final_kw.pop("length", 100))
41
+ width = float(self.final_kw.pop("width", 4))
42
+
43
+ self._text = str(self.final_kw.pop("text", ""))
44
+ self._dash = self.final_kw.pop("dash", None)
45
+ # No fallback: validated above.
46
+ self._font = self.final_kw.pop("font")
47
+
48
+ # 🔑 VERTICAL HOUSING REMAP: Programmatically upscale horizontal bounds to fit text
49
+ if self._text and width <= 4:
50
+ width = 28
51
+
52
+ if self._orientation == "vertical":
53
+ height = length
54
+ elif self._orientation == "horizontal":
55
+ height = width
56
+ width = length
57
+ else:
58
+ raise ValueError(
59
+ f"The value for orientation is incorrect: \"{self._orientation}\". Should be \"vertical\" or \"horizontal\"")
60
+
61
+ # 3. Initialize CustomTkinter's base structure using finalized parameters
62
+ ctk.CTkBaseClass.__init__(
63
+ self,
64
+ master=master,
65
+ width=width,
66
+ height=height,
67
+ bg_color=self.final_kw.get("bg_color", "transparent")
68
+ )
69
+
70
+ self._custom_current_state = "normal"
71
+ self._corner_radius = self.final_kw.get("corner_radius")
72
+ self._fg_color = self._check_color_type(self.final_kw.get("fg_color"))
73
+
74
+ # Map text color vectors safely out of the extracted dictionary layout layers
75
+ # No CTkLabel fallback: text_color is validated above, so borrowing another
76
+ # widget class's colour would only mask a theme gap.
77
+ self._text_color = self._check_color_type(self.final_kw.get("text_color"))
78
+
79
+ # 4. Canvas and render configurations
80
+ self._canvas = ctk.CTkCanvas(self, highlightthickness=0)
81
+ self._canvas.place(x=0, y=0, relwidth=1, relheight=1)
82
+ self._draw_engine = ctk.DrawEngine(self._canvas)
83
+
84
+ # 5. Bind layout adjustments to bypass CTkBaseClass strict bind filters safely
85
+ super(ctk.CTkBaseClass, self).bind("<Configure>", lambda e: self._draw(), add="+")
86
+
87
+ # 6. Trigger the initial render loop pass
88
+ self._draw(no_color_updates=True)
89
+
90
+ # 🔑 7. REGISTER LIFECYCLE HANDSHAKE HOOK: Pushes notifications up to Pygubu systems cleanly.
91
+ self._finalize_themeable_lifecycle()
92
+
93
+ def _validate_theme_keys(self) -> None:
94
+ """
95
+ Hard-fails at construction on an incomplete theme block, naming the
96
+ missing key and where it belongs.
97
+
98
+ Raises:
99
+ KeyError: naming the first missing key found.
100
+ """
101
+ name = self.__class__.__name__
102
+ for key in self._REQUIRED_THEME_KEYS:
103
+ if self._local_defaults.get(key) is None:
104
+ raise KeyError(
105
+ f"'{name}' theme block is missing '{key}' at the top level "
106
+ f"of sCTkThemes.json."
107
+ )
108
+ for key in self._REQUIRED_DISABLED_KEYS:
109
+ if self._custom_disabled_map.get(key) is None:
110
+ raise KeyError(
111
+ f"'{name}' theme block is missing '{key}' in disabled_map."
112
+ )
113
+
114
+ def _set_appearance_mode(self, mode_string: str):
115
+ """Native look catcher ensuring active or disabled tracks repaint fluidly on theme shifts."""
116
+ if hasattr(super(), "_set_appearance_mode"):
117
+ try:
118
+ super()._set_appearance_mode(mode_string)
119
+ except Exception:
120
+ pass
121
+ self._update_current_visual_state()
122
+ def _update_current_visual_state(self):
123
+ """Forwards global theme preference swipes directly to our core draw layout routine."""
124
+ self._draw()
125
+
126
+ def _draw(self, no_color_updates=False):
127
+ if hasattr(super(), "_draw"):
128
+ try: super()._draw(no_color_updates)
129
+ except Exception: pass
130
+ current_w = self.winfo_width() if self.winfo_width() > 1 else self._current_width
131
+ current_h = self.winfo_height() if self.winfo_height() > 1 else self._current_height
132
+ self._canvas.delete("all")
133
+
134
+ detected_bg = self._detect_color_of_master()
135
+ if detected_bg == "transparent" or detected_bg is None:
136
+ detected_bg = ctk.ThemeManager.theme["CTk"]["fg_color"]
137
+
138
+ is_disabled = getattr(self, "_custom_current_state", "normal") == "disabled"
139
+ # FIX: these previously carried hardcoded fallbacks, and since this
140
+ # widget's theme block had no disabled_map at all, the fallbacks were
141
+ # ALWAYS taken -- a disabled separator never used the configured
142
+ # theme. disabled_map is now required, so no fallback is needed. (The
143
+ # old text fallback also used the Tk colour name "gray50" rather than
144
+ # a hex pair, the only such value in the library.)
145
+ target_fg = self._custom_disabled_map.get("fg_color") if is_disabled else self._fg_color
146
+ target_txt = self._custom_disabled_map.get("text_color") if is_disabled else self._text_color
147
+
148
+ fg_rendered = self._apply_appearance_mode(self._check_color_type(target_fg))
149
+ txt_rendered = self._apply_appearance_mode(self._check_color_type(target_txt))
150
+ self._canvas.configure(bg=self._apply_appearance_mode(detected_bg))
151
+
152
+ if self._orientation == "horizontal":
153
+ line_thickness = self._current_height if self._current_height < current_h else 4
154
+ if line_thickness > 10: line_thickness = 4
155
+ else:
156
+ line_thickness = self._current_width if self._current_width < current_w else 4
157
+ if line_thickness > 10: line_thickness = 4
158
+
159
+ if self._text:
160
+ t_id = self._canvas.create_text(current_w / 2, current_h / 2, text=self._text, font=self._font, fill=txt_rendered)
161
+ bbox = self._canvas.bbox(t_id)
162
+ if bbox:
163
+ text_width = bbox[2] - bbox[0]
164
+ text_height = bbox[3] - bbox[1]
165
+ tw, th = text_width + 16, text_height + 8
166
+ x1 = (current_w / 2) - (tw / 2)
167
+ x2 = (current_w / 2) + (tw / 2)
168
+
169
+ if self._orientation == "horizontal":
170
+ y1, y2 = 1, current_h - 1
171
+ self._canvas.create_line(x1, y1, x1, y2, fill=fg_rendered, width=2)
172
+ self._canvas.create_line(x2, y1, x2, y2, fill=fg_rendered, width=2)
173
+ mid_y = current_h / 2
174
+ self._canvas.create_line(0, mid_y, x1, mid_y, fill=fg_rendered, width=line_thickness, dash=self._dash)
175
+ self._canvas.create_line(x2, mid_y, current_w, mid_y, fill=fg_rendered, width=line_thickness, dash=self._dash)
176
+ else:
177
+ x1_v, x2_v = 1, current_w - 1
178
+ y1 = (current_h / 2) - (th / 2)
179
+ y2 = (current_h / 2) + (th / 2)
180
+ self._canvas.create_line(x1_v, y1, x2_v, y1, fill=fg_rendered, width=2)
181
+ self._canvas.create_line(x1_v, y2, x2_v, y2, fill=fg_rendered, width=2)
182
+ mid_x = current_w / 2
183
+ self._canvas.create_line(mid_x, 0, mid_x, y1, fill=fg_rendered, width=line_thickness, dash=self._dash)
184
+ self._canvas.create_line(mid_x, y2, mid_x, current_h, fill=fg_rendered, width=line_thickness, dash=self._dash)
185
+ else:
186
+ if self._dash:
187
+ if self._orientation == "horizontal":
188
+ self._canvas.create_line(0, current_h / 2, current_w, current_h / 2, fill=fg_rendered, width=line_thickness, dash=self._dash)
189
+ else:
190
+ self._canvas.create_line(current_w / 2, 0, current_w / 2, current_h, fill=fg_rendered, width=line_thickness, dash=self._dash)
191
+ else:
192
+ self._draw_engine.draw_rounded_rect_with_border(current_w, current_h, self._apply_widget_scaling(self._corner_radius), 0)
193
+ self._canvas.itemconfig("inner_parts", outline=fg_rendered, fill=fg_rendered)
194
+
195
+ def configure(self, *args, **kwargs):
196
+ """Processes Pygubu designer workspace queries and manages theme state updates cleanly."""
197
+ if args and len(args) == 1:
198
+ pname = args[0]
199
+ if pname == "state": return ("state", "state", "state", "normal", self.get_state())
200
+ if pname in ["fg_color", "text_color"]:
201
+ val = self._custom_disabled_map.get(pname) if self.get_state() == "disabled" else self._local_defaults.get(pname)
202
+ return (pname, pname, pname, self._query_value(self._theme_default(pname)), self._query_value(val))
203
+ return self._configure_query(pname)
204
+
205
+ # FIX: was `if args and isinstance(args, dict)`. args is ALWAYS a
206
+ # tuple, so this never fired and the dict form of configure() was
207
+ # dead code. Same tautology fixed across the batch-one widgets.
208
+ if len(args) == 1 and isinstance(args[0], dict): kwargs = {**args[0], **kwargs}
209
+ # Runtime overrides have to reach the map a repaint reads, or the
210
+ # repaint puts the theme value straight back -- see
211
+ # ThemeableWidget._record_theme_overrides().
212
+ self._record_theme_overrides(kwargs)
213
+
214
+ if "state" in kwargs: self.state(kwargs.pop("state"))
215
+ if "text" in kwargs: self._text = str(kwargs.pop("text"))
216
+ if "dash" in kwargs: self._dash = kwargs.pop("dash")
217
+
218
+ # FIX: orientation was accepted by __init__ and registered as a Pygubu
219
+ # property, but configure() never popped it -- so changing it at
220
+ # runtime fell through to native CTkBaseClass.configure(), whose
221
+ # check_kwargs_empty() raised "['orientation'] are not supported
222
+ # arguments". Construction worked; reconfiguration did not, which is
223
+ # exactly what the Designer does when the dropdown is changed.
224
+ #
225
+ # Unlike the other properties here, orientation has a GEOMETRIC
226
+ # consequence: __init__ assigns height=length for a vertical
227
+ # separator and width=length for a horizontal one. Switching
228
+ # orientation therefore has to swap the two dimensions, or a vertical
229
+ # separator turned horizontal stays tall and thin.
230
+ if "orientation" in kwargs:
231
+ new_orientation = str(kwargs.pop("orientation")).lower()
232
+ if new_orientation not in ("vertical", "horizontal"):
233
+ raise ValueError(
234
+ f'The value for orientation is incorrect: "{new_orientation}". '
235
+ f'Should be "vertical" or "horizontal"'
236
+ )
237
+ if new_orientation != self._orientation:
238
+ self._orientation = new_orientation
239
+ # Swap, rather than recompute from length/width: those were
240
+ # consumed at construction and aren't retained, and the current
241
+ # dimensions already reflect any resizing since.
242
+ kwargs.setdefault("width", self._current_height)
243
+ kwargs.setdefault("height", self._current_width)
244
+
245
+ # length is a construction-time alias for whichever dimension the
246
+ # orientation makes the long one. Translated here so the Designer can
247
+ # edit it after the fact.
248
+ if "length" in kwargs:
249
+ new_length = int(kwargs.pop("length"))
250
+ if self._orientation == "vertical":
251
+ kwargs["height"] = new_length
252
+ else:
253
+ kwargs["width"] = new_length
254
+
255
+ self._drop_cleared(kwargs)
256
+ if kwargs: super().configure(**kwargs)
257
+ self._draw()
258
+
259
+ config = configure
260
+ def get_state(self) -> str: return str(getattr(self, "_custom_current_state", "normal")).lower()
261
+ def state(self, mode: str = None) -> str:
262
+ if mode is None: return self.get_state()
263
+ self._custom_current_state = mode.lower()
264
+ self._draw()
265
+ return self._custom_current_state
266
+
267
+ def cget(self, attribute_name: str):
268
+ if attribute_name == "height": raise ValueError("Use length and width arguments instead.")
269
+ if attribute_name == "state": return self.get_state()
270
+ mapping = {"corner_radius": self._corner_radius, "fg_color": self._fg_color,
271
+ "text_color": self._text_color, "orientation": self._orientation,
272
+ "text": self._text, "dash": self._dash}
273
+ # CHECKED FIRST, not passed as get()'s default.
274
+ #
275
+ # This read `mapping.get(name, super().cget(name))`, and Python works
276
+ # out the default BEFORE calling get() -- so super().cget(name) ran on
277
+ # every call and raised for anything the native base class does not
278
+ # know, including every property listed above. cget("text") and
279
+ # cget("fg_color") both failed on a separator that knew their values
280
+ # perfectly well. text_color was also missing from the list.
281
+ if attribute_name in mapping:
282
+ return mapping[attribute_name]
283
+ return super().cget(attribute_name)
284
+
285
+ def bind(self, sequence=None, command=None, add=True):
286
+ """
287
+ Routes bindings to the internal canvas, which is what actually
288
+ receives events -- CTkBaseClass filters direct binds on the widget.
289
+
290
+ FIX: the `add` argument was previously accepted and then ignored,
291
+ with add=True hardcoded in the forwarded call. A caller passing
292
+ add=False expecting to REPLACE existing bindings would silently
293
+ accumulate them instead.
294
+ """
295
+ self._canvas.bind(sequence, command, add=add)
296
+
297
+ def unbind(self, sequence=None, funcid=None):
298
+ """
299
+ Removes a binding from the internal canvas.
300
+
301
+ FIX: funcid was previously accepted and then discarded, so this always
302
+ removed EVERY binding for the sequence rather than the single one the
303
+ caller identified. That is the same destructive behaviour that made
304
+ unbind() unusable for blocking scrollbar drags in
305
+ sCTkScrollableFrame -- Tk's unbind() with no funcid wipes bindings
306
+ this widget never installed, with no way to restore them.
307
+ """
308
+ self._canvas.unbind(sequence, funcid)
@@ -0,0 +1,264 @@
1
+ #!/usr/bin/python3
2
+ """
3
+ sCTkSlider
4
+
5
+ A theme-compliant linear adjustment slider widget. Inherits directly from
6
+ ctk.CTkSlider so CustomTkinter handles native mouse-drag handling, scaling,
7
+ and coordinate snapping; this class layers automatic light/dark theme
8
+ resolution and a distinct enabled/disabled visual state on top.
9
+
10
+ Base class order matters here: `class sCTkSlider(ctk.CTkSlider,
11
+ ThemeableWidget)` puts the native CTk class first, so every `super()` call in
12
+ this file's own methods resolves to ctk.CTkSlider -- and, beneath it,
13
+ tkinter.Misc -- never to ThemeableWidget. ThemeableWidget's own
14
+ configure()/cget()/_set_appearance_mode() overrides have been removed entirely
15
+ for this reason (see themeable_widget.py's docstring); this widget owns all of
16
+ its own runtime color-swapping logic.
17
+
18
+ Unlike every other widget in this project, state is NOT tracked in a parallel
19
+ self._custom_current_state attribute -- state() and _apply_custom_theme_colors()
20
+ both read the NATIVE widget's own "state" property directly via
21
+ super().cget("state"), treating it as the single source of truth rather than
22
+ duplicating it. Disabling routes "state" straight through to the native
23
+ widget's own configure(), which is the same underlying mechanism confirmed
24
+ correct elsewhere in this library (native state="disabled" blocks
25
+ interaction; manual event-unbinding does not).
26
+ """
27
+ from typing import Any, Callable, Optional
28
+ import customtkinter as ctk
29
+ from .themeable_widget import ThemeableWidget, defer
30
+
31
+
32
+ class sCTkSlider(ctk.CTkSlider, ThemeableWidget):
33
+ """Themeable linear adjustment slider.
34
+
35
+ Adds to native ctk.CTkSlider:
36
+ - Automatic light/dark theme resolution from sCTkThemes.json (via
37
+ ThemeableWidget.__init__ -- see that class's docstring for what it does,
38
+ and just as importantly, what it no longer does).
39
+ - A distinct enabled/disabled visual state, read directly from the
40
+ native widget's own state (see module docstring) rather than a
41
+ parallel instance attribute.
42
+ - Pygubu Designer property introspection for `state`, `fg_color`,
43
+ `progress_color`, `button_color`, and `button_hover_color` via a
44
+ single-argument configure() call.
45
+
46
+ Colors are passed through to configure() as raw (light, dark) tuples rather
47
+ than pre-resolved to a single value, so CustomTkinter's own appearance-mode
48
+ tracking repaints them automatically on a light/dark switch -- the same
49
+ approach validated on sCTkComboBox, sCTkSegmentedButton, and the button
50
+ family. Not separately re-confirmed for this specific widget.
51
+ """
52
+
53
+ def __init__(self, master: Optional[Any] = None, **kw: Any) -> None:
54
+ """
55
+ Args:
56
+ master: Parent container.
57
+ **kw: `command` (callable) and `variable` (tkinter.Variable) are
58
+ pulled out explicitly below. Everything else is either a
59
+ native CTkSlider argument or a theme-key override (see the
60
+ "sCTkSlider" block in sCTkThemes.json).
61
+ """
62
+ # 1. Capture widget-specific attributes early, before the mixin's own
63
+ # configuration pass.
64
+ command = kw.pop("command", None)
65
+ variable = kw.pop("variable", None)
66
+
67
+ # 2. Fire our shared theme logic. This resolves final_kw
68
+ # (construction-time properties) and the disabled color map. See
69
+ # ThemeableWidget.__init__ for what actually happens here.
70
+ ThemeableWidget.__init__(self, kw)
71
+ self._local_defaults = dict(self.final_kw)
72
+ self._custom_disabled_map = dict(self._widget_disabled_map)
73
+
74
+ # Remove any stray "state" key from final_kw -- prevents native
75
+ # constructor validation collisions.
76
+ self.final_kw.pop("state", None)
77
+
78
+ # 3. Initialize CustomTkinter natively with the clean final kwargs array.
79
+ super().__init__(master, **self.final_kw)
80
+
81
+ if command is not None: super().configure(command=command)
82
+ if variable is not None: super().configure(variable=variable)
83
+
84
+ # Deferred slightly, matching this file's original startup pattern --
85
+ # applies the initial theme colors shortly after construction.
86
+ defer(self, 10, self._apply_custom_theme_colors)
87
+ self._finalize_themeable_lifecycle()
88
+
89
+ def configure(self, *args: Any, **kwargs: Any) -> Any:
90
+ """
91
+ Standard widget configuration, with Pygubu/positional-argument handling.
92
+
93
+ Args:
94
+ *args: At most one positional argument is meaningful:
95
+ - a dict: merged into kwargs and processed normally below.
96
+ - the literal string "state": returns a Tkinter-style
97
+ (name, name, name, default, current) tuple.
98
+ - one of "fg_color"/"progress_color"/"button_color"/
99
+ "button_hover_color": returns the same style of tuple, with
100
+ `current` reflecting the disabled or normal value as
101
+ appropriate. Note the returned value is str(value), where
102
+ value may itself be a (light, dark) tuple rather than a
103
+ single resolved color -- a known limitation shared with the
104
+ wider Pygubu-query investigation set aside elsewhere in this
105
+ project, not fixed here.
106
+ - anything else: forwarded directly to the native widget's
107
+ configure(), which does not support single-argument property
108
+ queries for arbitrary properties (same limitation).
109
+ **kwargs: Standard CTkSlider configuration options, plus:
110
+ `command`/`variable` are routed through super().configure()
111
+ individually. Note `state` is NOT specially intercepted here
112
+ -- it flows straight through to the native widget's own
113
+ configure(state=...), which is what makes this widget's
114
+ disable mechanism correct (see module docstring).
115
+
116
+ Returns:
117
+ The query tuple described above for the single-argument case, or
118
+ whatever super().configure() returns for the keyword-argument case
119
+ (typically None).
120
+ """
121
+ # args[0] was already correctly unwrapped here in the original file --
122
+ # unlike most other widgets in this library, this one never had the
123
+ # `pname = args` tuple-comparison bug. Preserved as-is.
124
+ if len(args) == 1:
125
+ if isinstance(args[0], dict):
126
+ kwargs = {**args[0], **kwargs}
127
+ else:
128
+ pname = args[0]
129
+ if pname == "state":
130
+ return ("state", "state", "state", "normal", str(self.state()))
131
+
132
+ if pname in ["fg_color", "progress_color", "button_color", "button_hover_color"]:
133
+ current_state = str(self.state()).lower()
134
+ val = self._custom_disabled_map.get(pname) if current_state == "disabled" else self._local_defaults.get(pname)
135
+
136
+ # The default falls back to the CURRENT value when the
137
+ # theme has nothing to offer. sCTkThemes.json has no
138
+ # sCTkSlider block at all, so every one of these lookups
139
+ # returns None -- and pygubu hands that None straight back
140
+ # to the widget:
141
+ #
142
+ # color is None, for transparency set color='transparent'
143
+ #
144
+ # With the fallback, clearing such a field is a no-op
145
+ # rather than an error. Adding the theme block is the real
146
+ # fix; this stops the Designer breaking in the meantime.
147
+ theme_value = self._theme_default(pname)
148
+ if theme_value is None:
149
+ theme_value = val if val is not None else super().cget(pname)
150
+ return (pname, pname, pname,
151
+ self._query_value(theme_value),
152
+ self._query_value(val if val is not None else super().cget(pname)))
153
+
154
+ return self._configure_query(pname)
155
+
156
+ # Runtime overrides have to reach the map a repaint reads, or
157
+ # _apply_custom_theme_colors() puts the theme value straight back --
158
+ # see ThemeableWidget._record_theme_overrides().
159
+ self._record_theme_overrides(kwargs)
160
+
161
+ if "command" in kwargs: super().configure(command=kwargs.pop("command"))
162
+ if "variable" in kwargs: super().configure(variable=kwargs.pop("variable"))
163
+
164
+ # "state" is deliberately left in kwargs here, not popped -- it flows
165
+ # through to super().configure(**kwargs) below, reaching the native
166
+ # widget directly. has_state only tracks whether a repaint is needed
167
+ # afterward.
168
+ has_state = "state" in kwargs
169
+ self._drop_cleared(kwargs)
170
+
171
+ if kwargs:
172
+ super().configure(**kwargs)
173
+
174
+ if has_state:
175
+ self._apply_custom_theme_colors()
176
+
177
+ # Tkinter/CTk convention binds .config to .configure as a SEPARATE class
178
+ # attribute -- it does not automatically track whichever configure() a
179
+ # subclass defines. Without this line, calling .config(...) on an instance
180
+ # would silently skip this entire override and land on the native widget's
181
+ # configure() directly, bypassing theming entirely.
182
+ config = configure
183
+
184
+ def _set_appearance_mode(self, mode_string: str) -> None:
185
+ """
186
+ Forwards CustomTkinter's internal light/dark mode change notification to
187
+ the native widget.
188
+
189
+ No longer manually re-triggers _apply_custom_theme_colors(). That
190
+ method now passes raw (light, dark) tuples straight through to
191
+ configure() instead of pre-resolving to a single color, so CTk's own
192
+ appearance-mode tracking should repaint correctly on its own -- the
193
+ same approach validated on sCTkComboBox, sCTkSegmentedButton, and the
194
+ button family.
195
+
196
+ Args:
197
+ mode_string: The new appearance mode ("Light" or "Dark"), as passed
198
+ by CustomTkinter's internal appearance-mode change machinery.
199
+ """
200
+ if hasattr(super(), "_set_appearance_mode"):
201
+ try: super()._set_appearance_mode(mode_string)
202
+ except Exception: pass
203
+
204
+ def state(self, mode: Optional[str] = None) -> str:
205
+ """
206
+ Gets or sets the widget's enabled/disabled state.
207
+
208
+ Unlike every other widget in this library, this reads the query
209
+ directly from the native widget's own cget("state") rather than a
210
+ parallel self._custom_current_state -- see module docstring.
211
+
212
+ Args:
213
+ mode: If None, returns the native widget's current "state" value
214
+ directly. Otherwise, forwarded to configure(state=mode),
215
+ which reaches the native widget's own state handling.
216
+
217
+ Returns:
218
+ If querying: whatever the native widget's "state" cget returns,
219
+ lowercased. If setting: echoes back the `mode` argument exactly as
220
+ given (not re-queried from the native widget afterward).
221
+ """
222
+ if mode is None:
223
+ return str(super().cget("state")).lower()
224
+ self.configure(state=mode)
225
+ return mode
226
+
227
+ def get_state(self) -> str:
228
+ """Equivalent to calling state() with no argument."""
229
+ return self.state()
230
+
231
+ def _apply_custom_theme_colors(self) -> None:
232
+ """
233
+ Recomputes and applies this widget's colors from the theme file, based
234
+ on the current state (read directly from the native widget -- see
235
+ module docstring).
236
+
237
+ Called shortly after construction (via after(10, ...)), on every
238
+ state()/configure(state=...) change, and (indirectly, via
239
+ _set_appearance_mode) on appearance-mode changes.
240
+
241
+ Passes raw (light, dark) tuples straight through to configure() instead
242
+ of resolving to a single color first, so CTk's native tracking can
243
+ handle appearance-mode repaints without help from _set_appearance_mode.
244
+ Every value here traces back to sCTkThemes.json; there are no
245
+ hardcoded colors in this method.
246
+
247
+ While disabled, button_hover_color is forced to match button_color --
248
+ since hover can't trigger once natively disabled, this just ensures
249
+ nothing else could show a stale hover color if queried directly.
250
+ """
251
+ is_disabled = str(super().cget("state")).lower() == "disabled"
252
+ target_map = self._custom_disabled_map if is_disabled else self._local_defaults
253
+
254
+ fg_payload = {}
255
+ for key in ("fg_color", "progress_color", "button_color", "button_hover_color"):
256
+ val = target_map.get(key)
257
+ if val is not None:
258
+ fg_payload[key] = val
259
+
260
+ if is_disabled and "button_color" in fg_payload:
261
+ fg_payload["button_hover_color"] = fg_payload["button_color"]
262
+
263
+ if fg_payload:
264
+ super().configure(**fg_payload)