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,557 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
"""
|
|
3
|
+
sCTkSMeterBar
|
|
4
|
+
|
|
5
|
+
A standalone, low-profile horizontal discrete LED segment bar widget displaying
|
|
6
|
+
simultaneous, independent tracks for incoming S-Units and transmitter SWR ratio levels.
|
|
7
|
+
Inherits directly from ctk.CTkFrame to bypass nested framework validation bugs,
|
|
8
|
+
and integrates with ThemeableWidget to read centralized style sheets safely.
|
|
9
|
+
"""
|
|
10
|
+
import os
|
|
11
|
+
import math
|
|
12
|
+
|
|
13
|
+
import customtkinter as ctk
|
|
14
|
+
from .themeable_widget import ThemeableWidget, defer
|
|
15
|
+
|
|
16
|
+
class sCTkSMeterBar(ctk.CTkFrame, ThemeableWidget):
|
|
17
|
+
# Required at the TOP LEVEL of the theme block. inactive_color is distinct
|
|
18
|
+
# from the disabled state: it greys the SWR/PWR labels when those ROWS are
|
|
19
|
+
# switched off via configure_visibility(), which is a visibility flag, not
|
|
20
|
+
# a widget state. Both can apply at once.
|
|
21
|
+
_REQUIRED_THEME_KEYS = ("fg_color", "text_color", "alarm_color",
|
|
22
|
+
"led_on_color", "led_off_color",
|
|
23
|
+
"font", "scale_font", "inactive_color")
|
|
24
|
+
|
|
25
|
+
# Required inside disabled_map. fg_color deliberately excluded -- the
|
|
26
|
+
# background stays put when disabled and the face carries the signal.
|
|
27
|
+
_REQUIRED_DISABLED_KEYS = ("text_color", "alarm_color",
|
|
28
|
+
"led_on_color", "led_off_color")
|
|
29
|
+
|
|
30
|
+
def __init__(self, master=None, swr_max_value=5.0, swr_visible=True, pwr_visible=True, hide_lower_row=False, hide_sig_row=False, width=320, height=110, state="normal", **kw):
|
|
31
|
+
# 1. Initialize our Themeable mixin tracker cleanly
|
|
32
|
+
ThemeableWidget.__init__(self, kw)
|
|
33
|
+
self._local_defaults = dict(self.final_kw)
|
|
34
|
+
|
|
35
|
+
# Snapshot of the THEME's fonts, taken before any per-instance
|
|
36
|
+
# override. Clearing the font field in the Designer restores these
|
|
37
|
+
# rather than leaving the override in place, matching what generated
|
|
38
|
+
# code produces when the property is omitted.
|
|
39
|
+
self._theme_font_defaults = {
|
|
40
|
+
"font": self._local_defaults.get("font"),
|
|
41
|
+
"scale_font": self._local_defaults.get("scale_font"),
|
|
42
|
+
}
|
|
43
|
+
self._custom_disabled_map = dict(self._widget_disabled_map)
|
|
44
|
+
self._state = "normal" if str(state).lower() == "normal" else "disabled"
|
|
45
|
+
self._validate_theme_keys()
|
|
46
|
+
|
|
47
|
+
# 2. Background track. Popped because super().__init__() takes it as a
|
|
48
|
+
# separate argument -- but RETAINED on the instance, which is the fix
|
|
49
|
+
# for a real bug: _draw_meter() and _update_theme_colors() both did
|
|
50
|
+
# self._local_defaults.get("fg_color", hardcoded) AFTER this pop
|
|
51
|
+
# removed the key, so the background always rendered the hardcoded
|
|
52
|
+
# fallback and the configured fg_color never applied.
|
|
53
|
+
theme_bg_raw = self._local_defaults.pop("fg_color")
|
|
54
|
+
self._theme_bg_raw = theme_bg_raw
|
|
55
|
+
# Untouched copy, so clearing fg_color in the Designer can return to
|
|
56
|
+
# the theme rather than to whatever was last set.
|
|
57
|
+
self._theme_bg_default = theme_bg_raw
|
|
58
|
+
|
|
59
|
+
# 🔑 THE STRUCTURAL CLEAN PURGE: Clear custom analog gauge properties explicitly out of self.final_kw
|
|
60
|
+
# This completely guarantees the native CustomTkinter constructor won't throw a ValueError!
|
|
61
|
+
for led_custom_key in ["fg_color", "text_color", "alarm_color", "led_on_color",
|
|
62
|
+
"led_off_color", "font", "scale_font", "inactive_color"]:
|
|
63
|
+
self.final_kw.pop(led_custom_key, None)
|
|
64
|
+
|
|
65
|
+
# 3. Pass dimensions and sanitized kwargs down to the native parent frame engine safely
|
|
66
|
+
super().__init__(master, width=width, height=height, fg_color=theme_bg_raw, **self.final_kw)
|
|
67
|
+
|
|
68
|
+
self.swr_max_value = float(swr_max_value)
|
|
69
|
+
self._default_swr_max_value = 5.0
|
|
70
|
+
self._default_swr_visible = True
|
|
71
|
+
self._default_pwr_visible = True
|
|
72
|
+
self._default_hide_lower_row = False
|
|
73
|
+
self._default_hide_sig_row = False
|
|
74
|
+
self._default_width = 320
|
|
75
|
+
self._default_height = 110
|
|
76
|
+
|
|
77
|
+
self._swr_visible = bool(swr_visible)
|
|
78
|
+
self._pwr_visible = bool(pwr_visible)
|
|
79
|
+
self._hide_lower_row = bool(hide_lower_row)
|
|
80
|
+
# Hides the S row, leaving SWR and PWR.
|
|
81
|
+
#
|
|
82
|
+
# The mirror of hide_lower_row, and it exists for the split-meter
|
|
83
|
+
# layout: an S bar under the receive frequency and a PWR/SWR bar under
|
|
84
|
+
# the transmit one, each half the instrument rather than two full ones
|
|
85
|
+
# with a dead half each.
|
|
86
|
+
self._hide_sig_row = bool(hide_sig_row)
|
|
87
|
+
|
|
88
|
+
self._current_s_value = 0.0
|
|
89
|
+
self._current_swr_value = 1.0
|
|
90
|
+
self._current_pwr_value = 0.0
|
|
91
|
+
# A reading in words, drawn at the right end of the S row's caption
|
|
92
|
+
# line -- "-63 dBFS", say. Empty draws nothing. See set().
|
|
93
|
+
self._current_sig_text = ""
|
|
94
|
+
|
|
95
|
+
# 4. Resolve the frame backplane color string and build the Tkinter drawing canvas
|
|
96
|
+
bg_resolved = self._resolve_color(theme_bg_raw)
|
|
97
|
+
self.canvas = ctk.CTkCanvas(self, highlightthickness=0, bd=0, bg=bg_resolved)
|
|
98
|
+
self.canvas.pack(fill="both", expand=True, padx=0, pady=0)
|
|
99
|
+
|
|
100
|
+
self.pack_propagate(False)
|
|
101
|
+
self.grid_propagate(False)
|
|
102
|
+
|
|
103
|
+
self.canvas.bind("<Configure>", lambda e: self._draw_meter())
|
|
104
|
+
self._finalize_themeable_lifecycle()
|
|
105
|
+
|
|
106
|
+
def _validate_theme_keys(self) -> None:
|
|
107
|
+
"""
|
|
108
|
+
Hard-fails at construction on an incomplete theme block, naming the
|
|
109
|
+
missing key and where it belongs.
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
KeyError: naming the first missing key found.
|
|
113
|
+
"""
|
|
114
|
+
# Resolved the way ThemeableWidget resolves it, so the message names
|
|
115
|
+
# the block actually read rather than a subclass's own name -- the
|
|
116
|
+
# Designer's preview classes are subclasses.
|
|
117
|
+
name = getattr(self, '_THEME_BLOCK_NAME', None) or self.__class__.__name__
|
|
118
|
+
for key in self._REQUIRED_THEME_KEYS:
|
|
119
|
+
if self._local_defaults.get(key) is None:
|
|
120
|
+
raise KeyError(
|
|
121
|
+
f"'{name}' theme block is missing '{key}' at the top level "
|
|
122
|
+
f"of sCTkThemes.json."
|
|
123
|
+
)
|
|
124
|
+
for key in self._REQUIRED_DISABLED_KEYS:
|
|
125
|
+
if self._custom_disabled_map.get(key) is None:
|
|
126
|
+
raise KeyError(
|
|
127
|
+
f"'{name}' theme block is missing '{key}' in disabled_map."
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def _themed(self, key: str):
|
|
131
|
+
"""
|
|
132
|
+
Returns the disabled-state value for a key when disabled and one
|
|
133
|
+
exists, otherwise the normal value.
|
|
134
|
+
|
|
135
|
+
Distinct from inactive_color, which greys the SWR/PWR labels when
|
|
136
|
+
those rows are switched off. A row can be hidden on an enabled widget,
|
|
137
|
+
and a disabled widget still shows whichever rows are visible.
|
|
138
|
+
"""
|
|
139
|
+
if self._state == "disabled" and self._custom_disabled_map.get(key) is not None:
|
|
140
|
+
return self._custom_disabled_map[key]
|
|
141
|
+
return self._local_defaults.get(key)
|
|
142
|
+
|
|
143
|
+
def get_state(self) -> str:
|
|
144
|
+
"""Returns the current state, "normal" or "disabled"."""
|
|
145
|
+
return self._state
|
|
146
|
+
|
|
147
|
+
def state(self, mode: str = None) -> str:
|
|
148
|
+
"""
|
|
149
|
+
Gets or sets the widget state.
|
|
150
|
+
|
|
151
|
+
This is an output-only instrument -- there is nothing to lock out --
|
|
152
|
+
so disabling dims the face rather than blocking interaction. It exists
|
|
153
|
+
for consistency with the rest of the library, so a panel can disable
|
|
154
|
+
every widget it contains uniformly.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
mode: None to query. "normal"/"enabled"/"active" or "disabled" to set.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
The resulting state.
|
|
161
|
+
"""
|
|
162
|
+
if mode is None:
|
|
163
|
+
return self._state
|
|
164
|
+
target = str(mode).lower()
|
|
165
|
+
self._state = "normal" if target in ("normal", "enabled", "active") else "disabled"
|
|
166
|
+
if hasattr(self, "canvas") and self.canvas.winfo_exists():
|
|
167
|
+
self._draw_meter()
|
|
168
|
+
return self._state
|
|
169
|
+
|
|
170
|
+
def configure_visibility(self, swr_visible=None, pwr_visible=None, hide_lower_row=None, hide_sig_row=None):
|
|
171
|
+
"""Public configuration mapping hook to alter the lower row layout matrix states live."""
|
|
172
|
+
if swr_visible is not None: self._swr_visible = bool(swr_visible)
|
|
173
|
+
if pwr_visible is not None: self._pwr_visible = bool(pwr_visible)
|
|
174
|
+
if hide_lower_row is not None: self._hide_lower_row = bool(hide_lower_row)
|
|
175
|
+
if hide_sig_row is not None: self._hide_sig_row = bool(hide_sig_row)
|
|
176
|
+
if self.canvas.winfo_exists(): self._draw_meter()
|
|
177
|
+
|
|
178
|
+
def configure(self, *args, **kwargs):
|
|
179
|
+
"""Handles Pygubu layout inspector dictionary merges and parameter modifications safely."""
|
|
180
|
+
if args and len(args) == 1:
|
|
181
|
+
# FIX: was `pname = args`, leaving pname as a TUPLE -- every
|
|
182
|
+
# comparison below failed, so all three single-argument queries
|
|
183
|
+
# were dead and fell through to super(). Pygubu could not read any
|
|
184
|
+
# of them. Same bug fixed in sCTkFileExplorer.
|
|
185
|
+
pname = args[0]
|
|
186
|
+
if pname == "state":
|
|
187
|
+
return ("state", "state", "state", "normal", self._state)
|
|
188
|
+
if pname in ("swr_visible", "pwr_visible", "hide_lower_row", "hide_sig_row"):
|
|
189
|
+
defaults = {"swr_visible": self._default_swr_visible,
|
|
190
|
+
"pwr_visible": self._default_pwr_visible,
|
|
191
|
+
"hide_lower_row": self._default_hide_lower_row,
|
|
192
|
+
"hide_sig_row": self._default_hide_sig_row}
|
|
193
|
+
currents = {"swr_visible": self._swr_visible,
|
|
194
|
+
"pwr_visible": self._pwr_visible,
|
|
195
|
+
"hide_lower_row": self._hide_lower_row,
|
|
196
|
+
"hide_sig_row": self._hide_sig_row}
|
|
197
|
+
return (pname, pname, pname,
|
|
198
|
+
str(defaults[pname]), str(currents[pname]))
|
|
199
|
+
|
|
200
|
+
if pname in ["width", "height", "swr_max_value"]:
|
|
201
|
+
fallback = self._default_width if pname == "width" else (self._default_height if pname == "height" else self._default_swr_max_value)
|
|
202
|
+
current = super().cget("width") if pname == "width" else (super().cget("height") if pname == "height" else self.swr_max_value)
|
|
203
|
+
return (pname, pname, pname, self._query_value(fallback), self._query_value(current))
|
|
204
|
+
# FIX: was `return super().configure(*args, **kwargs)`.
|
|
205
|
+
#
|
|
206
|
+
# Native CTkFrame.configure() is declared
|
|
207
|
+
# configure(self, require_redraw=False, **kwargs), so the property
|
|
208
|
+
# NAME was passed as require_redraw and the call returned None.
|
|
209
|
+
# Pygubu's _get_default_value() expects a five-element tuple, got
|
|
210
|
+
# None, and handed that None straight back to _set_property() --
|
|
211
|
+
# where CustomTkinter raised "float() argument must be a string or
|
|
212
|
+
# a real number, not 'NoneType'".
|
|
213
|
+
#
|
|
214
|
+
# Reached whenever a field is blanked in the Designer inspector.
|
|
215
|
+
# The library-wide sweep that fixed this matched on `pname` and
|
|
216
|
+
# `require_redraw` as the argument name and missed the *args form
|
|
217
|
+
# used here.
|
|
218
|
+
if pname == "fg_color":
|
|
219
|
+
return (pname, pname, pname,
|
|
220
|
+
self._query_value(self._theme_bg_default, pname),
|
|
221
|
+
self._query_value(self._theme_bg_raw, pname))
|
|
222
|
+
|
|
223
|
+
if pname in ("font", "scale_font"):
|
|
224
|
+
return (pname, pname, pname,
|
|
225
|
+
self._query_value(self._theme_font_defaults.get(pname), pname),
|
|
226
|
+
self._query_value(self._local_defaults.get(pname), pname))
|
|
227
|
+
|
|
228
|
+
return self._configure_query(pname)
|
|
229
|
+
|
|
230
|
+
# FIX: was `if args and isinstance(args, dict)`. args is ALWAYS a
|
|
231
|
+
# tuple, so this never fired and the dict form was dead code.
|
|
232
|
+
if len(args) == 1 and isinstance(args[0], dict): kwargs = {**args[0], **kwargs}
|
|
233
|
+
# font and scale_font are per-instance overrides of the theme keys of
|
|
234
|
+
# the same name. An empty value restores the theme's, matching what
|
|
235
|
+
# generated code does when the property is omitted -- and matching the
|
|
236
|
+
# default the query branch above reports.
|
|
237
|
+
for _font_key in ("font", "scale_font"):
|
|
238
|
+
if _font_key in kwargs:
|
|
239
|
+
_new = kwargs.pop(_font_key)
|
|
240
|
+
if _new:
|
|
241
|
+
self._local_defaults[_font_key] = _new
|
|
242
|
+
else:
|
|
243
|
+
self._local_defaults[_font_key] = \
|
|
244
|
+
self._theme_font_defaults.get(_font_key)
|
|
245
|
+
self._draw_meter()
|
|
246
|
+
|
|
247
|
+
# state is this library's own property, not a native CTkFrame one.
|
|
248
|
+
# Runtime overrides have to reach the map a repaint reads, or the
|
|
249
|
+
# repaint puts the theme value straight back -- see
|
|
250
|
+
# ThemeableWidget._record_theme_overrides().
|
|
251
|
+
self._record_theme_overrides(kwargs)
|
|
252
|
+
|
|
253
|
+
# fg_color paints the CANVAS, which covers the frame entirely.
|
|
254
|
+
#
|
|
255
|
+
# It is popped out of _local_defaults at construction into
|
|
256
|
+
# _theme_bg_raw, so _record_theme_overrides() never sees it -- and
|
|
257
|
+
# forwarding it to the frame underneath changes a surface nobody can
|
|
258
|
+
# see. Handled here instead: an empty value restores the theme's.
|
|
259
|
+
if "fg_color" in kwargs:
|
|
260
|
+
_bg = kwargs.pop("fg_color")
|
|
261
|
+
self._theme_bg_raw = _bg if _bg else self._theme_bg_default
|
|
262
|
+
if hasattr(self, "canvas") and self.canvas.winfo_exists():
|
|
263
|
+
self.canvas.configure(bg=self._resolve_color(self._theme_bg_raw))
|
|
264
|
+
|
|
265
|
+
if "state" in kwargs: self.state(kwargs.pop("state"))
|
|
266
|
+
if "width" in kwargs:
|
|
267
|
+
w = kwargs["width"]
|
|
268
|
+
kwargs["width"] = int(w) if (w and str(w).strip()) else self._default_width
|
|
269
|
+
if "height" in kwargs:
|
|
270
|
+
h = kwargs["height"]
|
|
271
|
+
kwargs["height"] = int(h) if (h and str(h).strip()) else self._default_height
|
|
272
|
+
|
|
273
|
+
# FIX: the three visibility flags were constructor arguments with a
|
|
274
|
+
# configure_visibility() method and cget() support, but no configure()
|
|
275
|
+
# branch -- so a Designer edit reached native CTkFrame.configure() and
|
|
276
|
+
# raised:
|
|
277
|
+
#
|
|
278
|
+
# ['pwr_visible'] are not supported arguments.
|
|
279
|
+
#
|
|
280
|
+
# swr_max_value had a branch, which is why only these three failed.
|
|
281
|
+
#
|
|
282
|
+
# An empty value restores the constructor default, matching the query
|
|
283
|
+
# branch above and what generated code does when the property is
|
|
284
|
+
# omitted.
|
|
285
|
+
_visibility = {
|
|
286
|
+
"swr_visible": ("_swr_visible", self._default_swr_visible),
|
|
287
|
+
"pwr_visible": ("_pwr_visible", self._default_pwr_visible),
|
|
288
|
+
"hide_lower_row": ("_hide_lower_row", self._default_hide_lower_row),
|
|
289
|
+
"hide_sig_row": ("_hide_sig_row", self._default_hide_sig_row),
|
|
290
|
+
}
|
|
291
|
+
_visibility_changed = False
|
|
292
|
+
for _key, (_attr, _fallback) in _visibility.items():
|
|
293
|
+
if _key not in kwargs:
|
|
294
|
+
continue
|
|
295
|
+
_val = kwargs.pop(_key)
|
|
296
|
+
if _val == "" or _val is None:
|
|
297
|
+
setattr(self, _attr, _fallback)
|
|
298
|
+
elif isinstance(_val, str):
|
|
299
|
+
setattr(self, _attr, _val.strip().lower() in ("true", "1", "yes"))
|
|
300
|
+
else:
|
|
301
|
+
setattr(self, _attr, bool(_val))
|
|
302
|
+
_visibility_changed = True
|
|
303
|
+
if _visibility_changed:
|
|
304
|
+
self._draw_meter()
|
|
305
|
+
|
|
306
|
+
if "swr_max_value" in kwargs:
|
|
307
|
+
val = kwargs.pop("swr_max_value")
|
|
308
|
+
self.swr_max_value = self._default_swr_max_value if (val == "" or not str(val).strip()) else float(val)
|
|
309
|
+
|
|
310
|
+
if kwargs: super().configure(**kwargs)
|
|
311
|
+
if self.canvas.winfo_exists(): self._draw_meter()
|
|
312
|
+
|
|
313
|
+
config = configure
|
|
314
|
+
def cget(self, attribute_name):
|
|
315
|
+
"""Public register parameter property getter lookup."""
|
|
316
|
+
if attribute_name == "state": return self._state
|
|
317
|
+
if attribute_name == "swr_max_value": return self.swr_max_value
|
|
318
|
+
if attribute_name == "swr_visible": return self._swr_visible
|
|
319
|
+
if attribute_name == "pwr_visible": return self._pwr_visible
|
|
320
|
+
if attribute_name == "hide_lower_row": return self._hide_lower_row
|
|
321
|
+
if attribute_name == "hide_sig_row": return self._hide_sig_row
|
|
322
|
+
return super().cget(attribute_name)
|
|
323
|
+
|
|
324
|
+
def _set_appearance_mode(self, mode_string: str):
|
|
325
|
+
"""Native look catcher ensuring canvas backplanes repaint fluidly on theme skin shifts."""
|
|
326
|
+
if hasattr(super(), "_set_appearance_mode"):
|
|
327
|
+
try: super()._set_appearance_mode(mode_string)
|
|
328
|
+
except Exception: pass
|
|
329
|
+
if hasattr(self, "canvas") and self.canvas.winfo_exists():
|
|
330
|
+
defer(self, 15, self._update_theme_colors)
|
|
331
|
+
|
|
332
|
+
def _update_theme_colors(self):
|
|
333
|
+
"""Refreshes the canvas widget background color directly from your asset maps."""
|
|
334
|
+
bg_color = self._resolve_color(self._theme_bg_raw)
|
|
335
|
+
self.canvas.configure(bg=bg_color)
|
|
336
|
+
self._draw_meter()
|
|
337
|
+
|
|
338
|
+
def _scaled_font(self, font):
|
|
339
|
+
"""
|
|
340
|
+
A theme font with its size scaled for the display.
|
|
341
|
+
|
|
342
|
+
A canvas text item takes a plain font tuple, which CustomTkinter's own
|
|
343
|
+
font scaling never sees -- so without this the meter's captions and
|
|
344
|
+
scale stayed the same size while everything around them grew on a
|
|
345
|
+
scaled display. Same as sCTkNotebook and sCTkFrequencyDisplay.
|
|
346
|
+
|
|
347
|
+
THE WHOLE TUPLE GOES IN, not the size: _apply_font_scaling raises on
|
|
348
|
+
a bare integer, and a try/except around that mistake would quietly
|
|
349
|
+
stop scaling. A named font (a string) is left as it is.
|
|
350
|
+
"""
|
|
351
|
+
if not isinstance(font, (list, tuple)):
|
|
352
|
+
return font
|
|
353
|
+
try:
|
|
354
|
+
return self._apply_font_scaling(tuple(font))
|
|
355
|
+
except Exception:
|
|
356
|
+
return tuple(font)
|
|
357
|
+
|
|
358
|
+
def _set_scaling(self, *args, **kwargs):
|
|
359
|
+
"""
|
|
360
|
+
Redraws when the scaling changes (CustomTkinter calls this on every
|
|
361
|
+
widget when the window moves to a display with another scale, or the
|
|
362
|
+
app sets one): the canvas text is drawn, so nothing else would
|
|
363
|
+
update it.
|
|
364
|
+
"""
|
|
365
|
+
super()._set_scaling(*args, **kwargs)
|
|
366
|
+
if getattr(self, "canvas", None) is not None and self.canvas.winfo_exists():
|
|
367
|
+
self._draw_meter()
|
|
368
|
+
|
|
369
|
+
@staticmethod
|
|
370
|
+
def _font_metrics(font):
|
|
371
|
+
"""
|
|
372
|
+
Height and a width-measuring callable for a font.
|
|
373
|
+
|
|
374
|
+
Every vertical offset in _draw_meter() was a hardcoded pixel count
|
|
375
|
+
tuned for the default font -- captions sat 6px below the bar, tick
|
|
376
|
+
labels 14px above it, SWR and PWR 20px up. A larger font grew past
|
|
377
|
+
those gaps and overlapped the scale it labelled.
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
(line_height, measure) where measure(text) gives a pixel width.
|
|
381
|
+
Falls back to a rough estimate if Tk cannot supply a font object,
|
|
382
|
+
which keeps the meter drawing rather than raising.
|
|
383
|
+
"""
|
|
384
|
+
try:
|
|
385
|
+
import tkinter.font as tkfont
|
|
386
|
+
f = tkfont.Font(font=font)
|
|
387
|
+
return f.metrics("linespace"), f.measure
|
|
388
|
+
except Exception:
|
|
389
|
+
size = 10
|
|
390
|
+
try:
|
|
391
|
+
# abs(): a scaled font's size is negative (pixels).
|
|
392
|
+
size = abs(int(font[1])) or 10
|
|
393
|
+
except Exception:
|
|
394
|
+
pass
|
|
395
|
+
return int(size * 1.4), (lambda text: int(len(str(text)) * size * 0.6))
|
|
396
|
+
|
|
397
|
+
def _draw_meter(self):
|
|
398
|
+
"""Wipes and paints fresh discrete LED lines, scale calibrations, and labels using adaptive look tokens."""
|
|
399
|
+
self.canvas.delete("all")
|
|
400
|
+
width, height = self.canvas.winfo_width(), self.canvas.winfo_height()
|
|
401
|
+
if width < 10 or height < 10: return
|
|
402
|
+
|
|
403
|
+
# No fallbacks: _validate_theme_keys() hard-failed at construction if
|
|
404
|
+
# any of these were missing, so every lookup is guaranteed to resolve.
|
|
405
|
+
bg_color = self._resolve_color(self._theme_bg_raw)
|
|
406
|
+
amber_color = self._resolve_color(self._themed("text_color"))
|
|
407
|
+
red_color = self._resolve_color(self._themed("alarm_color"))
|
|
408
|
+
led_on_color = self._resolve_color(self._themed("led_on_color"))
|
|
409
|
+
led_off_color = self._resolve_color(self._themed("led_off_color"))
|
|
410
|
+
label_font = self._scaled_font(self._local_defaults.get("font"))
|
|
411
|
+
scale_font = self._scaled_font(self._local_defaults.get("scale_font"))
|
|
412
|
+
# FIX: was a fully hardcoded self._resolve_color(("#94A3B8", "#334155"))
|
|
413
|
+
# with no theme lookup at all -- the only colour in this widget the
|
|
414
|
+
# theme could not reach.
|
|
415
|
+
disabled_color = self._resolve_color(self._local_defaults.get("inactive_color"))
|
|
416
|
+
self.canvas.configure(bg=bg_color)
|
|
417
|
+
|
|
418
|
+
num_led_segments = 30
|
|
419
|
+
# Right margin sized for the LAST top-scale label rather than a fixed
|
|
420
|
+
# 30px. That label is centred on the bar's end, so half of it sits
|
|
421
|
+
# beyond -- and it is "+60 dB", the widest string on the scale, so a
|
|
422
|
+
# larger scale_font ran the "B" off the canvas.
|
|
423
|
+
#
|
|
424
|
+
# Measured from bar_scale_mappings below rather than from a guessed
|
|
425
|
+
# list: an earlier version measured "dB" and reserved room for a string
|
|
426
|
+
# a third of the real width, which held until about 14pt and then
|
|
427
|
+
# clipped again.
|
|
428
|
+
scale_h, scale_measure = self._font_metrics(scale_font)
|
|
429
|
+
label_h, _ = self._font_metrics(label_font)
|
|
430
|
+
last_label = "+60 dB"
|
|
431
|
+
start_x, end_x = 10, width - max(30, int(scale_measure(last_label) / 2) + 12)
|
|
432
|
+
total_length = end_x - start_x
|
|
433
|
+
# Row positions follow which rows are actually drawn, so a
|
|
434
|
+
# single-row meter is centred rather than sitting where it would
|
|
435
|
+
# have been in a two-row one.
|
|
436
|
+
show_sig = not self._hide_sig_row
|
|
437
|
+
show_lower = not self._hide_lower_row
|
|
438
|
+
if show_sig and show_lower:
|
|
439
|
+
sig_y, lower_y = int(height * 0.28), int(height * 0.70)
|
|
440
|
+
elif show_sig:
|
|
441
|
+
sig_y, lower_y = int(height * 0.50), int(height * 0.70)
|
|
442
|
+
else:
|
|
443
|
+
sig_y, lower_y = int(height * 0.28), int(height * 0.50)
|
|
444
|
+
segment_width = (total_length / num_led_segments) - 1.5
|
|
445
|
+
|
|
446
|
+
if show_sig:
|
|
447
|
+
val = self._current_s_value
|
|
448
|
+
s_fraction = (max(0.0, val) / 9.0) * 0.60 if val <= 9.0 else 0.60 + ((min(69.0, val) - 9.0) / 60.0) * 0.40
|
|
449
|
+
active_sig_segments = int(num_led_segments * max(0.0, min(1.0, s_fraction)))
|
|
450
|
+
|
|
451
|
+
for i in range(num_led_segments):
|
|
452
|
+
seg_start_x = start_x + (i * (total_length / num_led_segments))
|
|
453
|
+
is_sig_redzone = i >= int(num_led_segments * 0.60)
|
|
454
|
+
fill = (red_color if is_sig_redzone else led_on_color) if i < active_sig_segments else led_off_color
|
|
455
|
+
self.canvas.create_rectangle(seg_start_x, sig_y - 4, seg_start_x + segment_width, sig_y + 1, fill=fill, outline="")
|
|
456
|
+
|
|
457
|
+
bar_scale_mappings = [(0.0, ""), (0.066, "1"), (0.20, "3"), (0.333, "5"), (0.466, "7"), (0.60, "9"), (0.733, "+20"), (0.866, "+40"), (1.0, "+60 dB")]
|
|
458
|
+
for pct, label_str in bar_scale_mappings:
|
|
459
|
+
tx = start_x + (total_length * pct)
|
|
460
|
+
color = red_color if pct >= 0.60 else amber_color
|
|
461
|
+
self.canvas.create_line(tx, sig_y, tx, sig_y - 6, fill=color, width=1)
|
|
462
|
+
# Above the tick, clear of it by the label's own height.
|
|
463
|
+
if label_str: self.canvas.create_text(tx, sig_y - 8 - (scale_h / 2), text=label_str, fill=color, font=scale_font, anchor="center")
|
|
464
|
+
|
|
465
|
+
self.canvas.create_text(start_x, sig_y - 8 - (scale_h / 2), text="S", fill=amber_color, font=scale_font, anchor="center")
|
|
466
|
+
self.canvas.create_text(start_x + (total_length * 0.5), sig_y + 5, text="SIG", fill=amber_color, font=label_font, anchor="n")
|
|
467
|
+
# THE READING, IN WORDS, at the right end of the caption line --
|
|
468
|
+
# level with "SIG", aligned to the end of the bar. The bar shows
|
|
469
|
+
# roughly where a signal is; this says exactly, in whatever units
|
|
470
|
+
# the caller has (the meter does not know what they are).
|
|
471
|
+
if self._current_sig_text:
|
|
472
|
+
self.canvas.create_text(end_x, sig_y + 5, text=self._current_sig_text,
|
|
473
|
+
fill=amber_color, font=label_font, anchor="ne")
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
if not show_lower:
|
|
477
|
+
return
|
|
478
|
+
|
|
479
|
+
mid_gap_start, mid_gap_end = 13, 17
|
|
480
|
+
def get_swr_fraction(swr_val):
|
|
481
|
+
if swr_val <= 1.0: return 0.0
|
|
482
|
+
if swr_val >= self.swr_max_value: return 1.0
|
|
483
|
+
return (math.log10(swr_val + 0.5) - math.log10(1.5)) / (math.log10(self.swr_max_value + 0.5) - math.log10(1.5))
|
|
484
|
+
|
|
485
|
+
active_swr_segments = int(mid_gap_start * get_swr_fraction(self._current_swr_value))
|
|
486
|
+
pwr_total_segments = num_led_segments - mid_gap_end
|
|
487
|
+
active_pwr_segments = int(pwr_total_segments * (max(0.0, min(100.0, self._current_pwr_value)) / 100.0))
|
|
488
|
+
|
|
489
|
+
for i in range(num_led_segments):
|
|
490
|
+
seg_start_x = start_x + (i * (total_length / num_led_segments))
|
|
491
|
+
if mid_gap_start <= i < mid_gap_end:
|
|
492
|
+
fill = bg_color
|
|
493
|
+
elif i < mid_gap_start:
|
|
494
|
+
is_swr_redzone = (i / mid_gap_start) >= get_swr_fraction(2.0)
|
|
495
|
+
fill = (red_color if is_swr_redzone else led_on_color) if (i < active_swr_segments and self._swr_visible) else led_off_color
|
|
496
|
+
else:
|
|
497
|
+
pwr_index = i - mid_gap_end
|
|
498
|
+
is_pwr_redzone = pwr_index >= int(pwr_total_segments * 0.8)
|
|
499
|
+
fill = (red_color if is_pwr_redzone else led_on_color) if (pwr_index < active_pwr_segments and self._pwr_visible) else led_off_color
|
|
500
|
+
self.canvas.create_rectangle(seg_start_x, lower_y - 4, seg_start_x + segment_width, lower_y + 1, fill=fill, outline="")
|
|
501
|
+
|
|
502
|
+
swr_label_color = amber_color if self._swr_visible else disabled_color
|
|
503
|
+
pwr_label_color = amber_color if self._pwr_visible else disabled_color
|
|
504
|
+
|
|
505
|
+
# THE ROW FLIPS WHEN IT IS ALONE.
|
|
506
|
+
#
|
|
507
|
+
# With both rows drawn these two are mirror images on purpose: the S
|
|
508
|
+
# row puts its scale above and its caption below, this one puts its
|
|
509
|
+
# captions above and its scale below, so the captions meet in the
|
|
510
|
+
# middle and the two scales face outward. That only works as a pair.
|
|
511
|
+
#
|
|
512
|
+
# Hide the S row and this one is left upside down relative to a
|
|
513
|
+
# single-row S meter -- caption at the top, numbers at the bottom.
|
|
514
|
+
# Alone, it takes the same arrangement the S row uses.
|
|
515
|
+
caption_above = show_sig
|
|
516
|
+
caption_y = (lower_y - 6 - label_h) if caption_above else (lower_y + 5)
|
|
517
|
+
caption_anchor = "n"
|
|
518
|
+
scale_side = 1 if caption_above else -1
|
|
519
|
+
|
|
520
|
+
self.canvas.create_text(start_x + (total_length * ((mid_gap_start / num_led_segments) * 0.5)), caption_y, text="SWR", fill=swr_label_color, font=label_font, anchor=caption_anchor)
|
|
521
|
+
self.canvas.create_text(start_x + (total_length * ((mid_gap_end / num_led_segments) + ((1.0 - (mid_gap_end / num_led_segments)) * 0.5))), caption_y, text="PWR", fill=pwr_label_color, font=label_font, anchor=caption_anchor)
|
|
522
|
+
|
|
523
|
+
swr_ticks = [1.0, 1.5, 2.0]
|
|
524
|
+
if self.swr_max_value > 2.0:
|
|
525
|
+
swr_ticks.extend([round(2.0 + ((self.swr_max_value - 2.0) / 2.0), 1), round(self.swr_max_value, 1)])
|
|
526
|
+
|
|
527
|
+
for val in swr_ticks:
|
|
528
|
+
tx = start_x + (total_length * (get_swr_fraction(val) * (mid_gap_start / num_led_segments)))
|
|
529
|
+
color = (red_color if val >= 2.0 else amber_color) if self._swr_visible else disabled_color
|
|
530
|
+
label = (str(int(val)) if val.is_integer() else str(val)) + ("+" if val == self.swr_max_value else "")
|
|
531
|
+
self.canvas.create_line(tx, lower_y, tx, lower_y + (6 * scale_side), fill=color, width=1)
|
|
532
|
+
if caption_above:
|
|
533
|
+
self.canvas.create_text(tx, lower_y + 8, text=label, fill=color, font=scale_font, anchor="n")
|
|
534
|
+
else:
|
|
535
|
+
self.canvas.create_text(tx, lower_y - 8 - (scale_h / 2), text=label, fill=color, font=scale_font, anchor="center")
|
|
536
|
+
|
|
537
|
+
for val, label in [(0, "0"), (50, "50"), (100, "100%")]:
|
|
538
|
+
tx = start_x + (total_length * ((mid_gap_end / num_led_segments) + ((val / 100.0) * (1.0 - (mid_gap_end / num_led_segments)))))
|
|
539
|
+
color = (red_color if val >= 80 else amber_color) if self._pwr_visible else disabled_color
|
|
540
|
+
self.canvas.create_line(tx, lower_y, tx, lower_y + (6 * scale_side), fill=color, width=1)
|
|
541
|
+
if caption_above:
|
|
542
|
+
self.canvas.create_text(tx - 4 if val == 100 else tx, lower_y + 8, text=label, fill=color, font=scale_font, anchor="n")
|
|
543
|
+
else:
|
|
544
|
+
self.canvas.create_text(tx - 4 if val == 100 else tx, lower_y - 8 - (scale_h / 2), text=label, fill=color, font=scale_font, anchor="center")
|
|
545
|
+
def set(self, s_value=None, swr_value=None, pwr_value=None, sig_text=None):
|
|
546
|
+
"""
|
|
547
|
+
Update any telemetry channel row independently.
|
|
548
|
+
|
|
549
|
+
sig_text: the S reading in words -- "-63 dBFS" -- drawn at the right
|
|
550
|
+
end of the S row's caption line. "" removes it; None leaves it as
|
|
551
|
+
it is. Runtime state like the values, not a property.
|
|
552
|
+
"""
|
|
553
|
+
if sig_text is not None: self._current_sig_text = str(sig_text)
|
|
554
|
+
if s_value is not None: self._current_s_value = float(s_value)
|
|
555
|
+
if swr_value is not None: self._current_swr_value = float(swr_value)
|
|
556
|
+
if pwr_value is not None: self._current_pwr_value = float(pwr_value)
|
|
557
|
+
if self.canvas.winfo_exists(): self._draw_meter()
|