added multidevice support
This commit is contained in:
@@ -325,7 +325,7 @@ class AppWindow(
|
||||
|
||||
def _apply_initial_radar_limits(self) -> None:
|
||||
"""Apply startup radar-limits strategy according to selected radar mode."""
|
||||
if self._radar_mode.currentText() == "native":
|
||||
if self._defaults_config.radar.driver_mode == "native":
|
||||
self._refresh_radar_limits_from_device()
|
||||
return
|
||||
self._apply_radar_limits_to_ui(None)
|
||||
@@ -364,15 +364,15 @@ class AppWindow(
|
||||
|
||||
level_upper = level.upper()
|
||||
palette = {
|
||||
"INFO": ("#7fb1ff", "#dce8f8", "#8ba2be"),
|
||||
"WARN": ("#f4bf4f", "#f4dca0", "#9f8b55"),
|
||||
"ERROR": ("#ff5f6d", "#ffd1d5", "#b5878c"),
|
||||
"INFO": ("#1d5fbf", "#1f2937", "#526277"),
|
||||
"WARN": ("#9a5b00", "#5c4300", "#7a6640"),
|
||||
"ERROR": ("#c43d4d", "#6b1f2a", "#8b5d66"),
|
||||
}
|
||||
accent_color, message_color, detail_color = palette.get(level_upper, palette["INFO"])
|
||||
timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
|
||||
header_html = (
|
||||
f"<span style='color:{accent_color}; font-weight:700;'>{html.escape(level_upper)}</span>"
|
||||
f" <span style='color:#7f94af;'>{html.escape(timestamp)}</span>"
|
||||
f" <span style='color:#60758d;'>{html.escape(timestamp)}</span>"
|
||||
f" <span style='color:{message_color};'>{self._escape_log_text(text)}</span>"
|
||||
)
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ from PyQt6.QtCore import QSignalBlocker
|
||||
from PyQt6.QtWidgets import QFileDialog
|
||||
|
||||
from python_app.models.gui_profile_model import GuiProfileModel
|
||||
from python_app.models.run_config_model import RunConfigModel
|
||||
from python_app.orchestration.preprocess_assets import (
|
||||
VISIBLE_PREPROCESS_ASSET_KEYS,
|
||||
preprocess_asset_model,
|
||||
@@ -32,12 +33,36 @@ class AppWindowConfigProfileIOMixin:
|
||||
|
||||
def _set_combo_selection_mode(self, mode: str) -> None:
|
||||
"""Highlight current combo mode and enable only the relevant editors."""
|
||||
if self._is_multi_device_model_selected():
|
||||
self._run_combos_select_button.setChecked(True)
|
||||
self._single_combo_select_button.setChecked(False)
|
||||
self._combos_text.setText(self._fixed_multi_combo_text())
|
||||
self._combos_text.setEnabled(False)
|
||||
self._single_combo_output.setEnabled(True)
|
||||
self._single_combo_input.setEnabled(True)
|
||||
self._run_combos_select_button.setEnabled(False)
|
||||
self._single_combo_select_button.setEnabled(False)
|
||||
return
|
||||
|
||||
text_selected = mode != "single"
|
||||
self._run_combos_select_button.setChecked(text_selected)
|
||||
self._single_combo_select_button.setChecked(not text_selected)
|
||||
self._combos_text.setEnabled(text_selected)
|
||||
self._single_combo_output.setEnabled(not text_selected)
|
||||
self._single_combo_input.setEnabled(not text_selected)
|
||||
self._run_combos_select_button.setEnabled(True)
|
||||
self._single_combo_select_button.setEnabled(True)
|
||||
|
||||
def _is_multi_device_model_selected(self) -> bool:
|
||||
"""Return whether the loaded config targets LibreVNA multi-device acquisition."""
|
||||
return bool(self._defaults_config.is_multi_device)
|
||||
|
||||
def _fixed_multi_combo_text(self) -> str:
|
||||
"""Return the canonical virtual combo matrix shown for multi-device mode."""
|
||||
return ",".join(
|
||||
f"{int(combo.input)}:{int(combo.output)}"
|
||||
for combo in RunConfigModel.build_multi_device_virtual_combos()
|
||||
)
|
||||
|
||||
def _sync_pass_through_y_controls(self) -> None:
|
||||
"""Enable Y-range editors only when fixed Y mode is active."""
|
||||
@@ -138,14 +163,13 @@ class AppWindowConfigProfileIOMixin:
|
||||
"""Apply already parsed profile to GUI state without restarting the pipeline."""
|
||||
config = profile.run_config.clone()
|
||||
gui_state = profile.gui if profile.gui is not None else self._default_gui_state_for_config(config)
|
||||
self._defaults_config = config
|
||||
selected_preprocess_sets = {
|
||||
key: str(preprocess_asset_model(config, key).set_name)
|
||||
for key in VISIBLE_PREPROCESS_ASSET_KEYS
|
||||
}
|
||||
|
||||
radio_widgets = (
|
||||
self._serial_input,
|
||||
self._radar_mode,
|
||||
self._start_hz_input,
|
||||
self._stop_hz_input,
|
||||
self._points_input,
|
||||
@@ -202,8 +226,6 @@ class AppWindowConfigProfileIOMixin:
|
||||
for widget in radio_widgets:
|
||||
blockers.enter_context(QSignalBlocker(widget))
|
||||
|
||||
self._serial_input.setText(str(config.radar.serial))
|
||||
self._set_combo_current_text(self._radar_mode, str(config.radar.driver_mode))
|
||||
self._start_hz_input.setText(f"{config.radar.sweep.start_hz:g}")
|
||||
self._stop_hz_input.setText(f"{config.radar.sweep.stop_hz:g}")
|
||||
self._points_input.setText(str(int(config.radar.sweep.points)))
|
||||
@@ -271,7 +293,6 @@ class AppWindowConfigProfileIOMixin:
|
||||
self._save_path_input.setText(str(gui_state.data_actions.save_path))
|
||||
self._save_name_input.setText(str(gui_state.data_actions.save_name))
|
||||
|
||||
self._defaults_config = config
|
||||
self._gui_defaults = gui_state
|
||||
self._selected_preprocess_sets = selected_preprocess_sets
|
||||
self._selected_preprocess_radar_key = self._radar_key(config)
|
||||
|
||||
@@ -8,16 +8,6 @@ from python_app.hardware_full.librevna_service import LibreVnaService
|
||||
class AppWindowRadarLimitsMixin:
|
||||
"""Handle LibreVNA capability probing and dependent UI clamping."""
|
||||
|
||||
def _on_radar_identity_changed(self, *_args) -> None:
|
||||
"""Refresh device limits when radar identity/mode changes."""
|
||||
self._reset_preprocess_selection_after_radar_key_change()
|
||||
if self._radar_mode.currentText() != "native":
|
||||
self._apply_radar_limits_to_ui(None)
|
||||
return
|
||||
changed = self._refresh_radar_limits_from_device()
|
||||
if changed:
|
||||
self._on_processing_live_settings_changed()
|
||||
|
||||
def _on_radar_sweep_limits_changed(self) -> None:
|
||||
"""Clamp processing frequency bounds after sweep start/stop edits."""
|
||||
self._reset_preprocess_selection_after_radar_key_change()
|
||||
@@ -26,7 +16,7 @@ class AppWindowRadarLimitsMixin:
|
||||
|
||||
def _refresh_radar_limits_from_device(self) -> bool:
|
||||
"""Query native LibreVNA limits and apply them to GUI fields."""
|
||||
serial = self._serial_input.text().strip()
|
||||
serial = self._defaults_config.radar.serial
|
||||
radar_service = LibreVnaService(serial=serial or None)
|
||||
if not radar_service.driver_available:
|
||||
self._fallback_to_mock_mode("LibreVNA Python driver is not available for device limits query")
|
||||
@@ -56,11 +46,8 @@ class AppWindowRadarLimitsMixin:
|
||||
self._radar_points_label.setText("Points")
|
||||
self._radar_ifbw_label.setText("IF BW Hz")
|
||||
self._radar_power_label.setText("Stimulus Power dBm")
|
||||
if self._radar_mode.currentText() == "native":
|
||||
self._radar_limits_hint.setText("Device limits unavailable in native mode (device not connected).")
|
||||
else:
|
||||
self._radar_limits_hint.setText("Mock mode: device limits are not applied.")
|
||||
self._power_input.setToolTip("Device power limits are available only in native mode.")
|
||||
self._radar_limits_hint.setText("Device limits are not available.")
|
||||
self._power_input.setToolTip("Stimulus power configured in the active profile.")
|
||||
return False
|
||||
|
||||
min_freq_hz = float(limits["min_frequency_hz"])
|
||||
|
||||
@@ -82,6 +82,11 @@ class AppWindowConfigStateBuildersMixin:
|
||||
@staticmethod
|
||||
def _format_combos_text_from_config(config: RunConfigModel) -> str:
|
||||
"""Render configured combos for UI text editor, keeping full matrix as empty."""
|
||||
if config.is_multi_device:
|
||||
return ",".join(
|
||||
f"{int(combo.input)}:{int(combo.output)}"
|
||||
for combo in RunConfigModel.build_multi_device_virtual_combos()
|
||||
)
|
||||
combos = list(config.combos)
|
||||
full_combos = config.build_full_combos(config.input_switch.positions, config.output_switch.positions)
|
||||
if len(combos) == len(full_combos) and all(
|
||||
@@ -215,8 +220,18 @@ class AppWindowConfigStateBuildersMixin:
|
||||
"""Build GUI-only persistent state from current widget values."""
|
||||
return GuiStateModel(
|
||||
switches=GuiSwitchStateModel(
|
||||
combo_mode="single" if self._single_combo_select_button.isChecked() else "text",
|
||||
combos_text=self._combos_text.text().strip(),
|
||||
combo_mode=(
|
||||
"text"
|
||||
if self._is_multi_device_model_selected()
|
||||
else "single"
|
||||
if self._single_combo_select_button.isChecked()
|
||||
else "text"
|
||||
),
|
||||
combos_text=(
|
||||
self._fixed_multi_combo_text()
|
||||
if self._is_multi_device_model_selected()
|
||||
else self._combos_text.text().strip()
|
||||
),
|
||||
single_input=self._single_combo_input.text().strip(),
|
||||
single_output=self._single_combo_output.text().strip(),
|
||||
),
|
||||
@@ -292,19 +307,24 @@ class AppWindowConfigStateBuildersMixin:
|
||||
config.runtime.settling_ms = int(self._settling_ms.text().strip())
|
||||
config.runtime.processing_live_config_path = str(self._live_config_writer.path)
|
||||
|
||||
if self._single_combo_select_button.isChecked():
|
||||
config.combos = [
|
||||
ComboModel(
|
||||
input=int(self._single_combo_input.text().strip()),
|
||||
output=int(self._single_combo_output.text().strip()),
|
||||
)
|
||||
]
|
||||
if config.is_multi_device:
|
||||
if len(config.radar.multi_device.slave_serials) != 2:
|
||||
raise ValueError("LibreVNA multi-device mode requires exactly two slave serials")
|
||||
config.apply_device_model_constraints()
|
||||
else:
|
||||
combo_text = self._combos_text.text()
|
||||
config.combos = parse_combos_from_text(combo_text)
|
||||
config.ensure_combos()
|
||||
if self._switches_are_effectively_static(config):
|
||||
config.combos = [ComboModel(input=0, output=0)]
|
||||
if self._single_combo_select_button.isChecked():
|
||||
config.combos = [
|
||||
ComboModel(
|
||||
input=int(self._single_combo_input.text().strip()),
|
||||
output=int(self._single_combo_output.text().strip()),
|
||||
)
|
||||
]
|
||||
else:
|
||||
combo_text = self._combos_text.text()
|
||||
config.combos = parse_combos_from_text(combo_text)
|
||||
config.ensure_combos()
|
||||
if self._switches_are_effectively_static(config):
|
||||
config.combos = [ComboModel(input=0, output=0)]
|
||||
|
||||
for key in PREPROCESS_ASSET_KEYS:
|
||||
preprocess_asset_model(config, key).bundle_path = ""
|
||||
@@ -331,23 +351,36 @@ class AppWindowConfigStateBuildersMixin:
|
||||
sweep_points=config.radar.sweep.points,
|
||||
ifbw_hz=config.radar.sweep.if_bandwidth_hz,
|
||||
power_dbm=config.radar.sweep.power_dbm,
|
||||
extra_serials=(
|
||||
config.radar.multi_device.slave_serials
|
||||
if config.is_multi_device
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
def _radar_key_from_ui(self) -> str:
|
||||
"""Build current radar key directly from radar widgets only."""
|
||||
model_name = self._defaults_config.radar.model or RunConfigModel.LIBREVNA_MODEL
|
||||
return radar_key_from_config(
|
||||
model_name=self._defaults_config.radar.model,
|
||||
serial=self._serial_input.text().strip(),
|
||||
model_name=model_name,
|
||||
serial=self._defaults_config.radar.serial,
|
||||
sweep_start_hz=float(self._start_hz_input.text().strip()),
|
||||
sweep_stop_hz=float(self._stop_hz_input.text().strip()),
|
||||
sweep_points=int(self._points_input.text().strip()),
|
||||
ifbw_hz=float(self._ifbw_input.text().strip()),
|
||||
power_dbm=float(self._power_input.text().strip()),
|
||||
extra_serials=(
|
||||
self._defaults_config.radar.multi_device.slave_serials
|
||||
if model_name == RunConfigModel.LIBREVNA_MULTI_MODEL
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _switches_are_effectively_static(config: RunConfigModel) -> bool:
|
||||
"""Return `True` when switch setup effectively yields one fixed combo."""
|
||||
if config.is_multi_device:
|
||||
return False
|
||||
has_single_position = config.input_switch.positions <= 1 and config.output_switch.positions <= 1
|
||||
both_mock = config.input_switch.driver_mode == "mock" and config.output_switch.driver_mode == "mock"
|
||||
return has_single_position or both_mock
|
||||
|
||||
@@ -153,9 +153,9 @@ class AppWindowPipelineMixin:
|
||||
self._stop_run()
|
||||
|
||||
try:
|
||||
if self._radar_mode.currentText() == "native":
|
||||
self._refresh_radar_limits_from_device()
|
||||
config = self._build_config()
|
||||
if config.radar.driver_mode == "native":
|
||||
self._refresh_radar_limits_from_device()
|
||||
run_signature = self._build_run_history_signature(config)
|
||||
if processor_only_running and self._processor_requires_restart(run_signature):
|
||||
self._stop_all_processes()
|
||||
@@ -185,6 +185,9 @@ class AppWindowPipelineMixin:
|
||||
if config.radar.driver_mode != "native":
|
||||
self._log("Radar pre-configuration skipped (mock mode)")
|
||||
return
|
||||
if config.is_multi_device:
|
||||
self._log("Multi-device raw producer will configure all LibreVNA devices")
|
||||
return
|
||||
|
||||
radar_service = LibreVnaService(serial=config.radar.serial or None)
|
||||
if not radar_service.driver_available:
|
||||
|
||||
@@ -128,10 +128,13 @@ def rebuild_bscan_history_from_results(
|
||||
|
||||
def pick_bscan_display_key(
|
||||
history_by_combo: dict[tuple[int, int], deque[np.ndarray]],
|
||||
preferred_key: tuple[int, int] | None = None,
|
||||
) -> tuple[int, int] | None:
|
||||
"""Choose combo key to display when multiple histories are present."""
|
||||
if not history_by_combo:
|
||||
return None
|
||||
if preferred_key is not None and preferred_key in history_by_combo:
|
||||
return preferred_key
|
||||
return next(iter(history_by_combo.keys()))
|
||||
|
||||
|
||||
@@ -267,9 +270,31 @@ class AppWindowBscanPlotMixin:
|
||||
|
||||
def _pick_bscan_display_key(self) -> tuple[int, int] | None:
|
||||
"""Choose combo history key to render."""
|
||||
display_key = pick_bscan_display_key(self._bscan_history_by_combo)
|
||||
requested_key = self._requested_bscan_display_key()
|
||||
display_key = pick_bscan_display_key(
|
||||
self._bscan_history_by_combo,
|
||||
preferred_key=requested_key,
|
||||
)
|
||||
available_keys = sorted(self._bscan_history_by_combo.keys())
|
||||
if requested_key is not None and requested_key != display_key and available_keys:
|
||||
combo_signature = ",".join(f"{input_pos}:{output_pos}" for input_pos, output_pos in available_keys)
|
||||
details = "\n".join(
|
||||
f"- in{input_pos}/out{output_pos}"
|
||||
for input_pos, output_pos in available_keys
|
||||
)
|
||||
self._log_warning(
|
||||
f"B-scan display combo in{requested_key[0]}/out{requested_key[1]} is not available; "
|
||||
f"rendering in{display_key[0]}/out{display_key[1]} instead.",
|
||||
details=details,
|
||||
once_key=(
|
||||
f"bscan_requested_combo_missing_{requested_key[0]}_{requested_key[1]}_"
|
||||
f"{combo_signature}"
|
||||
),
|
||||
)
|
||||
return display_key
|
||||
if display_key is not None and len(available_keys) > 1:
|
||||
if requested_key == display_key:
|
||||
return display_key
|
||||
combo_signature = ",".join(f"{input_pos}:{output_pos}" for input_pos, output_pos in available_keys)
|
||||
details = "\n".join(
|
||||
f"- in{input_pos}/out{output_pos}"
|
||||
@@ -286,6 +311,26 @@ class AppWindowBscanPlotMixin:
|
||||
)
|
||||
return display_key
|
||||
|
||||
def _requested_bscan_display_key(self) -> tuple[int, int] | None:
|
||||
"""Return the multi-device B-scan display combo requested by the GUI."""
|
||||
if not (
|
||||
hasattr(self, "_is_multi_device_model_selected")
|
||||
and self._is_multi_device_model_selected()
|
||||
and hasattr(self, "_single_combo_input")
|
||||
and hasattr(self, "_single_combo_output")
|
||||
):
|
||||
return None
|
||||
|
||||
input_text = self._single_combo_input.text().strip()
|
||||
output_text = self._single_combo_output.text().strip()
|
||||
if not input_text or not output_text:
|
||||
return None
|
||||
|
||||
try:
|
||||
return int(input_text), int(output_text)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def _bscan_lookup_table(self, axis_mode: str) -> np.ndarray:
|
||||
"""Return lookup table for current B-scan axis mode."""
|
||||
return bscan_lookup_table(axis_mode)
|
||||
|
||||
@@ -102,7 +102,9 @@ class AppWindowSnapshotMixin:
|
||||
|
||||
config_profile_path = self._vna_json_config_profile_path(output_dir)
|
||||
try:
|
||||
self._write_gui_profile_to_path(config_profile_path, allow_overwrite=False)
|
||||
# Snapshot and VNA JSON exports intentionally share the same output directory stem.
|
||||
# Refresh the companion GUI profile in place so a prior dataset save does not block JSON export.
|
||||
self._write_gui_profile_to_path(config_profile_path, allow_overwrite=True)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
exported_preview = "\n".join(str(path) for path in output_paths[:8])
|
||||
if len(output_paths) > 8:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from PyQt6.QtWidgets import QComboBox, QGroupBox, QLabel, QLineEdit, QVBoxLayout
|
||||
from PyQt6.QtWidgets import QGroupBox, QLabel, QLineEdit, QVBoxLayout
|
||||
|
||||
from python_app.gui.controllers.sections.layout_helpers import build_two_column_form_widget
|
||||
|
||||
@@ -15,22 +15,12 @@ def build_radar_group(owner) -> QGroupBox:
|
||||
layout.setSpacing(8)
|
||||
defaults = owner._defaults_config.radar
|
||||
|
||||
owner._serial_input = QLineEdit(defaults.serial)
|
||||
owner._serial_input.setPlaceholderText("Optional: empty = auto-detect first LibreVNA")
|
||||
|
||||
owner._radar_mode = QComboBox()
|
||||
owner._radar_mode.addItems(["mock", "native"])
|
||||
owner._radar_mode.setToolTip("mock: synthetic signal, native: real LibreVNA hardware")
|
||||
owner._set_combo_current_text(owner._radar_mode, defaults.driver_mode)
|
||||
|
||||
owner._start_hz_input = QLineEdit(f"{defaults.sweep.start_hz:g}")
|
||||
owner._stop_hz_input = QLineEdit(f"{defaults.sweep.stop_hz:g}")
|
||||
owner._points_input = QLineEdit(str(defaults.sweep.points))
|
||||
owner._ifbw_input = QLineEdit(f"{defaults.sweep.if_bandwidth_hz:g}")
|
||||
owner._power_input = QLineEdit(f"{defaults.sweep.power_dbm:g}")
|
||||
owner._power_input.setToolTip("Device power limits are available only in native mode.")
|
||||
owner._serial_input.editingFinished.connect(owner._on_radar_identity_changed)
|
||||
owner._radar_mode.currentTextChanged.connect(owner._on_radar_identity_changed)
|
||||
owner._power_input.setToolTip("Stimulus power configured in the active profile.")
|
||||
owner._start_hz_input.editingFinished.connect(owner._on_radar_sweep_limits_changed)
|
||||
owner._stop_hz_input.editingFinished.connect(owner._on_radar_sweep_limits_changed)
|
||||
owner._points_input.editingFinished.connect(owner._on_radar_sweep_limits_changed)
|
||||
@@ -43,7 +33,7 @@ def build_radar_group(owner) -> QGroupBox:
|
||||
owner._radar_ifbw_label = QLabel("IF BW Hz")
|
||||
owner._radar_power_label = QLabel("Stimulus Power dBm")
|
||||
|
||||
owner._radar_limits_hint = QLabel("Mock mode: device limits are not applied.")
|
||||
owner._radar_limits_hint = QLabel("Device limits are not available.")
|
||||
owner._radar_limits_hint.setObjectName("hintLabel")
|
||||
|
||||
layout.addWidget(
|
||||
@@ -56,7 +46,6 @@ def build_radar_group(owner) -> QGroupBox:
|
||||
(owner._radar_ifbw_label, owner._ifbw_input),
|
||||
(owner._radar_power_label, owner._power_input),
|
||||
],
|
||||
split_index=3,
|
||||
)
|
||||
)
|
||||
layout.addWidget(owner._radar_limits_hint)
|
||||
|
||||
@@ -63,6 +63,8 @@ def build_switch_group(owner) -> QGroupBox:
|
||||
|
||||
owner._run_combos_select_button.clicked.connect(lambda: owner._set_combo_selection_mode("text"))
|
||||
owner._single_combo_select_button.clicked.connect(lambda: owner._set_combo_selection_mode("single"))
|
||||
owner._single_combo_output.editingFinished.connect(owner._on_processing_live_settings_changed)
|
||||
owner._single_combo_input.editingFinished.connect(owner._on_processing_live_settings_changed)
|
||||
owner._set_combo_selection_mode(str(switch_defaults.combo_mode))
|
||||
|
||||
return group
|
||||
|
||||
@@ -21,7 +21,8 @@ def main() -> int:
|
||||
"""Run Qt event loop and show main radar control window."""
|
||||
app = QApplication(sys.argv)
|
||||
apply_light_theme(app)
|
||||
pg.setConfigOptions(antialias=True, background="#ffffff", foreground="#334155")
|
||||
# PyQtGraph foreground controls axis lines, tick text, labels, and titles.
|
||||
pg.setConfigOptions(antialias=True, background="#ffffff", foreground="#ffffff")
|
||||
window = AppWindow(PROJECT_ROOT)
|
||||
window.showMaximized()
|
||||
return app.exec()
|
||||
|
||||
@@ -66,8 +66,11 @@ def build_run_history_signature(
|
||||
combos_signature = tuple((int(combo.input), int(combo.output)) for combo in config.combos)
|
||||
preprocess_signature = tuple(preprocess_asset_model(config, key).set_name for key in PREPROCESS_ASSET_KEYS)
|
||||
return (
|
||||
str(config.radar.model),
|
||||
str(config.radar.driver_mode),
|
||||
str(config.radar.serial),
|
||||
tuple(str(value) for value in config.radar.multi_device.slave_serials),
|
||||
bool(config.radar.multi_device.force_external_reference),
|
||||
float(config.radar.sweep.start_hz),
|
||||
float(config.radar.sweep.stop_hz),
|
||||
int(config.radar.sweep.points),
|
||||
|
||||
@@ -103,6 +103,9 @@ QTextEdit#runtimeLogBox {
|
||||
font-family: "DejaVu Sans Mono";
|
||||
font-size: 12px;
|
||||
padding: 6px 8px;
|
||||
color: #1f2937;
|
||||
background-color: #fbfdff;
|
||||
border-color: #c9d4e1;
|
||||
}
|
||||
|
||||
QComboBox::drop-down {
|
||||
|
||||
Reference in New Issue
Block a user