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,1854 @@
1
+ import tkinter as tk
2
+
3
+ import customtkinter as ctk
4
+ from customtkinter.windows.widgets.core_widget_classes import CTkBaseClass
5
+
6
+ import scustomtkinter_pygubu.designer.properties
7
+
8
+ from pygubu.component.plugin_engine import IDesignerPlugin
9
+ from pygubu.stockimage import StockImageCache, StockImage
10
+ from pygubu.utils.widget import crop_widget
11
+ from pygubu.plugins.pygubu.designer.basehelpers import (
12
+ ToplevelPreviewBaseBO,
13
+ ToplevelPreviewFactory,
14
+ ToplevelPreviewMixin,
15
+ )
16
+
17
+ from scustomtkinter.sctk_frame import sCTkFrame
18
+ from scustomtkinter_pygubu.sCTkFramebo import (sCTkFrameBO, builder_id as sCTkFrame_builder_id)
19
+
20
+ from scustomtkinter.sctk_frame_labeled_primary import sCTkFrameLabeledPrimary
21
+ from scustomtkinter_pygubu.sCTkFrameLabeledPrimarybo import (sCTkFrameLabeledPrimaryBO, builder_id as sCTkFrameLabeledPrimary_builder_id)
22
+ from scustomtkinter.sctk_frame_labeled_secondary import sCTkFrameLabeledSecondary
23
+ from scustomtkinter_pygubu.sCTkFrameLabeledSecondarybo import (sCTkFrameLabeledSecondaryBO, builder_id as sCTkFrameLabeledSecondary_builder_id)
24
+
25
+ from scustomtkinter.sctk_scrollable_frame import sCTkScrollableFrame
26
+ from scustomtkinter_pygubu.sCTkScrollableFramebo import (sCTkScrollableFrameBO, builder_id as sCTkScrollableFrame_builder_id)
27
+
28
+ from scustomtkinter.sctk_optionmenu_secondary import sCTkOptionMenuSecondary
29
+
30
+
31
+ from scustomtkinter_pygubu.sCTkOptionMenuSecondarybo import (sCTkOptionMenuSecondaryBO, builder_id as sCTkOptionMenuSecondary_builder_id)
32
+
33
+ from scustomtkinter.sctk_file_explorer import sCTkFileExplorer
34
+ from scustomtkinter_pygubu.sCTkFileExplorerbo import (sCTkFileExplorerBO, builder_id as sCTkFileExplorer_builder_id)
35
+
36
+ from scustomtkinter.sctk_path_chooser import sCTkPathChooser
37
+ from scustomtkinter_pygubu.sCTkPathChooserbo import (sCTkPathChooserBO, builder_id as sCTkPathChooser_builder_id)
38
+
39
+ from scustomtkinter.sctk_separator import sCTkSeparator
40
+ from scustomtkinter_pygubu.sCTkSeparatorbo import (sCTkSeparatorBuilder, builder_id as sCTkSeparator_builder_id)
41
+
42
+ from scustomtkinter.sctk_dial import (sCTkDialContinuous, sCTkDialRange,
43
+ sCTkDialSelector)
44
+ from scustomtkinter_pygubu.sCTkDialbo import (
45
+ sCTkDialContinuousBO, id_continuous as sCTkDialContinuous_builder_id,
46
+ sCTkDialRangeBO, id_range as sCTkDialRange_builder_id,
47
+ sCTkDialSelectorBO, id_selector as sCTkDialSelector_builder_id,
48
+ )
49
+
50
+ from scustomtkinter.sctk_dialog import sCTkDialog
51
+ from scustomtkinter_pygubu.sCTkDialogbo import (sCTkDialogBO, builder_id as sCTkDialog_builder_id)
52
+
53
+ from scustomtkinter.sctk_segmentedbutton import sCTkSegmentedButton
54
+ from scustomtkinter_pygubu.sCTkSegmentedButtonbo import (sCTkSegmentedButtonBO, builder_id as sCTkSegmentedButton_builder_id)
55
+
56
+ from scustomtkinter.sctk_selector import sCTkSelector
57
+ from scustomtkinter.sctk_checkbox import sCTkCheckBox # Needs importing because selector made up of checkboxes and we need
58
+ # to search to find the clickable master frame
59
+ from scustomtkinter_pygubu.sCTkSelectorbo import (sCTkSelectorBO, builder_id as sCTkSelector_builder_id)
60
+
61
+ from scustomtkinter.sctk_spinbox import sCTkSpinbox
62
+ from scustomtkinter_pygubu.sCTkSpinboxbo import (sCTkSpinboxBO, builder_id as sCTkSpinbox_builder_id)
63
+
64
+ from scustomtkinter.sctk_tableview import sCTkTableview
65
+ from scustomtkinter_pygubu.sCTkTableviewbo import (sCTkTableviewBO, builder_id as sCTkTableview_builder_id)
66
+
67
+ from scustomtkinter.sctk_radiobutton import sCTkRadioButton
68
+ from scustomtkinter_pygubu.sCTkRadioButtonbo import (sCTkRadioButtonBO, builder_id as sCTkRadioButton_builder_id)
69
+
70
+ from scustomtkinter.sctk_tabview import sCTkTabview
71
+ from scustomtkinter_pygubu.sCTkTabviewbo import (sCTkTabviewBO, builder_id as sCTkTabview_builder_id)
72
+
73
+ from scustomtkinter.sctk_frequency_display import sCTkFrequencyDisplay
74
+ from scustomtkinter_pygubu.sCTkFrequencyDisplaybo import (
75
+ sCTkFrequencyDisplayBO, builder_id as sCTkFrequencyDisplay_builder_id)
76
+
77
+ from scustomtkinter.sctk_notebook import sCTkNotebook
78
+ from scustomtkinter_pygubu.sCTkNotebookbo import (sCTkNotebookBO, builder_id as sCTkNotebook_builder_id)
79
+
80
+ # TOP-LEVEL WIDGETS.
81
+ #
82
+ # NOTE the module name for sCTk: it lives in sctk_core, NOT sctk_sctk. The
83
+ # rest of this library follows sctk_<widgetname>, but that convention produces
84
+ # an absurd name for the root window class, so this one file breaks it
85
+ # deliberately. Import sCTk from sctk_core everywhere.
86
+ from scustomtkinter.sctk_core import sCTk
87
+ from scustomtkinter.sctk_toplevel import sCTkToplevel
88
+
89
+ # The builder-object module follows the same naming break: sCTkCorebo, not
90
+ # sCTkbo, matching sctk_core on the widget side.
91
+ from scustomtkinter_pygubu.sCTkCorebo import builder_id as sCTk_builder_id
92
+ from scustomtkinter_pygubu.sCTkToplevelbo import builder_id as sCTkToplevel_builder_id
93
+
94
+
95
+ # =====================================================================
96
+ # THEME BLOCK NAMING
97
+ #
98
+ # ThemeableWidget resolves a widget's theme block by self.__class__.__name__.
99
+ # Every preview subclass below has a DIFFERENT class name than the widget it
100
+ # previews -- "sCTkTableviewForPreview" rather than "sCTkTableview" -- so the
101
+ # lookup finds no block and the widget is built with an empty theme.
102
+ #
103
+ # For widgets with no theme validation that means an unthemed preview. For
104
+ # the ones that hard-fail on a missing key (sCTkTableview, sCTkSelector,
105
+ # sCTkPathChooser, sCTkSpinbox, the labeled frames) it means a KeyError and a
106
+ # dead preview panel.
107
+ #
108
+ # _THEME_BLOCK_NAME below tells ThemeableWidget which block to read instead.
109
+ # It requires this one-line change in themeable_widget.py:
110
+ #
111
+ # class_name = getattr(self, "_THEME_BLOCK_NAME", None) or self.__class__.__name__
112
+ #
113
+ # The attribute is generally useful beyond preview: any subclass that should
114
+ # inherit its parent's theme rather than silently losing it can set it.
115
+ # =====================================================================
116
+
117
+
118
+ # =====================================================================
119
+ # TRANSPARENT BACKGROUNDS IN THE DESIGN VIEW
120
+ #
121
+ # A theme block may legitimately set a colour key to "transparent", meaning
122
+ # "show whatever is behind me". At runtime that always resolves against a
123
+ # themed parent, so it follows light/dark correctly. The Designer canvas does
124
+ # NOT participate in appearance mode -- it is a fixed light grey -- so a
125
+ # transparent widget rendered on it keeps a light background while its TEXT
126
+ # still follows the appearance mode. In dark mode that leaves dark-on-grey
127
+ # text, or a bright band where a separator should be.
128
+ #
129
+ # Confirmed against sCTkSelector and sCTkSeparator; ten theme blocks currently
130
+ # use "transparent" and any of them can show it, given text or a visible fill.
131
+ #
132
+ # preview_opaque() stamps a concrete background onto a preview subclass so the
133
+ # design view stays legible in both modes. This affects the DESIGNER ONLY --
134
+ # the real widget keeps its transparent background and its runtime appearance
135
+ # is unchanged.
136
+ #
137
+ # Written as one decorator rather than a hand-written __init__ per widget:
138
+ # this is the third widget to hit it and there will be more, so the per-widget
139
+ # version would keep growing.
140
+ PREVIEW_OPAQUE_BG = ("#FFFFFF", "#111827")
141
+
142
+
143
+ def preview_opaque(colour_key="fg_color", colour=PREVIEW_OPAQUE_BG):
144
+ """
145
+ Class decorator giving a preview subclass a concrete background.
146
+
147
+ Args:
148
+ colour_key: The theme key carrying the background. "fg_color" for
149
+ most widgets; sCTkSeparator and sCTkTreeview use "bg_color".
150
+ colour: The (light, dark) pair to substitute. Defaults to the pair
151
+ used by sCTkScrollableFrame, so a stamped widget matches the
152
+ containers it would normally sit inside.
153
+
154
+ Returns:
155
+ The class, with __init__ wrapped to supply the background.
156
+ """
157
+ def decorate(cls):
158
+ original_init = cls.__init__
159
+
160
+ def __init__(self, master=None, **kwargs):
161
+ # setdefault, not assignment: an explicit value set in the
162
+ # Designer inspector must still win.
163
+ kwargs.setdefault(colour_key, colour)
164
+ original_init(self, master, **kwargs)
165
+
166
+ cls.__init__ = __init__
167
+ return cls
168
+ return decorate
169
+
170
+
171
+ #
172
+ # Preview class for sCTkFrame
173
+ #
174
+ @preview_opaque()
175
+ class sCTkFrameForPreview(sCTkFrame):
176
+ _THEME_BLOCK_NAME = "sCTkFrame"
177
+
178
+ def winfo_children(self):
179
+ # CTkFrame has a hidden canvas inside. So, to make it
180
+ # clickable on preview we need a hack.
181
+ return super(tk.Frame, self).winfo_children()
182
+
183
+
184
+ class sCTkFrameLabeledPrimaryForPreview(sCTkFrameLabeledPrimary):
185
+ _THEME_BLOCK_NAME = "sCTkFrameLabeledPrimary"
186
+
187
+ def winfo_children(self):
188
+ # sCTkFrameLabeledPrimary has a hidden canvas inside. So, to make it
189
+ # clickable on preview we need a hack.
190
+ return super(tk.Frame, self).winfo_children()
191
+
192
+
193
+ class sCTkFrameLabeledSecondaryForPreview(sCTkFrameLabeledSecondary):
194
+ """
195
+ Designer preview for sCTkFrameLabeledSecondary.
196
+
197
+ NOT SELECTABLE BY CLICKING. CTkScrollableFrame inverts the usual
198
+ arrangement: the widget IS the inner frame, created inside a canvas owned
199
+ by a separate outer frame. So the surface the user sees is this widget's
200
+ PARENT, and the widget has no real children of its own --
201
+ winfo_children() reports an empty list.
202
+
203
+ That defeats plain forwarding. The Designer's handler ends up on
204
+ _parent_canvas, resolves event.widget to the canvas, and walking up from
205
+ there never reaches this widget -- because the widget is a DESCENDANT of
206
+ the canvas, not an ancestor. Binding the canvas directly is worse: it
207
+ replaces the Designer's own handler.
208
+
209
+ A transparent overlay was tried and Tk refuses it. The idea was to
210
+ separate PARENT from POSITION -- an overlay whose parent is this frame, so
211
+ the Designer resolves it correctly, displayed across the outer canvas via
212
+ place(in_=...). But place requires its target to be the widget's own parent
213
+ or a descendant of that parent, and the canvas is the overlay's
214
+ GRANDPARENT:
215
+
216
+ can't place ".!...!sctkframelabeledprimaryforpreview.!frame"
217
+ relative to ".!...!ctkframe2.!canvas"
218
+
219
+ A child dropped inside IS selectable, because it is a real descendant.
220
+ Select the frame itself from the widget tree.
221
+ """
222
+ _THEME_BLOCK_NAME = "sCTkFrameLabeledSecondary"
223
+
224
+ def winfo_children(self):
225
+ return super(tk.Frame, self).winfo_children()
226
+
227
+
228
+ class sCTkScrollableFrameForPreview(sCTkScrollableFrame):
229
+ """
230
+ Designer preview for sCTkScrollableFrame.
231
+
232
+ NOT SELECTABLE BY CLICKING. CTkScrollableFrame inverts the usual
233
+ arrangement: the widget IS the inner frame, created inside a canvas owned
234
+ by a separate outer frame. So the surface the user sees is this widget's
235
+ PARENT, and the widget has no real children of its own --
236
+ winfo_children() reports an empty list.
237
+
238
+ That defeats plain forwarding. The Designer's handler ends up on
239
+ _parent_canvas, resolves event.widget to the canvas, and walking up from
240
+ there never reaches this widget -- because the widget is a DESCENDANT of
241
+ the canvas, not an ancestor. Binding the canvas directly is worse: it
242
+ replaces the Designer's own handler.
243
+
244
+ A transparent overlay was tried and Tk refuses it. The idea was to
245
+ separate PARENT from POSITION -- an overlay whose parent is this frame, so
246
+ the Designer resolves it correctly, displayed across the outer canvas via
247
+ place(in_=...). But place requires its target to be the widget's own parent
248
+ or a descendant of that parent, and the canvas is the overlay's
249
+ GRANDPARENT:
250
+
251
+ can't place ".!...!sctkframelabeledprimaryforpreview.!frame"
252
+ relative to ".!...!ctkframe2.!canvas"
253
+
254
+ A child dropped inside IS selectable, because it is a real descendant.
255
+ Select the frame itself from the widget tree.
256
+ """
257
+ _THEME_BLOCK_NAME = "sCTkScrollableFrame"
258
+
259
+ def winfo_children(self):
260
+ return super(tk.Frame, self).winfo_children()
261
+
262
+
263
+ class sCTkFileExplorerForPreview(sCTkFileExplorer):
264
+ """
265
+ Designer preview for sCTkFileExplorer.
266
+
267
+ Two problems, the second only visible once the first is fixed.
268
+
269
+ The explorer draws on an internal canvas that CTkFrame hides from
270
+ winfo_children(), so the Designer's binding pass never reached it and the
271
+ widget could not be clicked at all.
272
+
273
+ Exposing the canvas made the OUTER EDGE selectable, but nothing else: the
274
+ path entry, the navigation buttons and every file row are widgets the
275
+ explorer builds for itself, so they are not in the builder's map and a
276
+ click on one resolves to None. They are hidden from the binding pass and
277
+ bound here instead, forwarding to the canvas -- which is where
278
+ CTkFrame.bind() puts the Designer's own handler.
279
+
280
+ Rows are rebuilt on every navigation, so the binding is reapplied after
281
+ each fill.
282
+ """
283
+ _THEME_BLOCK_NAME = "sCTkFileExplorer"
284
+
285
+ def __init__(self, *args, **kwargs):
286
+ super().__init__(*args, **kwargs)
287
+ self._bind_own_parts_to_self()
288
+
289
+ def _own_part_roots(self):
290
+ """
291
+ The widgets this explorer built for itself.
292
+
293
+ Only the two DIRECT children matter -- `top_frame` and
294
+ `main_container` -- because the tree walk in
295
+ _bind_own_parts_to_self() reaches everything beneath them: the scroll
296
+ canvas, the scrollbar, `explorer_frame` and every file row.
297
+
298
+ An earlier version listed `canvas` and `explorer_frame` instead, which
299
+ was wrong twice: `explorer_frame` is a child of the canvas rather than
300
+ of the widget, so excluding it had no effect at this level, and the
301
+ canvas sits inside `main_container` rather than directly under the
302
+ widget -- so the Designer kept binding `main_container`, and clicking
303
+ the empty scrolling area still resolved to None.
304
+
305
+ `_canvas` is deliberately absent: that is CTkFrame's own background,
306
+ where the Designer's handler actually lives, and it must stay visible
307
+ to the binding pass or a click on the outer edge selects nothing.
308
+ """
309
+ return [w for w in (getattr(self, "top_frame", None),
310
+ getattr(self, "main_container", None))
311
+ if w is not None]
312
+
313
+ def _bind_own_parts_to_self(self):
314
+ """Makes a click anywhere inside the explorer select the explorer."""
315
+ def select_self(event, target=self):
316
+ # _canvas, NOT canvas.
317
+ #
318
+ # This widget has TWO canvases: self._canvas, which CTkFrame draws
319
+ # its background on, and self.canvas, the scroll canvas holding the
320
+ # file rows. CTkFrame.bind() redirects every binding to _canvas, so
321
+ # that is where the Designer's click handler ended up.
322
+ #
323
+ # Generating the event on self.canvas dispatched into a widget with
324
+ # nothing bound -- the forward succeeded and nothing happened. It is
325
+ # also why clicking the outer EDGE always worked: the edge is
326
+ # _canvas.
327
+ try:
328
+ canvas = getattr(target, "_canvas", None) or target
329
+ canvas.event_generate("<Button-1>", x=1, y=1, when="now")
330
+ except Exception:
331
+ pass
332
+ return "break"
333
+
334
+ def bind_tree(widget, depth=0):
335
+ if widget is None or depth > 4:
336
+ return
337
+ for w in (widget,
338
+ getattr(widget, "_canvas", None),
339
+ getattr(widget, "_text_label", None)):
340
+ if w is None:
341
+ continue
342
+ try:
343
+ w.bind("<Button-1>", select_self)
344
+ except Exception:
345
+ pass
346
+ try:
347
+ children = tk.Misc.winfo_children(widget)
348
+ except Exception:
349
+ children = []
350
+ for child in children:
351
+ bind_tree(child, depth + 1)
352
+
353
+ for root in self._own_part_roots():
354
+ bind_tree(root)
355
+
356
+ def _fill_explorer(self, *args, **kwargs):
357
+ """Rebinds the rows, which this call destroys and recreates."""
358
+ result = super()._fill_explorer(*args, **kwargs)
359
+ self._bind_own_parts_to_self()
360
+ return result
361
+
362
+ def winfo_children(self):
363
+ """
364
+ Hides the explorer's own parts from the Designer's binding pass.
365
+
366
+ The internal canvas is KEPT -- it is the visible background, and
367
+ dropping it would stop a click on empty space selecting anything.
368
+ """
369
+ own = set(self._own_part_roots())
370
+ return [w for w in super(tk.Frame, self).winfo_children() if w not in own]
371
+
372
+
373
+ class sCTkPathChooserForPreview(sCTkPathChooser):
374
+ """
375
+ Designer preview for sCTkPathChooser.
376
+
377
+ The canvas hack alone made only the outer EDGE selectable. The entry and
378
+ the browse button are widgets this chooser builds for itself, so they are
379
+ not in the builder's map and a click on one resolved to None -- and they
380
+ cover most of the widget.
381
+
382
+ They are hidden from the Designer's binding pass and bound here instead,
383
+ forwarding to _canvas, which is where CTkFrame.bind() puts the Designer's
384
+ own handler. Same treatment as sCTkFileExplorer and sCTkDialog.
385
+ """
386
+ _THEME_BLOCK_NAME = "sCTkPathChooser"
387
+
388
+ def __init__(self, *args, **kwargs):
389
+ super().__init__(*args, **kwargs)
390
+ self._bind_own_parts_to_self()
391
+
392
+ def _own_part_roots(self):
393
+ """The widgets this chooser builds for itself."""
394
+ return [w for w in (getattr(self, "entry", None),
395
+ getattr(self, "btn", None))
396
+ if w is not None]
397
+
398
+ def _bind_own_parts_to_self(self):
399
+ """Makes a click on the entry or the button select the chooser."""
400
+ def select_self(event, target=self):
401
+ try:
402
+ canvas = getattr(target, "_canvas", None) or target
403
+ canvas.event_generate("<Button-1>", x=1, y=1, when="now")
404
+ except Exception:
405
+ pass
406
+ return "break"
407
+
408
+ def bind_tree(widget, depth=0):
409
+ if widget is None or depth > 3:
410
+ return
411
+ for w in (widget,
412
+ getattr(widget, "_canvas", None),
413
+ getattr(widget, "_text_label", None),
414
+ getattr(widget, "_entry", None)):
415
+ if w is None:
416
+ continue
417
+ try:
418
+ w.bind("<Button-1>", select_self)
419
+ except Exception:
420
+ pass
421
+ try:
422
+ children = tk.Misc.winfo_children(widget)
423
+ except Exception:
424
+ children = []
425
+ for child in children:
426
+ bind_tree(child, depth + 1)
427
+
428
+ for root in self._own_part_roots():
429
+ bind_tree(root)
430
+
431
+ def winfo_children(self):
432
+ """
433
+ Hides the chooser's own parts from the Designer's binding pass.
434
+
435
+ The internal canvas is KEPT -- it is the visible background, and
436
+ dropping it would stop a click on empty space selecting anything.
437
+ """
438
+ own = set(self._own_part_roots())
439
+ return [w for w in super(tk.Frame, self).winfo_children()
440
+ if w not in own]
441
+
442
+
443
+ class sCTkTableviewForPreview(sCTkTableview):
444
+ _THEME_BLOCK_NAME = "sCTkTableview"
445
+
446
+ def winfo_children(self):
447
+ internal = []
448
+ internal.extend(self._header_widgets)
449
+ for row in self._cell_widgets:
450
+ internal.extend(row)
451
+ clist = [self._scrollbar]
452
+ for widget in internal:
453
+ for cwidget in widget.winfo_children():
454
+ clist.append(cwidget)
455
+ return clist
456
+
457
+
458
+ # sCTkSeparator carries its transparency on bg_color rather than fg_color, so
459
+ # the decorator is told which key to stamp. Its _draw() calls
460
+ # _detect_color_of_master() and falls back to the CTk theme's own fg_color when
461
+ # that returns transparent, which on the Designer canvas produces a bright band
462
+ # in dark mode -- more conspicuous than the Selector's low-contrast text,
463
+ # because the band is the full canvas height rather than a few glyphs.
464
+ @preview_opaque(colour_key="bg_color")
465
+ class sCTkSeparatorForPreview(sCTkSeparator):
466
+ _THEME_BLOCK_NAME = "sCTkSeparator"
467
+
468
+
469
+ @preview_opaque()
470
+ class sCTkSegmentedButtonForPreview(sCTkSegmentedButton):
471
+ """
472
+ Designer preview for sCTkSegmentedButton.
473
+
474
+ WITHOUT THIS THE DESIGNER CRASHES ON DROP. CTkSegmentedButton.bind() raises
475
+ NotImplementedError unconditionally, and pygubu's bind_preview_widget()
476
+ calls bind() on every widget it walks:
477
+
478
+ File ".../ctk_segmented_button.py", line 471, in bind
479
+ raise NotImplementedError
480
+ NotImplementedError
481
+
482
+ Forwarding to the children instead lets the walk complete. CustomTkinter's
483
+ own designer plugin does exactly this for its CTkSegmentedButton, with the
484
+ note that selection still does not work -- their comment reads "I can't
485
+ select a segmented button in preview". So this makes the widget usable in
486
+ the Designer without making it selectable on the canvas; select it from the
487
+ widget tree.
488
+
489
+ SELECTION. CustomTkinter's own attempt stops at "I can't select a
490
+ segmented button in preview". The same problem was solved for sCTkDialog
491
+ earlier, and the same solution applies here:
492
+
493
+ - Pygubu resolves a clicked widget through builder.get_widget_id(). The
494
+ segments are CTkButtons this widget creates for itself, so they are not
495
+ in the builder's map and a click on one resolves to None.
496
+ - winfo_children() below hides them from pygubu's binding pass, so it
497
+ binds this widget and not the segments.
498
+ - _bind_segments_to_self() then binds them to forward their click here.
499
+ - The forwarded event goes to self._canvas, NOT to self. CTkFrame.bind()
500
+ redirects every binding to its internal canvas, so that is where
501
+ pygubu's handler actually ended up -- generating the event on the
502
+ widget itself dispatches into nothing.
503
+ """
504
+ _THEME_BLOCK_NAME = "sCTkSegmentedButton"
505
+
506
+ def __init__(self, master=None, **kw):
507
+ super().__init__(master, **kw)
508
+ self._bind_segments_to_self()
509
+
510
+ def _segment_widgets(self):
511
+ """The CTkButtons this widget builds for itself, canvas excluded."""
512
+ return [w for w in super(tk.Frame, self).winfo_children()
513
+ if isinstance(w, ctk.CTkButton)]
514
+
515
+ def _bind_segments_to_self(self):
516
+ """
517
+ Makes a click on any segment select the whole widget.
518
+
519
+ A CTkButton draws on an internal canvas that receives the click before
520
+ the widget does, and puts its label in a separate tk.Label, so all
521
+ three are bound.
522
+ """
523
+ def select_self(event, target=self):
524
+ try:
525
+ canvas = getattr(target, "_canvas", None) or target
526
+ canvas.event_generate("<Button-1>", x=1, y=1, when="now")
527
+ except Exception:
528
+ pass
529
+ return "break"
530
+
531
+ for segment in self._segment_widgets():
532
+ for widget in (segment,
533
+ getattr(segment, "_canvas", None),
534
+ getattr(segment, "_text_label", None)):
535
+ if widget is None:
536
+ continue
537
+ try:
538
+ widget.bind("<Button-1>", select_self)
539
+ except Exception:
540
+ pass
541
+
542
+ def configure(self, *args, **kwargs):
543
+ """
544
+ Rebinds after any change that could rebuild the segments.
545
+
546
+ Setting `values` destroys the existing buttons and creates new ones,
547
+ which would otherwise be left unbound.
548
+ """
549
+ result = super().configure(*args, **kwargs)
550
+ if not (len(args) == 1 and not isinstance(args[0], dict)):
551
+ self._bind_segments_to_self()
552
+ return result
553
+
554
+ def winfo_children(self):
555
+ """
556
+ Hides the segments from the Designer's binding pass.
557
+
558
+ Pygubu binds a click handler to everything it finds here, and that
559
+ handler resolves the clicked widget through get_widget_id() -- which
560
+ knows nothing about buttons this widget created for itself. Excluding
561
+ them leaves the bindings installed by _bind_segments_to_self() intact.
562
+
563
+ The internal canvas is KEPT: it is this widget's visible background,
564
+ and dropping it would stop a click on empty space selecting anything.
565
+ """
566
+ return [w for w in super(tk.Frame, self).winfo_children()
567
+ if not isinstance(w, ctk.CTkButton)]
568
+
569
+ def bind(self, sequence=None, func=None, add=None):
570
+ for child in self.winfo_children():
571
+ try:
572
+ child.bind(sequence, func, True)
573
+ except Exception:
574
+ # A child that refuses the binding must not take the whole
575
+ # walk down with it -- that is the failure being fixed here.
576
+ pass
577
+
578
+
579
+ class sCTkRadioButtonForPreview(sCTkRadioButton):
580
+ """
581
+ Designer preview for sCTkRadioButton.
582
+
583
+ Without this the widget cannot be selected by clicking it: CTkFrame hides
584
+ its internal canvas from winfo_children(), the Designer walks that list to
585
+ bind its click handler, and the canvas is the only thing there is to click.
586
+ """
587
+ _THEME_BLOCK_NAME = "sCTkRadioButton"
588
+
589
+ def winfo_children(self):
590
+ return super(tk.Frame, self).winfo_children()
591
+
592
+
593
+ class sCTkFrequencyDisplayForPreview(sCTkFrequencyDisplay):
594
+ """
595
+ Designer preview for sCTkFrequencyDisplay.
596
+
597
+ The readout draws itself on a canvas filling the whole widget, and
598
+ CTkFrame hides its own background canvas from winfo_children() -- so the
599
+ Designer's binding walk finds nothing to bind and the widget cannot be
600
+ selected at all.
601
+
602
+ Exposing the canvases is only half of it: clicking a digit is a real
603
+ interaction, so without neutralizing it a click selects a digit instead
604
+ of the widget and there is no way to select the widget itself.
605
+ """
606
+ _THEME_BLOCK_NAME = "sCTkFrequencyDisplay"
607
+
608
+ def winfo_children(self):
609
+ return super(tk.Frame, self).winfo_children()
610
+
611
+
612
+ class sCTkNotebookForPreview(sCTkNotebook):
613
+ """
614
+ Designer preview for sCTkNotebook.
615
+
616
+ The notebook draws itself on a canvas that fills the whole widget, and
617
+ CTkFrame hides its own background canvas from winfo_children() -- so the
618
+ Designer's binding walk found nothing to bind and the widget could not be
619
+ selected at all, empty or not.
620
+
621
+ Exposing the canvases is only half of it. Clicking a tab is a real
622
+ interaction: without configure_for_preview() neutralizing it, a click on
623
+ the strip switches tabs instead of selecting the notebook, and there is no
624
+ way to select the widget itself.
625
+ """
626
+ _THEME_BLOCK_NAME = "sCTkNotebook"
627
+
628
+ def winfo_children(self):
629
+ return super(tk.Frame, self).winfo_children()
630
+
631
+
632
+ class sCTkTabviewForPreview(sCTkTabview):
633
+ """
634
+ Designer preview for sCTkTabview.
635
+
636
+ An EMPTY tabview could not be selected at all -- with no tabs there is
637
+ nothing on the canvas but the widget's own hidden background, which the
638
+ Designer never saw. Once a tab existed, the tab button was clickable and
639
+ masked the problem.
640
+
641
+ The internal CTkSegmentedButton is EXCLUDED from that list, for two
642
+ reasons. Its bind() raises NotImplementedError unconditionally, so the
643
+ Designer's binding walk crashed on load:
644
+
645
+ File ".../ctk_segmented_button.py", line 471, in bind
646
+ raise NotImplementedError
647
+
648
+ And leaving it unbound keeps tab switching working -- the clicks go to
649
+ CustomTkinter's own handler rather than being intercepted for selection.
650
+
651
+ THE CONSEQUENCE is that clicking a tab switches the page without selecting
652
+ that tab in the inspector, which goes on showing whatever was selected
653
+ before. Clicking a widget inside the new page corrects it.
654
+
655
+ An attempt to fix that has been removed rather than left in place. Binding
656
+ the segmented button's inner buttons with add=True worked as far as it
657
+ went -- the tab switched, the handler fired, and the newly revealed page
658
+ was found -- but forwarding a click to that page selected nothing, because
659
+ the pages are sCTkFrames this widget creates in add() at runtime and are
660
+ not in the builder's widget map. get_widget_id() returns None for them, the
661
+ same reason sCTkDialog's own parts did not resolve.
662
+
663
+ Select tabs from the widget tree. CustomTkinter's own plugin carries a
664
+ commented-out attempt at the same problem.
665
+ """
666
+ _THEME_BLOCK_NAME = "sCTkTabview"
667
+
668
+ def winfo_children(self):
669
+ children = super(tk.Frame, self).winfo_children()
670
+ segmented = getattr(self, "_segmented_button", None)
671
+ if segmented is None:
672
+ return children
673
+ return [w for w in children if w is not segmented]
674
+
675
+
676
+ class sCTkSelectorForPreview(sCTkSelector):
677
+ _THEME_BLOCK_NAME = "sCTkSelector"
678
+
679
+ def winfo_children(self):
680
+ internal = [
681
+ self.search_bar,
682
+ self.checkboxes_frame,
683
+ self.checkboxes_frame._parent_frame,
684
+ self.checkboxes_frame._parent_canvas,
685
+ ]
686
+ clist = []
687
+ for widget in internal:
688
+ for cwidget in widget.winfo_children():
689
+ clist.append(cwidget)
690
+ if isinstance(cwidget, sCTkCheckBox):
691
+ clist.append(cwidget._text_label)
692
+ clist.append(cwidget._canvas)
693
+ return clist
694
+
695
+
696
+ class sCTkOptionMenuSecondaryForPreview(sCTkOptionMenuSecondary):
697
+ _THEME_BLOCK_NAME = "sCTkOptionMenuSecondary"
698
+
699
+ def winfo_children(self):
700
+ """
701
+ FIX: this used to walk self._menu, an inner CTkOptionMenu, because the
702
+ widget was a COMPOSITE -- a frame wrapping a menu, which was how it got
703
+ a border that native CTkOptionMenu cannot draw. It is now a plain
704
+ CTkOptionMenu subclass like Primary, with the border supplied by
705
+ sCTkOptionMenuBorderMixin, so that attribute is gone and the walk
706
+ raised:
707
+
708
+ AttributeError: 'sCTkOptionMenuSecondaryForPreview' object has no
709
+ attribute '_menu'
710
+
711
+ The ordinary hack applies instead: CTkFrame hides its internal canvas
712
+ from winfo_children(), and the Designer needs to see it to hit-test a
713
+ click.
714
+ """
715
+ return super(tk.Frame, self).winfo_children()
716
+
717
+
718
+ class sCTkSpinboxForPreview(sCTkSpinbox):
719
+ _THEME_BLOCK_NAME = "sCTkSpinbox"
720
+
721
+ def winfo_children(self):
722
+ internal = [
723
+ self.entry
724
+ ]
725
+ clist = []
726
+ for widget in internal:
727
+ for cwidget in widget.winfo_children():
728
+ clist.append(cwidget)
729
+ return clist
730
+
731
+
732
+ #
733
+ # Builder for Preview
734
+ #
735
+ class sCTkFramePreviewBO(sCTkFrameBO):
736
+ class_ = sCTkFrameForPreview
737
+
738
+
739
+ class sCTkFrameLabeledPrimaryForPreviewBO(sCTkFrameLabeledPrimaryBO):
740
+ class_ = sCTkFrameLabeledPrimaryForPreview
741
+
742
+
743
+ class sCTkFrameLabeledSecondaryForPreviewBO(sCTkFrameLabeledSecondaryBO):
744
+ class_ = sCTkFrameLabeledSecondaryForPreview
745
+
746
+
747
+ class sCTkScrollableFrameForPreviewBO(sCTkScrollableFrameBO):
748
+ class_ = sCTkScrollableFrameForPreview
749
+
750
+
751
+ class sCTkFileExplorerForPreviewBO(sCTkFileExplorerBO):
752
+ class_ = sCTkFileExplorerForPreview
753
+
754
+
755
+ class sCTkPathChooserForPreviewBO(sCTkPathChooserBO):
756
+ class_ = sCTkPathChooserForPreview
757
+
758
+
759
+ class sCTkTableviewForPreviewBO(sCTkTableviewBO):
760
+ class_ = sCTkTableviewForPreview
761
+
762
+
763
+ class sCTkDialContinuousForPreview(sCTkDialContinuous):
764
+ """
765
+ Designer preview for sCTkDialContinuous.
766
+
767
+ Without this the dial cannot be selected by clicking it. Every dial draws
768
+ itself on an internal canvas that CTkFrame hides from winfo_children(),
769
+ and the Designer walks that list to bind its click handler -- so it never
770
+ reached the only part of the widget there is to click.
771
+ """
772
+ _THEME_BLOCK_NAME = "sCTkDialContinuous"
773
+
774
+ def _inject_private_layer_bindings(self):
775
+ """
776
+ Deliberately does nothing in the Designer.
777
+
778
+ The real method binds the scroll sequences on three layers -- the dial
779
+ canvas, the widget, and CTkFrame's canvas -- with add="+", and it is
780
+ scheduled 50ms after construction. Both details defeat
781
+ configure_for_preview(): it has already run by then, and add="+"
782
+ appends rather than replaces, so the dial's own handlers sit alongside
783
+ the no-ops instead of being displaced by them.
784
+
785
+ The visible symptom was a dial that still turned under a trackpad in
786
+ the design canvas. Not injecting at all is simpler than trying to
787
+ neutralize afterwards, and a preview dial has no reason to scroll.
788
+ """
789
+ return
790
+
791
+ def state(self, mode=None):
792
+ """
793
+ Re-applies the Designer's bindings after a state change.
794
+
795
+ A dial's state() rebinds its own canvas handlers -- <Button-1>,
796
+ <Button-2>, <Button-3> and the scroll sequences -- every time it
797
+ returns to normal, and clears them when disabled. That wipes out
798
+ whatever configure_for_preview() installed, so a dial stopped being
799
+ selectable the moment its state was touched and did not recover when
800
+ set back to normal.
801
+
802
+ Deferred to idle so it runs after state() has finished its own
803
+ rebinding rather than racing it.
804
+ """
805
+ result = super().state(mode) if mode is not None else super().state()
806
+ if mode is not None:
807
+ try:
808
+ self.after_idle(self._reapply_preview_bindings)
809
+ except Exception:
810
+ pass
811
+ return result
812
+
813
+ def _reapply_preview_bindings(self):
814
+ """
815
+ Silences the dial's interactive bindings and forwards clicks for
816
+ selection -- the same treatment configure_for_preview() applies, in a
817
+ form the widget can re-run for itself.
818
+ """
819
+ face = getattr(self, "canvas", None)
820
+ if face is None:
821
+ return
822
+
823
+ for seq in (s for s in (_HOVER_CLICK + _DIAL_EXTRA + _SCROLL)
824
+ if s != "<Button-1>"):
825
+ try:
826
+ face.bind(seq, _no_op)
827
+ except Exception:
828
+ pass
829
+
830
+ def select_self(event, target=self):
831
+ try:
832
+ bg = getattr(target, "_canvas", None)
833
+ if bg is not None:
834
+ bg.event_generate("<Button-1>", x=1, y=1, when="now")
835
+ except Exception:
836
+ pass
837
+ return "break"
838
+
839
+ try:
840
+ face.bind("<Button-1>", select_self)
841
+ except Exception:
842
+ pass
843
+
844
+ def winfo_children(self):
845
+ """
846
+ Hides the dial's own drawing canvas from the Designer's binding pass,
847
+ keeping CTkFrame's background canvas so edge clicks still work.
848
+ """
849
+ face = getattr(self, "canvas", None)
850
+ return [w for w in super(tk.Frame, self).winfo_children() if w is not face]
851
+
852
+
853
+ class sCTkDialRangeForPreview(sCTkDialRange):
854
+ """Designer preview for sCTkDialRange. See sCTkDialContinuousForPreview."""
855
+ _THEME_BLOCK_NAME = "sCTkDialRange"
856
+
857
+ def _inject_private_layer_bindings(self):
858
+ """
859
+ Deliberately does nothing in the Designer.
860
+
861
+ The real method binds the scroll sequences on three layers -- the dial
862
+ canvas, the widget, and CTkFrame's canvas -- with add="+", and it is
863
+ scheduled 50ms after construction. Both details defeat
864
+ configure_for_preview(): it has already run by then, and add="+"
865
+ appends rather than replaces, so the dial's own handlers sit alongside
866
+ the no-ops instead of being displaced by them.
867
+
868
+ The visible symptom was a dial that still turned under a trackpad in
869
+ the design canvas. Not injecting at all is simpler than trying to
870
+ neutralize afterwards, and a preview dial has no reason to scroll.
871
+ """
872
+ return
873
+
874
+ def state(self, mode=None):
875
+ """
876
+ Re-applies the Designer's bindings after a state change.
877
+
878
+ A dial's state() rebinds its own canvas handlers -- <Button-1>,
879
+ <Button-2>, <Button-3> and the scroll sequences -- every time it
880
+ returns to normal, and clears them when disabled. That wipes out
881
+ whatever configure_for_preview() installed, so a dial stopped being
882
+ selectable the moment its state was touched and did not recover when
883
+ set back to normal.
884
+
885
+ Deferred to idle so it runs after state() has finished its own
886
+ rebinding rather than racing it.
887
+ """
888
+ result = super().state(mode) if mode is not None else super().state()
889
+ if mode is not None:
890
+ try:
891
+ self.after_idle(self._reapply_preview_bindings)
892
+ except Exception:
893
+ pass
894
+ return result
895
+
896
+ def _reapply_preview_bindings(self):
897
+ """
898
+ Silences the dial's interactive bindings and forwards clicks for
899
+ selection -- the same treatment configure_for_preview() applies, in a
900
+ form the widget can re-run for itself.
901
+ """
902
+ face = getattr(self, "canvas", None)
903
+ if face is None:
904
+ return
905
+
906
+ for seq in (s for s in (_HOVER_CLICK + _DIAL_EXTRA + _SCROLL)
907
+ if s != "<Button-1>"):
908
+ try:
909
+ face.bind(seq, _no_op)
910
+ except Exception:
911
+ pass
912
+
913
+ def select_self(event, target=self):
914
+ try:
915
+ bg = getattr(target, "_canvas", None)
916
+ if bg is not None:
917
+ bg.event_generate("<Button-1>", x=1, y=1, when="now")
918
+ except Exception:
919
+ pass
920
+ return "break"
921
+
922
+ try:
923
+ face.bind("<Button-1>", select_self)
924
+ except Exception:
925
+ pass
926
+
927
+ def winfo_children(self):
928
+ """
929
+ Hides the dial's own drawing canvas from the Designer's binding pass,
930
+ keeping CTkFrame's background canvas so edge clicks still work.
931
+ """
932
+ face = getattr(self, "canvas", None)
933
+ return [w for w in super(tk.Frame, self).winfo_children() if w is not face]
934
+
935
+
936
+ class sCTkDialSelectorForPreview(sCTkDialSelector):
937
+ """Designer preview for sCTkDialSelector. See sCTkDialContinuousForPreview."""
938
+ _THEME_BLOCK_NAME = "sCTkDialSelector"
939
+
940
+ def _inject_private_layer_bindings(self):
941
+ """
942
+ Deliberately does nothing in the Designer.
943
+
944
+ The real method binds the scroll sequences on three layers -- the dial
945
+ canvas, the widget, and CTkFrame's canvas -- with add="+", and it is
946
+ scheduled 50ms after construction. Both details defeat
947
+ configure_for_preview(): it has already run by then, and add="+"
948
+ appends rather than replaces, so the dial's own handlers sit alongside
949
+ the no-ops instead of being displaced by them.
950
+
951
+ The visible symptom was a dial that still turned under a trackpad in
952
+ the design canvas. Not injecting at all is simpler than trying to
953
+ neutralize afterwards, and a preview dial has no reason to scroll.
954
+ """
955
+ return
956
+
957
+ def state(self, mode=None):
958
+ """
959
+ Re-applies the Designer's bindings after a state change.
960
+
961
+ A dial's state() rebinds its own canvas handlers -- <Button-1>,
962
+ <Button-2>, <Button-3> and the scroll sequences -- every time it
963
+ returns to normal, and clears them when disabled. That wipes out
964
+ whatever configure_for_preview() installed, so a dial stopped being
965
+ selectable the moment its state was touched and did not recover when
966
+ set back to normal.
967
+
968
+ Deferred to idle so it runs after state() has finished its own
969
+ rebinding rather than racing it.
970
+ """
971
+ result = super().state(mode) if mode is not None else super().state()
972
+ if mode is not None:
973
+ try:
974
+ self.after_idle(self._reapply_preview_bindings)
975
+ except Exception:
976
+ pass
977
+ return result
978
+
979
+ def _reapply_preview_bindings(self):
980
+ """
981
+ Silences the dial's interactive bindings and forwards clicks for
982
+ selection -- the same treatment configure_for_preview() applies, in a
983
+ form the widget can re-run for itself.
984
+ """
985
+ face = getattr(self, "canvas", None)
986
+ if face is None:
987
+ return
988
+
989
+ for seq in (s for s in (_HOVER_CLICK + _DIAL_EXTRA + _SCROLL)
990
+ if s != "<Button-1>"):
991
+ try:
992
+ face.bind(seq, _no_op)
993
+ except Exception:
994
+ pass
995
+
996
+ def select_self(event, target=self):
997
+ try:
998
+ bg = getattr(target, "_canvas", None)
999
+ if bg is not None:
1000
+ bg.event_generate("<Button-1>", x=1, y=1, when="now")
1001
+ except Exception:
1002
+ pass
1003
+ return "break"
1004
+
1005
+ try:
1006
+ face.bind("<Button-1>", select_self)
1007
+ except Exception:
1008
+ pass
1009
+
1010
+ def winfo_children(self):
1011
+ """
1012
+ Hides the dial's own drawing canvas from the Designer's binding pass,
1013
+ keeping CTkFrame's background canvas so edge clicks still work.
1014
+ """
1015
+ face = getattr(self, "canvas", None)
1016
+ return [w for w in super(tk.Frame, self).winfo_children() if w is not face]
1017
+
1018
+
1019
+ class sCTkDialogForPreview(sCTkDialog):
1020
+ """
1021
+ Designer preview for sCTkDialog.
1022
+
1023
+ _MAKE_WINDOW = False is the whole point. At runtime this widget builds its
1024
+ own sCTkDialogToplevel and packs itself into it -- a dialog is always its
1025
+ own window. In the Designer that would spawn a real, separate, possibly
1026
+ MODAL window on every redraw, and a redraw happens on every property edit.
1027
+ A modal one would seize input and leave the Designer unusable.
1028
+
1029
+ With the flag off, sCTkDialog.__init__ takes its early return and
1030
+ behaves as an ordinary frame, so the dialog renders inline on the canvas
1031
+ where it can be laid out.
1032
+
1033
+ The window properties -- title, width, height, modal, offset_x, offset_y --
1034
+ remain editable in the inspector and still reach generated code. They
1035
+ simply have no effect on the preview, because there is no window for them
1036
+ to act on.
1037
+
1038
+ _THEME_BLOCK_NAME names the block this preview reads. It must be
1039
+ "sCTkDialog", not "sCTkFrame": the dialog now has a block of its own
1040
+ carrying heading_font and heading_text_color, and those keys are required.
1041
+ Pointing at sCTkFrame's block resolved without them and construction failed
1042
+ with a KeyError naming a block the widget never reads.
1043
+ """
1044
+ _THEME_BLOCK_NAME = "sCTkDialog"
1045
+ _MAKE_WINDOW = False
1046
+
1047
+ def __init__(self, master=None, **kw):
1048
+ super().__init__(master, **kw)
1049
+ self._bind_own_parts_to_self()
1050
+
1051
+ def _bind_own_parts_to_self(self):
1052
+ """
1053
+ Makes a click on the heading or a button select the DIALOG.
1054
+
1055
+ The Designer resolves a clicked widget through
1056
+ builder.get_widget_id(). The heading label and the three buttons are
1057
+ built by the dialog rather than by the builder, so they are not in its
1058
+ map: a click on one resolved to None and selected nothing. Only the
1059
+ very edge of the dialog -- its own background canvas -- worked.
1060
+
1061
+ Forwarding the click to the dialog gives the Designer a widget it knows
1062
+ about, which is what the user meant.
1063
+
1064
+ Done HERE, in the preview class's own __init__, rather than in
1065
+ configure_for_preview(): that hook was not reaching this widget, and
1066
+ this runs unconditionally at construction. It survives the Designer's
1067
+ own binding pass because winfo_children() below excludes these parts,
1068
+ so that pass never visits them.
1069
+
1070
+ CTk buttons draw on an internal canvas that receives the click before
1071
+ the widget does, so _canvas and _text_label are bound as well as the
1072
+ widget itself.
1073
+ """
1074
+ def select_dialog(event, dialog=self):
1075
+ # The event goes to the dialog's internal CANVAS, not to the
1076
+ # dialog widget.
1077
+ #
1078
+ # CTkFrame.bind() redirects every binding to self._canvas rather
1079
+ # than attaching it to the frame -- the same override that made
1080
+ # scroll bindings silently vanish in sCTkScrollableFrame. So when
1081
+ # the Designer bound its click handler to this dialog, the binding
1082
+ # landed on the canvas. Generating the event on the dialog found
1083
+ # nothing bound there and did nothing, which is why clicks were
1084
+ # forwarded successfully and still selected nothing.
1085
+ #
1086
+ # It also explains why clicking the very edge always worked: the
1087
+ # edge IS the canvas.
1088
+ target = getattr(dialog, "_canvas", None) or dialog
1089
+ try:
1090
+ target.event_generate("<Button-1>", x=1, y=1, when="now")
1091
+ except Exception:
1092
+ pass
1093
+ return "break"
1094
+
1095
+ # contentFrame is included, but only its CANVAS gets bound below --
1096
+ # the frame itself stays visible to the Designer's own binding pass so
1097
+ # that widgets the user drops inside it still select themselves. In
1098
+ # practice a user drops a frame in here to hold their own layout, and
1099
+ # clicking that frame should select it, not the dialog; clicking the
1100
+ # bare content area around it should reach the dialog, since there is
1101
+ # nothing else there to select.
1102
+ parts = [getattr(self, "heading_Label", None),
1103
+ getattr(self, "titleFrame", None),
1104
+ getattr(self, "actionFrame", None),
1105
+ getattr(self, "contentFrame", None)]
1106
+ for name in ("apply", "cancel", "reset"):
1107
+ parts.append(getattr(self, f"{name}_Button", None))
1108
+
1109
+ content = getattr(self, "contentFrame", None)
1110
+ for part in parts:
1111
+ if part is None:
1112
+ continue
1113
+ if part is content:
1114
+ # Canvas only. Binding the frame itself would intercept clicks
1115
+ # meant for the user's own widgets inside it.
1116
+ targets = (getattr(part, "_canvas", None),)
1117
+ else:
1118
+ targets = (part,
1119
+ getattr(part, "_canvas", None),
1120
+ getattr(part, "_text_label", None))
1121
+ for target in targets:
1122
+ if target is None:
1123
+ continue
1124
+ try:
1125
+ target.bind("<Button-1>", select_dialog)
1126
+ except Exception:
1127
+ pass
1128
+
1129
+ def set_buttons(self, count):
1130
+ """Rebuilds the button row, then re-binds the new buttons."""
1131
+ super().set_buttons(count)
1132
+ self._bind_own_parts_to_self()
1133
+
1134
+ def winfo_children(self):
1135
+ """
1136
+ Hides this widget's OWN parts from the Designer's binding pass.
1137
+
1138
+ pygubu walks winfo_children() binding a click handler to everything it
1139
+ finds, and that handler resolves the clicked widget through
1140
+ builder.get_widget_id(). The heading label and the three buttons are
1141
+ built by the dialog, not by the builder, so they are not in its map:
1142
+ clicking one resolved to None and selected nothing.
1143
+
1144
+ Returning only the content area means pygubu binds the dialog itself
1145
+ and whatever the user put inside it. The dialog's own parts are bound
1146
+ separately, in _bind_own_parts_to_self() -- and because they are not in
1147
+ this list, pygubu's pass does not visit them and cannot overwrite those
1148
+ bindings.
1149
+ """
1150
+ children = super(tk.Frame, self).winfo_children()
1151
+ content = getattr(self, "contentFrame", None)
1152
+ if content is None:
1153
+ return children
1154
+
1155
+ # Keep the internal canvas: it IS the dialog's visible background, so
1156
+ # dropping it stopped a click on empty space selecting anything.
1157
+ # Everything else the dialog builds for itself is excluded.
1158
+ own_parts = {getattr(self, name, None) for name in (
1159
+ "titleFrame", "actionFrame", "heading_Label",
1160
+ "apply_Button", "cancel_Button", "reset_Button")}
1161
+ keep = [w for w in children if w not in own_parts]
1162
+ if content not in keep:
1163
+ keep.append(content)
1164
+ return keep
1165
+
1166
+
1167
+ class sCTkSegmentedButtonForPreviewBO(sCTkSegmentedButtonBO):
1168
+ class_ = sCTkSegmentedButtonForPreview
1169
+
1170
+
1171
+ class sCTkRadioButtonForPreviewBO(sCTkRadioButtonBO):
1172
+ class_ = sCTkRadioButtonForPreview
1173
+
1174
+
1175
+ class sCTkFrequencyDisplayForPreviewBO(sCTkFrequencyDisplayBO):
1176
+ class_ = sCTkFrequencyDisplayForPreview
1177
+
1178
+
1179
+ class sCTkNotebookForPreviewBO(sCTkNotebookBO):
1180
+ class_ = sCTkNotebookForPreview
1181
+
1182
+
1183
+ class sCTkTabviewForPreviewBO(sCTkTabviewBO):
1184
+ class_ = sCTkTabviewForPreview
1185
+
1186
+
1187
+ class sCTkSelectorForPreviewBO(sCTkSelectorBO):
1188
+ class_ = sCTkSelectorForPreview
1189
+
1190
+
1191
+ class sCTkSeparatorForPreviewBO(sCTkSeparatorBuilder):
1192
+ class_ = sCTkSeparatorForPreview
1193
+
1194
+
1195
+ class sCTkDialContinuousForPreviewBO(sCTkDialContinuousBO):
1196
+ class_ = sCTkDialContinuousForPreview
1197
+
1198
+
1199
+ class sCTkDialRangeForPreviewBO(sCTkDialRangeBO):
1200
+ class_ = sCTkDialRangeForPreview
1201
+
1202
+
1203
+ class sCTkDialSelectorForPreviewBO(sCTkDialSelectorBO):
1204
+ class_ = sCTkDialSelectorForPreview
1205
+
1206
+
1207
+ class sCTkDialogForPreviewBO(sCTkDialogBO):
1208
+ class_ = sCTkDialogForPreview
1209
+
1210
+ def realize(self, parent, extra_init_args: dict = None):
1211
+ """
1212
+ Builds the preview WITHOUT the window properties.
1213
+
1214
+ sCTkDialogBO.realize() passes title/width/height/modal and the
1215
+ offsets to the constructor. With _MAKE_WINDOW False there is no
1216
+ toplevel to receive them, and modal=True in particular must not reach
1217
+ anything -- so they are dropped rather than passed and ignored.
1218
+ """
1219
+ master = parent.get_child_master() if hasattr(parent, "get_child_master") else parent
1220
+ self.widget = self.class_(master)
1221
+ return self.widget
1222
+
1223
+
1224
+ class sCTkOptionMenuSecondaryForPreviewBO(sCTkOptionMenuSecondaryBO):
1225
+ class_ = sCTkOptionMenuSecondaryForPreview
1226
+
1227
+
1228
+ class sCTkSpinboxForPreviewBO(sCTkSpinboxBO):
1229
+ class_ = sCTkSpinboxForPreview
1230
+
1231
+
1232
+ # =====================================================================
1233
+ # TOP-LEVEL WIDGET PREVIEWS: sCTkToplevel and sCTk
1234
+ #
1235
+ # A top-level window can't be previewed as a real window inside the designer
1236
+ # canvas, so pygubu renders it as a FRAME instead. That's why the factory
1237
+ # classes below list sCTkFrameForPreview among their bases rather than
1238
+ # sCTkToplevel: the preview IS a frame wearing the toplevel's properties.
1239
+ #
1240
+ # The consequence is that a handful of options a Toplevel accepts and a Frame
1241
+ # does not have to be routed around CTk's own configure(). That's what the
1242
+ # two mixins do -- they intercept those names and push them straight to the
1243
+ # underlying tkinter widget via super(CTkBaseClass, self).
1244
+ #
1245
+ # Mirrors CustomTkinter's own designer preview module.
1246
+ # =====================================================================
1247
+ class sCTkToplevelPreviewMixin:
1248
+ def configure(self, cnf=None, **kw):
1249
+ if cnf:
1250
+ return super().configure(cnf, **kw)
1251
+ # configure properties not supported by sCTkFrame but yes by sCTkToplevel
1252
+ props = ("borderwidth", "highlightbackground", "highlightthickness")
1253
+ for pname in props:
1254
+ if pname in kw:
1255
+ super(CTkBaseClass, self).configure(**{pname: kw.pop(pname)})
1256
+ return super().configure(cnf, **kw)
1257
+
1258
+
1259
+ sCTkToplevelPreview = ToplevelPreviewFactory(
1260
+ "sCTkToplevelPreview",
1261
+ (sCTkToplevelPreviewMixin, ToplevelPreviewMixin, sCTkFrameForPreview, object),
1262
+ {},
1263
+ )
1264
+
1265
+
1266
+ class sCTkToplevelPreviewBO(ToplevelPreviewBaseBO):
1267
+ class_ = sCTkToplevelPreview
1268
+ ro_properties = ToplevelPreviewBaseBO.ro_properties + (
1269
+ "background",
1270
+ "fg_color",
1271
+ )
1272
+
1273
+ def _process_property_value(self, pname, value):
1274
+ if pname in ("width", "height"):
1275
+ return int(value)
1276
+ return super()._process_property_value(pname, value)
1277
+
1278
+
1279
+ class sCTkPreviewMixin:
1280
+ def configure(self, cnf=None, **kw):
1281
+ if cnf:
1282
+ return super().configure(cnf, **kw)
1283
+ # configure properties not supported by sCTkFrame but yes by sCTk
1284
+ props = ("padx", "pady", "relief", "takefocus")
1285
+ for pname in props:
1286
+ if pname in kw:
1287
+ super(CTkBaseClass, self).configure(**{pname: kw.pop(pname)})
1288
+ return super().configure(cnf, **kw)
1289
+
1290
+
1291
+ sCTkPreview = ToplevelPreviewFactory(
1292
+ "sCTkPreview",
1293
+ (sCTkPreviewMixin, ToplevelPreviewMixin, sCTkFrameForPreview, object),
1294
+ {},
1295
+ )
1296
+
1297
+
1298
+ class sCTkPreviewBO(sCTkToplevelPreviewBO):
1299
+ class_ = sCTkPreview
1300
+ properties = ToplevelPreviewBaseBO.properties + ("appearance_mode",)
1301
+ ro_properties = ToplevelPreviewBaseBO.ro_properties + ("fg_color",)
1302
+
1303
+ def _set_property(self, target_widget, pname, value):
1304
+ """
1305
+ Applies a property to the preview.
1306
+
1307
+ appearance_mode and color_theme are global CustomTkinter settings
1308
+ rather than widget options, so they are routed to the module-level
1309
+ setters instead of reaching the widget.
1310
+
1311
+ FIX: both setters are guarded against an empty value. Blanking a
1312
+ property in the Designer inspector makes pygubu call
1313
+ unset_property(), which resolves the property's default -- None here,
1314
+ since these have no widget-level default to read back -- and passes
1315
+ it straight through. CustomTkinter's set_appearance_mode() then does
1316
+ mode_string.lower() and raises AttributeError on NoneType, taking out
1317
+ the whole preview update. CustomTkinter's own CTkPreviewBO has the
1318
+ same unguarded code and the same crash.
1319
+
1320
+ The two are guarded DIFFERENTLY, on purpose.
1321
+
1322
+ appearance_mode falls back to "System", which is a real state --
1323
+ CustomTkinter's own default, meaning "follow the OS". A user who
1324
+ picks Dark, then Light, then wants the system to decide again needs a
1325
+ way back, and clearing the field is that way. Ignoring the call would
1326
+ strand them on the last explicit mode, which is precisely what they
1327
+ are trying to escape. This package registers "System" as an explicit
1328
+ choice as well, so it is reachable without clearing anything --
1329
+ CustomTkinter's own property offers only blank, Light and Dark.
1330
+
1331
+ color_theme has no equivalent "unset" state: CustomTkinter always
1332
+ needs some theme loaded, and reverting to "blue" would be a guess
1333
+ rather than a reversion. Blank is therefore ignored, leaving the
1334
+ current theme in place.
1335
+ """
1336
+ if pname == "appearance_mode":
1337
+ ctk.set_appearance_mode(value or "System")
1338
+ elif pname == "color_theme":
1339
+ if value:
1340
+ ctk.set_default_color_theme(value)
1341
+ else:
1342
+ return super()._set_property(target_widget, pname, value)
1343
+
1344
+
1345
+ # Every builder id registered by this package is "scustomtkinter.<ClassName>",
1346
+ # so this prefix identifies our widgets and nothing else. Note it does NOT
1347
+ # collide with CustomTkinter's own "customtkinter." prefix -- their designer
1348
+ # plugin correctly ignores ours, and vice versa.
1349
+ namespace_prefix = "scustomtkinter."
1350
+
1351
+
1352
+ def _no_op(event=None):
1353
+ """Swallow an event during preview."""
1354
+ pass
1355
+
1356
+
1357
+ def _neutralize(widget, sequences):
1358
+ """
1359
+ Replace a widget's handlers for the given event sequences with no-ops.
1360
+
1361
+ Each bind is attempted independently: <TouchpadScroll> exists only on
1362
+ macOS and raises elsewhere, and a widget may legitimately not support a
1363
+ sequence. One failure must not skip the rest.
1364
+ """
1365
+ if widget is None:
1366
+ return
1367
+ for seq in sequences:
1368
+ try:
1369
+ widget.bind(seq, _no_op)
1370
+ except Exception:
1371
+ pass
1372
+
1373
+
1374
+ def _preview_canvas(widget):
1375
+ """
1376
+ Return the drawing canvas for a widget, or None.
1377
+
1378
+ Widgets inherited from CustomTkinter expose it as `_canvas`; the ones this
1379
+ library builds from scratch -- the dials, both S-meters, sCTkFileExplorer --
1380
+ create their own as `canvas`. CustomTkinter's plugin only checks `_canvas`,
1381
+ which would silently return None for half of this library.
1382
+ """
1383
+ for attr in ("_canvas", "canvas"):
1384
+ found = getattr(widget, attr, None)
1385
+ if found is not None:
1386
+ return found
1387
+ return None
1388
+
1389
+
1390
+ # Sequences that make a widget grab the pointer in the designer canvas.
1391
+ _HOVER_CLICK = ("<Enter>", "<Leave>", "<Button-1>")
1392
+ _FOCUS = ("<FocusIn>", "<FocusOut>")
1393
+ # Wheel and trackpad. Neutralizing these matters more here than upstream:
1394
+ # scrolling over an un-neutralized widget scrolls IT instead of the canvas.
1395
+ _SCROLL = ("<MouseWheel>", "<TouchpadScroll>", "<Button-4>", "<Button-5>")
1396
+ # The dials additionally step on middle/right click and drag with a modifier.
1397
+ _DIAL_EXTRA = ("<Button-2>", "<Button-3>",
1398
+ "<Shift-ButtonPress-1>", "<Shift-B1-Motion>")
1399
+
1400
+
1401
+ #
1402
+ # A Designer plugin for sCTk custom widgets
1403
+ #
1404
+ class sCTkDesignerPlugin(IDesignerPlugin):
1405
+
1406
+ def is_toplevel_widget(self, builder_uid: str) -> bool:
1407
+ """
1408
+ Declares which builder ids are application ROOTS rather than ordinary
1409
+ widgets. Consulted by the Designer's script generator.
1410
+
1411
+ WHAT BREAKS WITHOUT THIS. pygubudesigner/codegen/scriptgenerator.py
1412
+ decides which Mako template to use from:
1413
+
1414
+ toplevel_uids = ("tk.Tk", "tk.Toplevel", "customtkinter.CTk",
1415
+ "customtkinter.CTkToplevel",
1416
+ "tkmt.ThemedTKinterFrame")
1417
+ if target_class in toplevel_uids or \
1418
+ PluginManager.is_toplevel_widget(target_class):
1419
+ main_widget_is_toplevel = True
1420
+
1421
+ That tuple is hardcoded and does not include this package's ids, so
1422
+ sCTk fell to the WIDGET template, whose __main__ block reads:
1423
+
1424
+ root = tk.Tk()
1425
+ app = MyApp(root)
1426
+
1427
+ sCTk creates its own Tcl interpreter, so that produced a SECOND one.
1428
+ The consequence was nothing like the cause: a tk.StringVar built
1429
+ without an explicit master attaches to whichever root Tkinter
1430
+ considers default, so a variable bound to a widget in one interpreter
1431
+ was read from the other. The widget worked, the command callback fired
1432
+ with the right value, and variable.get() returned empty forever. Every
1433
+ variable-bound widget in generated code was affected -- combo boxes,
1434
+ radio buttons, switches, check boxes.
1435
+
1436
+ The `or` clause above is the supported fix. pygubu's own source carries
1437
+ a FIXME beside that tuple asking plugins to implement this method
1438
+ rather than the tuple being extended.
1439
+
1440
+ sCTkDialog is included for the same reason even though it is not a
1441
+ Toplevel subclass: it always BUILDS one. Without it the generated
1442
+ __main__ read
1443
+
1444
+ root = tk.Tk()
1445
+ app = MyDialog(root)
1446
+
1447
+ which put a small empty window on screen beside the dialog -- the
1448
+ application root, with nothing in it, because everything the design
1449
+ contains lives inside the dialog's own window. Setting `transient` made
1450
+ no difference, since the two are separate roots rather than parent and
1451
+ child.
1452
+
1453
+ Note that group=GROOT on register_widget() is a DIFFERENT thing --
1454
+ palette placement only. It does not affect code generation.
1455
+
1456
+ Args:
1457
+ builder_uid: The registered id being tested.
1458
+
1459
+ Returns:
1460
+ True if that id names an application root.
1461
+ """
1462
+ return builder_uid in (sCTk_builder_id, sCTkToplevel_builder_id,
1463
+ sCTkDialog_builder_id)
1464
+
1465
+ def get_preview_builder(self, builder_uid: str):
1466
+ """Return a BuilderObject subclass used to build a preview
1467
+ for the target builder_uid"""
1468
+
1469
+ if builder_uid == sCTkFrame_builder_id:
1470
+ return sCTkFramePreviewBO
1471
+ elif builder_uid == sCTkFrameLabeledPrimary_builder_id:
1472
+ return sCTkFrameLabeledPrimaryForPreviewBO
1473
+ elif builder_uid == sCTkFrameLabeledSecondary_builder_id:
1474
+ return sCTkFrameLabeledSecondaryForPreviewBO
1475
+ elif builder_uid == sCTkScrollableFrame_builder_id:
1476
+ return sCTkScrollableFrameForPreviewBO
1477
+ elif builder_uid == sCTkFileExplorer_builder_id:
1478
+ return sCTkFileExplorerForPreviewBO
1479
+ elif builder_uid == sCTkPathChooser_builder_id:
1480
+ return sCTkPathChooserForPreviewBO
1481
+ elif builder_uid == sCTkTableview_builder_id:
1482
+ return sCTkTableviewForPreviewBO
1483
+ elif builder_uid == sCTkSegmentedButton_builder_id:
1484
+ return sCTkSegmentedButtonForPreviewBO
1485
+ elif builder_uid == sCTkRadioButton_builder_id:
1486
+ return sCTkRadioButtonForPreviewBO
1487
+ elif builder_uid == sCTkFrequencyDisplay_builder_id:
1488
+ return sCTkFrequencyDisplayForPreviewBO
1489
+ elif builder_uid == sCTkNotebook_builder_id:
1490
+ return sCTkNotebookForPreviewBO
1491
+ elif builder_uid == sCTkTabview_builder_id:
1492
+ return sCTkTabviewForPreviewBO
1493
+ elif builder_uid == sCTkSelector_builder_id:
1494
+ return sCTkSelectorForPreviewBO
1495
+ elif builder_uid == sCTkSeparator_builder_id:
1496
+ return sCTkSeparatorForPreviewBO
1497
+ elif builder_uid == sCTkDialContinuous_builder_id:
1498
+ return sCTkDialContinuousForPreviewBO
1499
+ elif builder_uid == sCTkDialRange_builder_id:
1500
+ return sCTkDialRangeForPreviewBO
1501
+ elif builder_uid == sCTkDialSelector_builder_id:
1502
+ return sCTkDialSelectorForPreviewBO
1503
+ elif builder_uid == sCTkDialog_builder_id:
1504
+ return sCTkDialogForPreviewBO
1505
+ elif builder_uid == sCTkOptionMenuSecondary_builder_id:
1506
+ return sCTkOptionMenuSecondaryForPreviewBO
1507
+ elif builder_uid == sCTkSpinbox_builder_id:
1508
+ return sCTkSpinboxForPreviewBO
1509
+ elif builder_uid == sCTkToplevel_builder_id:
1510
+ return sCTkToplevelPreviewBO
1511
+ elif builder_uid == sCTk_builder_id:
1512
+ return sCTkPreviewBO
1513
+
1514
+ return None
1515
+
1516
+ def get_toplevel_preview_for(self, builder_uid: str, widget_id: str,
1517
+ builder, top_master):
1518
+ """Return the toplevel preview widget for a top-level builder_uid,
1519
+ or None if this plugin doesn't handle it.
1520
+
1521
+ Note this does NOT instantiate a BuilderObject itself. get_preview_builder()
1522
+ above has already told pygubu which BO class to use for this uid, so
1523
+ builder.get_object() constructs it correctly -- with a real WidgetMeta,
1524
+ which is what BuilderObject.__init__ actually expects. An earlier version
1525
+ here called preview_bo(builder, widget_id).realize(top_master), passing
1526
+ the id STRING where the meta belongs; that failed with
1527
+ "'str' object has no attribute 'properties'" as soon as
1528
+ _get_init_args() looked at self.wmeta.properties.
1529
+
1530
+ top_master is accepted for interface compatibility and deliberately
1531
+ unused: the builder places the preview itself.
1532
+
1533
+ The image-cache reset exists because building a toplevel creates a NEW
1534
+ tk root, and a StockImageCache is bound to the root it was created
1535
+ under. Without this, images resolved against the old root either fail
1536
+ or render blank in the new one.
1537
+
1538
+ Mirrors CustomTkinter's own designer plugin.
1539
+ """
1540
+ toplevel_uids = (sCTk_builder_id, sCTkToplevel_builder_id)
1541
+
1542
+ def on_root_created(root):
1543
+ builder.image_cache = StockImageCache(root, StockImage.registry)
1544
+
1545
+ if builder_uid in toplevel_uids:
1546
+ builder.on_first_object = on_root_created
1547
+ return builder.get_object(widget_id)
1548
+
1549
+ # sCTkDialog is not a toplevel, but it BUILDS one -- it always creates
1550
+ # its own sCTkDialogToplevel and packs itself into it.
1551
+ #
1552
+ # Returning None here let pygubu fall back to creating a host window of
1553
+ # its own, and that fallback constructs through the ordinary builder
1554
+ # object rather than the preview one -- so _MAKE_WINDOW = False never
1555
+ # applied, the real widget built its own window, and the result was TWO
1556
+ # windows: an empty host, and the dialog beside it.
1557
+ #
1558
+ # Handing back the dialog's own window instead means no host is created
1559
+ # and you preview the real thing, correctly sized and placed.
1560
+ if builder_uid == sCTkDialog_builder_id:
1561
+ builder.on_first_object = on_root_created
1562
+ widget = builder.get_object(widget_id)
1563
+ window = getattr(widget, "dialog_toplevel", None)
1564
+ if window is not None:
1565
+ return window
1566
+ # _MAKE_WINDOW was False, so there is no window of its own; fall
1567
+ # back to whatever it was built inside.
1568
+ return widget.winfo_toplevel()
1569
+
1570
+ return None
1571
+
1572
+ def ensure_visibility_in_preview(self, builder, selected_uid: str):
1573
+ """
1574
+ Switches the canvas to whichever tab holds the selected widget.
1575
+
1576
+ Selecting a tab in the widget tree previously did nothing visible: the
1577
+ canvas stayed on the tab it was already showing, so the selection
1578
+ outline was drawn around a widget that is not mapped -- which collapses
1579
+ to a small square near the origin. Selecting something INSIDE another
1580
+ tab had the same problem, and together they made the tree and the canvas
1581
+ disagree with no way to reconcile them.
1582
+
1583
+ This is the hook for the tree-to-canvas direction, and this plugin
1584
+ simply never implemented it. Modelled on CustomTkinter's own, which
1585
+ does the same for its CTkTabview.
1586
+
1587
+ Both tab class names are searched. Tabs may be registered as this
1588
+ library's own type or, where that registration is commented out,
1589
+ inherited from CustomTkinter -- and a .ui file written under either is
1590
+ still valid.
1591
+
1592
+ Args:
1593
+ builder: The preview builder, giving access to the parsed .ui and
1594
+ the realized objects.
1595
+ selected_uid: The id of the widget selected in the tree.
1596
+ """
1597
+ tab_classes = ("scustomtkinter.sCTkTabviewTab",
1598
+ "scustomtkinter.sCTkTabview.Tab",
1599
+ "customtkinter.CTkTabviewTab",
1600
+ # sCTkNotebook's pages work the same way: stacked in
1601
+ # one cell with only the selected one raised, so the
1602
+ # tree and the canvas disagree without this.
1603
+ "scustomtkinter.sCTkNotebookTab")
1604
+
1605
+ tabs = []
1606
+ for class_name in tab_classes:
1607
+ found = builder.uidefinition.root.findall(
1608
+ f".//object[@class='{class_name}']")
1609
+ if found:
1610
+ tabs.extend(found)
1611
+ if not tabs:
1612
+ return
1613
+
1614
+ for tab in tabs:
1615
+ tab_id = tab.get("id")
1616
+ if tab_id is None:
1617
+ continue
1618
+
1619
+ # The tab itself, or anything nested inside it.
1620
+ activate = tab_id == selected_uid
1621
+ if not activate:
1622
+ activate = tab.find(f".//object[@id='{selected_uid}']") is not None
1623
+ if not activate:
1624
+ continue
1625
+
1626
+ tab_builder = builder.objects.get(tab_id)
1627
+ if tab_builder is None or tab_builder.widget is None:
1628
+ return
1629
+ try:
1630
+ top = tab_builder.widget.winfo_toplevel()
1631
+ # CLIMBED, not taken as the immediate parent.
1632
+ #
1633
+ # A CTkTabview page's parent IS the tabview, but an
1634
+ # sCTkNotebook page is parented to an internal frame, so the
1635
+ # immediate parent has no set() to call. Walking up until
1636
+ # something offers one covers both without asking which is
1637
+ # which.
1638
+ container = tab_builder.widget
1639
+ for _ in range(4):
1640
+ parent_name = container.winfo_parent()
1641
+ if not parent_name:
1642
+ break
1643
+ container = top.nametowidget(parent_name)
1644
+ if hasattr(container, "set") and hasattr(container, "get"):
1645
+ break
1646
+ tabname = tab_builder.wmeta.properties.get("label")
1647
+ if tabname and hasattr(container, "get") \
1648
+ and container.get() != tabname:
1649
+ container.set(tabname)
1650
+ top.update()
1651
+ except Exception:
1652
+ # A tab that is not realized yet, or a parent that is not the
1653
+ # tabview. Failing to switch is a cosmetic loss; raising here
1654
+ # would break selection entirely.
1655
+ pass
1656
+ return
1657
+
1658
+ def configure_for_preview(self, builder_uid: str, widget):
1659
+ """Make a widget display with minimal functionality in the designer.
1660
+
1661
+ Two jobs: crop it to its allotted space, and stop it reacting to the
1662
+ pointer. A widget that responds to clicks and scrolling fights the
1663
+ designer -- clicking selects nothing, and scrolling over it moves the
1664
+ widget instead of the canvas.
1665
+
1666
+ NOTE ON A BUG UPSTREAM: CustomTkinter's own plugin tests
1667
+ `builder_uid.endswith(".CTKEntry")` -- capital K in the middle. The
1668
+ real uid is "customtkinter.CTkEntry", so that branch has never
1669
+ matched and their entry focus bindings were never actually
1670
+ neutralized. The equivalent branch below is spelled correctly.
1671
+ """
1672
+ if not builder_uid.startswith(namespace_prefix):
1673
+ return
1674
+
1675
+ crop_widget(widget, recursive=True)
1676
+
1677
+ canvas = _preview_canvas(widget)
1678
+
1679
+ # --- Widgets built on sCTkScrollableFrame ------------------------
1680
+ # Left alone deliberately, following CustomTkinter's own early return
1681
+ # for CTkScrollableFrame. This library's scrollable frame activates
1682
+ # bindings through four separate paths and rebinds on <Configure>, so
1683
+ # a neutralizing pass would be undone by the next rebind anyway --
1684
+ # and cropping fires <Configure>, which triggers exactly that.
1685
+ scrollable_family = (
1686
+ ".sCTkScrollableFrame",
1687
+ ".sCTkFrameLabeledPrimary",
1688
+ ".sCTkFrameLabeledSecondary",
1689
+ ".sCTkTableview",
1690
+ ".sCTkSelector",
1691
+ )
1692
+ if builder_uid.endswith(scrollable_family):
1693
+ return
1694
+
1695
+ if builder_uid.endswith(".sCTkFrequencyDisplay"):
1696
+ # The readout's canvas IS the widget: the digits and the
1697
+ # hit-testing all live on it. Left bound, a click selects a digit
1698
+ # instead of the widget.
1699
+ #
1700
+ # The click is FORWARDED rather than swallowed -- a no-op would
1701
+ # stop it before it reached _canvas, where CTkFrame.bind() puts
1702
+ # the Designer's own selection handler. Same treatment the dials
1703
+ # and the notebook needed.
1704
+ face = getattr(widget, "canvas", None)
1705
+ _neutralize(face, tuple(s for s in (_HOVER_CLICK + _SCROLL)
1706
+ if s != "<Button-1>"))
1707
+ if face is not None:
1708
+ def _select_readout(event, target=widget):
1709
+ try:
1710
+ bg = getattr(target, "_canvas", None)
1711
+ if bg is not None:
1712
+ bg.event_generate("<Button-1>", x=1, y=1, when="now")
1713
+ except Exception:
1714
+ pass
1715
+ return "break"
1716
+ try:
1717
+ face.bind("<Button-1>", _select_readout)
1718
+ except Exception:
1719
+ pass
1720
+ return
1721
+
1722
+ if builder_uid.endswith(".sCTkNotebook"):
1723
+ # The notebook draws itself on `canvas` -- the tab strip, the page
1724
+ # outline and all the hit-testing. Left bound, a click on a tab
1725
+ # switches pages instead of selecting the notebook, and the wheel
1726
+ # scrolls the strip.
1727
+ #
1728
+ # `_canvas` is a different object: CTkFrame's own background, and
1729
+ # where CTkFrame.bind() puts the Designer's selection handler. So
1730
+ # the click is FORWARDED to it rather than swallowed -- a no-op
1731
+ # would stop the click before it ever reached the handler. Same
1732
+ # treatment the dials needed, for the same reason.
1733
+ face = getattr(widget, "canvas", None)
1734
+ _neutralize(face, tuple(s for s in (_HOVER_CLICK + _SCROLL)
1735
+ if s != "<Button-1>"))
1736
+ if face is not None:
1737
+ def _select_notebook(event, target=widget):
1738
+ try:
1739
+ bg = getattr(target, "_canvas", None)
1740
+ if bg is not None:
1741
+ bg.event_generate("<Button-1>", x=1, y=1, when="now")
1742
+ except Exception:
1743
+ pass
1744
+ return "break"
1745
+ try:
1746
+ face.bind("<Button-1>", _select_notebook)
1747
+ except Exception:
1748
+ pass
1749
+ return
1750
+
1751
+ # --- Direct inheritors from CustomTkinter ------------------------
1752
+ if builder_uid.endswith((".sCTkEntryPrimary", ".sCTkEntrySecondary")):
1753
+ _neutralize(canvas, _FOCUS)
1754
+
1755
+ elif builder_uid.endswith(".sCTkSlider"):
1756
+ _neutralize(canvas, _HOVER_CLICK + ("<B1-Motion>",))
1757
+
1758
+ elif builder_uid.endswith((".sCTkOptionMenuPrimary",
1759
+ ".sCTkOptionMenuSecondary")):
1760
+ _neutralize(canvas, _HOVER_CLICK)
1761
+ _neutralize(getattr(widget, "_text_label", None), _HOVER_CLICK)
1762
+
1763
+ elif builder_uid.endswith(".sCTkComboBox"):
1764
+ # ComboBox binds by canvas TAG rather than on the widget, so the
1765
+ # dropdown arrow and its surrounding region need tag_bind.
1766
+ if canvas is not None:
1767
+ for tag in ("right_parts", "dropdown_arrow"):
1768
+ for seq in _HOVER_CLICK:
1769
+ try:
1770
+ canvas.tag_bind(tag, seq, _no_op)
1771
+ except Exception:
1772
+ pass
1773
+
1774
+ elif builder_uid.endswith((".sCTkSwitch", ".sCTkCheckBox")):
1775
+ # Not handled by CustomTkinter's plugin, but both toggle on click,
1776
+ # which in the designer reads as the widget refusing to be selected.
1777
+ _neutralize(canvas, _HOVER_CLICK)
1778
+ _neutralize(getattr(widget, "_text_label", None), _HOVER_CLICK)
1779
+
1780
+ # --- Widgets original to this library ----------------------------
1781
+ elif builder_uid.endswith((".sCTkDialContinuous",
1782
+ ".sCTkDialSelector",
1783
+ ".sCTkDialRange")):
1784
+ # The worst offender in the library: steps on left/middle/right
1785
+ # click, drags with Shift, and turns on wheel or trackpad. Scroll
1786
+ # bindings are installed on several layers, so both the widget and
1787
+ # its canvas are neutralized.
1788
+ #
1789
+ # <Configure> is deliberately NOT neutralized -- the dial redraws
1790
+ # itself from it, and a dial that never redraws shows an empty
1791
+ # canvas in the designer.
1792
+ # The dial's OWN canvas, not _preview_canvas()'s answer.
1793
+ #
1794
+ # A dial has TWO canvases: CTkFrame's background `_canvas`, and
1795
+ # `canvas`, which the dial creates and draws itself on.
1796
+ # _preview_canvas() returns _canvas first -- and that is where
1797
+ # CTkFrame.bind() puts the DESIGNER's click handler. Neutralizing
1798
+ # <Button-1> there removed the selection binding itself, so a dial
1799
+ # could not be selected on the canvas at all.
1800
+ #
1801
+ # The interactive bindings that need silencing are on the dial's
1802
+ # own canvas. _canvas keeps its handler.
1803
+ face = getattr(widget, "canvas", None)
1804
+
1805
+ # Everything EXCEPT <Button-1> is neutralized outright.
1806
+ sequences = tuple(s for s in (_HOVER_CLICK + _DIAL_EXTRA + _SCROLL)
1807
+ if s != "<Button-1>")
1808
+ _neutralize(face, sequences)
1809
+ _neutralize(widget, _SCROLL)
1810
+
1811
+ # <Button-1> is FORWARDED rather than swallowed.
1812
+ #
1813
+ # A no-op would stop the click at the dial face, so it never
1814
+ # reaches _canvas -- where CTkFrame.bind() puts the Designer's own
1815
+ # selection handler. The dial would stay unselectable, which is
1816
+ # what "the dial captures the click" looked like.
1817
+ #
1818
+ # Done here rather than in the preview class because this function
1819
+ # runs afterwards and would otherwise replace that binding.
1820
+ if face is not None:
1821
+ def _select_dial(event, target=widget):
1822
+ try:
1823
+ bg = getattr(target, "_canvas", None)
1824
+ if bg is not None:
1825
+ bg.event_generate("<Button-1>", x=1, y=1, when="now")
1826
+ except Exception:
1827
+ pass
1828
+ return "break"
1829
+ try:
1830
+ face.bind("<Button-1>", _select_dial)
1831
+ except Exception:
1832
+ pass
1833
+
1834
+ elif builder_uid.endswith(".sCTkFileExplorer"):
1835
+ # Rows are created dynamically and each binds click and
1836
+ # double-click, so neutralizing the canvas alone is not enough --
1837
+ # the row widgets are separate children.
1838
+ _neutralize(canvas, _SCROLL)
1839
+ _neutralize(widget, _SCROLL)
1840
+ try:
1841
+ for row in widget.explorer_frame.winfo_children():
1842
+ _neutralize(row, ("<Button-1>", "<Double-Button-1>"))
1843
+ except Exception:
1844
+ pass
1845
+
1846
+ elif builder_uid.endswith(".sCTkPathChooser"):
1847
+ # Its browse button opens a MODAL file explorer -- clicking that
1848
+ # inside the designer would trap the user in a dialog with no
1849
+ # obvious way back. The attribute is `btn`; note that a wrong name
1850
+ # here fails silently, since _neutralize() accepts None.
1851
+ _neutralize(getattr(widget, "btn", None), ("<Button-1>",))
1852
+
1853
+ # sCTkSMeter, sCTkSMeterBar and sCTkSeparator bind only <Configure>,
1854
+ # which they need in order to draw. Nothing to neutralize.