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,313 @@
1
+ #!/usr/bin/python3
2
+ """
3
+ sCTkScrollbar
4
+
5
+ An advanced, theme-compliant custom scrollbar component and native container.
6
+ Inherits cleanly from ctk.CTkScrollbar to preserve native macOS high-precision
7
+ touchpad event streams and keep global light/dark theme tracking active.
8
+ """
9
+ import tkinter as tk
10
+
11
+ import customtkinter as ctk
12
+ from .themeable_widget import ThemeableWidget
13
+ from .sctk_scroll_mixin import ScrollBindingMixin
14
+
15
+
16
+ class sCTkScrollbar(ctk.CTkScrollbar, ThemeableWidget):
17
+ # Required at the TOP LEVEL of the theme block. There is no disabled_map:
18
+ # CustomTkinter's scrollbar has no disabled state to lock, and containers
19
+ # that need an inert bar block dragging at the binding level instead --
20
+ # see ScrollBindingMixin._set_scroll_drag_blocked().
21
+ _REQUIRED_THEME_KEYS = ("button_color", "button_hover_color")
22
+
23
+ def __init__(self, master=None, **kwargs):
24
+ # 1. Run shared mixin logic first to parse master themes.json safely
25
+ ThemeableWidget.__init__(self, kwargs)
26
+ self._local_defaults = dict(self.final_kw)
27
+ self._validate_theme_keys()
28
+
29
+ # 2. Track orientation layout constraints cleanly.
30
+ #
31
+ # FIX: this previously read kwargs.get("orientation", ...) -- the RAW
32
+ # constructor dict, after ThemeableWidget.__init__ had already
33
+ # processed it. Reading the resolved final_kw instead means the value
34
+ # is picked up whether it came from the constructor or the theme
35
+ # block, and it cannot be affected by anything the mixin does to the
36
+ # incoming dict. str() guards against a non-string value reaching
37
+ # .lower().
38
+ orientation = str(self.final_kw.get("orientation", "vertical")).lower()
39
+ self._is_horizontal = orientation == "horizontal"
40
+
41
+ # 3. Securely set default sizes based on orientation layout rules
42
+ if self._is_horizontal:
43
+ self.final_kw.setdefault("height", 14)
44
+ else:
45
+ self.final_kw.setdefault("width", 14)
46
+
47
+ # 4. Initialize CustomTkinter natively so Apple Magic Mouse / trackpad streams stay intact
48
+ super().__init__(master, **self.final_kw)
49
+ self._apply_custom_theme_colors()
50
+ self._finalize_themeable_lifecycle()
51
+
52
+ def _validate_theme_keys(self) -> None:
53
+ """
54
+ Hard-fails at construction on an incomplete theme block, naming the
55
+ missing key. Replaces the hardcoded fallbacks previously used in
56
+ _apply_custom_theme_colors(), which silently substituted a plausible
57
+ colour and made an incomplete block look merely slightly-off rather
58
+ than broken.
59
+
60
+ Raises:
61
+ KeyError: naming the first missing key found.
62
+ """
63
+ name = self.__class__.__name__
64
+ for key in self._REQUIRED_THEME_KEYS:
65
+ if self._local_defaults.get(key) is None:
66
+ raise KeyError(
67
+ f"'{name}' theme block is missing '{key}' at the top level "
68
+ f"of sCTkThemes.json."
69
+ )
70
+
71
+ def _apply_custom_theme_colors(self):
72
+ """Cascades color profiles directly out of your centralized stylesheet json maps."""
73
+ # No fallbacks: validated at construction, so both lookups resolve.
74
+ normal_color = self._local_defaults.get("button_color")
75
+ normal_hover = self._local_defaults.get("button_hover_color")
76
+
77
+ super().configure(
78
+ button_color=tuple(normal_color) if isinstance(normal_color, list) else normal_color,
79
+ button_hover_color=tuple(normal_hover) if isinstance(normal_hover, list) else normal_hover
80
+ )
81
+
82
+ # Theme keys that _apply_custom_theme_colors() re-pushes on every repaint.
83
+ # configure() records these into _local_defaults so a runtime override
84
+ # survives the repaint instead of being reverted by it.
85
+ _THEME_TRACKED_KEYS = frozenset({"button_color", "button_hover_color"})
86
+
87
+ def configure(self, *args, **kwargs):
88
+ """
89
+ Processes standard configuration queries and manages visual refreshes.
90
+
91
+ Args:
92
+ *args: At most one positional argument is meaningful:
93
+ - a dict: merged into kwargs and processed normally.
94
+ - a tracked theme key name: returns a Tkinter-style
95
+ (name, name, name, default, current) query tuple.
96
+ - anything else: forwarded to the native widget.
97
+ **kwargs: Any native CTkScrollbar option.
98
+
99
+ Returns:
100
+ The query tuple described above for the single-argument case,
101
+ otherwise None.
102
+ """
103
+ # FIX: an earlier version tested `if args and isinstance(args, dict)`.
104
+ # args is ALWAYS a tuple, so that was never true -- the dict-merge
105
+ # branch was dead code, and there was no query branch at all, so
106
+ # configure("button_color") silently returned None instead of a
107
+ # property tuple. Same tautology fixed across the batch-one widgets.
108
+ if len(args) == 1:
109
+ if isinstance(args[0], dict):
110
+ kwargs = {**args[0], **kwargs}
111
+ else:
112
+ pname = args[0]
113
+ if pname in self._THEME_TRACKED_KEYS:
114
+ # The DEFAULT comes from the untouched theme, the CURRENT
115
+ # from the live map. Both slots used to read
116
+ # _local_defaults, which configure() writes overrides into
117
+ # -- so clearing a colour in the Designer reported the
118
+ # override as its own default and put it straight back.
119
+ return (pname, pname, pname,
120
+ self._query_value(self._theme_default(pname)),
121
+ self._query_value(self._local_defaults.get(pname)))
122
+ return self._configure_query(pname)
123
+
124
+ # FIX: record theme overrides BEFORE the repaint below.
125
+ #
126
+ # _apply_custom_theme_colors() re-pushes button_color and
127
+ # button_hover_color from self._local_defaults on every call. Since
128
+ # configure() never wrote to that dict, the sequence was:
129
+ # super().configure(button_color="red") applied red, then the repaint
130
+ # on the very next line overwrote it with the theme value -- so
131
+ # runtime color overrides silently did nothing.
132
+ #
133
+ # The repaint isn't wrong to exist (the _set_appearance_mode hook
134
+ # needs it); it was wrong to treat _local_defaults as authoritative
135
+ # when configure() had just introduced a newer value it never
136
+ # recorded. Writing the override in first makes the repaint reproduce
137
+ # it rather than revert it, and makes it survive later appearance-mode
138
+ # changes -- matching CustomTkinter's own semantics, where
139
+ # configure(button_color=...) sticks.
140
+ # Through the shared helper now rather than a local loop. This widget
141
+ # was where the pattern started; ThemeableWidget._record_theme_overrides()
142
+ # generalised it and adds two things this loop lacked -- it normalises
143
+ # a value from the Designer into the (light, dark) shape the theme maps
144
+ # hold, and it snapshots the theme before the first override so the
145
+ # query above has a real default to report.
146
+ self._record_theme_overrides(kwargs)
147
+
148
+ if kwargs:
149
+ super().configure(**kwargs)
150
+ self._apply_custom_theme_colors()
151
+
152
+ config = configure
153
+
154
+ def _set_appearance_mode(self, mode_string: str):
155
+ if hasattr(super(), "_set_appearance_mode"):
156
+ try:
157
+ super()._set_appearance_mode(mode_string)
158
+ except Exception:
159
+ pass
160
+ self._apply_custom_theme_colors()
161
+
162
+
163
+ class sCTkScrollArea(ctk.CTkFrame, ScrollBindingMixin):
164
+ """
165
+ An unblocked scrollable container frame supporting high-precision Apple momentum streams.
166
+ Bypasses rigid layout masks to give developers full opt-in control over child event bundling.
167
+ """
168
+
169
+ def __init__(self, master=None, **kwargs):
170
+ super().__init__(master, fg_color="transparent", border_width=0, **kwargs)
171
+
172
+ current_mode = str(ctk.get_appearance_mode()).lower()
173
+ initial_bg = "#FAFAFA" if current_mode == "light" else "#1A1A1A"
174
+
175
+ self.canvas = tk.Canvas(self, highlightthickness=0, bd=0, bg=initial_bg)
176
+ self.canvas.pack(side="left", fill="both", expand=True)
177
+
178
+ self.scroll_content = tk.Frame(self.canvas, bd=0, bg=initial_bg)
179
+ self.canvas_window = self.canvas.create_window((0, 0), window=self.scroll_content, anchor="nw")
180
+
181
+ # Widgets registered through propagate_scroll_events() that aren't
182
+ # descendants of scroll_content -- see that method.
183
+ self._extra_scroll_targets = []
184
+
185
+ # A scrollbar attached via hook_scrollbar(), so the wheel keeps
186
+ # working while the pointer is over the bar itself.
187
+ self._hooked_scrollbar = None
188
+
189
+ # Scroll handling now comes from ScrollBindingMixin, the library's
190
+ # single shared implementation. This class previously carried its own
191
+ # copy, which had drifted from the canonical version in three ways,
192
+ # all corrected by this change:
193
+ #
194
+ # - Two's-complement sign correction used `> 32768` rather than
195
+ # `>= 0x8000`. Those differ at exactly 32768, which is the
196
+ # smallest NEGATIVE value in a signed 16-bit field: the old form
197
+ # read it as +32768, inverting scroll direction at that value.
198
+ # - Windows wheel scaling doubled the delta (`/120 * 2`), scrolling
199
+ # twice as far per notch as everywhere else in the library.
200
+ # - The packed touchpad delta was decoded by reading event.delta_y
201
+ # when present and otherwise applying a 16-bit correction to what
202
+ # may be a 32-bit packed value, instead of bit-shifting out the
203
+ # signed 16-bit X and Y components.
204
+ #
205
+ # It also gains the nested-scrollable boundary guard, blocking
206
+ # handlers when scrolling is switched off, and the debounced
207
+ # content rebind.
208
+ self._init_scroll_state()
209
+ self._install_scroll_activation(content_widget=self.scroll_content)
210
+
211
+ self.scroll_content.bind("<Configure>", lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all")))
212
+ self.canvas.bind("<Configure>",
213
+ lambda e: self.canvas.itemconfig(self.canvas_window, width=self.canvas.winfo_width()))
214
+
215
+ # ------------------------------------------------------------------
216
+ # ScrollBindingMixin contract
217
+ # ------------------------------------------------------------------
218
+ def _scroll_target(self):
219
+ """This area builds its own canvas, so no parent lookup is needed."""
220
+ canvas = getattr(self, "canvas", None)
221
+ try:
222
+ if canvas is not None and canvas.winfo_exists():
223
+ return canvas
224
+ except Exception:
225
+ pass
226
+ return None
227
+
228
+ def _scroll_layers(self):
229
+ """
230
+ The area itself, its canvas, any hooked scrollbar, the full content
231
+ tree, and anything explicitly registered via propagate_scroll_events().
232
+
233
+ Returns:
234
+ An ordered, deduplicated list of widgets.
235
+ """
236
+ layers = [self]
237
+
238
+ canvas = self._scroll_target()
239
+ if canvas is not None:
240
+ layers.append(canvas)
241
+
242
+ if self._hooked_scrollbar is not None:
243
+ self._collect_scroll_descendants(self._hooked_scrollbar, layers)
244
+
245
+ content = getattr(self, "scroll_content", None)
246
+ if content is not None:
247
+ try:
248
+ if content.winfo_exists():
249
+ self._collect_scroll_descendants(content, layers)
250
+ except Exception:
251
+ pass
252
+
253
+ for widget in list(self._extra_scroll_targets):
254
+ try:
255
+ if widget.winfo_exists():
256
+ self._collect_scroll_descendants(widget, layers)
257
+ else:
258
+ self._extra_scroll_targets.remove(widget)
259
+ except Exception:
260
+ pass
261
+
262
+ return layers
263
+
264
+ def hook_scrollbar(self, scrollbar_widget):
265
+ """
266
+ Connects a scrollbar to this area's canvas in both directions.
267
+
268
+ The scrollbar is also recorded as a scroll layer, so the wheel keeps
269
+ working while the pointer is over the bar itself rather than dying
270
+ the moment it leaves the content.
271
+ """
272
+ self.canvas.configure(yscrollcommand=scrollbar_widget.set)
273
+ scrollbar_widget.configure(command=self.canvas.yview)
274
+ self._hooked_scrollbar = scrollbar_widget
275
+ self._activate_scroll_bindings()
276
+
277
+ def propagate_scroll_events(self, target_widget: tk.Widget):
278
+ """
279
+ Registers a widget outside this area's own content tree to receive
280
+ scroll events, along with its descendants.
281
+
282
+ RARELY NEEDED NOW. Content placed inside scroll_content is bound
283
+ automatically, and re-bound whenever it changes, so this is only for
284
+ widgets that sit outside that tree. Registered widgets are remembered
285
+ and re-bound on every subsequent pass, rather than bound once and
286
+ forgotten as the previous implementation did.
287
+
288
+ Args:
289
+ target_widget: The widget to register. Its descendants are
290
+ included automatically.
291
+ """
292
+ if target_widget not in self._extra_scroll_targets:
293
+ self._extra_scroll_targets.append(target_widget)
294
+ self._activate_scroll_bindings()
295
+
296
+ def process_incoming_scroll(self, event):
297
+ """
298
+ COMPATIBILITY SHIM. Scroll events are dispatched by
299
+ ScrollBindingMixin directly; this remains only for external callers
300
+ that bound this method themselves before the consolidation.
301
+
302
+ Delegates to the shared wheel handler. Trackpad <TouchpadScroll>
303
+ events are routed to the mixin's own accumulator-gated handler by its
304
+ bindings, so they do not pass through here.
305
+ """
306
+ return self._process_scroll_wheel(event)
307
+
308
+ def _set_appearance_mode(self, mode_string: str):
309
+ if hasattr(super(), "_set_appearance_mode"):
310
+ super()._set_appearance_mode(mode_string)
311
+ target_bg = "#FAFAFA" if str(mode_string).lower() == "light" else "#1A1A1A"
312
+ if hasattr(self, "canvas") and self.canvas: self.canvas.configure(bg=target_bg)
313
+ if hasattr(self, "scroll_content") and self.scroll_content: self.scroll_content.configure(bg=target_bg)