forked from chiyu1468/AirTrapMine
Update GUI 2.2.0: Settings tab
This commit is contained in:
+125
-38
@@ -1,10 +1,78 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
||||||
QPushButton, QLineEdit, QComboBox)
|
QPushButton, QLineEdit, QComboBox, QApplication)
|
||||||
|
from PyQt6.QtGui import QFont
|
||||||
from PyQt6.QtCore import pyqtSignal
|
from PyQt6.QtCore import pyqtSignal
|
||||||
import glob
|
import glob
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def _get_font_scale():
|
||||||
|
app = QApplication.instance()
|
||||||
|
if app is None:
|
||||||
|
return 1.0
|
||||||
|
scale = app.property("font_scale")
|
||||||
|
try:
|
||||||
|
return float(scale) if scale is not None else 1.0
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def _scale_stylesheet_font_sizes(stylesheet, scale):
|
||||||
|
if not stylesheet or 'font-size' not in stylesheet:
|
||||||
|
return stylesheet
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
def repl(match):
|
||||||
|
size = float(match.group(1))
|
||||||
|
unit = match.group(2)
|
||||||
|
scaled = max(1.0, size * scale)
|
||||||
|
text = f"{scaled:.2f}".rstrip('0').rstrip('.')
|
||||||
|
return f"font-size: {text}{unit}"
|
||||||
|
|
||||||
|
return re.sub(r'font-size\s*:\s*([0-9]+(?:\.[0-9]+)?)\s*(px|pt)', repl, stylesheet)
|
||||||
|
|
||||||
|
|
||||||
|
def _set_scaled_stylesheet(widget, stylesheet):
|
||||||
|
widget._base_stylesheet = stylesheet
|
||||||
|
scaled = _scale_stylesheet_font_sizes(stylesheet, _get_font_scale())
|
||||||
|
widget._applied_stylesheet = scaled
|
||||||
|
widget.setStyleSheet(scaled)
|
||||||
|
|
||||||
|
|
||||||
|
def _reapply_scaled_stylesheet(widget):
|
||||||
|
current_stylesheet = widget.styleSheet()
|
||||||
|
base_stylesheet = getattr(widget, '_base_stylesheet', None)
|
||||||
|
applied_stylesheet = getattr(widget, '_applied_stylesheet', None)
|
||||||
|
|
||||||
|
if current_stylesheet != applied_stylesheet:
|
||||||
|
base_stylesheet = current_stylesheet
|
||||||
|
widget._base_stylesheet = base_stylesheet
|
||||||
|
|
||||||
|
if base_stylesheet is not None:
|
||||||
|
scaled = _scale_stylesheet_font_sizes(base_stylesheet, _get_font_scale())
|
||||||
|
widget._applied_stylesheet = scaled
|
||||||
|
if current_stylesheet != scaled:
|
||||||
|
widget.setStyleSheet(scaled)
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_scaled_font(widget):
|
||||||
|
base_font = getattr(widget, '_base_font_for_scale', None)
|
||||||
|
if base_font is None:
|
||||||
|
app = QApplication.instance()
|
||||||
|
app_base_font = app.property("base_app_font") if app else None
|
||||||
|
base_font = QFont(app_base_font) if app_base_font is not None else QFont(widget.font())
|
||||||
|
widget._base_font_for_scale = QFont(base_font)
|
||||||
|
|
||||||
|
scaled_font = QFont(base_font)
|
||||||
|
scale = _get_font_scale()
|
||||||
|
if base_font.pointSizeF() > 0:
|
||||||
|
scaled_font.setPointSizeF(max(1.0, base_font.pointSizeF() * scale))
|
||||||
|
elif base_font.pointSize() > 0:
|
||||||
|
scaled_font.setPointSize(max(1, int(round(base_font.pointSize() * scale))))
|
||||||
|
widget.setFont(scaled_font)
|
||||||
|
|
||||||
class CommPanel(QWidget):
|
class CommPanel(QWidget):
|
||||||
"""通讯设置面板类"""
|
"""通讯设置面板类"""
|
||||||
|
|
||||||
@@ -26,6 +94,7 @@ class CommPanel(QWidget):
|
|||||||
self.ws_connections = []
|
self.ws_connections = []
|
||||||
self.serial_connections = []
|
self.serial_connections = []
|
||||||
self._init_ui()
|
self._init_ui()
|
||||||
|
self.apply_font_scale()
|
||||||
|
|
||||||
def _init_ui(self):
|
def _init_ui(self):
|
||||||
"""初始化UI"""
|
"""初始化UI"""
|
||||||
@@ -35,7 +104,7 @@ class CommPanel(QWidget):
|
|||||||
|
|
||||||
# ========== UDP MAVLink 區域 ==========
|
# ========== UDP MAVLink 區域 ==========
|
||||||
udp_title = QLabel("UDP")
|
udp_title = QLabel("UDP")
|
||||||
udp_title.setStyleSheet("""
|
_set_scaled_stylesheet(udp_title, """
|
||||||
color: #DDD;
|
color: #DDD;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -60,7 +129,7 @@ class CommPanel(QWidget):
|
|||||||
self.udp_ip_input = QLineEdit()
|
self.udp_ip_input = QLineEdit()
|
||||||
self.udp_ip_input.setText("127.0.0.1")
|
self.udp_ip_input.setText("127.0.0.1")
|
||||||
self.udp_ip_input.setPlaceholderText("IP")
|
self.udp_ip_input.setPlaceholderText("IP")
|
||||||
self.udp_ip_input.setStyleSheet("""
|
_set_scaled_stylesheet(self.udp_ip_input, """
|
||||||
QLineEdit {
|
QLineEdit {
|
||||||
background-color: #333;
|
background-color: #333;
|
||||||
color: #DDD;
|
color: #DDD;
|
||||||
@@ -74,7 +143,7 @@ class CommPanel(QWidget):
|
|||||||
self.udp_port_input.setText("14550")
|
self.udp_port_input.setText("14550")
|
||||||
self.udp_port_input.setPlaceholderText("Port")
|
self.udp_port_input.setPlaceholderText("Port")
|
||||||
self.udp_port_input.setFixedWidth(80)
|
self.udp_port_input.setFixedWidth(80)
|
||||||
self.udp_port_input.setStyleSheet("""
|
_set_scaled_stylesheet(self.udp_port_input, """
|
||||||
QLineEdit {
|
QLineEdit {
|
||||||
background-color: #333;
|
background-color: #333;
|
||||||
color: #DDD;
|
color: #DDD;
|
||||||
@@ -86,7 +155,7 @@ class CommPanel(QWidget):
|
|||||||
|
|
||||||
add_udp_btn = QPushButton("添加")
|
add_udp_btn = QPushButton("添加")
|
||||||
add_udp_btn.clicked.connect(self._handle_add_udp)
|
add_udp_btn.clicked.connect(self._handle_add_udp)
|
||||||
add_udp_btn.setStyleSheet("""
|
_set_scaled_stylesheet(add_udp_btn, """
|
||||||
QPushButton {
|
QPushButton {
|
||||||
background-color: #4CAF50;
|
background-color: #4CAF50;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -98,9 +167,13 @@ class CommPanel(QWidget):
|
|||||||
QPushButton:hover { background-color: #45a049; }
|
QPushButton:hover { background-color: #45a049; }
|
||||||
""")
|
""")
|
||||||
|
|
||||||
add_udp_layout.addWidget(QLabel("IP:", styleSheet="color: #DDD;"))
|
ip_label = QLabel("IP:")
|
||||||
|
_set_scaled_stylesheet(ip_label, "color: #DDD;")
|
||||||
|
add_udp_layout.addWidget(ip_label)
|
||||||
add_udp_layout.addWidget(self.udp_ip_input)
|
add_udp_layout.addWidget(self.udp_ip_input)
|
||||||
add_udp_layout.addWidget(QLabel("Port:", styleSheet="color: #DDD;"))
|
port_label = QLabel("Port:")
|
||||||
|
_set_scaled_stylesheet(port_label, "color: #DDD;")
|
||||||
|
add_udp_layout.addWidget(port_label)
|
||||||
add_udp_layout.addWidget(self.udp_port_input)
|
add_udp_layout.addWidget(self.udp_port_input)
|
||||||
add_udp_layout.addWidget(add_udp_btn)
|
add_udp_layout.addWidget(add_udp_btn)
|
||||||
|
|
||||||
@@ -113,7 +186,7 @@ class CommPanel(QWidget):
|
|||||||
|
|
||||||
# ========== Serial 區域 ==========
|
# ========== Serial 區域 ==========
|
||||||
serial_title = QLabel("Serial")
|
serial_title = QLabel("Serial")
|
||||||
serial_title.setStyleSheet("""
|
_set_scaled_stylesheet(serial_title, """
|
||||||
color: #DDD;
|
color: #DDD;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -136,7 +209,7 @@ class CommPanel(QWidget):
|
|||||||
add_serial_layout.setContentsMargins(0, 0, 0, 0)
|
add_serial_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
self.serial_port_combo = QComboBox()
|
self.serial_port_combo = QComboBox()
|
||||||
self.serial_port_combo.setStyleSheet("""
|
_set_scaled_stylesheet(self.serial_port_combo, """
|
||||||
QComboBox {
|
QComboBox {
|
||||||
background-color: #333;
|
background-color: #333;
|
||||||
color: #DDD;
|
color: #DDD;
|
||||||
@@ -160,7 +233,7 @@ class CommPanel(QWidget):
|
|||||||
refresh_ports_btn.setFixedWidth(35)
|
refresh_ports_btn.setFixedWidth(35)
|
||||||
refresh_ports_btn.clicked.connect(self._refresh_serial_ports)
|
refresh_ports_btn.clicked.connect(self._refresh_serial_ports)
|
||||||
refresh_ports_btn.setToolTip("重新掃描串口")
|
refresh_ports_btn.setToolTip("重新掃描串口")
|
||||||
refresh_ports_btn.setStyleSheet("""
|
_set_scaled_stylesheet(refresh_ports_btn, """
|
||||||
QPushButton {
|
QPushButton {
|
||||||
background-color: #444;
|
background-color: #444;
|
||||||
color: #DDD;
|
color: #DDD;
|
||||||
@@ -176,7 +249,7 @@ class CommPanel(QWidget):
|
|||||||
self.serial_baudrate_combo.addItems(['9600', '19200', '38400', '57600', '115200'])
|
self.serial_baudrate_combo.addItems(['9600', '19200', '38400', '57600', '115200'])
|
||||||
self.serial_baudrate_combo.setCurrentText('57600')
|
self.serial_baudrate_combo.setCurrentText('57600')
|
||||||
self.serial_baudrate_combo.setFixedWidth(100)
|
self.serial_baudrate_combo.setFixedWidth(100)
|
||||||
self.serial_baudrate_combo.setStyleSheet("""
|
_set_scaled_stylesheet(self.serial_baudrate_combo, """
|
||||||
QComboBox {
|
QComboBox {
|
||||||
background-color: #333;
|
background-color: #333;
|
||||||
color: #DDD;
|
color: #DDD;
|
||||||
@@ -197,7 +270,7 @@ class CommPanel(QWidget):
|
|||||||
|
|
||||||
add_serial_btn = QPushButton("添加")
|
add_serial_btn = QPushButton("添加")
|
||||||
add_serial_btn.clicked.connect(self._handle_add_serial)
|
add_serial_btn.clicked.connect(self._handle_add_serial)
|
||||||
add_serial_btn.setStyleSheet("""
|
_set_scaled_stylesheet(add_serial_btn, """
|
||||||
QPushButton {
|
QPushButton {
|
||||||
background-color: #4CAF50;
|
background-color: #4CAF50;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -209,10 +282,14 @@ class CommPanel(QWidget):
|
|||||||
QPushButton:hover { background-color: #45a049; }
|
QPushButton:hover { background-color: #45a049; }
|
||||||
""")
|
""")
|
||||||
|
|
||||||
add_serial_layout.addWidget(QLabel("Port:", styleSheet="color: #DDD;"))
|
serial_port_label = QLabel("Port:")
|
||||||
|
_set_scaled_stylesheet(serial_port_label, "color: #DDD;")
|
||||||
|
add_serial_layout.addWidget(serial_port_label)
|
||||||
add_serial_layout.addWidget(self.serial_port_combo)
|
add_serial_layout.addWidget(self.serial_port_combo)
|
||||||
add_serial_layout.addWidget(refresh_ports_btn)
|
add_serial_layout.addWidget(refresh_ports_btn)
|
||||||
add_serial_layout.addWidget(QLabel("Baud:", styleSheet="color: #DDD;"))
|
baud_label = QLabel("Baud:")
|
||||||
|
_set_scaled_stylesheet(baud_label, "color: #DDD;")
|
||||||
|
add_serial_layout.addWidget(baud_label)
|
||||||
add_serial_layout.addWidget(self.serial_baudrate_combo)
|
add_serial_layout.addWidget(self.serial_baudrate_combo)
|
||||||
add_serial_layout.addWidget(add_serial_btn)
|
add_serial_layout.addWidget(add_serial_btn)
|
||||||
|
|
||||||
@@ -225,7 +302,7 @@ class CommPanel(QWidget):
|
|||||||
|
|
||||||
# ========== WebSocket 區域 ==========
|
# ========== WebSocket 區域 ==========
|
||||||
ws_title = QLabel("WebSocket")
|
ws_title = QLabel("WebSocket")
|
||||||
ws_title.setStyleSheet("""
|
_set_scaled_stylesheet(ws_title, """
|
||||||
color: #DDD;
|
color: #DDD;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -249,7 +326,7 @@ class CommPanel(QWidget):
|
|||||||
|
|
||||||
self.ws_url_input = QLineEdit()
|
self.ws_url_input = QLineEdit()
|
||||||
self.ws_url_input.setPlaceholderText("host")
|
self.ws_url_input.setPlaceholderText("host")
|
||||||
self.ws_url_input.setStyleSheet("""
|
_set_scaled_stylesheet(self.ws_url_input, """
|
||||||
QLineEdit {
|
QLineEdit {
|
||||||
background-color: #333;
|
background-color: #333;
|
||||||
color: #DDD;
|
color: #DDD;
|
||||||
@@ -261,7 +338,7 @@ class CommPanel(QWidget):
|
|||||||
|
|
||||||
add_ws_btn = QPushButton("添加")
|
add_ws_btn = QPushButton("添加")
|
||||||
add_ws_btn.clicked.connect(self._handle_add_ws)
|
add_ws_btn.clicked.connect(self._handle_add_ws)
|
||||||
add_ws_btn.setStyleSheet("""
|
_set_scaled_stylesheet(add_ws_btn, """
|
||||||
QPushButton {
|
QPushButton {
|
||||||
background-color: #4CAF50;
|
background-color: #4CAF50;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -273,7 +350,9 @@ class CommPanel(QWidget):
|
|||||||
QPushButton:hover { background-color: #45a049; }
|
QPushButton:hover { background-color: #45a049; }
|
||||||
""")
|
""")
|
||||||
|
|
||||||
add_ws_layout.addWidget(QLabel("URL:", styleSheet="color: #DDD;"))
|
url_label = QLabel("URL:")
|
||||||
|
_set_scaled_stylesheet(url_label, "color: #DDD;")
|
||||||
|
add_ws_layout.addWidget(url_label)
|
||||||
add_ws_layout.addWidget(self.ws_url_input)
|
add_ws_layout.addWidget(self.ws_url_input)
|
||||||
add_ws_layout.addWidget(add_ws_btn)
|
add_ws_layout.addWidget(add_ws_btn)
|
||||||
|
|
||||||
@@ -437,7 +516,7 @@ class CommPanel(QWidget):
|
|||||||
def create_udp_connection_panel(self, conn):
|
def create_udp_connection_panel(self, conn):
|
||||||
"""創建 UDP 連接面板"""
|
"""創建 UDP 連接面板"""
|
||||||
panel = QWidget()
|
panel = QWidget()
|
||||||
panel.setStyleSheet("""
|
_set_scaled_stylesheet(panel, """
|
||||||
QWidget {
|
QWidget {
|
||||||
background-color: #2A2A2A;
|
background-color: #2A2A2A;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
@@ -451,22 +530,22 @@ class CommPanel(QWidget):
|
|||||||
|
|
||||||
# 連接資訊
|
# 連接資訊
|
||||||
info_label = QLabel(f"{conn['name']} - {conn['ip']}:{conn['port']}")
|
info_label = QLabel(f"{conn['name']} - {conn['ip']}:{conn['port']}")
|
||||||
info_label.setStyleSheet("color: #DDD; font-size: 12px;")
|
_set_scaled_stylesheet(info_label, "color: #DDD; font-size: 12px;")
|
||||||
|
|
||||||
# 狀態指示器
|
# 狀態指示器
|
||||||
status_label = QLabel("●")
|
status_label = QLabel("●")
|
||||||
if conn.get('enabled', False):
|
if conn.get('enabled', False):
|
||||||
status_label.setStyleSheet("color: #4CAF50; font-size: 16px;")
|
_set_scaled_stylesheet(status_label, "color: #4CAF50; font-size: 16px;")
|
||||||
status_label.setToolTip("運行中")
|
status_label.setToolTip("運行中")
|
||||||
else:
|
else:
|
||||||
status_label.setStyleSheet("color: #888; font-size: 16px;")
|
_set_scaled_stylesheet(status_label, "color: #888; font-size: 16px;")
|
||||||
status_label.setToolTip("已停止")
|
status_label.setToolTip("已停止")
|
||||||
|
|
||||||
# 控制按鈕
|
# 控制按鈕
|
||||||
toggle_btn = QPushButton("停止" if conn.get('enabled', False) else "啟動")
|
toggle_btn = QPushButton("停止" if conn.get('enabled', False) else "啟動")
|
||||||
toggle_btn.setFixedWidth(60)
|
toggle_btn.setFixedWidth(60)
|
||||||
toggle_btn.clicked.connect(lambda: self.udp_connection_toggled.emit(conn, toggle_btn, status_label))
|
toggle_btn.clicked.connect(lambda: self.udp_connection_toggled.emit(conn, toggle_btn, status_label))
|
||||||
toggle_btn.setStyleSheet("""
|
_set_scaled_stylesheet(toggle_btn, """
|
||||||
QPushButton {
|
QPushButton {
|
||||||
background-color: #444;
|
background-color: #444;
|
||||||
color: #DDD;
|
color: #DDD;
|
||||||
@@ -481,7 +560,7 @@ class CommPanel(QWidget):
|
|||||||
remove_btn = QPushButton("移除")
|
remove_btn = QPushButton("移除")
|
||||||
remove_btn.setFixedWidth(60)
|
remove_btn.setFixedWidth(60)
|
||||||
remove_btn.clicked.connect(lambda: self.udp_connection_removed.emit(conn, panel))
|
remove_btn.clicked.connect(lambda: self.udp_connection_removed.emit(conn, panel))
|
||||||
remove_btn.setStyleSheet("""
|
_set_scaled_stylesheet(remove_btn, """
|
||||||
QPushButton {
|
QPushButton {
|
||||||
background-color: #d32f2f;
|
background-color: #d32f2f;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -509,7 +588,7 @@ class CommPanel(QWidget):
|
|||||||
def create_ws_connection_panel(self, conn):
|
def create_ws_connection_panel(self, conn):
|
||||||
"""創建 WebSocket 連接面板"""
|
"""創建 WebSocket 連接面板"""
|
||||||
panel = QWidget()
|
panel = QWidget()
|
||||||
panel.setStyleSheet("""
|
_set_scaled_stylesheet(panel, """
|
||||||
QWidget {
|
QWidget {
|
||||||
background-color: #2A2A2A;
|
background-color: #2A2A2A;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
@@ -523,22 +602,22 @@ class CommPanel(QWidget):
|
|||||||
|
|
||||||
# 連接資訊
|
# 連接資訊
|
||||||
info_label = QLabel(f"{conn['name']} - {conn['url']}")
|
info_label = QLabel(f"{conn['name']} - {conn['url']}")
|
||||||
info_label.setStyleSheet("color: #DDD; font-size: 12px;")
|
_set_scaled_stylesheet(info_label, "color: #DDD; font-size: 12px;")
|
||||||
|
|
||||||
# 狀態指示器
|
# 狀態指示器
|
||||||
status_label = QLabel("●")
|
status_label = QLabel("●")
|
||||||
if conn.get('enabled', False):
|
if conn.get('enabled', False):
|
||||||
status_label.setStyleSheet("color: #4CAF50; font-size: 16px;")
|
_set_scaled_stylesheet(status_label, "color: #4CAF50; font-size: 16px;")
|
||||||
status_label.setToolTip("運行中")
|
status_label.setToolTip("運行中")
|
||||||
else:
|
else:
|
||||||
status_label.setStyleSheet("color: #888; font-size: 16px;")
|
_set_scaled_stylesheet(status_label, "color: #888; font-size: 16px;")
|
||||||
status_label.setToolTip("已停止")
|
status_label.setToolTip("已停止")
|
||||||
|
|
||||||
# 控制按鈕
|
# 控制按鈕
|
||||||
toggle_btn = QPushButton("停止" if conn.get('enabled', False) else "啟動")
|
toggle_btn = QPushButton("停止" if conn.get('enabled', False) else "啟動")
|
||||||
toggle_btn.setFixedWidth(60)
|
toggle_btn.setFixedWidth(60)
|
||||||
toggle_btn.clicked.connect(lambda: self.ws_connection_toggled.emit(conn, toggle_btn, status_label))
|
toggle_btn.clicked.connect(lambda: self.ws_connection_toggled.emit(conn, toggle_btn, status_label))
|
||||||
toggle_btn.setStyleSheet("""
|
_set_scaled_stylesheet(toggle_btn, """
|
||||||
QPushButton {
|
QPushButton {
|
||||||
background-color: #444;
|
background-color: #444;
|
||||||
color: #DDD;
|
color: #DDD;
|
||||||
@@ -553,7 +632,7 @@ class CommPanel(QWidget):
|
|||||||
remove_btn = QPushButton("移除")
|
remove_btn = QPushButton("移除")
|
||||||
remove_btn.setFixedWidth(60)
|
remove_btn.setFixedWidth(60)
|
||||||
remove_btn.clicked.connect(lambda: self.ws_connection_removed.emit(conn, panel))
|
remove_btn.clicked.connect(lambda: self.ws_connection_removed.emit(conn, panel))
|
||||||
remove_btn.setStyleSheet("""
|
_set_scaled_stylesheet(remove_btn, """
|
||||||
QPushButton {
|
QPushButton {
|
||||||
background-color: #d32f2f;
|
background-color: #d32f2f;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -605,7 +684,7 @@ class CommPanel(QWidget):
|
|||||||
def create_serial_connection_panel(self, conn):
|
def create_serial_connection_panel(self, conn):
|
||||||
"""創建 Serial 連接面板"""
|
"""創建 Serial 連接面板"""
|
||||||
panel = QWidget()
|
panel = QWidget()
|
||||||
panel.setStyleSheet("""
|
_set_scaled_stylesheet(panel, """
|
||||||
QWidget {
|
QWidget {
|
||||||
background-color: #2A2A2A;
|
background-color: #2A2A2A;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
@@ -619,22 +698,22 @@ class CommPanel(QWidget):
|
|||||||
|
|
||||||
# 連接資訊
|
# 連接資訊
|
||||||
info_label = QLabel(f"{conn['name']} - {conn['port']} @ {conn['baudrate']}")
|
info_label = QLabel(f"{conn['name']} - {conn['port']} @ {conn['baudrate']}")
|
||||||
info_label.setStyleSheet("color: #DDD; font-size: 12px;")
|
_set_scaled_stylesheet(info_label, "color: #DDD; font-size: 12px;")
|
||||||
|
|
||||||
# 狀態指示器
|
# 狀態指示器
|
||||||
status_label = QLabel("●")
|
status_label = QLabel("●")
|
||||||
if conn.get('enabled', False):
|
if conn.get('enabled', False):
|
||||||
status_label.setStyleSheet("color: #4CAF50; font-size: 16px;")
|
_set_scaled_stylesheet(status_label, "color: #4CAF50; font-size: 16px;")
|
||||||
status_label.setToolTip("運行中")
|
status_label.setToolTip("運行中")
|
||||||
else:
|
else:
|
||||||
status_label.setStyleSheet("color: #888; font-size: 16px;")
|
_set_scaled_stylesheet(status_label, "color: #888; font-size: 16px;")
|
||||||
status_label.setToolTip("已停止")
|
status_label.setToolTip("已停止")
|
||||||
|
|
||||||
# 控制按鈕
|
# 控制按鈕
|
||||||
toggle_btn = QPushButton("停止" if conn.get('enabled', False) else "啟動")
|
toggle_btn = QPushButton("停止" if conn.get('enabled', False) else "啟動")
|
||||||
toggle_btn.setFixedWidth(60)
|
toggle_btn.setFixedWidth(60)
|
||||||
toggle_btn.clicked.connect(lambda: self.serial_connection_toggled.emit(conn, toggle_btn, status_label))
|
toggle_btn.clicked.connect(lambda: self.serial_connection_toggled.emit(conn, toggle_btn, status_label))
|
||||||
toggle_btn.setStyleSheet("""
|
_set_scaled_stylesheet(toggle_btn, """
|
||||||
QPushButton {
|
QPushButton {
|
||||||
background-color: #444;
|
background-color: #444;
|
||||||
color: #DDD;
|
color: #DDD;
|
||||||
@@ -649,7 +728,7 @@ class CommPanel(QWidget):
|
|||||||
remove_btn = QPushButton("移除")
|
remove_btn = QPushButton("移除")
|
||||||
remove_btn.setFixedWidth(60)
|
remove_btn.setFixedWidth(60)
|
||||||
remove_btn.clicked.connect(lambda: self.serial_connection_removed.emit(conn, panel))
|
remove_btn.clicked.connect(lambda: self.serial_connection_removed.emit(conn, panel))
|
||||||
remove_btn.setStyleSheet("""
|
_set_scaled_stylesheet(remove_btn, """
|
||||||
QPushButton {
|
QPushButton {
|
||||||
background-color: #d32f2f;
|
background-color: #d32f2f;
|
||||||
color: white;
|
color: white;
|
||||||
@@ -684,4 +763,12 @@ class CommPanel(QWidget):
|
|||||||
def remove_serial_connection_from_list(self, conn):
|
def remove_serial_connection_from_list(self, conn):
|
||||||
"""從列表中移除 Serial 連接"""
|
"""從列表中移除 Serial 連接"""
|
||||||
if conn in self.serial_connections:
|
if conn in self.serial_connections:
|
||||||
self.serial_connections.remove(conn)
|
self.serial_connections.remove(conn)
|
||||||
|
|
||||||
|
def apply_font_scale(self):
|
||||||
|
"""重新套用目前字體倍率到通訊面板。"""
|
||||||
|
_apply_scaled_font(self)
|
||||||
|
_reapply_scaled_stylesheet(self)
|
||||||
|
for child in self.findChildren(QWidget):
|
||||||
|
_apply_scaled_font(child)
|
||||||
|
_reapply_scaled_stylesheet(child)
|
||||||
|
|||||||
+121
-30
@@ -1,10 +1,77 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QCheckBox)
|
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QCheckBox, QApplication)
|
||||||
from PyQt6.QtCore import pyqtSignal
|
from PyQt6.QtCore import pyqtSignal
|
||||||
from PyQt6.QtGui import QPainter, QPen, QColor, QFont, QPolygonF
|
from PyQt6.QtGui import QPainter, QPen, QColor, QFont, QPolygonF
|
||||||
from PyQt6.QtCore import QPointF, Qt
|
from PyQt6.QtCore import QPointF, Qt
|
||||||
import math
|
import math
|
||||||
|
|
||||||
|
|
||||||
|
def _get_font_scale():
|
||||||
|
app = QApplication.instance()
|
||||||
|
if app is None:
|
||||||
|
return 1.0
|
||||||
|
scale = app.property("font_scale")
|
||||||
|
try:
|
||||||
|
return float(scale) if scale is not None else 1.0
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def _scale_stylesheet_font_sizes(stylesheet, scale):
|
||||||
|
if not stylesheet or 'font-size' not in stylesheet:
|
||||||
|
return stylesheet
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
def repl(match):
|
||||||
|
size = float(match.group(1))
|
||||||
|
unit = match.group(2)
|
||||||
|
scaled = max(1.0, size * scale)
|
||||||
|
text = f"{scaled:.2f}".rstrip('0').rstrip('.')
|
||||||
|
return f"font-size: {text}{unit}"
|
||||||
|
|
||||||
|
return re.sub(r'font-size\s*:\s*([0-9]+(?:\.[0-9]+)?)\s*(px|pt)', repl, stylesheet)
|
||||||
|
|
||||||
|
|
||||||
|
def _set_scaled_stylesheet(widget, stylesheet):
|
||||||
|
widget._base_stylesheet = stylesheet
|
||||||
|
scaled = _scale_stylesheet_font_sizes(stylesheet, _get_font_scale())
|
||||||
|
widget._applied_stylesheet = scaled
|
||||||
|
widget.setStyleSheet(scaled)
|
||||||
|
|
||||||
|
|
||||||
|
def _reapply_scaled_stylesheet(widget):
|
||||||
|
current_stylesheet = widget.styleSheet()
|
||||||
|
base_stylesheet = getattr(widget, '_base_stylesheet', None)
|
||||||
|
applied_stylesheet = getattr(widget, '_applied_stylesheet', None)
|
||||||
|
|
||||||
|
if current_stylesheet != applied_stylesheet:
|
||||||
|
base_stylesheet = current_stylesheet
|
||||||
|
widget._base_stylesheet = base_stylesheet
|
||||||
|
|
||||||
|
if base_stylesheet is not None:
|
||||||
|
scaled = _scale_stylesheet_font_sizes(base_stylesheet, _get_font_scale())
|
||||||
|
widget._applied_stylesheet = scaled
|
||||||
|
if current_stylesheet != scaled:
|
||||||
|
widget.setStyleSheet(scaled)
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_scaled_font(widget):
|
||||||
|
base_font = getattr(widget, '_base_font_for_scale', None)
|
||||||
|
if base_font is None:
|
||||||
|
app = QApplication.instance()
|
||||||
|
app_base_font = app.property("base_app_font") if app else None
|
||||||
|
base_font = QFont(app_base_font) if app_base_font is not None else QFont(widget.font())
|
||||||
|
widget._base_font_for_scale = QFont(base_font)
|
||||||
|
|
||||||
|
scaled_font = QFont(base_font)
|
||||||
|
scale = _get_font_scale()
|
||||||
|
if base_font.pointSizeF() > 0:
|
||||||
|
scaled_font.setPointSizeF(max(1.0, base_font.pointSizeF() * scale))
|
||||||
|
elif base_font.pointSize() > 0:
|
||||||
|
scaled_font.setPointSize(max(1, int(round(base_font.pointSize() * scale))))
|
||||||
|
widget.setFont(scaled_font)
|
||||||
|
|
||||||
class DronePanel(QWidget):
|
class DronePanel(QWidget):
|
||||||
"""單個無人機面板類別"""
|
"""單個無人機面板類別"""
|
||||||
|
|
||||||
@@ -32,12 +99,13 @@ class DronePanel(QWidget):
|
|||||||
|
|
||||||
self.attitude_indicator = None
|
self.attitude_indicator = None
|
||||||
self._init_ui()
|
self._init_ui()
|
||||||
|
self.apply_font_scale()
|
||||||
|
|
||||||
def _init_ui(self):
|
def _init_ui(self):
|
||||||
"""初始化UI"""
|
"""初始化UI"""
|
||||||
self.setObjectName(f"panel_{self.drone_id}")
|
self.setObjectName(f"panel_{self.drone_id}")
|
||||||
self.setFixedHeight(140)
|
self.setFixedHeight(140)
|
||||||
self.setStyleSheet("""
|
_set_scaled_stylesheet(self, """
|
||||||
background-color: #2A2A2A;
|
background-color: #2A2A2A;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
""")
|
""")
|
||||||
@@ -49,7 +117,7 @@ class DronePanel(QWidget):
|
|||||||
|
|
||||||
# 創建內容容器(包含 info 和 control)
|
# 創建內容容器(包含 info 和 control)
|
||||||
content_widget = QWidget()
|
content_widget = QWidget()
|
||||||
content_widget.setStyleSheet("background-color: #333; border-radius: 6px;")
|
_set_scaled_stylesheet(content_widget, "background-color: #333; border-radius: 6px;")
|
||||||
content_layout = QHBoxLayout(content_widget)
|
content_layout = QHBoxLayout(content_widget)
|
||||||
content_layout.setContentsMargins(8, 8, 8, 8)
|
content_layout.setContentsMargins(8, 8, 8, 8)
|
||||||
content_layout.setSpacing(8)
|
content_layout.setSpacing(8)
|
||||||
@@ -82,7 +150,7 @@ class DronePanel(QWidget):
|
|||||||
# 勾選框
|
# 勾選框
|
||||||
self.checkbox = QCheckBox()
|
self.checkbox = QCheckBox()
|
||||||
self.checkbox.setObjectName(f"{self.drone_id}_checkbox")
|
self.checkbox.setObjectName(f"{self.drone_id}_checkbox")
|
||||||
self.checkbox.setStyleSheet("""
|
_set_scaled_stylesheet(self.checkbox, """
|
||||||
QCheckBox {
|
QCheckBox {
|
||||||
color: #DDD;
|
color: #DDD;
|
||||||
}
|
}
|
||||||
@@ -103,8 +171,8 @@ class DronePanel(QWidget):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# ID 顯示
|
# ID 顯示
|
||||||
id_label = QLabel(self.display_id)
|
self.id_label = QLabel(self.display_id)
|
||||||
id_label.setStyleSheet("""
|
_set_scaled_stylesheet(self.id_label, """
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #7FFFD4;
|
color: #7FFFD4;
|
||||||
@@ -112,7 +180,7 @@ class DronePanel(QWidget):
|
|||||||
""")
|
""")
|
||||||
|
|
||||||
header_layout.addWidget(self.checkbox)
|
header_layout.addWidget(self.checkbox)
|
||||||
header_layout.addWidget(id_label)
|
header_layout.addWidget(self.id_label)
|
||||||
header_layout.addStretch()
|
header_layout.addStretch()
|
||||||
|
|
||||||
info_layout.addWidget(header)
|
info_layout.addWidget(header)
|
||||||
@@ -148,15 +216,15 @@ class DronePanel(QWidget):
|
|||||||
status_layout.setContentsMargins(0, 0, 0, 0)
|
status_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
status_title = QLabel("狀態:")
|
status_title = QLabel("狀態:")
|
||||||
status_title.setStyleSheet("color: #888; min-width: 50px;")
|
_set_scaled_stylesheet(status_title, "color: #888; min-width: 50px;")
|
||||||
|
|
||||||
self.mode_label = QLabel("--")
|
self.mode_label = QLabel("--")
|
||||||
self.mode_label.setObjectName(f"{self.drone_id}_mode")
|
self.mode_label.setObjectName(f"{self.drone_id}_mode")
|
||||||
self.mode_label.setStyleSheet("color: #DDD;")
|
_set_scaled_stylesheet(self.mode_label, "color: #DDD;")
|
||||||
|
|
||||||
self.armed_label = QLabel("--")
|
self.armed_label = QLabel("--")
|
||||||
self.armed_label.setObjectName(f"{self.drone_id}_armed")
|
self.armed_label.setObjectName(f"{self.drone_id}_armed")
|
||||||
self.armed_label.setStyleSheet("color: #DDD;")
|
_set_scaled_stylesheet(self.armed_label, "color: #DDD;")
|
||||||
|
|
||||||
status_layout.addWidget(status_title)
|
status_layout.addWidget(status_title)
|
||||||
status_layout.addWidget(self.mode_label)
|
status_layout.addWidget(self.mode_label)
|
||||||
@@ -172,15 +240,15 @@ class DronePanel(QWidget):
|
|||||||
connection_layout.setContentsMargins(0, 0, 0, 0)
|
connection_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
connection_title = QLabel("Socket:")
|
connection_title = QLabel("Socket:")
|
||||||
connection_title.setStyleSheet("color: #888; min-width: 50px;")
|
_set_scaled_stylesheet(connection_title, "color: #888; min-width: 50px;")
|
||||||
|
|
||||||
# 根據解析的 drone_id 資訊設定初始值
|
# 根據解析的 drone_id 資訊設定初始值
|
||||||
self.socket_seq_label = QLabel(self.socket_seq)
|
self.socket_seq_label = QLabel(self.socket_seq)
|
||||||
self.socket_seq_label.setObjectName(f"{self.drone_id}_socket_seq")
|
self.socket_seq_label.setObjectName(f"{self.drone_id}_socket_seq")
|
||||||
self.socket_seq_label.setStyleSheet("color: #DDD;")
|
_set_scaled_stylesheet(self.socket_seq_label, "color: #DDD;")
|
||||||
|
|
||||||
connection_sep = QLabel(" - ")
|
connection_sep = QLabel(" - ")
|
||||||
connection_sep.setStyleSheet("color: #DDD;")
|
_set_scaled_stylesheet(connection_sep, "color: #DDD;")
|
||||||
|
|
||||||
# 設定連接方式顯示
|
# 設定連接方式顯示
|
||||||
connection_type_map = {
|
connection_type_map = {
|
||||||
@@ -193,7 +261,7 @@ class DronePanel(QWidget):
|
|||||||
|
|
||||||
self.connection_type_label = QLabel(connection_type)
|
self.connection_type_label = QLabel(connection_type)
|
||||||
self.connection_type_label.setObjectName(f"{self.drone_id}_connection_type")
|
self.connection_type_label.setObjectName(f"{self.drone_id}_connection_type")
|
||||||
self.connection_type_label.setStyleSheet("color: #DDD;")
|
_set_scaled_stylesheet(self.connection_type_label, "color: #DDD;")
|
||||||
|
|
||||||
connection_layout.addWidget(connection_title)
|
connection_layout.addWidget(connection_title)
|
||||||
connection_layout.addWidget(self.socket_seq_label)
|
connection_layout.addWidget(self.socket_seq_label)
|
||||||
@@ -210,29 +278,29 @@ class DronePanel(QWidget):
|
|||||||
battery_layout.setContentsMargins(0, 0, 0, 0)
|
battery_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
# 顯示百分比
|
# 顯示百分比
|
||||||
battery_title = QLabel("電池:")
|
battery_title = QLabel("電池:")
|
||||||
battery_title.setStyleSheet("color: #888; min-width: 50px;")
|
_set_scaled_stylesheet(battery_title, "color: #888; min-width: 50px;")
|
||||||
|
|
||||||
self.battery_pct_label = QLabel("--")
|
self.battery_pct_label = QLabel("--")
|
||||||
self.battery_pct_label.setObjectName(f"{self.drone_id}_battery_pct")
|
self.battery_pct_label.setObjectName(f"{self.drone_id}_battery_pct")
|
||||||
self.battery_pct_label.setStyleSheet("color: #DDD;")
|
_set_scaled_stylesheet(self.battery_pct_label, "color: #DDD;")
|
||||||
|
|
||||||
# 分隔符
|
# 分隔符
|
||||||
separator1 = QLabel(" - ")
|
separator1 = QLabel(" - ")
|
||||||
separator1.setStyleSheet("color: #DDD;")
|
_set_scaled_stylesheet(separator1, "color: #DDD;")
|
||||||
|
|
||||||
# 顯示電壓
|
# 顯示電壓
|
||||||
self.battery_vol_label = QLabel("--")
|
self.battery_vol_label = QLabel("--")
|
||||||
self.battery_vol_label.setObjectName(f"{self.drone_id}_battery_vol")
|
self.battery_vol_label.setObjectName(f"{self.drone_id}_battery_vol")
|
||||||
self.battery_vol_label.setStyleSheet("color: #DDD;")
|
_set_scaled_stylesheet(self.battery_vol_label, "color: #DDD;")
|
||||||
|
|
||||||
# 分隔符
|
# 分隔符
|
||||||
separator2 = QLabel(" - ")
|
separator2 = QLabel(" - ")
|
||||||
separator2.setStyleSheet("color: #DDD;")
|
_set_scaled_stylesheet(separator2, "color: #DDD;")
|
||||||
|
|
||||||
# 顯示電池節數 (S count)
|
# 顯示電池節數 (S count)
|
||||||
self.battery_cells_label = QLabel("--")
|
self.battery_cells_label = QLabel("--")
|
||||||
self.battery_cells_label.setObjectName(f"{self.drone_id}_battery_cells")
|
self.battery_cells_label.setObjectName(f"{self.drone_id}_battery_cells")
|
||||||
self.battery_cells_label.setStyleSheet("color: #DDD;")
|
_set_scaled_stylesheet(self.battery_cells_label, "color: #DDD;")
|
||||||
|
|
||||||
battery_layout.addWidget(battery_title)
|
battery_layout.addWidget(battery_title)
|
||||||
battery_layout.addWidget(self.battery_pct_label)
|
battery_layout.addWidget(self.battery_pct_label)
|
||||||
@@ -251,18 +319,18 @@ class DronePanel(QWidget):
|
|||||||
altitude_layout.setContentsMargins(0, 0, 0, 0)
|
altitude_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
altitude_title = QLabel("高度:")
|
altitude_title = QLabel("高度:")
|
||||||
altitude_title.setStyleSheet("color: #888; min-width: 50px;")
|
_set_scaled_stylesheet(altitude_title, "color: #888; min-width: 50px;")
|
||||||
|
|
||||||
self.altitude_label = QLabel("--")
|
self.altitude_label = QLabel("--")
|
||||||
self.altitude_label.setObjectName(f"{self.drone_id}_altitude")
|
self.altitude_label.setObjectName(f"{self.drone_id}_altitude")
|
||||||
self.altitude_label.setStyleSheet("color: #DDD;")
|
_set_scaled_stylesheet(self.altitude_label, "color: #DDD;")
|
||||||
|
|
||||||
speed_title = QLabel("速度:")
|
speed_title = QLabel("速度:")
|
||||||
speed_title.setStyleSheet("color: #888; margin-left: 10px;")
|
_set_scaled_stylesheet(speed_title, "color: #888; margin-left: 10px;")
|
||||||
|
|
||||||
self.speed_label = QLabel("--")
|
self.speed_label = QLabel("--")
|
||||||
self.speed_label.setObjectName(f"{self.drone_id}_speed")
|
self.speed_label.setObjectName(f"{self.drone_id}_speed")
|
||||||
self.speed_label.setStyleSheet("color: #DDD;")
|
_set_scaled_stylesheet(self.speed_label, "color: #DDD;")
|
||||||
|
|
||||||
altitude_layout.addWidget(altitude_title)
|
altitude_layout.addWidget(altitude_title)
|
||||||
altitude_layout.addWidget(self.altitude_label)
|
altitude_layout.addWidget(self.altitude_label)
|
||||||
@@ -320,6 +388,16 @@ class DronePanel(QWidget):
|
|||||||
def is_checked(self):
|
def is_checked(self):
|
||||||
"""獲取勾選狀態"""
|
"""獲取勾選狀態"""
|
||||||
return self.checkbox.isChecked()
|
return self.checkbox.isChecked()
|
||||||
|
|
||||||
|
def apply_font_scale(self):
|
||||||
|
"""重新套用目前字體倍率到 panel 內所有元件。"""
|
||||||
|
_apply_scaled_font(self)
|
||||||
|
_reapply_scaled_stylesheet(self)
|
||||||
|
for child in self.findChildren(QWidget):
|
||||||
|
_apply_scaled_font(child)
|
||||||
|
_reapply_scaled_stylesheet(child)
|
||||||
|
if self.attitude_indicator:
|
||||||
|
self.attitude_indicator.update()
|
||||||
|
|
||||||
class SocketGroupPanel(QWidget):
|
class SocketGroupPanel(QWidget):
|
||||||
# 定義信號
|
# 定義信號
|
||||||
@@ -331,11 +409,12 @@ class SocketGroupPanel(QWidget):
|
|||||||
self.color = color
|
self.color = color
|
||||||
self.socket_type = socket_type
|
self.socket_type = socket_type
|
||||||
self._init_ui()
|
self._init_ui()
|
||||||
|
self.apply_font_scale()
|
||||||
|
|
||||||
def _init_ui(self):
|
def _init_ui(self):
|
||||||
"""初始化UI"""
|
"""初始化UI"""
|
||||||
self.setObjectName(f"socket_group_{self.socket_id}")
|
self.setObjectName(f"socket_group_{self.socket_id}")
|
||||||
self.setStyleSheet("""
|
_set_scaled_stylesheet(self, """
|
||||||
background-color: #1E1E1E;
|
background-color: #1E1E1E;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
""")
|
""")
|
||||||
@@ -352,7 +431,7 @@ class SocketGroupPanel(QWidget):
|
|||||||
# 分組勾選框
|
# 分組勾選框
|
||||||
self.group_checkbox = QCheckBox()
|
self.group_checkbox = QCheckBox()
|
||||||
self.group_checkbox.setObjectName(f"socket_{self.socket_id}_checkbox")
|
self.group_checkbox.setObjectName(f"socket_{self.socket_id}_checkbox")
|
||||||
self.group_checkbox.setStyleSheet(f"""
|
_set_scaled_stylesheet(self.group_checkbox, f"""
|
||||||
QCheckBox {{ color: #DDD; }}
|
QCheckBox {{ color: #DDD; }}
|
||||||
QCheckBox::indicator {{
|
QCheckBox::indicator {{
|
||||||
width: 14px;
|
width: 14px;
|
||||||
@@ -380,7 +459,7 @@ class SocketGroupPanel(QWidget):
|
|||||||
else:
|
else:
|
||||||
title_text = f"Socket {self.socket_id}"
|
title_text = f"Socket {self.socket_id}"
|
||||||
self.title_label = QLabel(title_text)
|
self.title_label = QLabel(title_text)
|
||||||
self.title_label.setStyleSheet(f"""
|
_set_scaled_stylesheet(self.title_label, f"""
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: {self.color};
|
color: {self.color};
|
||||||
@@ -430,6 +509,14 @@ class SocketGroupPanel(QWidget):
|
|||||||
"""設置分組勾選狀態(支持半選)"""
|
"""設置分組勾選狀態(支持半選)"""
|
||||||
self.group_checkbox.setCheckState(state)
|
self.group_checkbox.setCheckState(state)
|
||||||
|
|
||||||
|
def apply_font_scale(self):
|
||||||
|
"""重新套用目前字體倍率到 socket 分組面板。"""
|
||||||
|
_apply_scaled_font(self)
|
||||||
|
_reapply_scaled_stylesheet(self)
|
||||||
|
for child in self.findChildren(QWidget):
|
||||||
|
_apply_scaled_font(child)
|
||||||
|
_reapply_scaled_stylesheet(child)
|
||||||
|
|
||||||
|
|
||||||
class AttitudeIndicator(QWidget):
|
class AttitudeIndicator(QWidget):
|
||||||
"""
|
"""
|
||||||
@@ -503,7 +590,9 @@ class AttitudeIndicator(QWidget):
|
|||||||
|
|
||||||
# pitch ladder (every 10°, ±30°)
|
# pitch ladder (every 10°, ±30°)
|
||||||
p.setPen(QPen(QColor(255, 255, 255, 180), 1))
|
p.setPen(QPen(QColor(255, 255, 255, 180), 1))
|
||||||
p.setFont(QFont("Arial", 6))
|
ladder_font = QFont("Arial")
|
||||||
|
ladder_font.setPointSizeF(max(1.0, 6 * _get_font_scale()))
|
||||||
|
p.setFont(ladder_font)
|
||||||
for deg in range(-30, 31, 10):
|
for deg in range(-30, 31, 10):
|
||||||
if deg == 0:
|
if deg == 0:
|
||||||
continue
|
continue
|
||||||
@@ -581,6 +670,8 @@ class AttitudeIndicator(QWidget):
|
|||||||
|
|
||||||
# heading text centred (bigger)
|
# heading text centred (bigger)
|
||||||
p.setPen(QPen(QColor("#FFFFFF")))
|
p.setPen(QPen(QColor("#FFFFFF")))
|
||||||
p.setFont(QFont("Arial", 10, QFont.Weight.Bold))
|
heading_font = QFont("Arial", weight=QFont.Weight.Bold)
|
||||||
|
heading_font.setPointSizeF(max(1.0, 10 * _get_font_scale()))
|
||||||
|
p.setFont(heading_font)
|
||||||
hdg_str = f"{int(self.heading)}°"
|
hdg_str = f"{int(self.heading)}°"
|
||||||
p.drawText(0, int(strip_y), w, strip_h, Qt.AlignmentFlag.AlignCenter, hdg_str)
|
p.drawText(0, int(strip_y), w, strip_h, Qt.AlignmentFlag.AlignCenter, hdg_str)
|
||||||
|
|||||||
+311
-12
@@ -4,15 +4,16 @@ from PyQt6.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout
|
|||||||
QWidget, QLabel, QSplitter, QScrollArea,
|
QWidget, QLabel, QSplitter, QScrollArea,
|
||||||
QSizePolicy, QTabWidget, QTableWidget, QTableWidgetItem,
|
QSizePolicy, QTabWidget, QTableWidget, QTableWidgetItem,
|
||||||
QHeaderView, QPushButton, QCheckBox, QLineEdit,
|
QHeaderView, QPushButton, QCheckBox, QLineEdit,
|
||||||
QComboBox, QDialog, QPlainTextEdit)
|
QComboBox, QDialog, QPlainTextEdit, QSlider)
|
||||||
from PyQt6.QtCore import Qt, QTimer, QObject, pyqtSignal
|
from PyQt6.QtCore import Qt, QTimer, QObject, pyqtSignal, QEvent
|
||||||
from PyQt6.QtGui import QColor
|
from PyQt6.QtGui import QColor, QFont
|
||||||
import sys
|
import sys
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
def _log(level, message):
|
def _log(level, message):
|
||||||
@@ -33,7 +34,8 @@ from mission_planner import FormationPlanner, MissionType
|
|||||||
from command_sender import MavlinkSender, Ros2CommandSender
|
from command_sender import MavlinkSender, Ros2CommandSender
|
||||||
from mission_executor import MissionExecutor, MissionState
|
from mission_executor import MissionExecutor, MissionState
|
||||||
from mission_group import (
|
from mission_group import (
|
||||||
MissionGroup, GroupPanel, DroneAssignDialog, GROUP_COLORS
|
MissionGroup, GroupPanel, DroneAssignDialog, GROUP_COLORS,
|
||||||
|
DEFAULT_MISSION_PARAM_VALUES
|
||||||
)
|
)
|
||||||
# ================================================================================
|
# ================================================================================
|
||||||
|
|
||||||
@@ -71,7 +73,10 @@ class StreamRedirector(QObject):
|
|||||||
self._buffer = ""
|
self._buffer = ""
|
||||||
|
|
||||||
class ControlStationUI(QMainWindow):
|
class ControlStationUI(QMainWindow):
|
||||||
VERSION = '2.1.0'
|
VERSION = '2.2.0'
|
||||||
|
FONT_SCALE_MIN = 70
|
||||||
|
FONT_SCALE_MAX = 180
|
||||||
|
FONT_SCALE_DEFAULT = 100
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -175,12 +180,22 @@ class ControlStationUI(QMainWindow):
|
|||||||
self._group_counter = 0 # 用來產生 group_id
|
self._group_counter = 0 # 用來產生 group_id
|
||||||
self._pending_box_assign = None # 框選後直接分配到的 group_id
|
self._pending_box_assign = None # 框選後直接分配到的 group_id
|
||||||
# ================================================================================
|
# ================================================================================
|
||||||
|
self.global_mission_defaults = dict(DEFAULT_MISSION_PARAM_VALUES)
|
||||||
|
self.font_scale = self.FONT_SCALE_DEFAULT / 100.0
|
||||||
|
self.pending_font_scale = self.font_scale
|
||||||
|
self._font_scale_applying = False
|
||||||
|
self._base_app_font = QFont(QApplication.instance().font())
|
||||||
|
|
||||||
self.init_ui()
|
self.init_ui()
|
||||||
self._setup_stream_redirector()
|
self._setup_stream_redirector()
|
||||||
|
app = QApplication.instance()
|
||||||
|
app.setProperty("font_scale", self.font_scale)
|
||||||
|
app.setProperty("base_app_font", self._base_app_font)
|
||||||
|
app.installEventFilter(self)
|
||||||
|
|
||||||
def init_ui(self):
|
def init_ui(self):
|
||||||
main_splitter = QSplitter(Qt.Orientation.Horizontal)
|
main_splitter = QSplitter(Qt.Orientation.Horizontal)
|
||||||
|
self.main_splitter = main_splitter
|
||||||
|
|
||||||
# 左側 TabWidget
|
# 左側 TabWidget
|
||||||
self.left_tab = QTabWidget()
|
self.left_tab = QTabWidget()
|
||||||
@@ -220,6 +235,10 @@ class ControlStationUI(QMainWindow):
|
|||||||
|
|
||||||
self.left_tab.addTab(self.comm_panel, "通訊")
|
self.left_tab.addTab(self.comm_panel, "通訊")
|
||||||
|
|
||||||
|
# — 分頁 5:全域設定
|
||||||
|
self.settings_tab = self._create_settings_tab()
|
||||||
|
self.left_tab.addTab(self.settings_tab, "設定")
|
||||||
|
|
||||||
# 右侧容器
|
# 右侧容器
|
||||||
right_container = QWidget()
|
right_container = QWidget()
|
||||||
right_layout = QVBoxLayout(right_container)
|
right_layout = QVBoxLayout(right_container)
|
||||||
@@ -227,6 +246,11 @@ class ControlStationUI(QMainWindow):
|
|||||||
right_layout.setSpacing(10)
|
right_layout.setSpacing(10)
|
||||||
|
|
||||||
# ========== 任務群組 Tab ==========
|
# ========== 任務群組 Tab ==========
|
||||||
|
self.group_container = QWidget()
|
||||||
|
group_container_layout = QVBoxLayout(self.group_container)
|
||||||
|
group_container_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
group_container_layout.setSpacing(6)
|
||||||
|
|
||||||
group_header = QHBoxLayout()
|
group_header = QHBoxLayout()
|
||||||
|
|
||||||
# 標題 + 收起/展開按鈕
|
# 標題 + 收起/展開按鈕
|
||||||
@@ -261,7 +285,7 @@ class ControlStationUI(QMainWindow):
|
|||||||
clear_traj_btn.clicked.connect(self.drone_map.clear_trajectories)
|
clear_traj_btn.clicked.connect(self.drone_map.clear_trajectories)
|
||||||
group_header.addWidget(clear_traj_btn)
|
group_header.addWidget(clear_traj_btn)
|
||||||
|
|
||||||
right_layout.addLayout(group_header)
|
group_container_layout.addLayout(group_header)
|
||||||
|
|
||||||
self.group_tab_widget = QTabWidget()
|
self.group_tab_widget = QTabWidget()
|
||||||
self.group_tab_widget.setStyleSheet("""
|
self.group_tab_widget.setStyleSheet("""
|
||||||
@@ -274,9 +298,9 @@ class ControlStationUI(QMainWindow):
|
|||||||
QTabBar::tab:selected { background-color: #2B2B2B; color: #FFF; border-bottom-color: #2B2B2B; }
|
QTabBar::tab:selected { background-color: #2B2B2B; color: #FFF; border-bottom-color: #2B2B2B; }
|
||||||
QTabBar::tab:hover { background-color: #3A3A3A; }
|
QTabBar::tab:hover { background-color: #3A3A3A; }
|
||||||
""")
|
""")
|
||||||
self.group_tab_widget.setFixedHeight(150)
|
self.group_tab_widget.setMinimumHeight(80)
|
||||||
self.group_tab_widget.currentChanged.connect(self._on_group_tab_changed)
|
self.group_tab_widget.currentChanged.connect(self._on_group_tab_changed)
|
||||||
right_layout.addWidget(self.group_tab_widget)
|
group_container_layout.addWidget(self.group_tab_widget)
|
||||||
|
|
||||||
# 🌟 新增:保存群組面板的展開狀態
|
# 🌟 新增:保存群組面板的展開狀態
|
||||||
self.group_panel_expanded = True
|
self.group_panel_expanded = True
|
||||||
@@ -284,8 +308,17 @@ class ControlStationUI(QMainWindow):
|
|||||||
# 預設建立 Group A
|
# 預設建立 Group A
|
||||||
self._add_mission_group()
|
self._add_mission_group()
|
||||||
|
|
||||||
|
# 任務群組與地圖之間使用垂直 splitter,可上下拖曳調整高度
|
||||||
|
self.right_vertical_splitter = QSplitter(Qt.Orientation.Vertical)
|
||||||
|
self.right_vertical_splitter.addWidget(self.group_container)
|
||||||
|
self.right_vertical_splitter.addWidget(self.drone_map.get_widget())
|
||||||
|
self.right_vertical_splitter.setChildrenCollapsible(False)
|
||||||
|
self.right_vertical_splitter.setStretchFactor(0, 0)
|
||||||
|
self.right_vertical_splitter.setStretchFactor(1, 1)
|
||||||
|
self.right_vertical_splitter.setSizes([170, 700])
|
||||||
|
right_layout.addWidget(self.right_vertical_splitter)
|
||||||
|
|
||||||
# 添加地圖
|
# 添加地圖
|
||||||
right_layout.addWidget(self.drone_map.get_widget())
|
|
||||||
self.drone_map.get_gps_signal().connect(self.handle_map_click)
|
self.drone_map.get_gps_signal().connect(self.handle_map_click)
|
||||||
self.drone_map.get_drone_clicked_signal().connect(self.handle_drone_clicked)
|
self.drone_map.get_drone_clicked_signal().connect(self.handle_drone_clicked)
|
||||||
self.drone_map.get_clear_all_drone_selection_signal().connect(self.handle_clear_all_drone_selection)
|
self.drone_map.get_clear_all_drone_selection_signal().connect(self.handle_clear_all_drone_selection)
|
||||||
@@ -347,6 +380,259 @@ class ControlStationUI(QMainWindow):
|
|||||||
|
|
||||||
return widget
|
return widget
|
||||||
|
|
||||||
|
def _create_settings_tab(self):
|
||||||
|
"""建立字體設定分頁。"""
|
||||||
|
widget = QWidget()
|
||||||
|
layout = QVBoxLayout(widget)
|
||||||
|
layout.setContentsMargins(10, 10, 10, 10)
|
||||||
|
layout.setSpacing(8)
|
||||||
|
|
||||||
|
scale_panel = QWidget()
|
||||||
|
scale_panel.setStyleSheet("""
|
||||||
|
QWidget {
|
||||||
|
background-color: #2A2A2A;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
scale_layout = QVBoxLayout(scale_panel)
|
||||||
|
scale_layout.setContentsMargins(12, 12, 12, 12)
|
||||||
|
scale_layout.setSpacing(8)
|
||||||
|
|
||||||
|
self.font_scale_label = QLabel()
|
||||||
|
self.font_scale_label.setStyleSheet("color: #DDD; font-size: 13px; font-weight: bold;")
|
||||||
|
scale_layout.addWidget(self.font_scale_label)
|
||||||
|
|
||||||
|
self.font_scale_slider = QSlider(Qt.Orientation.Horizontal)
|
||||||
|
self.font_scale_slider.setRange(self.FONT_SCALE_MIN, self.FONT_SCALE_MAX)
|
||||||
|
self.font_scale_slider.setValue(self.FONT_SCALE_DEFAULT)
|
||||||
|
self.font_scale_slider.setTickInterval(10)
|
||||||
|
self.font_scale_slider.setTickPosition(QSlider.TickPosition.TicksBelow)
|
||||||
|
self.font_scale_slider.setStyleSheet("""
|
||||||
|
QSlider::groove:horizontal {
|
||||||
|
border: 1px solid #444;
|
||||||
|
height: 8px;
|
||||||
|
background: #333;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
QSlider::handle:horizontal {
|
||||||
|
background: #4A9EFF;
|
||||||
|
border: 1px solid #3A8EEF;
|
||||||
|
width: 18px;
|
||||||
|
margin: -6px 0;
|
||||||
|
border-radius: 9px;
|
||||||
|
}
|
||||||
|
QSlider::sub-page:horizontal {
|
||||||
|
background: #4A9EFF;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
QSlider::add-page:horizontal {
|
||||||
|
background: #2A2A2A;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
self.font_scale_slider.valueChanged.connect(self._handle_font_scale_slider_changed)
|
||||||
|
scale_layout.addWidget(self.font_scale_slider)
|
||||||
|
|
||||||
|
layout.addWidget(scale_panel)
|
||||||
|
layout.addStretch()
|
||||||
|
|
||||||
|
settings_button_row = QHBoxLayout()
|
||||||
|
settings_button_row.addStretch()
|
||||||
|
apply_btn = QPushButton("套用")
|
||||||
|
apply_btn.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
background-color: #4A9EFF;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
QPushButton:hover { background-color: #3A8EEF; }
|
||||||
|
""")
|
||||||
|
apply_btn.clicked.connect(self._apply_pending_font_scale)
|
||||||
|
settings_button_row.addWidget(apply_btn)
|
||||||
|
|
||||||
|
layout.addLayout(settings_button_row)
|
||||||
|
self._update_font_scale_label()
|
||||||
|
return widget
|
||||||
|
|
||||||
|
def _update_font_scale_label(self):
|
||||||
|
percent = int(round(self.pending_font_scale * 100))
|
||||||
|
if hasattr(self, 'font_scale_label'):
|
||||||
|
self.font_scale_label.setText(f"字體倍率:{percent}%")
|
||||||
|
|
||||||
|
def _handle_font_scale_slider_changed(self, value):
|
||||||
|
self.pending_font_scale = value / 100.0
|
||||||
|
self._update_font_scale_label()
|
||||||
|
|
||||||
|
def _apply_pending_font_scale(self):
|
||||||
|
self._apply_font_scale(self.pending_font_scale)
|
||||||
|
|
||||||
|
def _scale_stylesheet_font_sizes(self, stylesheet, scale):
|
||||||
|
if not stylesheet or 'font-size' not in stylesheet:
|
||||||
|
return stylesheet
|
||||||
|
|
||||||
|
def repl(match):
|
||||||
|
size = float(match.group(1))
|
||||||
|
unit = match.group(2)
|
||||||
|
scaled = max(1.0, size * scale)
|
||||||
|
if unit == 'px':
|
||||||
|
text = f"{scaled:.2f}".rstrip('0').rstrip('.')
|
||||||
|
else:
|
||||||
|
text = f"{scaled:.3f}".rstrip('0').rstrip('.')
|
||||||
|
return f"font-size: {text}{unit}"
|
||||||
|
|
||||||
|
return re.sub(r'font-size\s*:\s*([0-9]+(?:\.[0-9]+)?)\s*(px|pt)', repl, stylesheet)
|
||||||
|
|
||||||
|
def _apply_font_scale_to_widget(self, widget):
|
||||||
|
if hasattr(widget, 'apply_font_scale'):
|
||||||
|
widget.apply_font_scale()
|
||||||
|
return
|
||||||
|
|
||||||
|
if hasattr(widget, '_base_stylesheet') and hasattr(widget, '_applied_stylesheet'):
|
||||||
|
current_stylesheet = widget.styleSheet()
|
||||||
|
base_stylesheet = widget._base_stylesheet
|
||||||
|
if current_stylesheet != widget._applied_stylesheet:
|
||||||
|
base_stylesheet = current_stylesheet
|
||||||
|
widget._base_stylesheet = base_stylesheet
|
||||||
|
|
||||||
|
scaled_stylesheet = self._scale_stylesheet_font_sizes(base_stylesheet, self.font_scale)
|
||||||
|
widget._applied_stylesheet = scaled_stylesheet
|
||||||
|
if current_stylesheet != scaled_stylesheet:
|
||||||
|
widget.setStyleSheet(scaled_stylesheet)
|
||||||
|
return
|
||||||
|
|
||||||
|
current_stylesheet = widget.styleSheet()
|
||||||
|
source_stylesheet = getattr(widget, '_font_scale_source_stylesheet', current_stylesheet)
|
||||||
|
applied_stylesheet = getattr(widget, '_font_scale_applied_stylesheet', None)
|
||||||
|
if current_stylesheet != applied_stylesheet:
|
||||||
|
source_stylesheet = current_stylesheet
|
||||||
|
|
||||||
|
scaled_stylesheet = self._scale_stylesheet_font_sizes(source_stylesheet, self.font_scale)
|
||||||
|
widget._font_scale_source_stylesheet = source_stylesheet
|
||||||
|
widget._font_scale_applied_stylesheet = scaled_stylesheet
|
||||||
|
|
||||||
|
if current_stylesheet != scaled_stylesheet:
|
||||||
|
widget.setStyleSheet(scaled_stylesheet)
|
||||||
|
|
||||||
|
def _apply_font_scale_to_widget_tree(self, root):
|
||||||
|
if root is None:
|
||||||
|
return
|
||||||
|
if isinstance(root, QWidget):
|
||||||
|
self._apply_font_scale_to_widget(root)
|
||||||
|
for child in root.findChildren(QWidget):
|
||||||
|
self._apply_font_scale_to_widget(child)
|
||||||
|
|
||||||
|
def _apply_font_scale(self, scale):
|
||||||
|
app = QApplication.instance()
|
||||||
|
splitter_sizes = None
|
||||||
|
left_width = None
|
||||||
|
old_left_min = None
|
||||||
|
old_left_max = None
|
||||||
|
old_window_size = self.size()
|
||||||
|
old_window_min = self.minimumSize()
|
||||||
|
old_window_max = self.maximumSize()
|
||||||
|
lock_window_size = not self.isMaximized() and not self.isFullScreen()
|
||||||
|
if hasattr(self, 'main_splitter') and self.main_splitter:
|
||||||
|
splitter_sizes = self.main_splitter.sizes()
|
||||||
|
if hasattr(self, 'left_tab') and self.left_tab:
|
||||||
|
left_width = self.left_tab.width()
|
||||||
|
old_left_min = self.left_tab.minimumWidth()
|
||||||
|
old_left_max = self.left_tab.maximumWidth()
|
||||||
|
if left_width > 0:
|
||||||
|
self.left_tab.setMinimumWidth(left_width)
|
||||||
|
self.left_tab.setMaximumWidth(left_width)
|
||||||
|
|
||||||
|
self._font_scale_applying = True
|
||||||
|
try:
|
||||||
|
self.font_scale = scale
|
||||||
|
app.setProperty("font_scale", scale)
|
||||||
|
|
||||||
|
if hasattr(self, '_base_app_font'):
|
||||||
|
scaled_font = QFont(self._base_app_font)
|
||||||
|
if self._base_app_font.pointSizeF() > 0:
|
||||||
|
scaled_font.setPointSizeF(max(1.0, self._base_app_font.pointSizeF() * scale))
|
||||||
|
elif self._base_app_font.pixelSize() > 0:
|
||||||
|
scaled_font.setPixelSize(max(1, int(round(self._base_app_font.pixelSize() * scale))))
|
||||||
|
app.setFont(scaled_font)
|
||||||
|
|
||||||
|
if lock_window_size:
|
||||||
|
self.setMinimumSize(old_window_size)
|
||||||
|
self.setMaximumSize(old_window_size)
|
||||||
|
self._update_font_scale_label()
|
||||||
|
self._apply_font_scale_to_widget_tree(self)
|
||||||
|
|
||||||
|
if hasattr(self, 'comm_panel') and self.comm_panel:
|
||||||
|
self.comm_panel.apply_font_scale()
|
||||||
|
|
||||||
|
for panel in getattr(self, 'drones', {}).values():
|
||||||
|
if hasattr(panel, 'apply_font_scale'):
|
||||||
|
panel.apply_font_scale()
|
||||||
|
|
||||||
|
for panel in getattr(self, 'socket_groups', {}).values():
|
||||||
|
if hasattr(panel, 'apply_font_scale'):
|
||||||
|
panel.apply_font_scale()
|
||||||
|
|
||||||
|
for panel in getattr(self, 'group_panels', {}).values():
|
||||||
|
if hasattr(panel, 'apply_font_scale'):
|
||||||
|
panel.apply_font_scale()
|
||||||
|
|
||||||
|
if hasattr(self, 'drone_map') and self.drone_map:
|
||||||
|
self.drone_map.set_font_scale(scale)
|
||||||
|
|
||||||
|
if splitter_sizes:
|
||||||
|
self.main_splitter.setSizes(splitter_sizes)
|
||||||
|
|
||||||
|
if lock_window_size:
|
||||||
|
self.resize(old_window_size)
|
||||||
|
self.left_tab.updateGeometry()
|
||||||
|
self.update()
|
||||||
|
finally:
|
||||||
|
self._font_scale_applying = False
|
||||||
|
|
||||||
|
def _restore_locked_sizes():
|
||||||
|
if lock_window_size:
|
||||||
|
self.setMinimumSize(old_window_min)
|
||||||
|
self.setMaximumSize(old_window_max)
|
||||||
|
self.resize(old_window_size)
|
||||||
|
|
||||||
|
if self.left_tab and left_width and left_width > 0:
|
||||||
|
self.left_tab.setMinimumWidth(left_width)
|
||||||
|
self.left_tab.setMaximumWidth(left_width)
|
||||||
|
|
||||||
|
if splitter_sizes and self.main_splitter:
|
||||||
|
self.main_splitter.setSizes(splitter_sizes)
|
||||||
|
|
||||||
|
def _release_size_locks():
|
||||||
|
if self.left_tab and old_left_min is not None and old_left_max is not None:
|
||||||
|
self.left_tab.setMinimumWidth(old_left_min)
|
||||||
|
self.left_tab.setMaximumWidth(old_left_max)
|
||||||
|
|
||||||
|
QTimer.singleShot(0, _restore_locked_sizes)
|
||||||
|
QTimer.singleShot(30, _restore_locked_sizes)
|
||||||
|
QTimer.singleShot(100, _restore_locked_sizes)
|
||||||
|
QTimer.singleShot(150, _release_size_locks)
|
||||||
|
|
||||||
|
def eventFilter(self, obj, event):
|
||||||
|
if self._font_scale_applying:
|
||||||
|
return super().eventFilter(obj, event)
|
||||||
|
|
||||||
|
if isinstance(obj, QWidget) and event.type() in {
|
||||||
|
QEvent.Type.StyleChange,
|
||||||
|
QEvent.Type.Polish,
|
||||||
|
QEvent.Type.Show,
|
||||||
|
}:
|
||||||
|
self._font_scale_applying = True
|
||||||
|
try:
|
||||||
|
self._apply_font_scale_to_widget(obj)
|
||||||
|
finally:
|
||||||
|
self._font_scale_applying = False
|
||||||
|
|
||||||
|
return super().eventFilter(obj, event)
|
||||||
|
|
||||||
def _clear_message_history(self):
|
def _clear_message_history(self):
|
||||||
"""清空訊息歷史。"""
|
"""清空訊息歷史。"""
|
||||||
self.message_history.clear()
|
self.message_history.clear()
|
||||||
@@ -668,15 +954,28 @@ class ControlStationUI(QMainWindow):
|
|||||||
"""🌟 收起/展開任務群組面板"""
|
"""🌟 收起/展開任務群組面板"""
|
||||||
if self.group_panel_expanded:
|
if self.group_panel_expanded:
|
||||||
# 收起
|
# 收起
|
||||||
self.group_tab_widget.setFixedHeight(0)
|
if hasattr(self, 'right_vertical_splitter'):
|
||||||
|
sizes = self.right_vertical_splitter.sizes()
|
||||||
|
if sizes and sizes[0] > 0:
|
||||||
|
self._last_group_panel_height = sizes[0]
|
||||||
self.group_tab_widget.hide()
|
self.group_tab_widget.hide()
|
||||||
|
header_height = self.group_container.sizeHint().height()
|
||||||
|
self.group_container.setMaximumHeight(header_height)
|
||||||
|
if hasattr(self, 'right_vertical_splitter'):
|
||||||
|
total = sum(self.right_vertical_splitter.sizes()) or self.height()
|
||||||
|
self.right_vertical_splitter.setSizes([header_height, max(1, total - header_height)])
|
||||||
self.toggle_group_btn.setText("▶")
|
self.toggle_group_btn.setText("▶")
|
||||||
self.toggle_group_btn.setToolTip("展開任務群組")
|
self.toggle_group_btn.setToolTip("展開任務群組")
|
||||||
self.group_panel_expanded = False
|
self.group_panel_expanded = False
|
||||||
else:
|
else:
|
||||||
# 展開
|
# 展開
|
||||||
self.group_tab_widget.setFixedHeight(150)
|
self.group_container.setMaximumHeight(16777215)
|
||||||
self.group_tab_widget.show()
|
self.group_tab_widget.show()
|
||||||
|
if hasattr(self, 'right_vertical_splitter'):
|
||||||
|
total = sum(self.right_vertical_splitter.sizes()) or self.height()
|
||||||
|
group_height = getattr(self, '_last_group_panel_height', 170)
|
||||||
|
group_height = min(max(group_height, 120), max(120, total - 120))
|
||||||
|
self.right_vertical_splitter.setSizes([group_height, max(1, total - group_height)])
|
||||||
self.toggle_group_btn.setText("▼")
|
self.toggle_group_btn.setText("▼")
|
||||||
self.toggle_group_btn.setToolTip("收起任務群組")
|
self.toggle_group_btn.setToolTip("收起任務群組")
|
||||||
self.group_panel_expanded = True
|
self.group_panel_expanded = True
|
||||||
@@ -688,7 +987,7 @@ class ControlStationUI(QMainWindow):
|
|||||||
group = MissionGroup(gid, color)
|
group = MissionGroup(gid, color)
|
||||||
self.mission_groups[gid] = group
|
self.mission_groups[gid] = group
|
||||||
|
|
||||||
panel = GroupPanel(group)
|
panel = GroupPanel(group, default_params=self.global_mission_defaults)
|
||||||
panel.assign_drones_requested.connect(self._handle_assign_drones)
|
panel.assign_drones_requested.connect(self._handle_assign_drones)
|
||||||
panel.mission_type_changed.connect(self._handle_mission_type_changed)
|
panel.mission_type_changed.connect(self._handle_mission_type_changed)
|
||||||
panel.start_requested.connect(self._handle_group_start)
|
panel.start_requested.connect(self._handle_group_start)
|
||||||
|
|||||||
+23
-10
@@ -16,6 +16,7 @@ class DroneMap:
|
|||||||
self.map_view = QWebEngineView()
|
self.map_view = QWebEngineView()
|
||||||
self.map_loaded = False
|
self.map_loaded = False
|
||||||
self.pending_map_updates = {}
|
self.pending_map_updates = {}
|
||||||
|
self.font_scale = 1.0
|
||||||
|
|
||||||
# 創建橋接對象
|
# 創建橋接對象
|
||||||
self.bridge = MapBridge()
|
self.bridge = MapBridge()
|
||||||
@@ -36,6 +37,7 @@ class DroneMap:
|
|||||||
<script src="https://unpkg.com/leaflet-rotatedmarker/leaflet.rotatedMarker.js"></script>
|
<script src="https://unpkg.com/leaflet-rotatedmarker/leaflet.rotatedMarker.js"></script>
|
||||||
<script src="qrc:///qtwebchannel/qwebchannel.js"></script>
|
<script src="qrc:///qtwebchannel/qwebchannel.js"></script>
|
||||||
<style>
|
<style>
|
||||||
|
:root { --ui-font-scale: 1; }
|
||||||
html, body, #map { height: 100%; margin: 0; }
|
html, body, #map { height: 100%; margin: 0; }
|
||||||
#map {
|
#map {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@@ -70,7 +72,7 @@ class DroneMap:
|
|||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 13px;
|
font-size: calc(13px * var(--ui-font-scale));
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||||
}
|
}
|
||||||
@@ -90,7 +92,7 @@ class DroneMap:
|
|||||||
}
|
}
|
||||||
.mission-info-row {
|
.mission-info-row {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
font-size: 12px;
|
font-size: calc(12px * var(--ui-font-scale));
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
.mission-info-label {
|
.mission-info-label {
|
||||||
@@ -109,7 +111,7 @@ class DroneMap:
|
|||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 13px;
|
font-size: calc(13px * var(--ui-font-scale));
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
@@ -136,7 +138,7 @@ class DroneMap:
|
|||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 13px;
|
font-size: calc(13px * var(--ui-font-scale));
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
transition: background-color 0.2s;
|
transition: background-color 0.2s;
|
||||||
}
|
}
|
||||||
@@ -154,7 +156,7 @@ class DroneMap:
|
|||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 13px;
|
font-size: calc(13px * var(--ui-font-scale));
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
transition: background-color 0.2s;
|
transition: background-color 0.2s;
|
||||||
}
|
}
|
||||||
@@ -172,7 +174,7 @@ class DroneMap:
|
|||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 13px;
|
font-size: calc(13px * var(--ui-font-scale));
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
transition: background-color 0.2s;
|
transition: background-color 0.2s;
|
||||||
}
|
}
|
||||||
@@ -327,7 +329,7 @@ class DroneMap:
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 12px;
|
font-size: calc(12px * var(--ui-font-scale));
|
||||||
text-shadow: 1px 1px 2px rgba(255,255,255,0.8), -1px -1px 2px rgba(255,255,255,0.8);
|
text-shadow: 1px 1px 2px rgba(255,255,255,0.8), -1px -1px 2px rgba(255,255,255,0.8);
|
||||||
">${sysid}</div>`,
|
">${sysid}</div>`,
|
||||||
iconSize: [16, 16],
|
iconSize: [16, 16],
|
||||||
@@ -569,7 +571,7 @@ class DroneMap:
|
|||||||
'align-items: center;' +
|
'align-items: center;' +
|
||||||
'justify-content: center;' +
|
'justify-content: center;' +
|
||||||
'font-weight: bold;' +
|
'font-weight: bold;' +
|
||||||
'font-size: 11px;' +
|
'font-size: calc(11px * var(--ui-font-scale));' +
|
||||||
'border: 2px solid white;' +
|
'border: 2px solid white;' +
|
||||||
'box-shadow: 0 2px 5px rgba(0,0,0,0.3);' +
|
'box-shadow: 0 2px 5px rgba(0,0,0,0.3);' +
|
||||||
'">' + idx + '</div>',
|
'">' + idx + '</div>',
|
||||||
@@ -622,6 +624,10 @@ class DroneMap:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// ================================================================================
|
// ================================================================================
|
||||||
|
|
||||||
|
function setFontScale(scale) {
|
||||||
|
document.documentElement.style.setProperty('--ui-font-scale', scale);
|
||||||
|
}
|
||||||
|
|
||||||
// 開始任務
|
// 開始任務
|
||||||
function startMission() {
|
function startMission() {
|
||||||
@@ -807,7 +813,7 @@ class DroneMap:
|
|||||||
'width: 22px; height: 22px;' +
|
'width: 22px; height: 22px;' +
|
||||||
'border-radius: 50%;' +
|
'border-radius: 50%;' +
|
||||||
'display: flex; align-items: center; justify-content: center;' +
|
'display: flex; align-items: center; justify-content: center;' +
|
||||||
'font-weight: bold; font-size: 10px;' +
|
'font-weight: bold; font-size: calc(10px * var(--ui-font-scale));' +
|
||||||
'border: 2px solid white;' +
|
'border: 2px solid white;' +
|
||||||
'box-shadow: 0 2px 5px rgba(0,0,0,0.3);' +
|
'box-shadow: 0 2px 5px rgba(0,0,0,0.3);' +
|
||||||
'">' + groupId + '</div>',
|
'">' + groupId + '</div>',
|
||||||
@@ -827,7 +833,7 @@ class DroneMap:
|
|||||||
'width: 22px; height: 22px;' +
|
'width: 22px; height: 22px;' +
|
||||||
'border-radius: 50%;' +
|
'border-radius: 50%;' +
|
||||||
'display: flex; align-items: center; justify-content: center;' +
|
'display: flex; align-items: center; justify-content: center;' +
|
||||||
'font-weight: bold; font-size: 14px;' +
|
'font-weight: bold; font-size: calc(14px * var(--ui-font-scale));' +
|
||||||
'border: 2px solid white;' +
|
'border: 2px solid white;' +
|
||||||
'box-shadow: 0 2px 5px rgba(0,0,0,0.3);' +
|
'box-shadow: 0 2px 5px rgba(0,0,0,0.3);' +
|
||||||
'">★</div>',
|
'">★</div>',
|
||||||
@@ -878,6 +884,7 @@ class DroneMap:
|
|||||||
"""地圖加載完成回調"""
|
"""地圖加載完成回調"""
|
||||||
if ok:
|
if ok:
|
||||||
self.map_loaded = True
|
self.map_loaded = True
|
||||||
|
self.set_font_scale(self.font_scale)
|
||||||
else:
|
else:
|
||||||
_log("ERROR", "地圖載入失敗")
|
_log("ERROR", "地圖載入失敗")
|
||||||
|
|
||||||
@@ -909,6 +916,12 @@ class DroneMap:
|
|||||||
"""聚焦到指定無人機"""
|
"""聚焦到指定無人機"""
|
||||||
if self.map_loaded:
|
if self.map_loaded:
|
||||||
self.map_view.page().runJavaScript(f"focusOn('{drone_id}');")
|
self.map_view.page().runJavaScript(f"focusOn('{drone_id}');")
|
||||||
|
|
||||||
|
def set_font_scale(self, scale):
|
||||||
|
"""設定地圖 HTML 控制項的字體倍率。"""
|
||||||
|
self.font_scale = scale
|
||||||
|
if self.map_loaded:
|
||||||
|
self.map_view.page().runJavaScript(f"setFontScale({scale:.3f});")
|
||||||
|
|
||||||
# ================================================================================
|
# ================================================================================
|
||||||
# 任務規劃視覺化方法
|
# 任務規劃視覺化方法
|
||||||
|
|||||||
+34
-13
@@ -23,6 +23,18 @@ GROUP_COLORS = [
|
|||||||
'#FF6B9D', # 粉
|
'#FF6B9D', # 粉
|
||||||
]
|
]
|
||||||
|
|
||||||
|
DEFAULT_MISSION_PARAM_VALUES = {
|
||||||
|
'spacing': '5.0',
|
||||||
|
'base_altitude': '10.0',
|
||||||
|
'altitude_diff': '2.0',
|
||||||
|
'radius': '10.0',
|
||||||
|
'altitude': '10.0',
|
||||||
|
'start_angle': '0',
|
||||||
|
'lateral_offset': '3.0',
|
||||||
|
'longitudinal_spacing': '5.0',
|
||||||
|
'line_spacing': '5.0',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class MissionGroup:
|
class MissionGroup:
|
||||||
"""單一任務群組的資料"""
|
"""單一任務群組的資料"""
|
||||||
@@ -157,10 +169,13 @@ class GroupPanel(QWidget):
|
|||||||
QPushButton:disabled {{ background-color: #444; color: #666; }}
|
QPushButton:disabled {{ background-color: #444; color: #666; }}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, group: MissionGroup, parent=None):
|
def __init__(self, group: MissionGroup, parent=None, default_params=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.group = group
|
self.group = group
|
||||||
self.all_btn_ref = None # 保存全選按鈕的參考
|
self.all_btn_ref = None # 保存全選按鈕的參考
|
||||||
|
self.default_params = dict(DEFAULT_MISSION_PARAM_VALUES)
|
||||||
|
if default_params:
|
||||||
|
self.default_params.update(default_params)
|
||||||
self._build_ui()
|
self._build_ui()
|
||||||
|
|
||||||
def _make_sep(self):
|
def _make_sep(self):
|
||||||
@@ -372,23 +387,23 @@ class GroupPanel(QWidget):
|
|||||||
# 每種任務類型的參數定義: (key, label, default_value)
|
# 每種任務類型的參數定義: (key, label, default_value)
|
||||||
self._param_defs = {
|
self._param_defs = {
|
||||||
'M_FORMATION': [
|
'M_FORMATION': [
|
||||||
('spacing', '間距 (m)', '5.0'),
|
('spacing', '間距 (m)', self.default_params['spacing']),
|
||||||
('base_altitude', '基準高度 (m)', '10.0'),
|
('base_altitude', '基準高度 (m)', self.default_params['base_altitude']),
|
||||||
('altitude_diff', '高低差 (m)', '2.0'),
|
('altitude_diff', '高低差 (m)', self.default_params['altitude_diff']),
|
||||||
],
|
],
|
||||||
'CIRCLE_FORMATION': [
|
'CIRCLE_FORMATION': [
|
||||||
('radius', '半徑 (m)', '10.0'),
|
('radius', '半徑 (m)', self.default_params['radius']),
|
||||||
('altitude', '高度 (m)', '10.0'),
|
('altitude', '高度 (m)', self.default_params['altitude']),
|
||||||
('start_angle', '起始角 (°)', '0'),
|
('start_angle', '起始角 (°)', self.default_params['start_angle']),
|
||||||
],
|
],
|
||||||
'LEADER_FOLLOWER': [
|
'LEADER_FOLLOWER': [
|
||||||
('lateral_offset', '橫向偏移 (m)', '3.0'),
|
('lateral_offset', '橫向偏移 (m)', self.default_params['lateral_offset']),
|
||||||
('longitudinal_spacing', '縱向間距 (m)', '5.0'),
|
('longitudinal_spacing', '縱向間距 (m)', self.default_params['longitudinal_spacing']),
|
||||||
('altitude', '高度 (m)', '10.0'),
|
('altitude', '高度 (m)', self.default_params['altitude']),
|
||||||
],
|
],
|
||||||
'GRID_SWEEP': [
|
'GRID_SWEEP': [
|
||||||
('line_spacing', '掃描線距 (m)', '5.0'),
|
('line_spacing', '掃描線距 (m)', self.default_params['line_spacing']),
|
||||||
('altitude', '高度 (m)', '10.0'),
|
('altitude', '高度 (m)', self.default_params['altitude']),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -532,6 +547,13 @@ class GroupPanel(QWidget):
|
|||||||
params[key] = float(default)
|
params[key] = float(default)
|
||||||
return params
|
return params
|
||||||
|
|
||||||
|
def set_param_value(self, key, value):
|
||||||
|
"""更新指定參數欄位的文字值。"""
|
||||||
|
if key not in self._param_widgets:
|
||||||
|
return
|
||||||
|
_row_w, inp = self._param_widgets[key]
|
||||||
|
inp.setText(str(value))
|
||||||
|
|
||||||
def update_mission_info(self, center_lat, center_lon, target_lat, target_lon):
|
def update_mission_info(self, center_lat, center_lon, target_lat, target_lon):
|
||||||
"""更新中心點 / 目標點顯示"""
|
"""更新中心點 / 目標點顯示"""
|
||||||
info_style = f"color: {self.group.color}; font-size: 11px; font-weight: bold;"
|
info_style = f"color: {self.group.color}; font-size: 11px; font-weight: bold;"
|
||||||
@@ -553,4 +575,3 @@ class GroupPanel(QWidget):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
alt = 10.0
|
alt = 10.0
|
||||||
self.takeoff_requested.emit(self.group.group_id, alt)
|
self.takeoff_requested.emit(self.group.group_id, alt)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user