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.
- scustomtkinter/__init__.py +81 -0
- scustomtkinter/assets/images/error.png +0 -0
- scustomtkinter/assets/images/info.png +0 -0
- scustomtkinter/assets/images/warning.png +0 -0
- scustomtkinter/assets/sCTkThemes.json +1963 -0
- scustomtkinter/license_morecustomtkinterwidgets.txt +22 -0
- scustomtkinter/sctk_button_primary.py +366 -0
- scustomtkinter/sctk_button_secondary.py +354 -0
- scustomtkinter/sctk_button_tertiary.py +375 -0
- scustomtkinter/sctk_checkbox.py +270 -0
- scustomtkinter/sctk_combobox.py +155 -0
- scustomtkinter/sctk_core.py +15 -0
- scustomtkinter/sctk_dial.py +1472 -0
- scustomtkinter/sctk_dialog.py +701 -0
- scustomtkinter/sctk_dialog_toplevel.py +268 -0
- scustomtkinter/sctk_entry_primary.py +400 -0
- scustomtkinter/sctk_entry_secondary.py +342 -0
- scustomtkinter/sctk_file_explorer.py +879 -0
- scustomtkinter/sctk_frame.py +210 -0
- scustomtkinter/sctk_frame_labeled_primary.py +667 -0
- scustomtkinter/sctk_frame_labeled_secondary.py +598 -0
- scustomtkinter/sctk_frequency_display.py +636 -0
- scustomtkinter/sctk_label_primary.py +283 -0
- scustomtkinter/sctk_label_secondary.py +275 -0
- scustomtkinter/sctk_label_tertiary.py +278 -0
- scustomtkinter/sctk_messagebox.py +275 -0
- scustomtkinter/sctk_notebook.py +1103 -0
- scustomtkinter/sctk_optionmenu_border_mixin.py +203 -0
- scustomtkinter/sctk_optionmenu_primary.py +378 -0
- scustomtkinter/sctk_optionmenu_secondary.py +346 -0
- scustomtkinter/sctk_path_chooser.py +516 -0
- scustomtkinter/sctk_progress_bar.py +274 -0
- scustomtkinter/sctk_radiobutton.py +360 -0
- scustomtkinter/sctk_scroll_mixin.py +546 -0
- scustomtkinter/sctk_scrollable_frame.py +795 -0
- scustomtkinter/sctk_scrollbar.py +313 -0
- scustomtkinter/sctk_segmentedbutton.py +424 -0
- scustomtkinter/sctk_selector.py +581 -0
- scustomtkinter/sctk_separator.py +308 -0
- scustomtkinter/sctk_slider.py +264 -0
- scustomtkinter/sctk_smeter.py +405 -0
- scustomtkinter/sctk_smeter_bar.py +557 -0
- scustomtkinter/sctk_spinbox.py +664 -0
- scustomtkinter/sctk_switch.py +463 -0
- scustomtkinter/sctk_tableview.py +869 -0
- scustomtkinter/sctk_tabview.py +409 -0
- scustomtkinter/sctk_textbox_primary.py +291 -0
- scustomtkinter/sctk_textbox_secondary.py +225 -0
- scustomtkinter/sctk_toplevel.py +153 -0
- scustomtkinter/themeable_widget.py +938 -0
- scustomtkinter-0.2.1.dist-info/METADATA +336 -0
- scustomtkinter-0.2.1.dist-info/RECORD +101 -0
- scustomtkinter-0.2.1.dist-info/WHEEL +5 -0
- scustomtkinter-0.2.1.dist-info/entry_points.txt +2 -0
- scustomtkinter-0.2.1.dist-info/licenses/LICENSE +21 -0
- scustomtkinter-0.2.1.dist-info/licenses/scustomtkinter/license_morecustomtkinterwidgets.txt +22 -0
- scustomtkinter-0.2.1.dist-info/top_level.txt +2 -0
- scustomtkinter_pygubu/__init__.py +1 -0
- scustomtkinter_pygubu/designer/__init__.py +0 -0
- scustomtkinter_pygubu/designer/plugin.py +1854 -0
- scustomtkinter_pygubu/designer/properties.py +247 -0
- scustomtkinter_pygubu/sCTkButtonPrimarybo.py +46 -0
- scustomtkinter_pygubu/sCTkButtonSecondarybo.py +45 -0
- scustomtkinter_pygubu/sCTkButtonTertiarybo.py +44 -0
- scustomtkinter_pygubu/sCTkCheckBoxbo.py +45 -0
- scustomtkinter_pygubu/sCTkComboBoxbo.py +45 -0
- scustomtkinter_pygubu/sCTkCorebo.py +120 -0
- scustomtkinter_pygubu/sCTkDialbo.py +371 -0
- scustomtkinter_pygubu/sCTkDialogbo.py +397 -0
- scustomtkinter_pygubu/sCTkEntryPrimarybo.py +100 -0
- scustomtkinter_pygubu/sCTkEntrySecondarybo.py +98 -0
- scustomtkinter_pygubu/sCTkFileExplorerbo.py +189 -0
- scustomtkinter_pygubu/sCTkFrameLabeledPrimarybo.py +69 -0
- scustomtkinter_pygubu/sCTkFrameLabeledSecondarybo.py +71 -0
- scustomtkinter_pygubu/sCTkFramebo.py +46 -0
- scustomtkinter_pygubu/sCTkFrequencyDisplaybo.py +175 -0
- scustomtkinter_pygubu/sCTkLabelPrimarybo.py +45 -0
- scustomtkinter_pygubu/sCTkLabelSecondarybo.py +44 -0
- scustomtkinter_pygubu/sCTkLabelTertiarybo.py +45 -0
- scustomtkinter_pygubu/sCTkNotebookbo.py +392 -0
- scustomtkinter_pygubu/sCTkOptionMenuPrimarybo.py +45 -0
- scustomtkinter_pygubu/sCTkOptionMenuSecondarybo.py +39 -0
- scustomtkinter_pygubu/sCTkPathChooserbo.py +204 -0
- scustomtkinter_pygubu/sCTkProgressBarbo.py +45 -0
- scustomtkinter_pygubu/sCTkRadioButtonbo.py +45 -0
- scustomtkinter_pygubu/sCTkSMeterBarbo.py +153 -0
- scustomtkinter_pygubu/sCTkSMeterbo.py +101 -0
- scustomtkinter_pygubu/sCTkScrollableFramebo.py +66 -0
- scustomtkinter_pygubu/sCTkScrollbarbo.py +48 -0
- scustomtkinter_pygubu/sCTkSegmentedButtonbo.py +47 -0
- scustomtkinter_pygubu/sCTkSelectorbo.py +167 -0
- scustomtkinter_pygubu/sCTkSeparatorbo.py +141 -0
- scustomtkinter_pygubu/sCTkSliderbo.py +45 -0
- scustomtkinter_pygubu/sCTkSpinboxbo.py +200 -0
- scustomtkinter_pygubu/sCTkSwitchbo.py +45 -0
- scustomtkinter_pygubu/sCTkTableviewbo.py +188 -0
- scustomtkinter_pygubu/sCTkTabviewbo.py +226 -0
- scustomtkinter_pygubu/sCTkTextboxPrimarybo.py +45 -0
- scustomtkinter_pygubu/sCTkTextboxSecondarybo.py +45 -0
- scustomtkinter_pygubu/sCTkToplevelbo.py +55 -0
- 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.
|