AI_MMI_Analyser/app/ui/components/signal_components.py

467 lines
17 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
신호 표시용 컴포넌트들
- 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()