SXXXXXXX_PyMsc/pymsc/gui/components/command_widgets.py
2026-01-12 12:38:47 +01:00

768 lines
31 KiB
Python

# -*- coding: utf-8 -*-
import tkinter as tk
from tkinter import ttk
import time
import logging
from typing import Any, Dict, Optional
from .base_widgets import BaseCommandFrame, ToolTip
from pymsc.utils.converters import (
RAD_TO_DEG, DEG_TO_RAD, NM_TO_METERS, METERS_TO_FEET,
FEET_TO_METERS, MS_TO_KNOTS, KNOTS_TO_MS
)
def _infer_unit_from_info(info: Dict, idx: int):
"""Infer a short unit string from a command registry `info` dict.
Looks for `lsb{idx}` / `scale{idx}` or fallback to `lsb`/`scale`.
Returns unit like 'deg', 'rad', 'kts', 'm', 'ft' or None.
"""
try:
suffix = '' if idx == -1 or idx == 1 else str(idx)
lsb = info.get(f'lsb{suffix}', info.get('lsb'))
scale = info.get(f'scale{suffix}', info.get('scale'))
# Angle in degrees if scale==RAD_TO_DEG
if scale == RAD_TO_DEG:
return 'deg'
# Speed in knots
if scale == MS_TO_KNOTS:
return 'kts'
# Feet/meter mapping
if scale == METERS_TO_FEET:
return 'ft'
# heuristics based on lsb
from pymsc.utils import converters as conv
if lsb in (getattr(conv, 'SEMICIRCLE_RAD_LSB', None), getattr(conv, 'SEMICIRCLE_LSB', None)):
# if a scale converts to degrees, label deg, else rad
if scale == RAD_TO_DEG:
return 'deg'
return 'rad'
if lsb == getattr(conv, 'GEOPOS_RAD_LSB', None):
return 'deg'
if lsb in (getattr(conv, 'CRS_SLAVE_RNG_METERS_LSB', None), getattr(conv, 'VELOCITY_METERS_LSB', None)):
# distance or velocity in meters -> show 'm' or 'ft' depending on scale
if scale == METERS_TO_FEET:
return 'ft'
return 'm'
except Exception:
return None
return None
# Registry for finding widgets by their label (used for automation/scripts)
WIDGET_MAP: Dict[str, Any] = {}
def get_correct_value(info: Dict, idx: int, value: float) -> float:
"""
Applies LSB and Scale factors to convert a raw value to a readable value.
Returns None if value is None (field not found).
"""
if value is None:
return None
suffix = str(idx) if idx != -1 else ""
lsb = info.get(f"lsb{suffix}", 1.0)
scale = info.get(f"scale{suffix}", 1.0)
result = value * (scale * lsb)
return result
def set_correct_value(info: Dict, idx: int, value: float) -> int:
"""
Applies LSB and Scale factors to convert a readable value to a raw integer.
"""
suffix = str(idx) if idx != -1 else ""
lsb = info.get(f"lsb{suffix}", 1.0)
scale = info.get(f"scale{suffix}", 1.0)
raw_val = value / (scale * lsb)
return int(raw_val)
class CommandFrameCheckBox(BaseCommandFrame):
"""
Widget containing a Label, a Command Checkbox, and a Tellback Checkbox.
"""
def __init__(self, parent: tk.Widget, command_info: Dict, **kwargs):
super().__init__(parent, command_info, **kwargs)
WIDGET_MAP[self.info['label']] = self
self._create_ui()
def _create_ui(self):
self.label = tk.Label(
self, text=self.info['label'], width=self.column_widths[0],
anchor="w", font=("Helvetica", 8)
)
self.label.grid(row=0, column=0, sticky="w", padx=5)
self.tooltip = ToolTip(self.label, self.info['description'])
if self.info.get('message'):
self.command_var = tk.BooleanVar()
self.command_check = tk.Checkbutton(
self, variable=self.command_var, command=self.on_toggle,
width=self.column_widths[1]
)
self.command_check.grid(row=0, column=1)
# attach tooltip on the interactive control: "<message_id> - <field>"
try:
msg_obj = self.info.get('message')
if msg_obj and hasattr(msg_obj, 'message_id'):
tt = f"{msg_obj.message_id} - {self.info.get('field','')}"
# append short unit to tooltip for editable control
unit = _infer_unit_from_info(self.info, 1)
if unit:
tt = f"{tt} ({unit})"
else:
tt = self.info.get('tooltip', self.info.get('description',''))
ToolTip(self.command_check, tt)
except Exception:
pass
initial_val = self.info['message'].get_value_for_field(self.info['field'])
self.command_var.set(bool(initial_val))
if self.info.get('message_tb'):
self.tellback_var = tk.BooleanVar()
self.tellback_check = tk.Checkbutton(
self, variable=self.tellback_var, state='disabled',
width=self.column_widths[2]
)
self.tellback_check.grid(row=0, column=2)
# attach tooltip to tellback showing its source message-field (B-message)
try:
msg_tb = self.info.get('message_tb')
field_tb = self.info.get('field_tb')
if msg_tb and hasattr(msg_tb, 'message_id') and field_tb:
tt_tb = f"{msg_tb.message_id} - {field_tb}"
unit_tb = _infer_unit_from_info(self.info, -1)
if unit_tb:
tt_tb = f"{tt_tb} ({unit_tb})"
else:
tt_tb = self.info.get('tooltip_tb', self.info.get('description',''))
ToolTip(self.tellback_check, tt_tb)
except Exception:
pass
def on_toggle(self):
# Skip if message is None (disabled field)
if not self.info.get('message'):
return
self.start_time = time.time()
val = 1 if self.command_var.get() else 0
self.info['message'].set_value_for_field(self.info['field'], val)
# Log the change
logger = logging.getLogger('ARTOS.MCS')
state = "ON" if val == 1 else "OFF"
logger.info(f"User changed '{self.info['label']}'{state}")
if self.script_manager.is_recording:
action_val = "on" if val == 1 else "off"
self.script_manager.write_command(self.info['label'], "toggle", action_val)
if self.info.get('message_tb'):
self.monitor_tellback()
def monitor_tellback(self):
msg_tb = self.info['message_tb']
field_tb = self.info['field_tb']
current_tb = bool(msg_tb.get_value_for_field(field_tb))
self.tellback_var.set(current_tb)
elapsed = (time.time() - self.start_time) * 1000
if current_tb == self.command_var.get():
self.label.config(bg="green" if current_tb else "yellow")
return
if elapsed > self.threshold_ms:
self.label.config(bg="red")
return
self.after(self.update_interval_ms, self.monitor_tellback)
def check_updated_value(self):
if self.info.get('message'):
current_val = bool(self.info['message'].get_value_for_field(self.info['field']))
self.command_var.set(current_val)
if self.info.get('message_tb'):
current_tb = bool(self.info['message_tb'].get_value_for_field(self.info['field_tb']))
self.tellback_var.set(current_tb)
class CommandFrameComboBox(BaseCommandFrame):
"""
Widget containing a Label, a Command ComboBox, and a Tellback Label.
"""
def __init__(self, parent: tk.Widget, command_info: Dict, **kwargs):
super().__init__(parent, command_info, **kwargs)
WIDGET_MAP[self.info['label']] = self
self._create_ui()
def _create_ui(self):
self.label = tk.Label(
self, text=self.info['label'], width=self.column_widths[0],
anchor="w", font=("Helvetica", 8)
)
self.label.grid(row=0, column=0, sticky="w", padx=5)
self.tooltip = ToolTip(self.label, self.info['description'])
self.cmd_var = tk.StringVar()
self.combo = ttk.Combobox(
self, textvariable=self.cmd_var, values=self.info["values"],
width=self.column_widths[1], state="readonly"
)
self.combo.grid(row=0, column=1)
self.combo.bind("<<ComboboxSelected>>", self.on_select)
# attach tooltip to combobox showing message-field when possible
try:
msg_obj = self.info.get('message')
if msg_obj and hasattr(msg_obj, 'message_id'):
tt = f"{msg_obj.message_id} - {self.info.get('field','')}"
else:
tt = self.info.get('tooltip', self.info.get('description',''))
ToolTip(self.combo, tt)
except Exception:
pass
if self.info.get('message_tb'):
self.tb_var = tk.StringVar(value="---")
self.tb_label = tk.Label(
self, textvariable=self.tb_var, bg="white",
width=self.column_widths[2], relief="sunken"
)
self.tb_label.grid(row=0, column=2)
# tooltip for tellback cell: show B-message.field mapping
try:
msg_tb = self.info.get('message_tb')
field_tb = self.info.get('field_tb')
if msg_tb and hasattr(msg_tb, 'message_id') and field_tb:
tt_tb = f"{msg_tb.message_id} - {field_tb}"
unit_tb = _infer_unit_from_info(self.info, -1)
if unit_tb:
tt_tb = f"{tt_tb} ({unit_tb})"
else:
tt_tb = self.info.get('tooltip_tb', self.info.get('description',''))
ToolTip(self.tb_label, tt_tb)
except Exception:
pass
def on_select(self, event=None):
self.start_time = time.time()
idx = self.combo.current()
self.info['message'].set_value_for_field(self.info['field'], idx)
# Log the change
logger = logging.getLogger('ARTOS.MCS')
logger.info(f"User changed '{self.info['label']}'{self.combo.get()}")
if self.script_manager.is_recording:
self.script_manager.write_command(self.info['label'], "set_value", self.combo.get())
if self.info.get('message_tb'):
self.monitor_tellback()
def monitor_tellback(self):
msg_tb = self.info['message_tb']
idx_tb = msg_tb.get_value_for_field(self.info['field_tb'])
# Handle None or invalid index
if idx_tb is None:
self.tb_var.set("---")
return
try:
val_tb_text = self.info["values_tb"][idx_tb]
except (IndexError, KeyError, TypeError):
val_tb_text = "ERR"
# append unit if available (combobox tellbacks are typically enums, but keep flexible)
unit = _infer_unit_from_info(self.info, -1)
if unit:
self.tb_var.set(f"{val_tb_text} ({unit})")
else:
self.tb_var.set(val_tb_text)
elapsed = (time.time() - self.start_time) * 1000
if idx_tb == self.combo.current():
self.label.config(bg="green")
return
if elapsed > self.threshold_ms:
self.label.config(bg="red")
return
self.after(self.update_interval_ms, self.monitor_tellback)
def check_updated_value(self):
if self.info.get('message'):
current_idx = self.info['message'].get_value_for_field(self.info['field'])
if current_idx is None:
self.cmd_var.set("---")
else:
try:
self.cmd_var.set(self.info['values'][current_idx])
except (IndexError, TypeError):
self.cmd_var.set("ERR")
if self.info.get('message_tb'):
current_tb_idx = self.info['message_tb'].get_value_for_field(self.info['field_tb'])
if current_tb_idx is None:
self.tb_var.set("---")
else:
try:
self.tb_var.set(self.info['values_tb'][current_tb_idx])
except (IndexError, TypeError):
self.tb_var.set("ERR")
class CommandFrameSpinBox(BaseCommandFrame):
"""
Widget containing a Label, a Command SpinBox, and a Tellback Label.
Supports scaling (LSB/Scale) and numeric validation.
"""
def __init__(self, parent: tk.Widget, command_info: Dict, **kwargs):
super().__init__(parent, command_info, **kwargs)
WIDGET_MAP[self.info['label']] = self
self.is_programmatic_change = False
self.user_editing = False # Track if user is actively editing
self._create_ui()
def _create_ui(self):
self.label = tk.Label(
self, text=self.info['label'], width=self.column_widths[0],
anchor="w", font=("Helvetica", 8)
)
self.label.grid(row=0, column=0, sticky="w", padx=5)
self.tooltip = ToolTip(self.label, self.info['description'])
is_int = self.info.get("number_format", "float") == "integer"
self.cmd_var = tk.IntVar() if is_int else tk.DoubleVar()
self.cmd_var.set(self.info.get('default_value', 0))
v_cmd = (self.register(self._validate), '%P')
self.spin = tk.Spinbox(
self, textvariable=self.cmd_var, from_=self.info["min_value"],
to=self.info["max_value"], increment=self.info.get("increment", 1),
width=self.column_widths[1], validate="key", validatecommand=v_cmd
)
self.spin.grid(row=0, column=1)
self.spin.bind("<Return>", self.on_change)
self.spin.bind("<FocusOut>", self.on_change)
# Track when user starts editing
self.spin.bind("<FocusIn>", self._on_focus_in)
self.spin.bind("<KeyPress>", self._on_key_press)
# Detect arrow button clicks - send value immediately after arrow click
self.spin.bind("<ButtonRelease-1>", self._on_arrow_click)
# Also use variable trace to detect arrow changes
self.cmd_var.trace_add("write", self._on_var_change)
# attach tooltip to spinbox showing message-field when possible
try:
msg_obj = self.info.get('message')
if msg_obj and hasattr(msg_obj, 'message_id'):
tt = f"{msg_obj.message_id} - {self.info.get('field','')}"
else:
tt = self.info.get('tooltip', self.info.get('description',''))
ToolTip(self.spin, tt)
except Exception:
pass
if self.info.get('message_tb'):
self.tb_var = tk.StringVar(value="0")
self.tb_label = tk.Label(
self, textvariable=self.tb_var, bg="white",
width=self.column_widths[2], relief="sunken"
)
self.tb_label.grid(row=0, column=2)
# tooltip for spinbox tellback cell
try:
msg_tb = self.info.get('message_tb')
field_tb = self.info.get('field_tb')
if msg_tb and hasattr(msg_tb, 'message_id') and field_tb:
tt_tb = f"{msg_tb.message_id} - {field_tb}"
unit_tb = _infer_unit_from_info(self.info, -1)
if unit_tb:
tt_tb = f"{tt_tb} ({unit_tb})"
else:
tt_tb = self.info.get('tooltip_tb', self.info.get('description',''))
ToolTip(self.tb_label, tt_tb)
except Exception:
pass
def _validate(self, p):
if p == "" or p == "-":
return True
try:
float(p)
return True
except ValueError:
return False
def _on_focus_in(self, event=None):
"""Called when spinbox receives focus - user is about to edit"""
self.user_editing = True
def _on_key_press(self, event=None):
"""Called when user types - mark as editing"""
self.user_editing = True
def _on_arrow_click(self, event=None):
"""Called when user clicks spinbox arrows - send value immediately"""
# Small delay to ensure the value has been updated by the spinbox
self.after(50, self._send_arrow_value)
def _on_var_change(self, *args):
"""Called when spinbox value changes (from arrows or typing)"""
# If we're not programmatically changing and not already sending
if not self.is_programmatic_change and not hasattr(self, '_sending_arrow_value'):
# User changed value, mark as editing
self.user_editing = True
def _send_arrow_value(self):
"""Send the value after arrow click"""
if self.is_programmatic_change:
return
self._sending_arrow_value = True
try:
val = float(self.cmd_var.get())
raw_val = set_correct_value(self.info, -1, val)
self.info['message'].set_value_for_field(self.info['field'], raw_val)
# Log the change
logger = logging.getLogger('ARTOS.MCS')
unit = self.info.get('unit', '')
unit_str = f" {unit}" if unit else ""
logger.info(f"User changed '{self.info['label']}'{val}{unit_str}")
# Clear editing flag after sending
self.user_editing = False
except (tk.TclError, ValueError):
pass
finally:
delattr(self, '_sending_arrow_value')
def on_change(self, event=None):
if self.is_programmatic_change:
return
# Skip if message is None (disabled field)
if not self.info.get('message'):
return
self.start_time = time.time()
try:
val = float(self.cmd_var.get())
except tk.TclError:
self.user_editing = False # Clear flag even on error
return
raw_val = set_correct_value(self.info, -1, val)
self.info['message'].set_value_for_field(self.info['field'], raw_val)
# Log the change
logger = logging.getLogger('ARTOS.MCS')
unit = self.info.get('unit', '')
unit_str = f" {unit}" if unit else ""
logger.info(f"User changed '{self.info['label']}'{val}{unit_str}")
if self.script_manager.is_recording:
self.script_manager.write_command(self.info['label'], "set_value", val)
# Clear editing flag after successful change
self.user_editing = False
if self.info.get('message_tb'):
self.monitor_tellback()
def monitor_tellback(self):
msg_tb = self.info['message_tb']
raw_tb = msg_tb.get_value_for_field(self.info['field_tb'])
readable_tb = get_correct_value(self.info, -1, raw_tb)
fmt = "%.0f" if self.info.get("number_format") == "integer" else "%.4f"
# append unit indication
unit = _infer_unit_from_info(self.info, -1)
try:
txt = fmt % readable_tb
except Exception:
txt = str(readable_tb)
if unit:
self.tb_var.set(f"{txt} ({unit})")
else:
self.tb_var.set(txt)
elapsed = (time.time() - self.start_time) * 1000
target = float(self.cmd_var.get())
if abs(readable_tb - target) < 0.0001:
self.label.config(bg="green")
return
if elapsed > self.threshold_ms:
self.label.config(bg="red")
return
self.after(self.update_interval_ms, self.monitor_tellback)
def check_updated_value(self):
# Skip if user is actively editing this field
if self.user_editing:
return
# Skip if message is None (disabled field)
if not self.info.get('message'):
return
self.is_programmatic_change = True
raw_val = self.info['message'].get_value_for_field(self.info['field'])
readable = get_correct_value(self.info, -1, raw_val)
if readable is not None:
self.cmd_var.set(readable)
else:
self.cmd_var.set(0) # Default value for None
self.is_programmatic_change = False
if self.info.get('message_tb'):
raw_tb = self.info['message_tb'].get_value_for_field(self.info['field_tb'])
readable_tb = get_correct_value(self.info, -1, raw_tb)
if readable_tb is not None:
fmt = "%.0f" if self.info.get("number_format") == "integer" else "%.4f"
self.tb_var.set(fmt % readable_tb)
else:
self.tb_var.set("---")
class CommandFrameLabels(tk.Frame):
"""
Widget for displaying up to 3 read-only values from the bus.
"""
def __init__(self, parent: tk.Widget, command_info: Dict, **kwargs):
super().__init__(parent)
self.info = command_info
self.vars = {}
self.column_widths = [20, 14, 14, 14]
WIDGET_MAP[self.info['label']] = self
self._create_ui()
def _create_ui(self):
self.label = tk.Label(
self, text=self.info['label'], width=self.column_widths[0],
anchor="w", font=("Helvetica", 8)
)
self.label.grid(row=0, column=0, sticky="w", padx=5)
ToolTip(self.label, self.info['description'])
for i in range(1, 4):
msg = self.info.get(f"message{i}")
if msg:
field = self.info[f"field{i}"]
self.vars[field] = tk.StringVar(value="0")
lbl = tk.Label(
self, textvariable=self.vars[field], bg="#f0f0f0",
width=self.column_widths[i], relief="groove", font=("Helvetica", 8)
)
lbl.grid(row=0, column=i, padx=2)
ToolTip(lbl, self.info.get(f"tooltip{i}", ""))
def check_updated_value(self):
for i in range(1, 4):
msg = self.info.get(f"message{i}")
if msg:
field = self.info[f"field{i}"]
raw = msg.get_value_for_field(field)
val = get_correct_value(self.info, i, raw)
if val is not None:
fmt = "%.0f" if self.info.get(f"number_format{i}") == "integer" else "%.4f"
unit = _infer_unit_from_info(self.info, i)
try:
txt = fmt % val
except Exception:
txt = str(val)
if unit:
self.vars[field].set(f"{txt} ({unit})")
else:
self.vars[field].set(txt)
else:
self.vars[field].set("N/A")
class CommandFrameControls(tk.Frame):
"""
Highly flexible widget that can combine up to 3 sub-controls.
"""
def __init__(self, parent: tk.Widget, command_info: Dict, **kwargs):
super().__init__(parent)
self.info = command_info
self.column_widths = [20, 14, 14, 14]
self.vars = {}
self.is_programmatic_change = False
self.editing_fields = {} # Track which fields are being edited
WIDGET_MAP[self.info['label']] = self
self._create_ui()
def _create_ui(self):
self.title_label = tk.Label(
self, text=self.info['label'], width=self.column_widths[0],
anchor="w", font=("Helvetica", 8)
)
self.title_label.grid(row=0, column=0, sticky="w", padx=5)
ToolTip(self.title_label, self.info['description'])
for i in range(1, 4):
ctrl_type = self.info.get(f"type{i}")
if not ctrl_type:
continue
self._add_sub_control(i, ctrl_type)
def _add_sub_control(self, i: int, ctrl_type: str):
field = self.info[f"field{i}"]
msg = self.info.get(f"message{i}")
if ctrl_type == "checkbox":
self.vars[field] = tk.BooleanVar()
lbl_text = self.info.get(f"label{i}", "")
cb = tk.Checkbutton(
self, text=lbl_text, variable=self.vars[field], width=self.column_widths[i],
command=lambda f=field, m=msg: self._on_check(f, m)
)
cb.grid(row=0, column=i)
# attach tooltip to checkbox
try:
if msg and hasattr(msg, 'message_id'):
tt = f"{msg.message_id} - {field}"
else:
tt = self.info.get(f"tooltip{i}", self.info.get('description',''))
ToolTip(cb, tt)
except Exception:
pass
WIDGET_MAP[field] = {"widget": cb, "var": self.vars[field]}
elif ctrl_type == "combobox":
self.vars[field] = tk.StringVar()
vals = self.info.get(f"values{i}", [])
cmb = ttk.Combobox(
self, textvariable=self.vars[field], values=vals,
width=self.column_widths[i], state="readonly"
)
cmb.grid(row=0, column=i)
cmb.bind("<<ComboboxSelected>>", lambda e, f=field, m=msg, idx=i: self._on_combo(f, m, idx))
# attach tooltip to combobox
try:
if msg and hasattr(msg, 'message_id'):
tt = f"{msg.message_id} - {field}"
unit = _infer_unit_from_info(self.info, i)
if unit:
tt = f"{tt} ({unit})"
else:
tt = self.info.get(f"tooltip{i}", self.info.get('description',''))
ToolTip(cmb, tt)
except Exception:
pass
WIDGET_MAP[field] = {"widget": cmb, "var": self.vars[field]}
elif ctrl_type == "spinbox":
number_format = self.info.get(f"number_format{i}", "float")
is_int = number_format == "integer"
self.vars[field] = tk.IntVar() if is_int else tk.DoubleVar()
self.editing_fields[field] = False # Initialize editing state
fmt = '%.0f' if is_int else '%.4f'
min_v = self.info.get(f"min_value{i}", -999999)
max_v = self.info.get(f"max_value{i}", 999999)
inc_v = self.info.get(f"increment{i}", 1)
sp = tk.Spinbox(
self, textvariable=self.vars[field], width=self.column_widths[i],
from_=min_v, to=max_v, increment=inc_v, format=fmt
)
sp.grid(row=0, column=i)
sp.bind("<Return>", lambda e, f=field, m=msg, idx=i: self._on_spin(f, m, idx))
sp.bind("<FocusOut>", lambda e, f=field, m=msg, idx=i: self._on_spin(f, m, idx))
# Track editing state
sp.bind("<FocusIn>", lambda e, f=field: self._set_editing(f, True))
sp.bind("<KeyPress>", lambda e, f=field: self._set_editing(f, True))
# Detect arrow button clicks
sp.bind("<ButtonRelease-1>", lambda e, f=field, m=msg, idx=i: self._on_spin_arrow(f, m, idx))
WIDGET_MAP[field] = {"widget": sp, "var": self.vars[field]}
# attach tooltip to spinbox (include unit if available)
try:
if msg and hasattr(msg, 'message_id'):
tt = f"{msg.message_id} - {field}"
else:
tt = self.info.get(f"tooltip{i}", self.info.get('description',''))
unit = _infer_unit_from_info(self.info, i)
if unit:
tt = f"{tt} ({unit})"
ToolTip(sp, tt)
except Exception:
pass
elif ctrl_type == "label":
self.vars[field] = tk.StringVar(value="0")
lbl = tk.Label(
self, textvariable=self.vars[field], relief="groove",
width=self.column_widths[i], anchor="w", font=("Helvetica", 8)
)
lbl.grid(row=0, column=i)
elif ctrl_type == "space":
lbl = tk.Label(self, text="", width=self.column_widths[i])
lbl.grid(row=0, column=i)
def _set_editing(self, field: str, is_editing: bool):
"""Track editing state for a specific field"""
self.editing_fields[field] = is_editing
def _on_check(self, field: str, msg: Any):
val = 1 if self.vars[field].get() else 0
msg.set_value_for_field(field, val)
from pymsc.gui.script_manager import get_script_manager
sm = get_script_manager()
if sm.is_recording:
sm.write_command(field, "toggle", "on" if val else "off")
def _on_combo(self, field: str, msg: Any, idx: int):
cmb = WIDGET_MAP[field]["widget"]
val_idx = cmb.current()
msg.set_value_for_field(field, val_idx)
from pymsc.gui.script_manager import get_script_manager
sm = get_script_manager()
if sm.is_recording:
sm.write_command(field, "set_value", cmb.get())
def _on_spin(self, field: str, msg: Any, idx: int):
if self.is_programmatic_change:
return
val = float(self.vars[field].get())
raw = set_correct_value(self.info, idx, val)
msg.set_value_for_field(field, raw)
# Clear editing flag after change
self.editing_fields[field] = False
from pymsc.gui.script_manager import get_script_manager
sm = get_script_manager()
if sm.is_recording:
sm.write_command(field, "set_value", val)
def _on_spin_arrow(self, field: str, msg: Any, idx: int):
"""Handle spinbox arrow button clicks - send value immediately with small delay"""
# Small delay to ensure the value has been updated
self.after(50, lambda: self._send_spin_arrow_value(field, msg, idx))
def _send_spin_arrow_value(self, field: str, msg: Any, idx: int):
"""Send spinbox value after arrow click"""
if self.is_programmatic_change:
return
try:
val = float(self.vars[field].get())
raw = set_correct_value(self.info, idx, val)
msg.set_value_for_field(field, raw)
# Clear editing flag
self.editing_fields[field] = False
except (ValueError, tk.TclError):
pass
def check_updated_value(self):
"""
Syncs sub-controls with their corresponding 1553 message fields.
Renamed from check_actual_value for consistency with MainWindow refresh loop.
"""
self.is_programmatic_change = True
for i in range(1, 4):
ctrl_type = self.info.get(f"type{i}")
if not ctrl_type or ctrl_type == "space":
continue
field = self.info.get(f"field{i}")
msg = self.info.get(f"message{i}")
if not msg or not field:
continue
# Skip spinbox refresh if user is actively editing
if ctrl_type == "spinbox" and self.editing_fields.get(field, False):
continue
raw = msg.get_value_for_field(field)
readable = get_correct_value(self.info, i, raw)
if ctrl_type == "checkbox":
self.vars[field].set(bool(raw))
elif ctrl_type == "combobox":
try:
self.vars[field].set(self.info[f"values{i}"][int(raw)])
except IndexError:
self.vars[field].set("ERR")
elif ctrl_type == "spinbox":
# Do not place unit inside editable spinbox value; just format number
fmt = '%.0f' if self.info.get(f"number_format{i}") == "integer" else '%.4f'
try:
self.vars[field].set(fmt % readable)
except Exception:
self.vars[field].set(readable)
elif ctrl_type == "label":
self.vars[field].set("%.4f" % readable)
self.is_programmatic_change = False