|
|
|
|
@ -4,15 +4,16 @@ from PyQt6.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout
|
|
|
|
|
QWidget, QLabel, QSplitter, QScrollArea,
|
|
|
|
|
QSizePolicy, QTabWidget, QTableWidget, QTableWidgetItem,
|
|
|
|
|
QHeaderView, QPushButton, QCheckBox, QLineEdit,
|
|
|
|
|
QComboBox, QDialog, QPlainTextEdit)
|
|
|
|
|
from PyQt6.QtCore import Qt, QTimer, QObject, pyqtSignal
|
|
|
|
|
from PyQt6.QtGui import QColor
|
|
|
|
|
QComboBox, QDialog, QPlainTextEdit, QSlider)
|
|
|
|
|
from PyQt6.QtCore import Qt, QTimer, QObject, pyqtSignal, QEvent
|
|
|
|
|
from PyQt6.QtGui import QColor, QFont
|
|
|
|
|
import sys
|
|
|
|
|
import asyncio
|
|
|
|
|
import json
|
|
|
|
|
import subprocess
|
|
|
|
|
import time
|
|
|
|
|
import traceback
|
|
|
|
|
import re
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _log(level, message):
|
|
|
|
|
@ -33,7 +34,8 @@ from mission_planner import FormationPlanner, MissionType
|
|
|
|
|
from command_sender import MavlinkSender, Ros2CommandSender
|
|
|
|
|
from mission_executor import MissionExecutor, MissionState
|
|
|
|
|
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 = ""
|
|
|
|
|
|
|
|
|
|
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):
|
|
|
|
|
super().__init__()
|
|
|
|
|
@ -175,12 +180,22 @@ class ControlStationUI(QMainWindow):
|
|
|
|
|
self._group_counter = 0 # 用來產生 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._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):
|
|
|
|
|
main_splitter = QSplitter(Qt.Orientation.Horizontal)
|
|
|
|
|
self.main_splitter = main_splitter
|
|
|
|
|
|
|
|
|
|
# 左側 TabWidget
|
|
|
|
|
self.left_tab = QTabWidget()
|
|
|
|
|
@ -220,6 +235,10 @@ class ControlStationUI(QMainWindow):
|
|
|
|
|
|
|
|
|
|
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_layout = QVBoxLayout(right_container)
|
|
|
|
|
@ -227,6 +246,11 @@ class ControlStationUI(QMainWindow):
|
|
|
|
|
right_layout.setSpacing(10)
|
|
|
|
|
|
|
|
|
|
# ========== 任務群組 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()
|
|
|
|
|
|
|
|
|
|
# 標題 + 收起/展開按鈕
|
|
|
|
|
@ -261,7 +285,7 @@ class ControlStationUI(QMainWindow):
|
|
|
|
|
clear_traj_btn.clicked.connect(self.drone_map.clear_trajectories)
|
|
|
|
|
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.setStyleSheet("""
|
|
|
|
|
@ -274,9 +298,9 @@ class ControlStationUI(QMainWindow):
|
|
|
|
|
QTabBar::tab:selected { background-color: #2B2B2B; color: #FFF; border-bottom-color: #2B2B2B; }
|
|
|
|
|
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)
|
|
|
|
|
right_layout.addWidget(self.group_tab_widget)
|
|
|
|
|
group_container_layout.addWidget(self.group_tab_widget)
|
|
|
|
|
|
|
|
|
|
# 🌟 新增:保存群組面板的展開狀態
|
|
|
|
|
self.group_panel_expanded = True
|
|
|
|
|
@ -284,8 +308,17 @@ class ControlStationUI(QMainWindow):
|
|
|
|
|
# 預設建立 Group A
|
|
|
|
|
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_drone_clicked_signal().connect(self.handle_drone_clicked)
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
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):
|
|
|
|
|
"""清空訊息歷史。"""
|
|
|
|
|
self.message_history.clear()
|
|
|
|
|
@ -668,15 +954,28 @@ class ControlStationUI(QMainWindow):
|
|
|
|
|
"""🌟 收起/展開任務群組面板"""
|
|
|
|
|
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()
|
|
|
|
|
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.setToolTip("展開任務群組")
|
|
|
|
|
self.group_panel_expanded = False
|
|
|
|
|
else:
|
|
|
|
|
# 展開
|
|
|
|
|
self.group_tab_widget.setFixedHeight(150)
|
|
|
|
|
self.group_container.setMaximumHeight(16777215)
|
|
|
|
|
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.setToolTip("收起任務群組")
|
|
|
|
|
self.group_panel_expanded = True
|
|
|
|
|
@ -688,7 +987,7 @@ class ControlStationUI(QMainWindow):
|
|
|
|
|
group = MissionGroup(gid, color)
|
|
|
|
|
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.mission_type_changed.connect(self._handle_mission_type_changed)
|
|
|
|
|
panel.start_requested.connect(self._handle_group_start)
|
|
|
|
|
|