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,636 @@
1
+ #!/usr/bin/python3
2
+ """
3
+ sCTkFrequencyDisplay
4
+
5
+ A grouped numeric readout you can click a digit of.
6
+
7
+ WHAT IT IS FOR
8
+
9
+ A frequency, shown as x.xxx.xxx.xxx, where clicking a digit selects it and
10
+ the selection IS the tuning step: pick the hundreds digit and a dial moves
11
+ 100 Hz per detent. That replaces a separate step control, and with it the
12
+ possibility of the two disagreeing about which digit is being tuned.
13
+
14
+ It also does signed offsets -- RIT, XIT, anything with a direction -- which
15
+ is why it is described as a grouped numeric readout rather than strictly a
16
+ frequency. Set `digits` and `signed` and the same drawing, selection and
17
+ theming serve both.
18
+
19
+ WHY A CANVAS, NOT A ROW OF LABELS
20
+
21
+ The first version assembled ten sCTkLabelPrimary widgets and coloured the
22
+ selected one's background. Three problems, all solved by drawing:
23
+
24
+ * The readout JITTERED. Proportional digits are not the same width, so the
25
+ row re-flowed as the numbers changed and the whole display shifted
26
+ sideways while tuning. Here every digit sits at a computed position and
27
+ nothing moves, whatever the font.
28
+
29
+ * Ten configure() calls per update, against one redraw.
30
+
31
+ * The selection highlight was a label background, so it could only be a
32
+ rectangle behind the glyph. Drawn, it can be anything -- and it takes its
33
+ colours from the theme rather than from hardcoded hex, which is what the
34
+ label version had resorted to.
35
+
36
+ A FIXED-WIDTH FONT IS THE DEFAULT for the same reason: even with computed
37
+ positions, a proportional font makes 1 and 8 different widths, so a digit
38
+ changing shifts the ink inside its own cell.
39
+ """
40
+ import tkinter as tk
41
+ import tkinter.font as tkfont
42
+
43
+ import customtkinter as ctk
44
+
45
+ from . import themeable_widget as _tw
46
+ from .themeable_widget import ThemeableWidget
47
+
48
+
49
+ class sCTkFrequencyDisplay(ctk.CTkFrame, ThemeableWidget):
50
+ """A grouped numeric readout with per-digit selection."""
51
+
52
+ # Required at the TOP LEVEL of the theme block.
53
+ _REQUIRED_THEME_KEYS = (
54
+ "fg_color", "font", "text_color", "delimiter_color",
55
+ "selected_color", "selected_text_color",
56
+ )
57
+
58
+ # Required inside disabled_map.
59
+ _REQUIRED_DISABLED_KEYS = ("text_color", "delimiter_color")
60
+
61
+ # Keys this widget draws with must not reach CTkFrame, which raises on
62
+ # anything it does not recognise.
63
+ _NATIVE_FRAME_KWARGS = frozenset({"width", "height", "bg_color"})
64
+
65
+ # Geometry, in UNSCALED pixels -- everything is put through
66
+ # _apply_widget_scaling() before it reaches the canvas.
67
+ PAD_X = 6 # left and right margin
68
+ PAD_Y = 4 # top and bottom margin
69
+ DIGIT_GAP = 0 # extra space between digits within a group
70
+ HIGHLIGHT_PAD = 2 # how far the selection box extends past a digit
71
+ HIGHLIGHT_RADIUS = 3
72
+
73
+ # Groups of three from the right, as a frequency is read.
74
+ GROUP = 3
75
+
76
+ def __init__(self, master=None, digits=10, signed=False, value=0,
77
+ delimiter=".", font_size=34, selectable=True,
78
+ min_digit=1, state="normal", command=None, **kw):
79
+ """
80
+ Args:
81
+ master: Parent container.
82
+ digits: How many digit positions. Ten reaches into the GHz as
83
+ x.xxx.xxx.xxx; four suits a signed offset.
84
+ signed: Show a leading + or -, for an offset rather than a
85
+ frequency.
86
+ value: Initial value, in the units the caller uses -- Hz for a
87
+ frequency.
88
+ delimiter: What separates the groups. Settable at any time.
89
+ font_size: Point size of the digits. The FAMILY comes from the
90
+ theme and should stay fixed-width; see the module docstring.
91
+ selectable: Whether clicking a digit selects it.
92
+ min_digit: The smallest selectable digit, as a power of ten. The
93
+ uBITX tunes in whole 10 Hz steps, so 1 means the units digit
94
+ is shown but cannot be the thing you are moving.
95
+ state: "normal" or "disabled".
96
+ command: Called with the step in units when the selection
97
+ changes -- 100 for the hundreds digit.
98
+ """
99
+ self._digit_count = int(digits)
100
+ self._signed = bool(signed)
101
+ self._value = int(value)
102
+ self._delimiter = str(delimiter)
103
+ self._font_size = int(font_size)
104
+ self._selectable = bool(selectable)
105
+ self._min_digit = max(0, int(min_digit))
106
+ self._command = command
107
+
108
+ ThemeableWidget.__init__(self, kw)
109
+
110
+ # Colours come from the RAW registry, not final_kw: ThemeableWidget
111
+ # strips its CUSTOM_VECTOR_KEYS out of final_kw, and reading them back
112
+ # from there finds nothing. See sCTkDial.md, "Reading theme colours".
113
+ raw_block = _tw.GLOBAL_THEME_REGISTRY.get(self.__class__.__name__) or {}
114
+ raw_colors = {k: v for k, v in raw_block.items()
115
+ if not isinstance(v, dict)}
116
+ self._local_defaults = ThemeableWidget._convert_lists_to_tuples(raw_colors)
117
+ self._local_defaults.update(self.final_kw)
118
+ self._custom_disabled_map = dict(self._widget_disabled_map)
119
+ self._validate_theme_keys()
120
+
121
+ native_kwargs = {k: v for k, v in self.final_kw.items()
122
+ if k in self._NATIVE_FRAME_KWARGS}
123
+ super().__init__(master, fg_color="transparent", border_width=0,
124
+ **native_kwargs)
125
+
126
+ self._state = "normal" if str(state).lower() == "normal" else "disabled"
127
+
128
+ # The selected digit, as a power of ten. Starts at the smallest
129
+ # selectable one, which is the least surprising default: a dial that
130
+ # arrives set to megahertz is a trap.
131
+ self._selected = self._min_digit
132
+
133
+ # canvas, NOT _canvas: CTkFrame keeps its own background canvas in
134
+ # _canvas, and assigning that name replaces the frame's reference to
135
+ # it. The dials and S-meters use the bare name for the same reason.
136
+ self.canvas = ctk.CTkCanvas(self, highlightthickness=0, bd=0)
137
+ self.canvas.pack(fill="both", expand=True)
138
+
139
+ self.canvas.bind("<Button-1>", self._on_click)
140
+ self.canvas.bind("<Configure>", lambda e: self._draw_display())
141
+
142
+ self._resize_to_fit()
143
+ self._finalize_themeable_lifecycle()
144
+
145
+ # ------------------------------------------------------------------
146
+ # Construction helpers
147
+ # ------------------------------------------------------------------
148
+ def _validate_theme_keys(self):
149
+ """
150
+ Hard-fails at construction on an incomplete theme block, naming the
151
+ missing key and where it belongs. The library-wide pattern: a missing
152
+ key is an error, never a silently substituted colour.
153
+ """
154
+ name = self.__class__.__name__
155
+ for key in self._REQUIRED_THEME_KEYS:
156
+ if self._local_defaults.get(key) is None:
157
+ raise KeyError(
158
+ f"'{name}' theme block is missing '{key}' at the top level "
159
+ f"of sCTkThemes.json.")
160
+ for key in self._REQUIRED_DISABLED_KEYS:
161
+ if self._custom_disabled_map.get(key) is None:
162
+ raise KeyError(
163
+ f"'{name}' theme block is missing '{key}' in disabled_map.")
164
+
165
+ def _sx(self, value):
166
+ """Scales a dimension for drawing."""
167
+ return self._apply_widget_scaling(value)
168
+
169
+ def _scaled_font(self):
170
+ """
171
+ The theme font at the configured size, scaled.
172
+
173
+ A canvas text item takes a plain tuple, which CustomTkinter's font
174
+ scaling never sees -- so without this the digits would stay put while
175
+ everything around them grew.
176
+
177
+ The FAMILY comes from the theme and the SIZE from this widget: one is
178
+ a house style, the other is how big this particular readout should be.
179
+ """
180
+ family, _, weight = self._font_parts()
181
+ # THE WHOLE TUPLE, not the size.
182
+ #
183
+ # _apply_font_scaling takes a font and returns it with its size
184
+ # scaled; handed a bare integer it raises:
185
+ #
186
+ # ValueError: Can not scale font '38' of type <class 'int'>
187
+ #
188
+ # Which is easy to write and easy to miss, because a widget that
189
+ # wraps the call in try/except just silently stops scaling.
190
+ try:
191
+ return self._apply_font_scaling((family, self._font_size, weight))
192
+ except Exception:
193
+ return (family, self._font_size, weight)
194
+
195
+ #
196
+ # Tk guarantees exactly three family names on every platform -- Courier,
197
+ # Helvetica and Times -- and maps each to something local. Everything
198
+ # else is a gamble: Menlo is macOS only, Consolas is Windows only,
199
+ # DejaVu Sans Mono is usually Linux. A theme naming one of those looks
200
+ # right wherever it was written and silently falls back to a
201
+ # PROPORTIONAL default everywhere else, which is precisely the jitter
202
+ # this widget exists to avoid.
203
+ #
204
+ # So the theme may name whatever it likes, and this list is tried in turn
205
+ # when that family is not installed.
206
+ _FIXED_WIDTH_FALLBACKS = ("Menlo", "Consolas", "DejaVu Sans Mono",
207
+ "Liberation Mono", "Courier New", "Courier")
208
+
209
+ def _font_parts(self):
210
+ """
211
+ Family, size and weight, with the family checked for existence.
212
+
213
+ Tk does not report a missing family -- it quietly substitutes its
214
+ default, which is proportional. Checking means a theme can name a
215
+ good local font without breaking the widget somewhere else.
216
+ """
217
+ font = self._local_defaults.get("font")
218
+ family, size, weight = "Courier", 34, "bold"
219
+ if isinstance(font, (list, tuple)):
220
+ if len(font) >= 1:
221
+ family = font[0]
222
+ if len(font) >= 2:
223
+ size = font[1]
224
+ if len(font) >= 3:
225
+ weight = font[2]
226
+ return self._available_family(family), size, weight
227
+
228
+ def _available_family(self, preferred):
229
+ """
230
+ The first installed family: the theme's, then the fallbacks.
231
+
232
+ Cached, because tkfont.families() is not cheap and the answer cannot
233
+ change while the application runs.
234
+ """
235
+ cached = getattr(self.__class__, "_resolved_family", None)
236
+ if cached is not None and cached[0] == preferred:
237
+ return cached[1]
238
+
239
+ chosen = preferred
240
+ try:
241
+ installed = {f.lower() for f in tkfont.families()}
242
+ if preferred.lower() not in installed:
243
+ for candidate in self._FIXED_WIDTH_FALLBACKS:
244
+ if candidate.lower() in installed:
245
+ chosen = candidate
246
+ break
247
+ else:
248
+ # Tk guarantees this one maps to something monospace on
249
+ # every platform, even when the enumeration comes back
250
+ # unhelpful.
251
+ chosen = "Courier"
252
+ except Exception:
253
+ chosen = "Courier"
254
+
255
+ self.__class__._resolved_family = (preferred, chosen)
256
+ return chosen
257
+
258
+ # ------------------------------------------------------------------
259
+ # Colour
260
+ # ------------------------------------------------------------------
261
+ def _colour(self, key):
262
+ """The current value for a key, honouring the disabled state."""
263
+ if self._state == "disabled":
264
+ val = self._custom_disabled_map.get(key)
265
+ if val is not None:
266
+ return self._resolve_color(val)
267
+ return self._resolve_color(self._local_defaults.get(key))
268
+
269
+ def _canvas_background(self):
270
+ """
271
+ A colour the canvas can actually be set to.
272
+
273
+ "transparent" means "take the parent's", which a raw canvas cannot
274
+ render -- so it has to be turned into whatever is really behind the
275
+ widget. The same accommodation sCTkNotebook and sCTkDial make.
276
+ """
277
+ colour = self._resolve_color(self._local_defaults.get("fg_color"))
278
+ if colour != "transparent":
279
+ return colour
280
+
281
+ parent = getattr(self, "master", None)
282
+ for _ in range(6):
283
+ if parent is None:
284
+ break
285
+ try:
286
+ candidate = self._resolve_color(parent.cget("fg_color"))
287
+ if candidate and candidate != "transparent":
288
+ return candidate
289
+ except Exception:
290
+ pass
291
+ parent = getattr(parent, "master", None)
292
+ return ("#1C1C1C" if str(ctk.get_appearance_mode()).lower() == "dark"
293
+ else "#F1F5F9")
294
+
295
+ # ------------------------------------------------------------------
296
+ # Layout: where each character sits
297
+ # ------------------------------------------------------------------
298
+ def _cells(self):
299
+ """
300
+ Every character to draw, left to right, as (kind, text, power).
301
+
302
+ kind is "sign", "digit" or "delimiter". power is the digit's place
303
+ value as a power of ten, or None -- that is what makes a click
304
+ translate into a step without counting positions.
305
+
306
+ LEADING GROUPS ARE SUPPRESSED until they hold something, so an HF
307
+ frequency does not sit behind two empty groups on a ten-digit
308
+ display. The positions do not move when a group appears: the layout
309
+ is computed for the full width and the leading cells are simply not
310
+ drawn.
311
+ """
312
+ text = f"{abs(self._value):0{self._digit_count}d}"[-self._digit_count:]
313
+ cells = []
314
+
315
+ if self._signed:
316
+ cells.append(("sign", "-" if self._value < 0 else "+", None))
317
+
318
+ for index, char in enumerate(text):
319
+ power = self._digit_count - 1 - index
320
+ cells.append(("digit", char, power))
321
+ # A delimiter after every third digit from the right, except at
322
+ # the very end.
323
+ if power % self.GROUP == 0 and power != 0:
324
+ cells.append(("delimiter", self._delimiter, None))
325
+ return cells
326
+
327
+ def _first_significant(self):
328
+ """
329
+ The index of the first cell worth drawing.
330
+
331
+ Anything before the first non-zero digit is a leading zero, and a
332
+ frequency does not show them. The last GROUP digits always draw, so a
333
+ value of zero reads as 0.000 rather than as nothing.
334
+ """
335
+ cells = self._cells()
336
+ for index, (kind, char, power) in enumerate(cells):
337
+ if kind == "digit" and char != "0":
338
+ return index
339
+ if kind == "digit" and power is not None and power < self.GROUP:
340
+ return index
341
+ return 0
342
+
343
+ def _metrics(self):
344
+ """Digit width, delimiter width and line height, measured once."""
345
+ try:
346
+ probe = tkfont.Font(font=self._scaled_font())
347
+ return (probe.measure("0"), probe.measure(self._delimiter),
348
+ probe.metrics("linespace"))
349
+ except Exception:
350
+ size = self._sx(self._font_size)
351
+ return int(size * 0.6), int(size * 0.3), int(size * 1.3)
352
+
353
+ def _positions(self):
354
+ """
355
+ (x, kind, text, power) for every cell, and the total width.
356
+
357
+ Computed from the metrics rather than from where the previous glyph
358
+ happened to end, so a digit changing cannot move its neighbours. This
359
+ is the whole reason the widget draws instead of packing labels.
360
+ """
361
+ digit_w, delim_w, _ = self._metrics()
362
+ gap = self._sx(self.DIGIT_GAP)
363
+ x = self._sx(self.PAD_X)
364
+ placed = []
365
+ for kind, char, power in self._cells():
366
+ width = delim_w if kind == "delimiter" else digit_w
367
+ placed.append((x, kind, char, power, width))
368
+ x += width + (gap if kind == "digit" else 0)
369
+ return placed, x + self._sx(self.PAD_X)
370
+
371
+ def _resize_to_fit(self):
372
+ """
373
+ Asks for exactly the room the readout needs.
374
+
375
+ Requested rather than imposed: a caller that wants a different size
376
+ can still say so, and the geometry manager decides. But the default
377
+ should fit, because a frequency clipped at the left edge is worse
378
+ than one that is too small.
379
+ """
380
+ _, width = self._positions()
381
+ _, _, line = self._metrics()
382
+ height = line + (2 * self._sx(self.PAD_Y))
383
+ try:
384
+ self.canvas.configure(width=int(width), height=int(height))
385
+ except Exception:
386
+ pass
387
+
388
+ # ------------------------------------------------------------------
389
+ # Drawing
390
+ # ------------------------------------------------------------------
391
+ def _draw_display(self):
392
+ """
393
+ Repaints the whole readout.
394
+
395
+ NOT named _draw(): CTkFrame has its own _draw(no_color_updates=...)
396
+ and calls it from its constructor, so a method of that name here
397
+ overrides it and the widget cannot be built.
398
+ """
399
+ if not hasattr(self, "canvas"):
400
+ return
401
+ try:
402
+ if not self.canvas.winfo_exists():
403
+ return
404
+ except Exception:
405
+ return
406
+
407
+ self.canvas.delete("all")
408
+ self.canvas.configure(bg=self._canvas_background())
409
+
410
+ font = self._scaled_font()
411
+ placed, _ = self._positions()
412
+ _, _, line = self._metrics()
413
+ y = self._sx(self.PAD_Y)
414
+ first = self._first_significant()
415
+
416
+ text_colour = self._colour("text_color")
417
+ delim_colour = self._colour("delimiter_color")
418
+
419
+ for index, (x, kind, char, power, width) in enumerate(placed):
420
+ # The SIGN always draws. It sits before the first significant
421
+ # digit, so the leading-zero rule would suppress it -- and an
422
+ # offset shown without its direction is worse than useless.
423
+ if index < first and kind != "sign":
424
+ continue # a leading zero, or the delimiter before one
425
+
426
+ if (kind == "digit" and power == self._selected
427
+ and self._selectable and self._state != "disabled"):
428
+ pad = self._sx(self.HIGHLIGHT_PAD)
429
+ self._rounded_box(x - pad, y - pad, x + width + pad,
430
+ y + line + pad,
431
+ self._colour("selected_color"))
432
+ colour = self._colour("selected_text_color")
433
+ elif kind == "delimiter":
434
+ colour = delim_colour
435
+ else:
436
+ colour = text_colour
437
+
438
+ self.canvas.create_text(x, y, text=char, anchor="nw",
439
+ fill=colour, font=font)
440
+
441
+ def _rounded_box(self, x0, y0, x1, y1, fill):
442
+ """The selection highlight, with its corners taken off."""
443
+ r = self._sx(self.HIGHLIGHT_RADIUS)
444
+ points = [x0 + r, y0, x1 - r, y0, x1, y0, x1, y0 + r,
445
+ x1, y1 - r, x1, y1, x1 - r, y1, x0 + r, y1,
446
+ x0, y1, x0, y1 - r, x0, y0 + r, x0, y0]
447
+ self.canvas.create_polygon(points, fill=fill, outline=fill,
448
+ smooth=True)
449
+
450
+ # ------------------------------------------------------------------
451
+ # Interaction
452
+ # ------------------------------------------------------------------
453
+ def _on_click(self, event):
454
+ """Selects the digit under the pointer, if it is selectable."""
455
+ if not self._selectable or self._state == "disabled":
456
+ return "break"
457
+
458
+ placed, _ = self._positions()
459
+ for x, kind, char, power, width in placed:
460
+ if kind != "digit" or power is None:
461
+ continue
462
+ if power < self._min_digit:
463
+ continue
464
+ if x <= event.x <= x + width:
465
+ self.select_digit(power)
466
+ break
467
+ return "break"
468
+
469
+ # ------------------------------------------------------------------
470
+ # Public API
471
+ # ------------------------------------------------------------------
472
+ def set(self, value):
473
+ """Sets the value. Does NOT fire the command -- see select_digit."""
474
+ self._value = int(value)
475
+ self._draw_display()
476
+
477
+ def get(self):
478
+ """The current value, as an integer."""
479
+ return self._value
480
+
481
+ def select_digit(self, power):
482
+ """
483
+ Selects a digit by its place value, and reports the new step.
484
+
485
+ Fires `command` because this is a USER action -- the whole point of
486
+ the widget is that choosing a digit chooses the step. set() stays
487
+ quiet, matching CustomTkinter and everything else here.
488
+ """
489
+ power = max(self._min_digit, min(self._digit_count - 1, int(power)))
490
+ if power == self._selected:
491
+ return
492
+ self._selected = power
493
+ self._draw_display()
494
+ if callable(self._command):
495
+ try:
496
+ self._command(self.step())
497
+ except TypeError:
498
+ self._command()
499
+
500
+ def step(self):
501
+ """The selected digit's place value, in units. 100 means 100 Hz."""
502
+ return 10 ** self._selected
503
+
504
+ def selected_digit(self):
505
+ """The selected digit as a power of ten."""
506
+ return self._selected
507
+
508
+ # ------------------------------------------------------------------
509
+ # State
510
+ # ------------------------------------------------------------------
511
+ def get_state(self):
512
+ """Equivalent to state() with no argument."""
513
+ return self.state()
514
+
515
+ def state(self, mode=None):
516
+ """
517
+ Gets or sets "normal"/"disabled".
518
+
519
+ Disabling dims the digits and stops selection. The value keeps
520
+ updating: this is a readout as well as a control, and a display that
521
+ froze while greyed would be indistinguishable from one showing a
522
+ current value -- which on a radio panel is actively misleading. Same
523
+ reasoning as the S-meters.
524
+ """
525
+ if mode is None:
526
+ return self._state
527
+ mode = str(mode).lower()
528
+ self._state = "disabled" if mode == "disabled" else "normal"
529
+ self._draw_display()
530
+ return self._state
531
+
532
+ # ------------------------------------------------------------------
533
+ # Configuration
534
+ # ------------------------------------------------------------------
535
+ def configure(self, *args, **kwargs):
536
+ """Standard configuration, plus Pygubu's single-argument query form."""
537
+ if len(args) == 1:
538
+ if isinstance(args[0], dict):
539
+ kwargs = {**args[0], **kwargs}
540
+ else:
541
+ pname = args[0]
542
+ simple = {
543
+ "state": ("normal", self._state),
544
+ "digits": (10, self._digit_count),
545
+ "signed": ("False", str(self._signed)),
546
+ "delimiter": (".", self._delimiter),
547
+ "font_size": (34, self._font_size),
548
+ "selectable": ("True", str(self._selectable)),
549
+ "min_digit": (1, self._min_digit),
550
+ "value": (0, self._value),
551
+ }
552
+ if pname in simple:
553
+ default, current = simple[pname]
554
+ return (pname, pname, pname, default, current)
555
+ return self._configure_query(pname)
556
+
557
+ self._record_theme_overrides(kwargs)
558
+
559
+ resize = False
560
+ if "state" in kwargs:
561
+ self.state(kwargs.pop("state"))
562
+ if "value" in kwargs:
563
+ self._value = int(kwargs.pop("value"))
564
+ if "digits" in kwargs:
565
+ self._digit_count = int(kwargs.pop("digits"))
566
+ resize = True
567
+ if "signed" in kwargs:
568
+ self._signed = self._as_bool(kwargs.pop("signed"))
569
+ resize = True
570
+ if "delimiter" in kwargs:
571
+ # Settable mid-stream: it is a display preference several windows
572
+ # share, and a readout that kept the delimiter it was born with
573
+ # would disagree with the rest of the interface the moment the
574
+ # setting changed.
575
+ self._delimiter = str(kwargs.pop("delimiter"))
576
+ resize = True
577
+ if "font_size" in kwargs:
578
+ self._font_size = int(kwargs.pop("font_size"))
579
+ resize = True
580
+ if "selectable" in kwargs:
581
+ self._selectable = self._as_bool(kwargs.pop("selectable"))
582
+ if "min_digit" in kwargs:
583
+ self._min_digit = max(0, int(kwargs.pop("min_digit")))
584
+ if self._selected < self._min_digit:
585
+ self._selected = self._min_digit
586
+ if "command" in kwargs:
587
+ self._command = kwargs.pop("command")
588
+
589
+ # Keys this widget draws with are consumed here: they are not native
590
+ # CTkFrame options, and forwarding one raises.
591
+ for key in list(kwargs):
592
+ if key in self._local_defaults and key not in self._NATIVE_FRAME_KWARGS:
593
+ kwargs.pop(key)
594
+
595
+ self._drop_cleared(kwargs)
596
+
597
+ result = super().configure(**kwargs) if kwargs else None
598
+ if resize:
599
+ self._resize_to_fit()
600
+ self._draw_display()
601
+ return result
602
+
603
+ config = configure
604
+
605
+ @staticmethod
606
+ def _as_bool(value):
607
+ """bool("False") is True, and the Designer sends strings."""
608
+ if isinstance(value, str):
609
+ return value.strip().lower() in ("true", "1", "yes", "on")
610
+ return bool(value)
611
+
612
+ def cget(self, attribute_name):
613
+ simple = {
614
+ "state": self._state, "digits": self._digit_count,
615
+ "signed": self._signed, "delimiter": self._delimiter,
616
+ "font_size": self._font_size, "selectable": self._selectable,
617
+ "min_digit": self._min_digit, "value": self._value,
618
+ }
619
+ if attribute_name in simple:
620
+ return simple[attribute_name]
621
+ if attribute_name in self._local_defaults and \
622
+ attribute_name not in self._NATIVE_FRAME_KWARGS:
623
+ return self._local_defaults.get(attribute_name)
624
+ return super().cget(attribute_name)
625
+
626
+ def _set_appearance_mode(self, mode_string):
627
+ if hasattr(super(), "_set_appearance_mode"):
628
+ try:
629
+ super()._set_appearance_mode(mode_string)
630
+ except Exception:
631
+ pass
632
+ self._draw_display()
633
+
634
+ def _update_current_visual_state(self):
635
+ """Repaint hook, found by ThemeableWidget._repaint_after_override()."""
636
+ self._draw_display()