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