scustomtkinter 0.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. scustomtkinter/__init__.py +81 -0
  2. scustomtkinter/assets/images/error.png +0 -0
  3. scustomtkinter/assets/images/info.png +0 -0
  4. scustomtkinter/assets/images/warning.png +0 -0
  5. scustomtkinter/assets/sCTkThemes.json +1963 -0
  6. scustomtkinter/license_morecustomtkinterwidgets.txt +22 -0
  7. scustomtkinter/sctk_button_primary.py +366 -0
  8. scustomtkinter/sctk_button_secondary.py +354 -0
  9. scustomtkinter/sctk_button_tertiary.py +375 -0
  10. scustomtkinter/sctk_checkbox.py +270 -0
  11. scustomtkinter/sctk_combobox.py +155 -0
  12. scustomtkinter/sctk_core.py +15 -0
  13. scustomtkinter/sctk_dial.py +1472 -0
  14. scustomtkinter/sctk_dialog.py +701 -0
  15. scustomtkinter/sctk_dialog_toplevel.py +268 -0
  16. scustomtkinter/sctk_entry_primary.py +400 -0
  17. scustomtkinter/sctk_entry_secondary.py +342 -0
  18. scustomtkinter/sctk_file_explorer.py +879 -0
  19. scustomtkinter/sctk_frame.py +210 -0
  20. scustomtkinter/sctk_frame_labeled_primary.py +667 -0
  21. scustomtkinter/sctk_frame_labeled_secondary.py +598 -0
  22. scustomtkinter/sctk_frequency_display.py +636 -0
  23. scustomtkinter/sctk_label_primary.py +283 -0
  24. scustomtkinter/sctk_label_secondary.py +275 -0
  25. scustomtkinter/sctk_label_tertiary.py +278 -0
  26. scustomtkinter/sctk_messagebox.py +275 -0
  27. scustomtkinter/sctk_notebook.py +1103 -0
  28. scustomtkinter/sctk_optionmenu_border_mixin.py +203 -0
  29. scustomtkinter/sctk_optionmenu_primary.py +378 -0
  30. scustomtkinter/sctk_optionmenu_secondary.py +346 -0
  31. scustomtkinter/sctk_path_chooser.py +516 -0
  32. scustomtkinter/sctk_progress_bar.py +274 -0
  33. scustomtkinter/sctk_radiobutton.py +360 -0
  34. scustomtkinter/sctk_scroll_mixin.py +546 -0
  35. scustomtkinter/sctk_scrollable_frame.py +795 -0
  36. scustomtkinter/sctk_scrollbar.py +313 -0
  37. scustomtkinter/sctk_segmentedbutton.py +424 -0
  38. scustomtkinter/sctk_selector.py +581 -0
  39. scustomtkinter/sctk_separator.py +308 -0
  40. scustomtkinter/sctk_slider.py +264 -0
  41. scustomtkinter/sctk_smeter.py +405 -0
  42. scustomtkinter/sctk_smeter_bar.py +557 -0
  43. scustomtkinter/sctk_spinbox.py +664 -0
  44. scustomtkinter/sctk_switch.py +463 -0
  45. scustomtkinter/sctk_tableview.py +869 -0
  46. scustomtkinter/sctk_tabview.py +409 -0
  47. scustomtkinter/sctk_textbox_primary.py +291 -0
  48. scustomtkinter/sctk_textbox_secondary.py +225 -0
  49. scustomtkinter/sctk_toplevel.py +153 -0
  50. scustomtkinter/themeable_widget.py +938 -0
  51. scustomtkinter-0.2.1.dist-info/METADATA +336 -0
  52. scustomtkinter-0.2.1.dist-info/RECORD +101 -0
  53. scustomtkinter-0.2.1.dist-info/WHEEL +5 -0
  54. scustomtkinter-0.2.1.dist-info/entry_points.txt +2 -0
  55. scustomtkinter-0.2.1.dist-info/licenses/LICENSE +21 -0
  56. scustomtkinter-0.2.1.dist-info/licenses/scustomtkinter/license_morecustomtkinterwidgets.txt +22 -0
  57. scustomtkinter-0.2.1.dist-info/top_level.txt +2 -0
  58. scustomtkinter_pygubu/__init__.py +1 -0
  59. scustomtkinter_pygubu/designer/__init__.py +0 -0
  60. scustomtkinter_pygubu/designer/plugin.py +1854 -0
  61. scustomtkinter_pygubu/designer/properties.py +247 -0
  62. scustomtkinter_pygubu/sCTkButtonPrimarybo.py +46 -0
  63. scustomtkinter_pygubu/sCTkButtonSecondarybo.py +45 -0
  64. scustomtkinter_pygubu/sCTkButtonTertiarybo.py +44 -0
  65. scustomtkinter_pygubu/sCTkCheckBoxbo.py +45 -0
  66. scustomtkinter_pygubu/sCTkComboBoxbo.py +45 -0
  67. scustomtkinter_pygubu/sCTkCorebo.py +120 -0
  68. scustomtkinter_pygubu/sCTkDialbo.py +371 -0
  69. scustomtkinter_pygubu/sCTkDialogbo.py +397 -0
  70. scustomtkinter_pygubu/sCTkEntryPrimarybo.py +100 -0
  71. scustomtkinter_pygubu/sCTkEntrySecondarybo.py +98 -0
  72. scustomtkinter_pygubu/sCTkFileExplorerbo.py +189 -0
  73. scustomtkinter_pygubu/sCTkFrameLabeledPrimarybo.py +69 -0
  74. scustomtkinter_pygubu/sCTkFrameLabeledSecondarybo.py +71 -0
  75. scustomtkinter_pygubu/sCTkFramebo.py +46 -0
  76. scustomtkinter_pygubu/sCTkFrequencyDisplaybo.py +175 -0
  77. scustomtkinter_pygubu/sCTkLabelPrimarybo.py +45 -0
  78. scustomtkinter_pygubu/sCTkLabelSecondarybo.py +44 -0
  79. scustomtkinter_pygubu/sCTkLabelTertiarybo.py +45 -0
  80. scustomtkinter_pygubu/sCTkNotebookbo.py +392 -0
  81. scustomtkinter_pygubu/sCTkOptionMenuPrimarybo.py +45 -0
  82. scustomtkinter_pygubu/sCTkOptionMenuSecondarybo.py +39 -0
  83. scustomtkinter_pygubu/sCTkPathChooserbo.py +204 -0
  84. scustomtkinter_pygubu/sCTkProgressBarbo.py +45 -0
  85. scustomtkinter_pygubu/sCTkRadioButtonbo.py +45 -0
  86. scustomtkinter_pygubu/sCTkSMeterBarbo.py +153 -0
  87. scustomtkinter_pygubu/sCTkSMeterbo.py +101 -0
  88. scustomtkinter_pygubu/sCTkScrollableFramebo.py +66 -0
  89. scustomtkinter_pygubu/sCTkScrollbarbo.py +48 -0
  90. scustomtkinter_pygubu/sCTkSegmentedButtonbo.py +47 -0
  91. scustomtkinter_pygubu/sCTkSelectorbo.py +167 -0
  92. scustomtkinter_pygubu/sCTkSeparatorbo.py +141 -0
  93. scustomtkinter_pygubu/sCTkSliderbo.py +45 -0
  94. scustomtkinter_pygubu/sCTkSpinboxbo.py +200 -0
  95. scustomtkinter_pygubu/sCTkSwitchbo.py +45 -0
  96. scustomtkinter_pygubu/sCTkTableviewbo.py +188 -0
  97. scustomtkinter_pygubu/sCTkTabviewbo.py +226 -0
  98. scustomtkinter_pygubu/sCTkTextboxPrimarybo.py +45 -0
  99. scustomtkinter_pygubu/sCTkTextboxSecondarybo.py +45 -0
  100. scustomtkinter_pygubu/sCTkToplevelbo.py +55 -0
  101. scustomtkinter_pygubu/sCTkWidgetSetForPygubuDesigner.py +141 -0
