"""
신호 표시용 컴포넌트들
- 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"{self.display_name}
{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"{self.signal_name}
{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"{self.display_name}
{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()