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,405 @@
1
+ #!/usr/bin/python3
2
+ """
3
+ sCTkSMeter - Piece 1 of 2
4
+
5
+ A custom, theme-compliant analog signal and power output instrument widget.
6
+ Inherits directly from ctk.CTkFrame to bypass sub-frame keyword replication bugs,
7
+ and integrates with ThemeableWidget to read centralized style sheets safely.
8
+ """
9
+ import os
10
+ import math
11
+
12
+ import customtkinter as ctk
13
+ from .themeable_widget import ThemeableWidget, defer
14
+
15
+ class sCTkSMeter(ctk.CTkFrame, ThemeableWidget):
16
+ # Required at the TOP LEVEL of the theme block. No `.get(key, fallback)`
17
+ # anywhere in the draw code: an incomplete block fails at construction
18
+ # naming the missing key, rather than silently substituting a plausible
19
+ # guess that makes a broken theme look merely slightly-off.
20
+ _REQUIRED_THEME_KEYS = ("fg_color", "text_color", "alarm_color",
21
+ "needle_color", "font", "scale_font")
22
+
23
+ # Required inside disabled_map. fg_color deliberately excluded -- the
24
+ # background stays put when disabled and the face carries the signal,
25
+ # matching sCTkScrollableFrame and the dial family.
26
+ _REQUIRED_DISABLED_KEYS = ("text_color", "alarm_color", "needle_color")
27
+
28
+ def __init__(self, master=None, width=250, height=130, state="normal", **kw):
29
+ # 1. Initialize our Themeable mixin tracker cleanly
30
+ ThemeableWidget.__init__(self, kw)
31
+ self._local_defaults = dict(self.final_kw)
32
+
33
+ # Snapshot of the THEME's fonts, taken before any per-instance
34
+ # override. Clearing the font field in the Designer restores these
35
+ # rather than leaving the override in place, matching what generated
36
+ # code produces when the property is omitted.
37
+ self._theme_font_defaults = {
38
+ "font": self._local_defaults.get("font"),
39
+ "scale_font": self._local_defaults.get("scale_font"),
40
+ }
41
+ self._custom_disabled_map = dict(self._widget_disabled_map)
42
+ self._state = "normal" if str(state).lower() == "normal" else "disabled"
43
+ self._validate_theme_keys()
44
+
45
+ # 2. Background track. Popped because super().__init__() takes it as a
46
+ # separate argument -- but RETAINED on the instance, which is the fix
47
+ # for a real bug: _draw_meter() and _update_theme_colors() both did
48
+ # self._local_defaults.get("fg_color", hardcoded) AFTER this pop
49
+ # removed the key, so the background always rendered the hardcoded
50
+ # fallback and the configured fg_color never applied.
51
+ theme_bg_raw = self._local_defaults.pop("fg_color")
52
+ self._theme_bg_raw = theme_bg_raw
53
+ # Untouched copy, so clearing fg_color in the Designer can return to
54
+ # the theme rather than to whatever was last set.
55
+ self._theme_bg_default = theme_bg_raw
56
+
57
+ # Forcefully scrub custom canvas-drawing parameters out of self.final_kw
58
+ for gauge_custom_key in ["fg_color", "text_color", "alarm_color", "needle_color",
59
+ "font", "scale_font"]:
60
+ self.final_kw.pop(gauge_custom_key, None)
61
+
62
+ # 3. Pass dimensions and sanitized kwargs down to the parent frame engine safely
63
+ super().__init__(master, width=width, height=height, fg_color=theme_bg_raw, **self.final_kw)
64
+ self._current_value = 0.0
65
+
66
+ self._default_width = 250
67
+ self._default_height = 130
68
+ self.start_angle = 38
69
+ self.extent_angle = 104
70
+
71
+ # 4. Resolve the frame backplane color string and build the Tkinter drawing canvas
72
+ bg_resolved = self._resolve_color(theme_bg_raw)
73
+ self.canvas = ctk.CTkCanvas(self, highlightthickness=0, bd=0, bg=bg_resolved)
74
+ self.canvas.pack(fill="both", expand=True, padx=0, pady=0)
75
+
76
+ self.pack_propagate(False)
77
+ self.grid_propagate(False)
78
+
79
+ self.canvas.bind("<Configure>", lambda e: self._draw_meter())
80
+ self._finalize_themeable_lifecycle()
81
+
82
+ def _validate_theme_keys(self) -> None:
83
+ """
84
+ Hard-fails at construction on an incomplete theme block, naming the
85
+ missing key and where it belongs. Same fail-loud principle used
86
+ across this project.
87
+
88
+ Raises:
89
+ KeyError: naming the first missing key found.
90
+ """
91
+ # Resolved the way ThemeableWidget resolves it, so the message names
92
+ # the block actually read rather than a subclass's own name -- the
93
+ # Designer's preview classes are subclasses.
94
+ name = getattr(self, '_THEME_BLOCK_NAME', None) or self.__class__.__name__
95
+ for key in self._REQUIRED_THEME_KEYS:
96
+ if self._local_defaults.get(key) is None:
97
+ raise KeyError(
98
+ f"'{name}' theme block is missing '{key}' at the top level "
99
+ f"of sCTkThemes.json."
100
+ )
101
+ for key in self._REQUIRED_DISABLED_KEYS:
102
+ if self._custom_disabled_map.get(key) is None:
103
+ raise KeyError(
104
+ f"'{name}' theme block is missing '{key}' in disabled_map."
105
+ )
106
+
107
+ def _themed(self, key: str):
108
+ """
109
+ Returns the disabled-state value for a key when disabled and one
110
+ exists, otherwise the normal value.
111
+
112
+ Args:
113
+ key: Theme key to resolve.
114
+
115
+ Returns:
116
+ A raw (light, dark) tuple -- pass through _resolve_color().
117
+ """
118
+ if self._state == "disabled" and self._custom_disabled_map.get(key) is not None:
119
+ return self._custom_disabled_map[key]
120
+ return self._local_defaults.get(key)
121
+
122
+ def get_state(self) -> str:
123
+ """Returns the current state, "normal" or "disabled"."""
124
+ return self._state
125
+
126
+ def state(self, mode: str = None) -> str:
127
+ """
128
+ Gets or sets the widget state.
129
+
130
+ This is an output-only instrument -- there is nothing to lock out --
131
+ so disabling dims the face rather than blocking interaction. It exists
132
+ for consistency with the rest of the library, so a panel can disable
133
+ every widget it contains uniformly.
134
+
135
+ Args:
136
+ mode: None to query. "normal"/"enabled"/"active" or "disabled" to set.
137
+
138
+ Returns:
139
+ The resulting state.
140
+ """
141
+ if mode is None:
142
+ return self._state
143
+ target = str(mode).lower()
144
+ self._state = "normal" if target in ("normal", "enabled", "active") else "disabled"
145
+ if hasattr(self, "canvas") and self.canvas.winfo_exists():
146
+ self._draw_meter()
147
+ return self._state
148
+
149
+ def cget(self, attribute_name):
150
+ """Standard accessor, extended to know about `state`."""
151
+ if attribute_name == "state":
152
+ return self._state
153
+ return super().cget(attribute_name)
154
+
155
+ def configure(self, *args, **kwargs):
156
+ """Handles Pygubu layout inspector dictionary merges and parameter modifications safely."""
157
+ if args and len(args) == 1:
158
+ pname = args[0]
159
+ if pname == "width":
160
+ return ('width', 'width', 'Width', self._default_width, super().cget("width"))
161
+ if pname == "height":
162
+ return ('height', 'height', 'Height', self._default_height, super().cget("height"))
163
+ if pname == "state":
164
+ return ('state', 'state', 'state', 'normal', self._state)
165
+ # FIX: was `return super().configure(*args, **kwargs)`.
166
+ #
167
+ # Native CTkFrame.configure() is declared
168
+ # configure(self, require_redraw=False, **kwargs), so the property
169
+ # NAME was passed as require_redraw and the call returned None.
170
+ # Pygubu's _get_default_value() expects a five-element tuple, got
171
+ # None, and handed that None straight back to _set_property() --
172
+ # where CustomTkinter raised "float() argument must be a string or
173
+ # a real number, not 'NoneType'".
174
+ #
175
+ # Reached whenever a field is blanked in the Designer inspector.
176
+ # The library-wide sweep that fixed this matched on `pname` and
177
+ # `require_redraw` as the argument name and missed the *args form
178
+ # used here.
179
+ if pname == "fg_color":
180
+ return (pname, pname, pname,
181
+ self._query_value(self._theme_bg_default, pname),
182
+ self._query_value(self._theme_bg_raw, pname))
183
+
184
+ if pname in ("font", "scale_font"):
185
+ return (pname, pname, pname,
186
+ self._query_value(self._theme_font_defaults.get(pname), pname),
187
+ self._query_value(self._local_defaults.get(pname), pname))
188
+
189
+ return self._configure_query(pname)
190
+
191
+ # FIX: was `if args and isinstance(args, dict)`. args is ALWAYS a
192
+ # tuple, so this never fired and the dict form of configure() was dead
193
+ # code. Same tautology fixed across the batch-one widgets.
194
+ if len(args) == 1 and isinstance(args[0], dict):
195
+ kwargs = {**args[0], **kwargs}
196
+
197
+ # font and scale_font are per-instance overrides of the theme keys of
198
+ # the same name. An empty value restores the theme's, matching what
199
+ # generated code does when the property is omitted -- and matching the
200
+ # default the query branch above reports.
201
+ for _font_key in ("font", "scale_font"):
202
+ if _font_key in kwargs:
203
+ _new = kwargs.pop(_font_key)
204
+ if _new:
205
+ self._local_defaults[_font_key] = _new
206
+ else:
207
+ self._local_defaults[_font_key] = \
208
+ self._theme_font_defaults.get(_font_key)
209
+ self._draw_meter()
210
+
211
+ # state is this library's own property, not a native CTkFrame one, and
212
+ # must be removed before the super() call below.
213
+ # Runtime overrides have to reach the map a repaint reads, or the
214
+ # repaint puts the theme value straight back -- see
215
+ # ThemeableWidget._record_theme_overrides().
216
+ self._record_theme_overrides(kwargs)
217
+
218
+ # fg_color paints the CANVAS, which covers the frame entirely.
219
+ #
220
+ # It is popped out of _local_defaults at construction into
221
+ # _theme_bg_raw, so _record_theme_overrides() never sees it -- and
222
+ # forwarding it to the frame underneath changes a surface nobody can
223
+ # see. Handled here instead: an empty value restores the theme's.
224
+ if "fg_color" in kwargs:
225
+ _bg = kwargs.pop("fg_color")
226
+ self._theme_bg_raw = _bg if _bg else self._theme_bg_default
227
+ if hasattr(self, "canvas") and self.canvas.winfo_exists():
228
+ self.canvas.configure(bg=self._resolve_color(self._theme_bg_raw))
229
+
230
+ if "state" in kwargs:
231
+ self.state(kwargs.pop("state"))
232
+
233
+ if "width" in kwargs:
234
+ w = kwargs["width"]
235
+ kwargs["width"] = int(w) if (w and str(w).strip()) else self._default_width
236
+ if "height" in kwargs:
237
+ h = kwargs["height"]
238
+ kwargs["height"] = int(h) if (h and str(h).strip()) else self._default_height
239
+
240
+ if kwargs:
241
+ super().configure(**kwargs)
242
+ if hasattr(self, "canvas") and self.canvas.winfo_exists():
243
+ self._draw_meter()
244
+
245
+ config = configure
246
+
247
+ def _set_appearance_mode(self, mode_string: str):
248
+ """Native look catcher ensuring canvas backplanes repaint fluidly on theme skin shifts."""
249
+ if hasattr(super(), "_set_appearance_mode"):
250
+ try:
251
+ super()._set_appearance_mode(mode_string)
252
+ except Exception:
253
+ pass
254
+ if hasattr(self, "canvas") and self.canvas.winfo_exists():
255
+ defer(self, 15, self._update_theme_colors)
256
+
257
+ def _update_theme_colors(self):
258
+ """Refreshes the canvas widget background color directly from your asset maps."""
259
+ bg_color = self._resolve_color(self._theme_bg_raw)
260
+ self.canvas.configure(bg=bg_color)
261
+ self._draw_meter()
262
+
263
+ def _scaled_font(self, font):
264
+ """
265
+ A theme font with its size scaled for the display.
266
+
267
+ A canvas text item takes a plain font tuple, which CustomTkinter's own
268
+ font scaling never sees -- so without this the meter's captions and
269
+ scale stayed the same size while everything around them grew on a
270
+ scaled display. Same as sCTkNotebook and sCTkFrequencyDisplay.
271
+
272
+ THE WHOLE TUPLE GOES IN, not the size: _apply_font_scaling raises on
273
+ a bare integer, and a try/except around that mistake would quietly
274
+ stop scaling. A named font (a string) is left as it is.
275
+ """
276
+ if not isinstance(font, (list, tuple)):
277
+ return font
278
+ try:
279
+ return self._apply_font_scaling(tuple(font))
280
+ except Exception:
281
+ return tuple(font)
282
+
283
+ def _set_scaling(self, *args, **kwargs):
284
+ """
285
+ Redraws when the scaling changes (CustomTkinter calls this on every
286
+ widget when the window moves to a display with another scale, or the
287
+ app sets one): the canvas text is drawn, so nothing else would
288
+ update it.
289
+ """
290
+ super()._set_scaling(*args, **kwargs)
291
+ if getattr(self, "canvas", None) is not None and self.canvas.winfo_exists():
292
+ self._draw_meter()
293
+
294
+ @staticmethod
295
+ def _font_metrics(font):
296
+ """
297
+ Height and a width-measuring callable for a font.
298
+
299
+ The scale labels sat at a hardcoded `radius_sig + 16`, tuned for the
300
+ default font. A larger one grew back across that gap and crowded the
301
+ arc it labelled -- and because the labels are CENTRED on their point,
302
+ half of each grew inward regardless of size.
303
+
304
+ Returns:
305
+ (line_height, measure) where measure(text) gives a pixel width.
306
+ Falls back to a rough estimate if Tk cannot supply a font object,
307
+ which keeps the meter drawing rather than raising.
308
+ """
309
+ try:
310
+ import tkinter.font as tkfont
311
+ f = tkfont.Font(font=font)
312
+ return f.metrics("linespace"), f.measure
313
+ except Exception:
314
+ size = 10
315
+ try:
316
+ # abs(): a scaled font's size is negative (pixels).
317
+ size = abs(int(font[1])) or 10
318
+ except Exception:
319
+ pass
320
+ return int(size * 1.4), (lambda text: int(len(str(text)) * size * 0.6))
321
+
322
+ def _draw_meter(self):
323
+ """Renders dial arcs, ticks, text readouts, and needles using a single layout coordinate base."""
324
+ self.canvas.delete("all")
325
+ width = self.canvas.winfo_width()
326
+ height = self.canvas.winfo_height()
327
+ if width < 10 or height < 10: return
328
+
329
+ # No fallbacks: _validate_theme_keys() hard-failed at construction if
330
+ # any of these were missing, so every lookup is guaranteed to resolve.
331
+ bg = self._resolve_color(self._theme_bg_raw)
332
+ amber = self._resolve_color(self._themed("text_color"))
333
+ red = self._resolve_color(self._themed("alarm_color"))
334
+ font = self._scaled_font(self._local_defaults.get("font"))
335
+ scale_font = self._scaled_font(self._local_defaults.get("scale_font"))
336
+ self.canvas.configure(bg=bg)
337
+
338
+ # UNIFIED GEOMETRY BASELINE: Establishes a singular, shared layout pivot for lines and needles
339
+ radius_sig = min(width * 0.52, height * 1.20)
340
+ center_x, center_y = width * 0.48, height * 0.35 + radius_sig
341
+
342
+ self.canvas.create_text(center_x, height * 0.12, text="SIGNAL", fill=amber, font=font)
343
+ self.canvas.create_text(center_x, height * 0.80, text="RF OUTPUT", fill=amber, font=font)
344
+
345
+ split_frac = 9.0 / 15.0
346
+ split_ang = self.start_angle + (self.extent_angle * (1.0 - split_frac))
347
+ bbox = (center_x - radius_sig, center_y - radius_sig, center_x + radius_sig, center_y + radius_sig)
348
+
349
+ self.canvas.create_arc(bbox, start=split_ang, extent=self.start_angle + self.extent_angle - split_ang,
350
+ style="arc", outline=amber, width=2)
351
+ self.canvas.create_arc(bbox, start=self.start_angle, extent=split_ang - self.start_angle, style="arc",
352
+ outline=red, width=2)
353
+
354
+ for i in range(16):
355
+ frac = i / 15.0
356
+ ang = math.radians(self.start_angle + (self.extent_angle * (1.0 - frac)))
357
+
358
+ major = (i in (0, 1, 5, 9, 11, 13, 15))
359
+ is_red = (i > 9)
360
+ l = 10 if major else 5
361
+
362
+ x1, y1 = center_x + radius_sig * math.cos(ang), center_y - radius_sig * math.sin(ang)
363
+ x2, y2 = center_x + (radius_sig + l) * math.cos(ang), center_y - (radius_sig + l) * math.sin(ang)
364
+ self.canvas.create_line(x1, y1, x2, y2, fill=red if is_red else amber, width=2.5 if major else 1.2)
365
+
366
+ if major:
367
+ label = "" if i == 0 else (f"{i}" if i <= 9 else {11: "+20", 13: "+40", 15: "+60"}.get(i, ""))
368
+ # Clear of the arc by the label's own half-height plus the
369
+ # tick length, rather than a fixed 16px that a larger font grew
370
+ # back across.
371
+ scale_h, _ = self._font_metrics(scale_font)
372
+ text_radius = radius_sig + 8 + (scale_h / 2)
373
+ tx, ty = center_x + text_radius * math.cos(ang), center_y - text_radius * math.sin(ang)
374
+ if label: self.canvas.create_text(tx, ty, text=label, fill=red if is_red else amber, font=scale_font)
375
+
376
+ if i == 1:
377
+ ang_zero = math.radians(self.start_angle + (self.extent_angle * (1.0 - 0.0)))
378
+ sx, sy = center_x + text_radius * math.cos(ang_zero), center_y - text_radius * math.sin(ang_zero)
379
+ self.canvas.create_text(sx, sy, text="S", fill=amber, font=scale_font)
380
+
381
+ self._execute_needle_draw(center_x, center_y, radius_sig)
382
+
383
+ def _execute_needle_draw(self, cx, cy, rad):
384
+ """Draws the needle matching the permanent non-linear visual face scale track securely."""
385
+ val = self._current_value
386
+ frac = (max(0.0, val) / 9.0) * (9.0 / 15.0) if val <= 9.0 else (9.0 / 15.0) + (
387
+ (min(69.0, val) - 9.0) / 60.0) * (6.0 / 15.0)
388
+ frac = max(0.0, min(1.0, frac))
389
+ ang = math.radians(self.start_angle + (self.extent_angle * (1.0 - frac)))
390
+
391
+ nx, ny = cx + (rad + 2) * math.cos(ang), cy - (rad + 2) * math.sin(ang)
392
+ bx, by = cx + (rad - 60) * math.cos(ang), cy - (rad - 60) * math.sin(ang)
393
+ color = self._resolve_color(self._themed("needle_color"))
394
+
395
+ self.canvas.delete("needle")
396
+ self.canvas.create_line(bx, by, nx, ny, fill=color, width=2, tags="needle")
397
+
398
+ def set(self, value):
399
+ """Update the needle position indicator (Expects tracking values between 0.0 and 69.0)."""
400
+ self._current_value = max(0.0, min(69.0, float(value)))
401
+ w, h = self.canvas.winfo_width(), self.canvas.winfo_height()
402
+ if w > 10 and h > 10:
403
+ rad = min(w * 0.52, h * 1.20)
404
+ self._execute_needle_draw(w * 0.48, h * 0.35 + rad, rad)
405
+