@@ -0,0 +1,701 @@
1
+ #!/usr/bin/python3
2
+ """
3
+ sCTkDialog
4
+
5
+ A consistent popup dialog: a heading, a content area you fill, and a row of
6
+ Apply / Cancel / Reset buttons.
7
+
8
+ WINDOW OWNERSHIP. This widget creates its own sCTkDialogToplevel and packs
9
+ itself into it, so a dialog is always its own window. Callers do not create
10
+ one, and there is no mixin to add.
11
+
12
+ That replaces an earlier arrangement in which sCTkDialog was a plain frame
13
+ and a separate sCTkDialogMixin supplied the window, which required every user
14
+ to edit their generated class by hand -- adding an import and changing the
15
+ inheritance line. That step is gone.
16
+
17
+ The layout below was originally a Pygubu-generated base class,
18
+ sCTkDialogui.py, that this file inherited. It has been folded in, matching
19
+ every other widget in this library: a widget builds its own children in
20
+ __init__ rather than depending on a generated file shipped inside the package.
21
+ That file also carried imports which only resolved in the directory it was
22
+ generated in ("from sCTkFrame import sCTkFrame"), so it could not be imported
23
+ at all once installed.
24
+
25
+ The content area is `contentFrame`. In Pygubu Designer, widgets dropped onto an
26
+ sCTkDialog land there automatically -- the builder object's
27
+ get_child_master() returns it.
28
+ """
29
+ import tkinter as tk
30
+
31
+ import customtkinter as ctk
32
+
33
+ from .sctk_frame import sCTkFrame
34
+ from .sctk_label_primary import sCTkLabelPrimary
35
+ from .sctk_button_primary import sCTkButtonPrimary
36
+ from .sctk_button_secondary import sCTkButtonSecondary
37
+ from .sctk_dialog_toplevel import sCTkDialogToplevel
38
+
39
+
40
+ class sCTkDialog(sCTkFrame):
41
+ """Popup dialog with a heading, a content area and an action button row."""
42
+
43
+ # Set False on a subclass to suppress window creation. Used by the Pygubu
44
+ # Designer preview, which renders the dialog inline on its canvas and must
45
+ # not spawn a real window on every redraw -- a modal one would seize input
46
+ # and leave the Designer unusable.
47
+ _MAKE_WINDOW = True
48
+
49
+ # Required in the "sCTkDialog" block of sCTkThemes.json.
50
+ #
51
+ # NOTE the CustomTkinter naming: fg_color is the BACKGROUND fill, not the
52
+ # text colour. It is a native frame option and reaches CTkFrame on its own.
53
+ # text_color is the dialog's foreground -- used for the heading, and
54
+ # available to caller content through cget("text_color"). heading_font is
55
+ # read in _build_layout().
56
+ _REQUIRED_THEME_KEYS = ("fg_color", "text_color", "heading_font")
57
+
58
+ # Subclasses read THIS block unless they say otherwise.
59
+ #
60
+ # ThemeableWidget resolves a theme block from the class name, which is
61
+ # right for widgets nobody subclasses. A dialog is the opposite case:
62
+ # subclassing is how it is meant to be used -- the Designer generates
63
+ # `class SettingsDialog(sCTkDialog)`, and the documented example does the
64
+ # same -- so resolving by class name demanded a "SettingsDialog" block that
65
+ # no one would think to write, and construction failed with
66
+ #
67
+ # KeyError: "'SettingsDialog' theme block is missing 'fg_color' ..."
68
+ #
69
+ # naming a block that should not need to exist.
70
+ #
71
+ # A subclass that genuinely wants its own styling overrides this with its
72
+ # own block name.
73
+ _THEME_BLOCK_NAME = "sCTkDialog"
74
+
75
+ # Natural width of the content area, and so of the dialog when no explicit
76
+ # width is given. Override on a subclass for a consistently wider or
77
+ # narrower dialog.
78
+ CONTENT_WIDTH = 500
79
+
80
+ # How many buttons each setting shows, in order. Apply is always present:
81
+ # a dialog with no way to accept is a message box, not a dialog.
82
+ _BUTTON_ORDER = ("apply", "cancel", "reset")
83
+
84
+ def __init__(self, master=None, *, title=None, width=None, height=None,
85
+ locate_over=None, offset_x=40, offset_y=40, modal=False,
86
+ transient=True, heading="Heading Title",
87
+ heading_anchor="center", heading_font=None,
88
+ heading_color=None,
89
+ buttons=3, apply_text="Apply", cancel_text="Cancel",
90
+ reset_text="Reset", apply_command=None, cancel_command=None,
91
+ reset_command=None, toplevel=None, **kw):
92
+ """
93
+ Args:
94
+ master: Parent widget. Also the default for locate_over.
95
+ title: Window title bar text.
96
+ width / height: WINDOW size in pixels. Omit to size to content.
97
+ Note these size the window, not the frame: the frame fills its
98
+ window, so its own width and height would have no effect.
99
+ configure() and cget() treat them the same way.
100
+ locate_over: The window to appear over and be transient to.
101
+ Defaults to master's own toplevel. Kept separate from master
102
+ because a dialog is often parented to a frame or a controller
103
+ while needing to position over the main application window --
104
+ see sCTkDialogToplevel.
105
+ offset_x / offset_y: Pixels right of and below locate_over's
106
+ top-left corner.
107
+ modal: True to block interaction with the rest of the application
108
+ while the dialog is open. run_and_wait() additionally blocks
109
+ the calling code until it closes.
110
+ transient: True to tie the window to its parent -- it stays above
111
+ that window, minimises with it, and usually keeps out of the
112
+ taskbar. False gives an independent window, which is what a
113
+ long-lived tool panel wants.
114
+ heading: Text shown above the content area.
115
+ heading_anchor: "w", "e" or "center".
116
+ heading_font: Overrides the theme's heading_font for this instance.
117
+ A (family, size) or (family, size, style) tuple, or a CTkFont.
118
+ None uses the theme value.
119
+ heading_color: Overrides the theme's text_color for the heading on
120
+ this instance. None uses the theme value.
121
+ buttons: How many action buttons to show -- 3 (Apply, Cancel,
122
+ Reset), 2 (Apply, Cancel) or 1 (Apply). Apply is always
123
+ present: a dialog with no way to accept is a message box.
124
+ Buttons not shown are never created, so `reset_Button` does
125
+ not exist when buttons is 2 or 1 -- use has_button() to test.
126
+ apply_text / cancel_text / reset_text: Button labels.
127
+ apply_command / cancel_command / reset_command: Click callbacks.
128
+ A callback given here takes precedence over the corresponding
129
+ apply_CB / cancel_CB / reset_CB method, which remain available
130
+ to override in a subclass.
131
+ toplevel: An existing window to use instead of creating one.
132
+ Rarely needed; present so a caller can supply a pre-configured
133
+ window.
134
+ **kw: Any native sCTkFrame argument.
135
+ """
136
+ self.dialog_parent = master
137
+ self._heading_text = heading
138
+ self._heading_anchor = heading_anchor
139
+ self._heading_font_override = heading_font
140
+ self._heading_color_override = heading_color
141
+
142
+ try:
143
+ self._button_count = max(1, min(3, int(buttons)))
144
+ except (TypeError, ValueError):
145
+ self._button_count = 3
146
+ self._button_text = {"apply": apply_text, "cancel": cancel_text,
147
+ "reset": reset_text}
148
+ self._button_command = {"apply": apply_command,
149
+ "cancel": cancel_command,
150
+ "reset": reset_command}
151
+
152
+ if self._MAKE_WINDOW:
153
+ if toplevel is None:
154
+ # master is passed explicitly. Creating a Toplevel with no
155
+ # master attaches it to whichever root Tkinter considers
156
+ # default, which is wrong in any application with more than one
157
+ # window -- and a real source of silently orphaned Tk variables.
158
+ toplevel = sCTkDialogToplevel(
159
+ master, title=title, width=width, height=height,
160
+ locate_over=locate_over, offset_x=offset_x,
161
+ offset_y=offset_y, modal=modal, transient=transient,
162
+ )
163
+ self.dialog_toplevel = toplevel
164
+ self.dialog_toplevel.protocol(
165
+ "WM_DELETE_WINDOW", self.on_delete_window)
166
+ # The content is built INSIDE the new window, so the toplevel has
167
+ # to exist before this call.
168
+ super().__init__(self.dialog_toplevel, **kw)
169
+ self.pack(expand=True, fill="both")
170
+ else:
171
+ self.dialog_toplevel = None
172
+ super().__init__(master, **kw)
173
+
174
+ self._validate_theme_keys()
175
+ self._build_layout()
176
+
177
+ # Re-measure and re-place now that the content exists.
178
+ #
179
+ # sCTkDialogToplevel sizes itself in its own __init__, which runs
180
+ # BEFORE this widget builds anything into it -- so winfo_reqheight()
181
+ # was measuring an empty window. A dialog that omitted `height` (or
182
+ # gave only a width) came up too short, with the button row squashed
183
+ # to slivers at the bottom.
184
+ #
185
+ # Doing it here rather than moving the sizing out of the toplevel keeps
186
+ # that class usable on its own: it still sizes itself correctly when
187
+ # given explicit dimensions.
188
+ if self.dialog_toplevel is not None:
189
+ self.dialog_toplevel.place_over()
190
+
191
+ # ------------------------------------------------------------------
192
+ # Theme
193
+ # ------------------------------------------------------------------
194
+ def _validate_theme_keys(self):
195
+ """
196
+ Hard-fails on an incomplete theme block, naming the missing key.
197
+
198
+ Follows the library-wide fail-loud rule: substituting a plausible
199
+ default hides a broken block behind output that merely looks slightly
200
+ wrong.
201
+
202
+ Raises:
203
+ KeyError: naming the first missing key.
204
+ """
205
+ # FIX: this read self.__class__.__name__, which for a subclass is the
206
+ # SUBCLASS's name -- so the Designer preview class reported
207
+ # "'sCTkDialogForPreview' theme block is missing 'heading_font'" while
208
+ # the block it actually reads is named by _THEME_BLOCK_NAME. Validation
209
+ # must resolve the block the same way ThemeableWidget does, or it
210
+ # reports a block that was never consulted.
211
+ name = getattr(self, "_THEME_BLOCK_NAME", None) or self.__class__.__name__
212
+ for key in self._REQUIRED_THEME_KEYS:
213
+ if self.final_kw.get(key) is None:
214
+ raise KeyError(
215
+ f"'{name}' theme block is missing '{key}' at the top "
216
+ f"level of sCTkThemes.json."
217
+ )
218
+
219
+ # ------------------------------------------------------------------
220
+ # Layout
221
+ # ------------------------------------------------------------------
222
+ def _build_layout(self):
223
+ """
224
+ Builds the three regions: heading, content, action row.
225
+
226
+ Every StringVar is given an explicit master. A variable created without
227
+ one attaches to whichever root Tkinter considers default, which in an
228
+ application with more than one window silently binds it to the wrong
229
+ interpreter -- the widget updates one variable while the application
230
+ reads another, and the symptom is a value that is always empty.
231
+ """
232
+ # --- heading -------------------------------------------------------
233
+ self.titleFrame = sCTkFrame(self)
234
+ self.heading_VAR = tk.StringVar(master=self, value=self._heading_text)
235
+ self.heading_Label = sCTkLabelPrimary(self.titleFrame)
236
+ # font and colour come from THIS widget's theme block, not from
237
+ # sCTkLabelPrimary's. A dialog heading is a distinct role and should be
238
+ # restyleable without moving every primary label in the application.
239
+ self.heading_Label.configure(
240
+ anchor=self._heading_anchor,
241
+ textvariable=self.heading_VAR,
242
+ # A font passed to the constructor wins over the theme, so a
243
+ # single dialog can be restyled without touching the theme file.
244
+ font=self._heading_font_override or self.final_kw.get("heading_font"),
245
+ text_color=(self._heading_color_override
246
+ or self.final_kw.get("text_color")),
247
+ )
248
+ self.heading_Label.pack(expand=True, fill="x", side="top")
249
+ # expand=False: only the CONTENT area should take spare vertical
250
+ # space. With all three regions expanding, the extra height was shared
251
+ # between them and the dialog came up taller than its content, with a
252
+ # gap above the button row.
253
+ self.titleFrame.pack(anchor="n", expand=False, fill="x",
254
+ padx=10, pady="20 10", side="top")
255
+
256
+ # --- content area --------------------------------------------------
257
+ # Where caller content goes. The Designer's builder object returns this
258
+ # from get_child_master(), so widgets dropped on the dialog land here
259
+ # rather than beside the heading or the buttons.
260
+ self.contentFrame = sCTkFrame(self)
261
+ # Width sets the dialog's natural width; height is collapsed to 1 so
262
+ # the content area asks for no more room than its children need.
263
+ #
264
+ # CTkFrame defaults to 200x200, and a frame with an explicit height
265
+ # requests THAT height regardless of what is inside it -- so leaving
266
+ # the default in place made every dialog at least 200px of content
267
+ # area, with the surplus showing as a gap above the button row. Two
268
+ # entries need about 80.
269
+ #
270
+ # An empty content area collapsing to nothing is fine: the window has
271
+ # its own floor, sCTkDialogToplevel.MIN_HEIGHT.
272
+ self.contentFrame.configure(width=self.CONTENT_WIDTH, height=1)
273
+
274
+ # --- action row ----------------------------------------------------
275
+ # Built BEFORE the content area is packed: it packs to the bottom, and
276
+ # pack processes side="bottom" claims in order, so the content area has
277
+ # to come afterwards to fill what is left rather than push the buttons
278
+ # off the edge.
279
+ self._build_action_row()
280
+
281
+ self.contentFrame.pack(expand=True, fill="both", padx=5, side="top")
282
+
283
+ def _build_action_row(self):
284
+ """
285
+ Builds the button row from the current button count.
286
+
287
+ Separate from _build_layout() so set_buttons() can rebuild just this
288
+ part. Only the requested buttons are created; attributes for the others
289
+ are set to None rather than left undefined, so `self.reset_Button` is
290
+ always a valid expression -- code reaching for a button that was not
291
+ requested gets None instead of AttributeError.
292
+ """
293
+ self.actionFrame = sCTkFrame(self)
294
+
295
+ self.apply_Button = self.cancel_Button = self.reset_Button = None
296
+ self.applyText_VAR = self.cancelText_VAR = self.resetText_VAR = None
297
+
298
+ button_classes = {"apply": sCTkButtonPrimary,
299
+ "cancel": sCTkButtonSecondary,
300
+ "reset": sCTkButtonSecondary}
301
+
302
+ for column, name in enumerate(self._BUTTON_ORDER[:self._button_count]):
303
+ var = tk.StringVar(master=self, value=self._button_text[name])
304
+ button = button_classes[name](self.actionFrame)
305
+ # A command passed to the constructor wins; otherwise the
306
+ # overridable method is used, so a subclass that only defines
307
+ # apply_CB() still works.
308
+ command = self._button_command[name] or getattr(self, f"{name}_CB")
309
+ button.configure(textvariable=var, command=command)
310
+ button.grid(column=column, padx=10, row=0)
311
+ setattr(self, f"{name}_Button", button)
312
+ setattr(self, f"{name}Text_VAR", var)
313
+
314
+ # expand=False, and packed to the BOTTOM so it stays against the edge
315
+ # while the content area grows into whatever is left.
316
+ self.actionFrame.pack(anchor="s", expand=False, fill="x",
317
+ padx=5, pady="10 20", side="bottom")
318
+ self.actionFrame.grid_anchor("s")
319
+
320
+ def set_buttons(self, count):
321
+ """
322
+ Changes how many action buttons the dialog shows, rebuilding the row.
323
+
324
+ The row is built once at construction, so changing the count means
325
+ destroying and rebuilding it -- there is no way to add a button that
326
+ was never created. Labels and commands set through the constructor are
327
+ preserved, since they live in _button_text and _button_command rather
328
+ than on the widgets.
329
+
330
+ Exists so the Pygubu Designer can honour a change to `buttons` on a
331
+ live widget. Relying on the Designer to rebuild the whole widget did
332
+ not work: the canvas kept showing the old count while the preview and
333
+ the generated code showed the new one.
334
+
335
+ Args:
336
+ count: 1, 2 or 3. Clamped into that range.
337
+ """
338
+ try:
339
+ count = max(1, min(3, int(count)))
340
+ except (TypeError, ValueError):
341
+ return
342
+ if count == self._button_count:
343
+ return
344
+
345
+ self._button_count = count
346
+ if getattr(self, "actionFrame", None) is not None:
347
+ self.actionFrame.destroy()
348
+ self._build_action_row()
349
+
350
+ # The new row packs to the bottom, which puts it BELOW the content area
351
+ # in pack order. Re-packing the content area restores the intended
352
+ # arrangement: heading, content, buttons.
353
+ content = getattr(self, "contentFrame", None)
354
+ if content is not None:
355
+ content.pack_forget()
356
+ content.pack(expand=True, fill="both", padx=5, side="top")
357
+
358
+ def set_button_text(self, name, text):
359
+ """
360
+ Sets one button's label, remembering it across a rebuild.
361
+
362
+ set_buttons() recreates the buttons, so a label set only on the widget
363
+ would be lost. This records it too.
364
+
365
+ Args:
366
+ name: "apply", "cancel" or "reset".
367
+ text: The label.
368
+ """
369
+ if name not in self._button_text:
370
+ return False
371
+ self._button_text[name] = text
372
+ var = getattr(self, f"{name}Text_VAR", None)
373
+ if var is not None:
374
+ var.set(text)
375
+ return True
376
+
377
+ # ------------------------------------------------------------------
378
+ # Configuration
379
+ # ------------------------------------------------------------------
380
+ # Properties this widget owns. None of them are native CTkFrame options,
381
+ # so each is consumed by configure() below before the rest is forwarded.
382
+ # width and height are listed here deliberately.
383
+ #
384
+ # sCTkFrame, this widget's base, also has width and height -- meaning the
385
+ # FRAME's size. For a dialog that meaning is empty: the frame is packed
386
+ # with expand=True, fill="both", so the geometry manager overrides whatever
387
+ # size it is given and the value has no visible effect.
388
+ #
389
+ # Left alone, that produced three answers for one name: the constructor's
390
+ # width sized the window, configure(width=...) sized the frame and did
391
+ # nothing, and cget("width") reported the frame. Routing both to the window
392
+ # makes width and height mean the same thing everywhere -- the size of the
393
+ # dialog, which is what anyone asking for a dialog's width means.
394
+ _OWN_PROPERTIES = frozenset({
395
+ "width", "height",
396
+ "title", "heading", "heading_anchor", "heading_font", "heading_color",
397
+ "buttons", "apply_text", "cancel_text", "reset_text",
398
+ "apply_command", "cancel_command", "reset_command",
399
+ })
400
+
401
+ # Constructor defaults for this widget's own properties, so a
402
+ # single-argument query can report one. Pygubu calls configure(name) to
403
+ # discover a default when a field is cleared in the inspector, then applies
404
+ # what it gets back -- so an accurate default here is what makes clearing a
405
+ # label restore "Cancel" rather than leaving it blank.
406
+ _PROPERTY_DEFAULTS = {
407
+ "title": "", "heading": "Heading Title", "heading_anchor": "center",
408
+ "heading_font": "", "heading_color": "", "buttons": 3,
409
+ "apply_text": "Apply", "cancel_text": "Cancel", "reset_text": "Reset",
410
+ "apply_command": "", "cancel_command": "", "reset_command": "",
411
+ "width": "", "height": "",
412
+ }
413
+
414
+ def configure(self, cnf=None, **kwargs):
415
+ """
416
+ Standard configuration, extended to accept this widget's own
417
+ properties.
418
+
419
+ FIX: without this, generated code raised
420
+
421
+ ValueError: ['apply_command'] are not supported arguments.
422
+
423
+ Pygubu emits command properties as configure() arguments through its
424
+ own code path, so intercepting them in the builder object was not
425
+ enough -- they still reached CTkFrame.configure(), which rejects any
426
+ keyword it does not recognise. Handling them here works whichever path
427
+ supplies them, and gives callers a runtime API that matches the
428
+ Designer's inspector.
429
+
430
+ Args:
431
+ cnf: Optional dict of options, merged into kwargs.
432
+ **kwargs: Any native sCTkFrame option, or any of _OWN_PROPERTIES.
433
+
434
+ Returns:
435
+ Whatever the native configure() returns for the remaining options.
436
+ """
437
+ # FIX: a single positional STRING is a query, not a dict of options.
438
+ # Pygubu calls configure(name) to read a property's default when the
439
+ # field is cleared in the inspector, and this method previously did
440
+ # {**cnf}, which raises TypeError on a string -- so clearing a label
441
+ # blew up the unset path and the label stayed as it was.
442
+ if isinstance(cnf, str):
443
+ name = cnf
444
+ if name in self._OWN_PROPERTIES:
445
+ default = self._PROPERTY_DEFAULTS.get(name, "")
446
+ current = self.cget(name)
447
+ return (name, name, name, default, current)
448
+ return super().cget(name)
449
+
450
+ if cnf:
451
+ kwargs = {**cnf, **kwargs}
452
+
453
+ for key in list(kwargs):
454
+ if key not in self._OWN_PROPERTIES:
455
+ continue
456
+ value = kwargs.pop(key)
457
+
458
+ if key in ("width", "height"):
459
+ if self.dialog_toplevel is not None:
460
+ self.dialog_toplevel.set_size(**{key: value})
461
+ else:
462
+ # No window of its own -- the Designer preview. Fall back
463
+ # to the frame's own size so the canvas shows something
464
+ # that reflects the setting.
465
+ super().configure(**{key: value})
466
+ elif key == "title":
467
+ self.set_title(value or "")
468
+ elif key == "heading":
469
+ self.set_heading(
470
+ heading=value if value else
471
+ self._PROPERTY_DEFAULTS["heading"])
472
+ elif key == "heading_anchor":
473
+ self.set_heading(anchor=value)
474
+ elif key == "heading_font":
475
+ self.set_heading_font(value)
476
+ elif key == "heading_color":
477
+ self.set_heading_color(value)
478
+ elif key == "buttons":
479
+ self.set_buttons(value)
480
+ elif key.endswith("_text"):
481
+ # An empty value means "use the default", matching what the
482
+ # generated code does -- the property is omitted and the
483
+ # constructor default applies.
484
+ self.set_button_text(
485
+ key[:-len("_text")],
486
+ value if value else self._PROPERTY_DEFAULTS[key])
487
+ elif key.endswith("_command"):
488
+ name = key[:-len("_command")]
489
+ # Recorded as well as applied, so it survives the rebuild that
490
+ # a `buttons` change triggers.
491
+ self._button_command[name] = value
492
+ button = getattr(self, f"{name}_Button", None)
493
+ if button is not None and value:
494
+ button.configure(command=value)
495
+
496
+ if kwargs:
497
+ return super().configure(**kwargs)
498
+ return None
499
+
500
+ config = configure
501
+
502
+ def cget(self, key):
503
+ """Extends native cget() to this widget's own properties."""
504
+ if key == "buttons":
505
+ return self._button_count
506
+ if key in ("apply_text", "cancel_text", "reset_text"):
507
+ return self._button_text[key[:-len("_text")]]
508
+ if key in ("apply_command", "cancel_command", "reset_command"):
509
+ return self._button_command[key[:-len("_command")]]
510
+ if key == "heading":
511
+ return self.heading_VAR.get()
512
+ if key == "heading_anchor":
513
+ return self._heading_anchor
514
+ if key == "heading_font":
515
+ return (self._heading_font_override
516
+ or self.final_kw.get("heading_font"))
517
+ if key == "heading_color":
518
+ return (self._heading_color_override
519
+ or self.final_kw.get("text_color"))
520
+ if key == "title":
521
+ return (self.dialog_toplevel.title()
522
+ if self.dialog_toplevel is not None else "")
523
+ if key in ("width", "height"):
524
+ if self.dialog_toplevel is None:
525
+ return super().cget(key)
526
+ size = self.dialog_toplevel.get_size()
527
+ return size[0] if key == "width" else size[1]
528
+ return super().cget(key)
529
+
530
+ # ------------------------------------------------------------------
531
+ # Button callbacks -- override in a subclass
532
+ # ------------------------------------------------------------------
533
+ def apply_CB(self):
534
+ """Called when Apply is clicked. Override."""
535
+ pass
536
+
537
+ def cancel_CB(self):
538
+ """Called when Cancel is clicked. Override."""
539
+ pass
540
+
541
+ def reset_CB(self):
542
+ """Called when Reset is clicked. Override."""
543
+ pass
544
+
545
+ # ------------------------------------------------------------------
546
+ # Window
547
+ # ------------------------------------------------------------------
548
+ def on_delete_window(self):
549
+ """Bound to the window manager's close button. Override to intercept."""
550
+ self.dialog_close()
551
+
552
+ def dialog_close(self):
553
+ """Closes and destroys the dialog window."""
554
+ if self.dialog_toplevel is not None:
555
+ self.dialog_toplevel.destroy()
556
+
557
+ def run_and_wait(self):
558
+ """
559
+ Makes the dialog modal and blocks until it closes.
560
+
561
+ Use when the calling code needs the result before continuing. A dialog
562
+ constructed with modal=True already blocks *interaction*; this also
563
+ blocks *execution*, which is the difference between a dialog you can
564
+ ignore and one you must answer.
565
+ """
566
+ if self.dialog_toplevel is None:
567
+ return
568
+ self.dialog_toplevel._apply_modal()
569
+ parent = self.dialog_parent or self.dialog_toplevel
570
+ parent.wait_window(self.dialog_toplevel)
571
+
572
+ def set_title(self, title):
573
+ """Sets the window title bar text."""
574
+ if self.dialog_toplevel is not None:
575
+ self.dialog_toplevel.title(title)
576
+
577
+ # ------------------------------------------------------------------
578
+ # Content
579
+ # ------------------------------------------------------------------
580
+ def set_heading(self, heading=None, anchor=None):
581
+ """
582
+ Sets the heading text above the content area, and optionally its
583
+ alignment.
584
+
585
+ Args:
586
+ heading: New text. None leaves it unchanged.
587
+ anchor: "w", "e" or "center". Any other value is ignored. None
588
+ leaves it unchanged.
589
+ """
590
+ if heading is not None:
591
+ self.heading_VAR.set(heading)
592
+ if anchor is not None and str(anchor).lower() in ("w", "e", "center"):
593
+ self._heading_anchor = str(anchor).lower()
594
+ self.heading_Label.configure(anchor=self._heading_anchor)
595
+
596
+ def set_heading_font(self, font):
597
+ """Overrides the heading font for this instance."""
598
+ self.heading_Label.configure(font=font)
599
+
600
+ def set_heading_color(self, text_color):
601
+ """
602
+ Overrides the heading colour for this instance.
603
+
604
+ Passing None restores the theme's text_color.
605
+ """
606
+ self.heading_Label.configure(
607
+ text_color=text_color or self.final_kw.get("text_color"))
608
+
609
+ def has_button(self, name):
610
+ """
611
+ Whether a given button exists on this dialog.
612
+
613
+ Args:
614
+ name: "apply", "cancel" or "reset".
615
+
616
+ Returns:
617
+ True if that button was created and still exists.
618
+ """
619
+ button = getattr(self, f"{name}_Button", None)
620
+ if button is None:
621
+ return False
622
+ try:
623
+ return bool(button.winfo_exists())
624
+ except Exception:
625
+ return False
626
+
627
+ def set_two_button(self):
628
+ """
629
+ Reduces the action row to Apply and Cancel by removing Reset.
630
+
631
+ Retained from the original API. Prefer buttons=2 at construction --
632
+ this destroys a button that was built a moment earlier. Irreversible;
633
+ set_reset_button() returns False afterwards rather than raising.
634
+ """
635
+ if self.has_button("reset"):
636
+ self.reset_Button.destroy()
637
+ self._button_count = min(self._button_count, 2)
638
+
639
+ def set_apply_button(self, button_name=None, button_command=None):
640
+ """
641
+ Sets the Apply button's label and callback.
642
+
643
+ Args:
644
+ button_name: New label. None leaves it unchanged.
645
+ button_command: New callback. None leaves it unchanged.
646
+
647
+ Returns:
648
+ True, or False if the button does not exist on this dialog.
649
+ """
650
+ if not self.has_button("apply"):
651
+ return False
652
+ if button_name is not None:
653
+ self.applyText_VAR.set(button_name)
654
+ if button_command is not None:
655
+ self.apply_Button.configure(command=button_command)
656
+ return True
657
+
658
+ def set_cancel_button(self, button_name=None, button_command=None):
659
+ """Sets the Cancel button's label and callback. See set_apply_button()."""
660
+ if not self.has_button("cancel"):
661
+ return False
662
+ if button_name is not None:
663
+ self.cancelText_VAR.set(button_name)
664
+ if button_command is not None:
665
+ self.cancel_Button.configure(command=button_command)
666
+ return True
667
+
668
+ def set_reset_button(self, button_name=None, button_command=None):
669
+ """
670
+ Sets the Reset button's label and callback.
671
+
672
+ Returns:
673
+ False if the button has been removed by set_two_button(), in which
674
+ case nothing is changed. True otherwise.
675
+ """
676
+ if not self.has_button("reset"):
677
+ return False
678
+ if button_name is not None:
679
+ self.resetText_VAR.set(button_name)
680
+ if button_command is not None:
681
+ self.reset_Button.configure(command=button_command)
682
+ return True
683
+
684
+ # ------------------------------------------------------------------
685
+ # Backwards-compatible aliases.
686
+ #
687
+ # The original methods were camelCase and inconsistent with each other:
688
+ # setApplyButton took `buttonCommand` while setCancelButton and
689
+ # setResetButton took `ButtonCommand`, so the documented call failed on one
690
+ # of the three. The snake_case methods above are the supported API; these
691
+ # keep existing code working.
692
+ # ------------------------------------------------------------------
693
+ setTitle = set_title
694
+ setHeading = set_heading
695
+ setTwoButton = set_two_button
696
+ setApplyButton = set_apply_button
697
+ setCancelButton = set_cancel_button
698
+ setResetButton = set_reset_button
699
+ onDeleteWindow = on_delete_window
700
+ dialogClose = dialog_close
701
+ runAndWait = run_and_wait