467 lines
17 KiB
Python
467 lines
17 KiB
Python
"""
|
||
신호 표시용 컴포넌트들
|
||
- OnOffSignalLabel: ON/OFF 신호 표시
|
||
- ModeSignalLabel: MODE 신호 표시 (여러 상태 중 하나)
|
||
- DataSignalLabel: 데이터 신호 표시 (신호명 + 값)
|
||
- SignalCard: 신호들을 그룹화하는 카드
|
||
"""
|
||
|
||
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
||
QFrame, QMenu, QGridLayout, QScrollArea, QSizePolicy)
|
||
from PySide6.QtCore import Qt, Signal, QPoint
|
||
from PySide6.QtGui import QCursor, QMouseEvent
|
||
|
||
# ============================================================================
|
||
# 1. ON/OFF 신호 라벨
|
||
# ============================================================================
|
||
class OnOffSignalLabel(QLabel):
|
||
"""
|
||
ON/OFF 신호 표시용 라벨
|
||
- ON: 연두색 배경 + 검은색 글자
|
||
- OFF: 어두운 회색 배경 + 흰색 글자
|
||
"""
|
||
|
||
clicked = Signal()
|
||
right_clicked = Signal(QPoint)
|
||
|
||
# 스타일 상수
|
||
STYLE_ON = "background-color: #76FF03; color: black; border-radius: 3px; padding: 2px 6px; font-weight: bold;"
|
||
STYLE_OFF = "background-color: #3C3C3C; color: #AAAAAA; border-radius: 3px; padding: 2px 6px;"
|
||
|
||
def __init__(self, signal_name: str, display_name: str = None,
|
||
description: str = "", on_list: list = None, off_list: list = None, parent=None):
|
||
super().__init__(display_name or signal_name, parent)
|
||
|
||
self.signal_name = signal_name
|
||
self.display_name = display_name or signal_name
|
||
self.description = description
|
||
self.on_list = on_list or [] # ON일 때 목록
|
||
self.off_list = off_list or [] # OFF일 때 목록
|
||
self._style_on_override = None
|
||
self._style_off_override = None
|
||
|
||
self._is_on = False
|
||
|
||
# UI 설정
|
||
self.setAlignment(Qt.AlignCenter)
|
||
self.setCursor(Qt.PointingHandCursor)
|
||
self.setMinimumSize(40, 18)
|
||
self.setMaximumHeight(22)
|
||
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
|
||
|
||
# 툴팁 설정
|
||
if description:
|
||
self.setToolTip(description)
|
||
|
||
self.set_status(False)
|
||
|
||
def set_status(self, is_on: bool):
|
||
"""신호 상태 설정"""
|
||
self._is_on = is_on
|
||
if is_on:
|
||
self.setStyleSheet(self._style_on_override or self.STYLE_ON)
|
||
else:
|
||
self.setStyleSheet(self._style_off_override or self.STYLE_OFF)
|
||
|
||
def set_style_override(self, on_style: str | None = None, off_style: str | None = None):
|
||
"""ON/OFF 스타일 오버라이드"""
|
||
self._style_on_override = on_style
|
||
self._style_off_override = off_style
|
||
# 현재 상태 즉시 반영
|
||
self.set_status(self._is_on)
|
||
|
||
def mousePressEvent(self, event: QMouseEvent):
|
||
if event.button() == Qt.LeftButton:
|
||
self.clicked.emit()
|
||
elif event.button() == Qt.RightButton:
|
||
self.right_clicked.emit(event.pos())
|
||
self._show_context_menu(event.globalPosition().toPoint())
|
||
super().mousePressEvent(event)
|
||
|
||
def _show_context_menu(self, global_pos):
|
||
menu = QMenu(self)
|
||
menu.setStyleSheet("""
|
||
QMenu { background-color: #2D2D30; color: white; border: 1px solid #555; }
|
||
QMenu::item:selected { background-color: #0078D7; }
|
||
""")
|
||
|
||
# ON일 때 목록
|
||
if self.on_list:
|
||
on_menu = menu.addMenu("🟢 ON 목록")
|
||
for item in self.on_list:
|
||
on_menu.addAction(item)
|
||
|
||
# OFF일 때 목록
|
||
if self.off_list:
|
||
off_menu = menu.addMenu("⚫ OFF 목록")
|
||
for item in self.off_list:
|
||
off_menu.addAction(item)
|
||
|
||
menu.addSeparator()
|
||
|
||
# 신호 설명
|
||
if self.description:
|
||
desc_action = menu.addAction("ℹ️ 신호 설명")
|
||
desc_action.triggered.connect(self._show_description_popup)
|
||
|
||
menu.exec(global_pos)
|
||
|
||
def _show_description_popup(self):
|
||
from app.ui.components.clickableLabel import HoverInfoPopup
|
||
popup = HoverInfoPopup(f"<b>{self.display_name}</b><br><br>{self.description}")
|
||
popup.move(QCursor.pos() + QPoint(10, 10))
|
||
popup.show()
|
||
|
||
|
||
# ============================================================================
|
||
# 2. MODE 신호 라벨 (여러 상태 중 하나)
|
||
# ============================================================================
|
||
class ModeSignalLabel(QLabel):
|
||
"""
|
||
MODE 신호 표시용 라벨
|
||
- 신호 없음: OFF (어두운 회색 배경 + 흰색 글자)
|
||
- 신호 있음: 해당 모드 표시 (연보라색 배경 + 검은색 글자)
|
||
"""
|
||
|
||
clicked = Signal()
|
||
right_clicked = Signal(QPoint)
|
||
|
||
STYLE_OFF = "background-color: #3C3C3C; color: #AAAAAA; border-radius: 3px; padding: 2px 6px;"
|
||
STYLE_ON = "background-color: #B388FF; color: black; border-radius: 3px; padding: 2px 6px; font-weight: bold;"
|
||
|
||
def __init__(self, signal_name: str, mode_list: list = None,
|
||
description: str = "", parent=None):
|
||
super().__init__("OFF", parent)
|
||
|
||
self.signal_name = signal_name
|
||
self.mode_list = mode_list or [] # 가능한 모드 목록
|
||
self.description = description
|
||
self._current_mode = None
|
||
|
||
# UI 설정
|
||
self.setAlignment(Qt.AlignCenter)
|
||
self.setCursor(Qt.PointingHandCursor)
|
||
self.setMinimumSize(45, 18)
|
||
self.setMaximumHeight(22)
|
||
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
|
||
|
||
if description:
|
||
self.setToolTip(description)
|
||
|
||
self.set_mode(None)
|
||
|
||
def set_mode(self, mode: str):
|
||
"""현재 모드 설정"""
|
||
self._current_mode = mode
|
||
if mode:
|
||
self.setText(mode)
|
||
self.setStyleSheet(self.STYLE_ON)
|
||
else:
|
||
self.setText("OFF")
|
||
self.setStyleSheet(self.STYLE_OFF)
|
||
|
||
def mousePressEvent(self, event: QMouseEvent):
|
||
if event.button() == Qt.LeftButton:
|
||
self.clicked.emit()
|
||
elif event.button() == Qt.RightButton:
|
||
self.right_clicked.emit(event.pos())
|
||
self._show_context_menu(event.globalPosition().toPoint())
|
||
super().mousePressEvent(event)
|
||
|
||
def _show_context_menu(self, global_pos):
|
||
menu = QMenu(self)
|
||
menu.setStyleSheet("""
|
||
QMenu { background-color: #2D2D30; color: white; border: 1px solid #555; }
|
||
QMenu::item:selected { background-color: #0078D7; }
|
||
""")
|
||
|
||
# 모드 목록
|
||
if self.mode_list:
|
||
mode_menu = menu.addMenu("📋 모드 목록")
|
||
for mode in self.mode_list:
|
||
action = mode_menu.addAction(mode)
|
||
if mode == self._current_mode:
|
||
action.setEnabled(False)
|
||
|
||
menu.addSeparator()
|
||
|
||
if self.description:
|
||
desc_action = menu.addAction("ℹ️ 신호 설명")
|
||
desc_action.triggered.connect(self._show_description_popup)
|
||
|
||
menu.exec(global_pos)
|
||
|
||
def _show_description_popup(self):
|
||
from app.ui.components.clickableLabel import HoverInfoPopup
|
||
popup = HoverInfoPopup(f"<b>{self.signal_name}</b><br><br>{self.description}")
|
||
popup.move(QCursor.pos() + QPoint(10, 10))
|
||
popup.show()
|
||
|
||
|
||
# ============================================================================
|
||
# 3. DATA 신호 라벨 (신호명 + 값) - 세로 배치
|
||
# ============================================================================
|
||
class DataSignalLabel(QFrame):
|
||
"""
|
||
데이터 신호 표시용 라벨 (신호명 + 값 2개 세로 조합)
|
||
- 상단: 신호명 (밝은 파랑 배경 + 흰색 글자)
|
||
- 하단: 신호값 (남색 배경 + 흰색 글자)
|
||
"""
|
||
|
||
clicked = Signal()
|
||
right_clicked = Signal(QPoint)
|
||
|
||
STYLE_NAME = "background-color: #2196F3; color: white; border-radius: 3px 0 0 3px; padding: 2px 6px; font-size: 9px;"
|
||
STYLE_VALUE = "background-color: #1A237E; color: white; border-radius: 0 3px 3px 0; padding: 2px 6px; font-weight: bold; font-size: 10px;"
|
||
|
||
def __init__(self, signal_name: str, display_name: str = None,
|
||
unit: str = "", possible_values: list = None,
|
||
description: str = "", parent=None):
|
||
super().__init__(parent)
|
||
|
||
self.signal_name = signal_name
|
||
self.display_name = display_name or signal_name
|
||
self.unit = unit
|
||
self.possible_values = possible_values or []
|
||
self.description = description
|
||
|
||
self._setup_ui()
|
||
|
||
if description:
|
||
self.setToolTip(description)
|
||
|
||
def _setup_ui(self):
|
||
# 가로 배치 (이름 | 값)
|
||
layout = QHBoxLayout(self)
|
||
layout.setContentsMargins(0, 0, 0, 0)
|
||
layout.setSpacing(0)
|
||
|
||
# 좌측: 신호명 라벨
|
||
self.lbl_name = QLabel(self.display_name)
|
||
self.lbl_name.setStyleSheet(self.STYLE_NAME)
|
||
self.lbl_name.setAlignment(Qt.AlignCenter)
|
||
self.lbl_name.setMinimumWidth(40)
|
||
self.lbl_name.setFixedHeight(22)
|
||
|
||
# 우측: 신호값 라벨
|
||
self.lbl_value = QLabel("-")
|
||
self.lbl_value.setStyleSheet(self.STYLE_VALUE)
|
||
self.lbl_value.setAlignment(Qt.AlignCenter)
|
||
self.lbl_value.setMinimumWidth(50)
|
||
self.lbl_value.setFixedHeight(22)
|
||
|
||
layout.addWidget(self.lbl_name)
|
||
layout.addWidget(self.lbl_value)
|
||
|
||
self.setFixedHeight(24) # 1행 높이
|
||
self.setCursor(Qt.PointingHandCursor)
|
||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||
|
||
def set_value(self, value):
|
||
"""신호 값 설정"""
|
||
if self.unit:
|
||
self.lbl_value.setText(f"{value}{self.unit}")
|
||
else:
|
||
self.lbl_value.setText(str(value))
|
||
|
||
def set_value_style(self, style: str):
|
||
"""값 라벨 스타일 커스텀 설정"""
|
||
self.lbl_value.setStyleSheet(self.STYLE_VALUE + style)
|
||
|
||
def mousePressEvent(self, event: QMouseEvent):
|
||
if event.button() == Qt.LeftButton:
|
||
self.clicked.emit()
|
||
elif event.button() == Qt.RightButton:
|
||
self.right_clicked.emit(event.pos())
|
||
self._show_context_menu(event.globalPosition().toPoint())
|
||
super().mousePressEvent(event)
|
||
|
||
def _show_context_menu(self, global_pos):
|
||
menu = QMenu(self)
|
||
menu.setStyleSheet("""
|
||
QMenu { background-color: #2D2D30; color: white; border: 1px solid #555; }
|
||
QMenu::item:selected { background-color: #0078D7; }
|
||
""")
|
||
|
||
# 가능한 값 목록
|
||
if self.possible_values:
|
||
val_menu = menu.addMenu("📋 가능한 값")
|
||
for val in self.possible_values:
|
||
val_menu.addAction(str(val))
|
||
|
||
menu.addSeparator()
|
||
|
||
if self.description:
|
||
desc_action = menu.addAction("ℹ️ 신호 설명")
|
||
desc_action.triggered.connect(self._show_description_popup)
|
||
|
||
menu.exec(global_pos)
|
||
|
||
def _show_description_popup(self):
|
||
from app.ui.components.clickableLabel import HoverInfoPopup
|
||
popup = HoverInfoPopup(f"<b>{self.display_name}</b><br><br>{self.description}")
|
||
popup.move(QCursor.pos() + QPoint(10, 10))
|
||
popup.show()
|
||
|
||
|
||
# ============================================================================
|
||
# 4. 신호 카드 (그룹화)
|
||
# ============================================================================
|
||
class SignalCard(QFrame):
|
||
"""
|
||
신호들을 그룹화하는 카드 컴포넌트
|
||
- 타이틀 바
|
||
- ON/OFF, MODE 컴포넌트: 1행 1열
|
||
- DATA 컴포넌트: 1행에 2~4열 차지 (가로로 넓게)
|
||
"""
|
||
|
||
def __init__(self, title: str, parent=None):
|
||
super().__init__(parent)
|
||
|
||
self.title = title
|
||
self.signals = {} # signal_name -> widget
|
||
self._onoff_widgets = [] # ON/OFF, MODE 위젯들
|
||
self._data_widgets = [] # DATA 위젯들
|
||
self._horizontal_data_mode = False # DATA 가로 배치 모드
|
||
|
||
self._setup_ui()
|
||
|
||
def _setup_ui(self):
|
||
self.setStyleSheet("""
|
||
SignalCard {
|
||
background-color: #252526;
|
||
border: 1px solid #3E3E42;
|
||
border-radius: 4px;
|
||
}
|
||
""")
|
||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
|
||
self.setMinimumWidth(150)
|
||
|
||
layout = QVBoxLayout(self)
|
||
layout.setContentsMargins(4, 4, 4, 4)
|
||
layout.setSpacing(2)
|
||
|
||
# 타이틀
|
||
self.lbl_title = QLabel(self.title)
|
||
self.lbl_title.setFixedHeight(20) # 제목 높이 고정
|
||
self.lbl_title.setStyleSheet("""
|
||
font-weight: bold;
|
||
color: #00B0FF;
|
||
font-size: 10px;
|
||
padding: 1px 3px;
|
||
border-bottom: 1px solid #3E3E42;
|
||
""")
|
||
layout.addWidget(self.lbl_title)
|
||
|
||
# 신호 컨테이너 (GridLayout 사용)
|
||
self.signal_container = QWidget()
|
||
self.signal_grid = QGridLayout(self.signal_container)
|
||
self.signal_grid.setContentsMargins(0, 2, 0, 0)
|
||
self.signal_grid.setSpacing(2)
|
||
|
||
layout.addWidget(self.signal_container)
|
||
|
||
# 현재 그리드 위치 추적
|
||
self._grid_row = 0
|
||
self._grid_col = 0
|
||
self._max_cols = 4 # 4열 배치 (DATA가 2~4열 차지 가능)
|
||
self._data_colspan = 2 # DATA 위젯의 기본 colspan (2~4열 조정 가능)
|
||
|
||
def set_horizontal_data_mode(self, enabled: bool):
|
||
"""DATA 위젯 가로 배치 모드 설정"""
|
||
self._horizontal_data_mode = enabled
|
||
|
||
def add_signal(self, widget, signal_name: str = None):
|
||
"""신호 위젯 추가 - 타입에 따라 배치 방식 다름"""
|
||
if isinstance(widget, DataSignalLabel):
|
||
if self._horizontal_data_mode:
|
||
# DATA 가로 배치 모드: 1행 1열 (ON/OFF처럼)
|
||
self.signal_grid.addWidget(widget, self._grid_row, self._grid_col)
|
||
self._grid_col += 1
|
||
if self._grid_col >= self._max_cols:
|
||
self._grid_col = 0
|
||
self._grid_row += 1
|
||
else:
|
||
# 기본 모드: DATA 위젯은 1행에 2~4열 차지 (가로로 넓게)
|
||
# 현재 열이 0이 아니면 다음 행으로 이동
|
||
if self._grid_col != 0:
|
||
self._grid_row += 1
|
||
self._grid_col = 0
|
||
|
||
# colspan 계산 (남은 공간에 따라 2~4열)
|
||
colspan = min(self._data_colspan, self._max_cols - self._grid_col)
|
||
self.signal_grid.addWidget(widget, self._grid_row, self._grid_col, 1, colspan)
|
||
self._grid_col += colspan
|
||
if self._grid_col >= self._max_cols:
|
||
self._grid_col = 0
|
||
self._grid_row += 1
|
||
self._data_widgets.append(widget)
|
||
else:
|
||
# ON/OFF, MODE 위젯: 1행 1열
|
||
self.signal_grid.addWidget(widget, self._grid_row, self._grid_col)
|
||
self._grid_col += 1
|
||
if self._grid_col >= self._max_cols:
|
||
self._grid_col = 0
|
||
self._grid_row += 1
|
||
self._onoff_widgets.append(widget)
|
||
|
||
if signal_name:
|
||
self.signals[signal_name] = widget
|
||
|
||
def get_signal(self, signal_name: str):
|
||
"""신호 위젯 가져오기"""
|
||
return self.signals.get(signal_name)
|
||
|
||
def finalize_layout(self):
|
||
"""레이아웃 마무리 - 남는 공간에 stretch 추가"""
|
||
# 마지막 행 다음에 stretch 추가
|
||
if self._grid_col > 0:
|
||
self._grid_row += 1
|
||
self.signal_grid.setRowStretch(self._grid_row, 1)
|
||
|
||
|
||
# ============================================================================
|
||
# 5. FlowLayout (하위 호환용 - 더 이상 사용하지 않음)
|
||
# ============================================================================
|
||
class FlowLayout(QVBoxLayout):
|
||
"""
|
||
간단한 FlowLayout 구현 (레거시 호환용)
|
||
"""
|
||
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self._widgets = []
|
||
self._row_layouts = []
|
||
self._spacing = 4
|
||
self.setSpacing(4)
|
||
|
||
def setSpacing(self, spacing):
|
||
self._spacing = spacing
|
||
super().setSpacing(spacing)
|
||
|
||
def addWidget(self, widget):
|
||
self._widgets.append(widget)
|
||
self._reflow()
|
||
|
||
def _reflow(self):
|
||
# 기존 레이아웃 정리
|
||
for row_layout in self._row_layouts:
|
||
while row_layout.count():
|
||
item = row_layout.takeAt(0)
|
||
|
||
for row_layout in self._row_layouts:
|
||
self.removeItem(row_layout)
|
||
self._row_layouts.clear()
|
||
|
||
# 새 행 생성
|
||
current_row = QHBoxLayout()
|
||
current_row.setSpacing(self._spacing)
|
||
self._row_layouts.append(current_row)
|
||
super().addLayout(current_row)
|
||
|
||
for widget in self._widgets:
|
||
current_row.addWidget(widget)
|
||
|
||
current_row.addStretch()
|
||
|