# -*- coding: utf-8 -*- """ 팝업 위젯 모듈 마우스 호버 시 표시되는 팝업 위젯을 정의합니다. """ from PySide6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QGraphicsDropShadowEffect ) from PySide6.QtCore import Qt, Signal, QTimer, QPoint from PySide6.QtGui import QFont, QColor from core.config import ConfigManager from core.logger import get_logger logger = get_logger(__name__) class PopupWidget(QWidget): """ 팝업 위젯 마우스 호버 시 표시되는 팝업 정보 창입니다. 일정 시간 후 자동으로 사라집니다. Signals: action_clicked: 액션 버튼 클릭 시그널 popup_closed: 팝업 닫힘 시그널 Examples: >>> popup = PopupWidget(title="정보") >>> popup.add_row("항목1", "값1") >>> popup.show_at(QPoint(100, 100)) """ action_clicked = Signal(str) # 액션 ID popup_closed = Signal() def __init__( self, parent=None, title: str = "", width: int = 280, auto_hide: bool = True, auto_hide_delay: int = 3000 ): super().__init__(parent) self.config = ConfigManager() self.title_text = title self._width = width self._auto_hide = auto_hide self._auto_hide_delay = auto_hide_delay # 숨김 타이머 self._hide_timer = QTimer() self._hide_timer.setSingleShot(True) self._hide_timer.timeout.connect(self.hide_popup) # 윈도우 설정 self.setWindowFlags( Qt.ToolTip | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint ) self.setAttribute(Qt.WA_TranslucentBackground) self._setup_ui() def _setup_ui(self): """UI 설정""" # 메인 레이아웃 main_layout = QVBoxLayout(self) main_layout.setContentsMargins(0, 0, 0, 0) # 컨테이너 self.container = QWidget() self.container.setObjectName("popupContainer") self.container.setFixedWidth(self._width) main_layout.addWidget(self.container) # 컨테이너 레이아웃 self.container_layout = QVBoxLayout(self.container) self.container_layout.setContentsMargins(16, 12, 16, 12) self.container_layout.setSpacing(8) # 헤더 if self.title_text: self.title_label = QLabel(self.title_text) self.title_label.setObjectName("popupTitle") self.title_label.setFont(QFont("GmarketSans", 13, QFont.Bold)) self.container_layout.addWidget(self.title_label) # 컨텐츠 영역 self.content_widget = QWidget() self.content_layout = QVBoxLayout(self.content_widget) self.content_layout.setContentsMargins(0, 0, 0, 0) self.content_layout.setSpacing(6) self.container_layout.addWidget(self.content_widget) # 액션 영역 self.action_widget = QWidget() self.action_layout = QHBoxLayout(self.action_widget) self.action_layout.setContentsMargins(0, 8, 0, 0) self.action_layout.setSpacing(8) self.action_widget.hide() self.container_layout.addWidget(self.action_widget) # 그림자 효과 shadow = QGraphicsDropShadowEffect() shadow.setBlurRadius(20) shadow.setOffset(0, 4) shadow.setColor(QColor(0, 0, 0, 60)) self.container.setGraphicsEffect(shadow) # 스타일 적용 self._apply_style() def _apply_style(self): """스타일 적용""" theme = self.config.theme if theme == 'dark': bg = "#1e293b" border = "#334155" text = "#f8fafc" secondary = "#94a3b8" else: bg = "#ffffff" border = "#e2e8f0" text = "#1e293b" secondary = "#64748b" self.setStyleSheet(f""" #popupContainer {{ background-color: {bg}; border: 1px solid {border}; border-radius: 12px; }} #popupTitle {{ color: {text}; font-weight: bold; }} QLabel {{ color: {text}; }} .secondary {{ color: {secondary}; }} """) def add_row(self, label: str, value: str, is_secondary: bool = False): """ 행 추가 Args: label: 라벨 value: 값 is_secondary: 보조 텍스트 여부 """ row = QWidget() row_layout = QHBoxLayout(row) row_layout.setContentsMargins(0, 0, 0, 0) row_layout.setSpacing(8) label_widget = QLabel(label) label_widget.setFont(QFont("GmarketSans", 12)) if is_secondary: label_widget.setProperty("class", "secondary") value_widget = QLabel(value) value_widget.setFont(QFont("GmarketSans", 12)) value_widget.setAlignment(Qt.AlignRight) row_layout.addWidget(label_widget) row_layout.addStretch() row_layout.addWidget(value_widget) self.content_layout.addWidget(row) def add_text(self, text: str, is_secondary: bool = False): """ 텍스트 추가 Args: text: 텍스트 is_secondary: 보조 텍스트 여부 """ label = QLabel(text) label.setFont(QFont("GmarketSans", 12)) label.setWordWrap(True) if is_secondary: label.setProperty("class", "secondary") self.content_layout.addWidget(label) def add_action(self, text: str, action_id: str, primary: bool = False): """ 액션 버튼 추가 Args: text: 버튼 텍스트 action_id: 액션 ID primary: 주요 버튼 여부 """ self.action_widget.show() btn = QPushButton(text) btn.setCursor(Qt.PointingHandCursor) btn.setFont(QFont("GmarketSans", 11)) btn.clicked.connect(lambda: self._on_action_clicked(action_id)) if primary: btn.setStyleSheet(""" QPushButton { background-color: #3b82f6; color: white; border: none; border-radius: 6px; padding: 6px 12px; } QPushButton:hover { background-color: #2563eb; } """) else: btn.setStyleSheet(""" QPushButton { background-color: transparent; color: #3b82f6; border: none; padding: 6px 12px; } QPushButton:hover { background-color: #f1f5f9; border-radius: 6px; } """) self.action_layout.addWidget(btn) def _on_action_clicked(self, action_id: str): """액션 버튼 클릭""" self.action_clicked.emit(action_id) self.hide_popup() def show_at(self, position: QPoint): """ 지정된 위치에 팝업 표시 Args: position: 표시 위치 """ self.adjustSize() self.move(position) self.show() if self._auto_hide: self._hide_timer.start(self._auto_hide_delay) def hide_popup(self): """팝업 숨기기""" self._hide_timer.stop() self.hide() self.popup_closed.emit() def clear(self): """컨텐츠 초기화""" # 컨텐츠 위젯 비우기 while self.content_layout.count(): item = self.content_layout.takeAt(0) if item.widget(): item.widget().deleteLater() # 액션 위젯 비우기 while self.action_layout.count(): item = self.action_layout.takeAt(0) if item.widget(): item.widget().deleteLater() self.action_widget.hide() def enterEvent(self, event): """마우스 진입 - 자동 숨김 중지""" self._hide_timer.stop() super().enterEvent(event) def leaveEvent(self, event): """마우스 이탈 - 자동 숨김 재시작""" if self._auto_hide: self._hide_timer.start(self._auto_hide_delay) super().leaveEvent(event)