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