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,354 @@
1
+ #!/usr/bin/python3
2
+ """
3
+ sCTkButtonSecondary
4
+
5
+ A theme-compliant secondary action button widget -- a lower-emphasis sibling
6
+ of sCTkButtonPrimary (see also sCTkButtonTertiary). Inherits directly from
7
+ ctk.CTkButton so CustomTkinter handles native rendering; this class layers
8
+ automatic light/dark theme resolution and a four-state visual model
9
+ (normal/disabled/pressed) on top. Unlike sCTkButtonPrimary, there is no
10
+ "alarm" state here.
11
+
12
+ Base class order matters here: `class sCTkButtonSecondary(ctk.CTkButton,
13
+ ThemeableWidget)` puts the native CTk class first, so every `super()` call in
14
+ this file's own methods resolves to ctk.CTkButton -- and, beneath it,
15
+ tkinter.Misc -- never to ThemeableWidget. ThemeableWidget's own
16
+ configure()/cget()/_set_appearance_mode() overrides have been removed entirely
17
+ for this reason (see themeable_widget.py's docstring); this widget owns all of
18
+ its own runtime color-swapping logic.
19
+ """
20
+ from typing import Any, Optional
21
+ import customtkinter as ctk
22
+ from .themeable_widget import ThemeableWidget, defer
23
+
24
+
25
+ class sCTkButtonSecondary(ctk.CTkButton, ThemeableWidget):
26
+ """Themeable secondary action button.
27
+
28
+ Adds to native ctk.CTkButton:
29
+ - Automatic light/dark theme resolution from sCTkThemes.json (via
30
+ ThemeableWidget.__init__ -- see that class's docstring for what it does,
31
+ and just as importantly, what it no longer does).
32
+ - A three-state visual model: normal, disabled, and pressed. Precedence
33
+ when both could apply is disabled > pressed (see
34
+ _update_current_visual_state() and the guard clause in set_pressed()).
35
+ - Pygubu Designer property introspection for a handful of properties via
36
+ a single-argument configure() call.
37
+
38
+ Colors are passed through to configure() as raw (light, dark) tuples rather
39
+ than pre-resolved to a single value, so CustomTkinter's own appearance-mode
40
+ tracking repaints them automatically on a light/dark switch -- the same
41
+ approach validated on sCTkComboBox, sCTkSegmentedButton, and sCTkButtonPrimary.
42
+
43
+ Disabling uses CTk's native state="disabled" rather than manually unbinding
44
+ mouse events -- an earlier version of this widget used the manual-unbind
45
+ approach (identical code to what sCTkButtonPrimary originally had), which
46
+ was directly tested and confirmed NOT to block clicks on that widget. The
47
+ fix is carried over here on the strength of both widgets sharing the same
48
+ underlying ctk.CTkButton click-handling; a confirmatory test on this
49
+ specific widget is still worthwhile.
50
+ """
51
+
52
+ def __init__(self, master: Optional[Any] = None, **kw: Any) -> None:
53
+ """
54
+ Args:
55
+ master: Parent container.
56
+ **kw: Any native CTkButton argument, or a theme-key override (see
57
+ the "sCTkButtonSecondary" block in sCTkThemes.json, including
58
+ its disabled_map, pressed_map and alarm_map sub-blocks).
59
+ """
60
+ # 1. Fire our shared theme logic first. This resolves final_kw
61
+ # (construction-time properties) and the disabled/pressed color maps.
62
+ # See ThemeableWidget.__init__ for what actually happens here.
63
+ # NO PLACEHOLDER TEXT. CustomTkinter defaults `text` to the class
64
+ # name, so a CTkButton built without text reads "CTkButton". Nothing wants
65
+ # that in a real interface, and the Designer made it worse: clearing
66
+ # the text field removes the property, the canvas rebuilds the widget
67
+ # from what is left, and the placeholder appeared in place of the
68
+ # blank that was asked for.
69
+ kw.setdefault("text", "")
70
+ ThemeableWidget.__init__(self, kw)
71
+
72
+ # 2. Deep-copy each resolved map onto this instance, so later changes
73
+ # here never leak back into the shared theme registry.
74
+ self._local_defaults = dict(self.final_kw)
75
+ self._custom_disabled_map = dict(self._widget_disabled_map)
76
+ self._custom_pressed_map = dict(self._widget_pressed_map)
77
+ self._custom_alarm_map = dict(self._widget_alarm_map)
78
+
79
+ # 3. Initialize CustomTkinter natively with the clean final kwargs array.
80
+ super().__init__(master, **self.final_kw)
81
+
82
+ self.is_pressed = False
83
+ self.is_alarm = False
84
+ self._custom_current_state = "normal"
85
+ self._update_current_visual_state()
86
+
87
+ # 4. Register lifecycle handshake hook, notifying Pygubu-style consumers
88
+ # that construction is complete.
89
+ self._finalize_themeable_lifecycle()
90
+
91
+ def configure(self, *args: Any, **kwargs: Any) -> Any:
92
+ """
93
+ Standard widget configuration, with Pygubu/positional-argument handling.
94
+
95
+ Args:
96
+ *args: At most one positional argument is meaningful:
97
+ - a dict: merged into kwargs and processed normally below.
98
+ - the literal string "state": returns a Tkinter-style
99
+ (name, name, name, default, current) tuple.
100
+ - one of "fg_color"/"border_color"/"text_color"/"hover_color":
101
+ returns the same style of tuple, with `current` reflecting
102
+ whichever of disabled/alarm/pressed/normal is currently active.
103
+ Note the returned value is str(value), where value may
104
+ itself be a (light, dark) tuple rather than a single
105
+ resolved color -- a known limitation shared with the wider
106
+ Pygubu-query investigation set aside elsewhere in this
107
+ project, not fixed here.
108
+ - anything else: forwarded directly to the native widget's
109
+ configure(), which does not support single-argument property
110
+ queries for arbitrary properties (same limitation).
111
+ **kwargs: Standard CTkButton configuration options, plus: passing
112
+ `state=...` routes through self.state() rather than being
113
+ forwarded as-is.
114
+
115
+ Returns:
116
+ The query tuple described above for the single-argument case, or
117
+ whatever super().configure() returns for the keyword-argument case
118
+ (typically None).
119
+ """
120
+ # args is always a tuple -- args[0] is the actual value passed, whether
121
+ # that's a string or a dict. An earlier version of this method compared
122
+ # the wrapped tuple directly (`pname = args`) and forwarded it unwrapped
123
+ # to super().configure(), which is not a valid call shape for the
124
+ # native widget. Don't reintroduce that.
125
+ if len(args) == 1:
126
+ if isinstance(args[0], dict):
127
+ kwargs = {**args[0], **kwargs}
128
+ else:
129
+ pname = args[0]
130
+ if pname == "state":
131
+ return ("state", "state", "state", "normal", str(self.state()))
132
+
133
+ if pname in ["fg_color", "border_color", "text_color", "hover_color"]:
134
+ current_state = str(self.state()).lower()
135
+ if current_state == "disabled" and self._custom_disabled_map:
136
+ val = self._custom_disabled_map.get(pname)
137
+ elif getattr(self, "is_alarm", False) and self._custom_alarm_map:
138
+ val = self._custom_alarm_map.get(pname)
139
+ elif getattr(self, "is_pressed", False) and self._custom_pressed_map:
140
+ val = self._custom_pressed_map.get(pname)
141
+ else:
142
+ val = self._local_defaults.get(pname)
143
+ return (pname, pname, pname, self._query_value(self._theme_default(pname)), self._query_value(val))
144
+
145
+ return self._configure_query(pname)
146
+
147
+ # Runtime overrides have to reach the map a repaint reads, or
148
+ # _update_current_visual_state() puts the theme value straight back --
149
+ # see ThemeableWidget._record_theme_overrides().
150
+ self._record_theme_overrides(kwargs)
151
+
152
+ if "state" in kwargs:
153
+ target_state = kwargs.pop("state")
154
+ self.state(target_state)
155
+
156
+ # Theme values cleared to "" are dropped; `text=""` is kept, so a
157
+ # button's caption can actually be blanked. See
158
+ # ThemeableWidget._CONTENT_KEYS.
159
+ self._drop_cleared(kwargs)
160
+ if kwargs:
161
+ return super().configure(**kwargs)
162
+ return None
163
+
164
+ # Tkinter/CTk convention binds .config to .configure as a SEPARATE class
165
+ # attribute -- it does not automatically track whichever configure() a
166
+ # subclass defines. Without this line, calling .config(...) on an instance
167
+ # would silently skip this entire override and land on the native widget's
168
+ # configure() directly, bypassing theming and state handling entirely.
169
+ config = configure
170
+
171
+ def _set_appearance_mode(self, mode_string: str) -> None:
172
+ """
173
+ Forwards CustomTkinter's internal light/dark mode change notification to
174
+ the native widget.
175
+
176
+ No longer manually re-triggers _update_current_visual_state(). That
177
+ method now passes raw (light, dark) tuples straight through to
178
+ configure() instead of pre-resolving to a single color, so CTk's own
179
+ appearance-mode tracking should repaint correctly on its own -- the same
180
+ approach validated on sCTkComboBox, sCTkSegmentedButton, and
181
+ sCTkButtonPrimary.
182
+
183
+ Args:
184
+ mode_string: The new appearance mode ("Light" or "Dark"), as passed
185
+ by CustomTkinter's internal appearance-mode change machinery.
186
+ """
187
+ if hasattr(super(), "_set_appearance_mode"):
188
+ try:
189
+ super()._set_appearance_mode(mode_string)
190
+ except Exception:
191
+ pass
192
+
193
+ def get_state(self) -> str:
194
+ """Equivalent to calling state() with no argument."""
195
+ return self.state()
196
+
197
+ def state(self, mode: Optional[str] = None) -> str:
198
+ """
199
+ Gets or sets the widget's enabled/disabled state.
200
+
201
+ Args:
202
+ mode: If None, returns the current state without changing anything.
203
+ Otherwise, only the literal string "disabled" (case-insensitive)
204
+ is treated as disabled; anything in ("normal", "enabled",
205
+ "active") is treated as enabled. Any other value falls through
206
+ without matching either branch, leaving the state unchanged.
207
+
208
+ Returns:
209
+ The resulting state ("normal" or "disabled", lowercase).
210
+ """
211
+ if mode is None:
212
+ return str(getattr(self, "_custom_current_state", "normal")).lower()
213
+
214
+ mode = mode.lower()
215
+ if mode in ("normal", "enabled", "active"):
216
+ self._custom_current_state = "normal"
217
+ super().configure(state="normal", hover=True)
218
+ # Re-establish the mouse bindings that disabling below may have
219
+ # torn down.
220
+ if hasattr(self, "_create_bindings"):
221
+ try: self._create_bindings()
222
+ except Exception: pass
223
+ # Deferred via after_idle rather than called immediately: on
224
+ # sCTkButtonPrimary, CTk's own state="normal" transition was found
225
+ # to schedule its own internal repaint that can run AFTER an
226
+ # immediate call here and silently overwrite fresh colors back to
227
+ # something stale. Queuing our reapply via after_idle lets CTk's
228
+ # own pending repaint settle first, so ours wins the final paint.
229
+ defer(self, "idle", self._update_current_visual_state)
230
+
231
+ elif mode == "disabled":
232
+ self._custom_current_state = "disabled"
233
+ # Uses CTk's native state="disabled" -- confirmed necessary (not
234
+ # optional) on sCTkButtonPrimary, which originally used the same
235
+ # manual-unbind code this method used to have. That approach was
236
+ # confirmed broken: clicks still fired the button's command while
237
+ # "disabled", almost certainly because CTkButton binds its click
238
+ # handler at the canvas-ITEM level (tag_bind on the drawn
239
+ # rectangle/text), not the canvas-WIDGET level -- so
240
+ # canvas.unbind() never reached it. Don't reintroduce manual event
241
+ # unbinding here.
242
+ super().configure(state="disabled", hover=False)
243
+ defer(self, "idle", self._update_current_visual_state)
244
+
245
+ return self._custom_current_state
246
+
247
+ def set_pressed(self, pressed: bool) -> None:
248
+ """
249
+ Toggles the visual "pressed" state.
250
+
251
+ No-op while disabled or in the alarm state -- see the precedence
252
+ order on _update_current_visual_state().
253
+
254
+ Args:
255
+ pressed: True to show the pressed-state colors, False to return to
256
+ normal (assuming not disabled or in alarm).
257
+ """
258
+ if getattr(self, "_custom_current_state", "normal") == "disabled" or self.is_alarm:
259
+ return
260
+ self.is_pressed = pressed
261
+ self._update_current_visual_state()
262
+
263
+ def set_alarm_state(self, active: bool) -> None:
264
+ """
265
+ Forces the button into (or out of) a high-visibility alarm state.
266
+
267
+ No-op while disabled. Turning alarm on clears any current "pressed"
268
+ state, since alarm takes visual precedence -- see
269
+ _update_current_visual_state().
270
+
271
+ Args:
272
+ active: True to enter the alarm state, False to leave it.
273
+ """
274
+ if getattr(self, "_custom_current_state", "normal") == "disabled":
275
+ return
276
+ self.is_alarm = active
277
+ if self.is_alarm:
278
+ self.is_pressed = False
279
+ self._update_current_visual_state()
280
+
281
+ def _update_current_visual_state(self) -> None:
282
+ """
283
+ Recomputes and applies this widget's colors from the theme file, based
284
+ on the current state. Precedence: disabled > alarm > pressed > normal -- only
285
+ the first matching branch applies.
286
+
287
+ Called after construction, on every state()/set_pressed() change, and
288
+ (indirectly, via after_idle) after every state() transition.
289
+
290
+ Passes raw (light, dark) tuples straight through to configure() instead
291
+ of resolving to a single color first, so CTk's native tracking can
292
+ handle appearance-mode repaints without help from _set_appearance_mode.
293
+ Every value here traces back to sCTkThemes.json; there are no
294
+ hardcoded colors in this method.
295
+
296
+ Only the normal (final else) branch also reapplies border_width,
297
+ corner_radius, and font -- these don't vary between
298
+ disabled/alarm/pressed/normal, so there's no need to repeat them in every
299
+ branch; they're set once here and otherwise left alone.
300
+ """
301
+ if getattr(self, "_custom_current_state", "normal") == "disabled":
302
+ config_payload = {}
303
+ for key in ("fg_color", "hover_color", "border_color"):
304
+ val = self._custom_disabled_map.get(key)
305
+ if val is not None:
306
+ config_payload[key] = val
307
+
308
+ # disabled_map's text_color goes to the NATIVE text_color_disabled.
309
+ #
310
+ # CTkButton has its own text_color_disabled option and its draw
311
+ # code uses that whenever state is "disabled", overriding whatever
312
+ # text_color was set to. Assigning the disabled value to text_color
313
+ # here was therefore discarded at draw time and the theme key had
314
+ # no visible effect at all.
315
+ #
316
+ # text_color keeps the NORMAL value, so the two options do not
317
+ # fight and CTk chooses between them as it is designed to.
318
+ disabled_text = self._custom_disabled_map.get("text_color")
319
+ if disabled_text is not None:
320
+ config_payload["text_color_disabled"] = disabled_text
321
+ normal_text = self._local_defaults.get("text_color")
322
+ if normal_text is not None:
323
+ config_payload["text_color"] = normal_text
324
+
325
+ if config_payload:
326
+ super().configure(**config_payload)
327
+ return
328
+
329
+ if getattr(self, "is_alarm", False):
330
+ config_payload = {}
331
+ for key in ("fg_color", "hover_color", "border_color", "text_color"):
332
+ val = self._custom_alarm_map.get(key)
333
+ if val is not None:
334
+ config_payload[key] = val
335
+ config_payload["hover"] = False
336
+ super().configure(**config_payload)
337
+
338
+ elif self.is_pressed:
339
+ config_payload = {}
340
+ for key in ("fg_color", "hover_color", "border_color", "text_color"):
341
+ val = self._custom_pressed_map.get(key)
342
+ if val is not None:
343
+ config_payload[key] = val
344
+ config_payload["hover"] = False
345
+ super().configure(**config_payload)
346
+ else:
347
+ config_payload = {}
348
+ for key in ("fg_color", "hover_color", "border_color", "text_color", "border_width", "corner_radius", "font"):
349
+ val = self._local_defaults.get(key)
350
+ if val is not None:
351
+ config_payload[key] = val
352
+ config_payload["hover"] = True
353
+ if config_payload:
354
+ super().configure(**config_payload)