# -*- coding: utf-8 -*- """ 기본 다이얼로그 클래스 모듈 모든 커스텀 다이얼로그의 기반 클래스를 정의합니다. - 프레임리스 다이얼로그 - 드래그 이동 및 크기 조정 지원 - 공통 버튼 레이아웃 중요: - 다이얼로그 내부 컴포넌트(필터카드 등)까지 QLabel 전역 스타일이 전파되어 폰트/높이 충돌로 잘림이 발생할 수 있어, QLabel 스타일 범위를 #dialogContainer 내부로 제한합니다. """ from PySide6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QWidget, QPushButton, QLabel, QGraphicsDropShadowEffect ) from PySide6.QtCore import Qt, Signal, QPoint from PySide6.QtGui import QColor, QFont, QMouseEvent, QKeyEvent from core.logger import get_logger from core.config import ConfigManager from core.signals import GlobalSignals from ui.styles.style_manager import StyleManager logger = get_logger(__name__) RESIZE_MARGIN = 8 class BaseDialog(QDialog): dialog_confirmed = Signal() dialog_cancelled = Signal() def __init__( self, parent=None, title: str = "", width: int = 400, height: int = 300, min_width: int = 300, min_height: int = 200, modal: bool = True, frameless: bool = True, resizable: bool = True ): super().__init__(parent) self.config = ConfigManager() self.signals = GlobalSignals() self.style_manager = StyleManager() self.title_text = title self._drag_position = QPoint() self._is_dragging = False self._resizable = resizable self._resize_direction = None self._resize_start_pos = QPoint() self._resize_start_geometry = None self._setup_window(width, height, min_width, min_height, modal, frameless, resizable) self._setup_base_ui() logger.debug("%s 초기화", self.__class__.__name__) def _setup_window( self, width: int, height: int, min_width: int, min_height: int, modal: bool, frameless: bool, resizable: bool ): self.resize(width, height) self.setMinimumSize(min_width, min_height) self.setModal(modal) if not resizable: self.setFixedSize(width, height) if frameless: self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint) self.setAttribute(Qt.WA_TranslucentBackground) self.setMouseTracking(True) def _setup_base_ui(self): main_layout = QVBoxLayout(self) main_layout.setContentsMargins(0, 0, 0, 0) main_layout.setSpacing(0) self.container = QWidget() self.container.setObjectName("dialogContainer") main_layout.addWidget(self.container) self.container_layout = QVBoxLayout(self.container) self.container_layout.setContentsMargins(24, 20, 24, 24) self.container_layout.setSpacing(16) self._create_header() self.content_widget = QWidget() self.content_layout = QVBoxLayout(self.content_widget) self.content_layout.setContentsMargins(0, 0, 0, 0) self.content_layout.setSpacing(12) self.container_layout.addWidget(self.content_widget, 1) self.button_widget = QWidget() self.button_layout = QHBoxLayout(self.button_widget) self.button_layout.setContentsMargins(0, 12, 0, 0) self.button_layout.setSpacing(12) self.container_layout.addWidget(self.button_widget) self._add_shadow() self._apply_style() def _create_header(self): header = QWidget() header_layout = QHBoxLayout(header) header_layout.setContentsMargins(0, 0, 0, 0) header_layout.setSpacing(0) self.title_label = QLabel(self.title_text) self.title_label.setObjectName("dialogTitle") header_layout.addWidget(self.title_label) header_layout.addStretch() self.close_btn = QPushButton("✕") self.close_btn.setObjectName("closeButton") self.close_btn.setFixedSize(32, 32) self.close_btn.setCursor(Qt.PointingHandCursor) self.close_btn.clicked.connect(self.close) header_layout.addWidget(self.close_btn) self.container_layout.addWidget(header) def _add_shadow(self): shadow = QGraphicsDropShadowEffect(self.container) shadow.setBlurRadius(30) shadow.setOffset(0, 10) shadow.setColor(QColor(0, 0, 0, 80)) self.container.setGraphicsEffect(shadow) def _apply_style(self): colors = self.style_manager.get_colors() dialog_font = self.style_manager.get_font("dialog", "title") label_font = self.style_manager.get_font("dialog", "label") dialog_style = self.style_manager.get_dialog_stylesheet() self.title_label.setFont(dialog_font) label_height = self.style_manager.calculate_label_height( font=label_font, area="dialog", style="label" ) self.setStyleSheet(f""" {dialog_style} #dialogContainer {{ background-color: {colors['bg_secondary']}; border: 1px solid {colors['border']}; border-radius: 16px; }} #dialogTitle {{ color: {colors['text_primary']}; font-family: '{dialog_font.family()}'; font-size: {dialog_font.pointSize()}pt; font-weight: bold; min-height: {label_height}px; }} #closeButton {{ background-color: transparent; border: none; color: {colors['text_primary']}; font-size: 16px; border-radius: 16px; }} #closeButton:hover {{ background-color: {colors['error']}; color: white; }} /* ✅ 중요: QLabel 전역 적용 금지 → 다이얼로그 컨테이너 내부로 범위 제한 */ #dialogContainer QLabel {{ color: {colors['text_primary']}; font-family: '{label_font.family()}'; font-size: {label_font.pointSize()}pt; min-height: {label_height}px; }} {self.style_manager.get_input_stylesheet(area="dialog", style="input")} {self.style_manager.get_label_stylesheet(area="dialog", style="label")} """) def add_button(self, text: str, callback=None, primary: bool = False, danger: bool = False) -> QPushButton: btn = QPushButton(text) btn.setFixedHeight(40) btn.setCursor(Qt.PointingHandCursor) if callback: btn.clicked.connect(callback) if primary: btn.setStyleSheet(""" QPushButton { background-color: #3b82f6; color: white; border: none; border-radius: 8px; padding: 0 24px; font-weight: bold; } QPushButton:hover { background-color: #2563eb; } QPushButton:pressed { background-color: #1d4ed8; } """) elif danger: btn.setStyleSheet(""" QPushButton { background-color: #ef4444; color: white; border: none; border-radius: 8px; padding: 0 24px; font-weight: bold; } QPushButton:hover { background-color: #dc2626; } QPushButton:pressed { background-color: #b91c1c; } """) else: btn.setStyleSheet(""" QPushButton { background-color: #64748b; color: white; border: none; border-radius: 8px; padding: 0 24px; } QPushButton:hover { background-color: #475569; } QPushButton:pressed { background-color: #334155; } """) self.button_layout.addWidget(btn) return btn def add_confirm_cancel_buttons(self, confirm_text: str = "확인", cancel_text: str = "취소"): self.button_layout.addStretch() self.cancel_btn = self.add_button(cancel_text, self._on_cancel) self.confirm_btn = self.add_button(confirm_text, self._on_confirm, primary=True) # 확인 버튼을 기본 버튼으로 설정 (엔터키로 활성화) self.confirm_btn.setDefault(True) self.confirm_btn.setAutoDefault(True) def _on_confirm(self): self.dialog_confirmed.emit() self.accept() def _on_cancel(self): self.dialog_cancelled.emit() self.reject() def keyPressEvent(self, event: QKeyEvent): """키보드 이벤트 처리""" # ESC키: 다이얼로그 닫기 if event.key() == Qt.Key_Escape: self._on_cancel() return # 엔터키: 확인 버튼 클릭 (입력 필드에 포커스가 있을 때는 제외) if event.key() in (Qt.Key_Return, Qt.Key_Enter): # 현재 포커스가 있는 위젯 확인 focused_widget = self.focusWidget() # 텍스트 입력 필드나 멀티라인 입력 필드에서는 기본 동작 허용 from PySide6.QtWidgets import QLineEdit, QTextEdit, QPlainTextEdit, QSpinBox, QComboBox if isinstance(focused_widget, (QLineEdit, QTextEdit, QPlainTextEdit, QSpinBox, QComboBox)): # 입력 필드에서 엔터키는 기본 동작 (줄바꿈 등) 허용 super().keyPressEvent(event) return # 확인 버튼이 있으면 클릭, 없으면 accept() if hasattr(self, 'confirm_btn') and self.confirm_btn: self.confirm_btn.click() else: # 확인 버튼이 없으면 기본 accept() 동작 self._on_confirm() return super().keyPressEvent(event) # ===== resize/drag 로직은 기존 유지 ===== def _get_resize_direction(self, pos: QPoint) -> str: if not self._resizable: return '' rect = self.rect() x, y = pos.x(), pos.y() w, h = rect.width(), rect.height() left = x < RESIZE_MARGIN right = x > w - RESIZE_MARGIN top = y < RESIZE_MARGIN bottom = y > h - RESIZE_MARGIN if top and left: return 'top-left' elif top and right: return 'top-right' elif bottom and left: return 'bottom-left' elif bottom and right: return 'bottom-right' elif left: return 'left' elif right: return 'right' elif top: return 'top' elif bottom: return 'bottom' return '' def _update_cursor(self, direction: str): cursors = { 'left': Qt.SizeHorCursor, 'right': Qt.SizeHorCursor, 'top': Qt.SizeVerCursor, 'bottom': Qt.SizeVerCursor, 'top-left': Qt.SizeFDiagCursor, 'bottom-right': Qt.SizeFDiagCursor, 'top-right': Qt.SizeBDiagCursor, 'bottom-left': Qt.SizeBDiagCursor, } self.setCursor(cursors.get(direction, Qt.ArrowCursor)) def mousePressEvent(self, event: QMouseEvent): if event.button() == Qt.LeftButton: pos = event.position().toPoint() direction = self._get_resize_direction(pos) if direction: self._resize_direction = direction self._resize_start_pos = event.globalPosition().toPoint() self._resize_start_geometry = self.geometry() else: self._drag_position = event.globalPosition().toPoint() - self.frameGeometry().topLeft() self._is_dragging = True super().mousePressEvent(event) def mouseMoveEvent(self, event: QMouseEvent): pos = event.position().toPoint() if self._resize_direction and event.buttons() == Qt.LeftButton: self._do_resize(event.globalPosition().toPoint()) elif self._is_dragging and event.buttons() == Qt.LeftButton: self.move(event.globalPosition().toPoint() - self._drag_position) else: direction = self._get_resize_direction(pos) self._update_cursor(direction) super().mouseMoveEvent(event) def _do_resize(self, global_pos: QPoint): if not self._resize_start_geometry: return dx = global_pos.x() - self._resize_start_pos.x() dy = global_pos.y() - self._resize_start_pos.y() geo = self._resize_start_geometry new_geo = self.geometry() min_w = self.minimumWidth() min_h = self.minimumHeight() direction = self._resize_direction if 'right' in direction: new_geo.setWidth(max(min_w, geo.width() + dx)) if 'left' in direction: new_w = max(min_w, geo.width() - dx) if new_w != geo.width(): new_geo.setLeft(geo.left() + (geo.width() - new_w)) new_geo.setWidth(new_w) if 'bottom' in direction: new_geo.setHeight(max(min_h, geo.height() + dy)) if 'top' in direction: new_h = max(min_h, geo.height() - dy) if new_h != geo.height(): new_geo.setTop(geo.top() + (geo.height() - new_h)) new_geo.setHeight(new_h) self.setGeometry(new_geo) def mouseReleaseEvent(self, event: QMouseEvent): self._is_dragging = False self._resize_direction = None self._resize_start_geometry = None super().mouseReleaseEvent(event) def resizeEvent(self, event): super().resizeEvent(event) if self._resizable: self._apply_text_scaling() def _apply_text_scaling(self): base_width = 400 base_height = 300 scale_x = self.width() / base_width scale_y = self.height() / base_height scale = min(scale_x, scale_y) scale = max(0.8, min(1.5, scale)) colors = self.style_manager.get_colors() base_title_font = self.style_manager.get_font("dialog", "title") title_size = int(base_title_font.pointSize() * scale) title_font = QFont(base_title_font.family(), title_size, base_title_font.weight()) self.title_label.setFont(title_font) base_label_font = self.style_manager.get_font("dialog", "label") label_size = int(base_label_font.pointSize() * scale) label_font = QFont(base_label_font.family(), label_size, base_label_font.weight()) base_input_font = self.style_manager.get_font("dialog", "input") input_size = int(base_input_font.pointSize() * scale) input_font = QFont(base_input_font.family(), input_size, base_input_font.weight()) label_height = self.style_manager.calculate_label_height(font=label_font, area="dialog", style="label") input_height = self.style_manager.calculate_input_height(font=input_font, area="dialog", style="input") self.setStyleSheet(f""" {self.style_manager.get_dialog_stylesheet()} #dialogContainer {{ background-color: {colors['bg_secondary']}; border: 1px solid {colors['border']}; border-radius: 16px; }} #dialogTitle {{ color: {colors['text_primary']}; font-family: '{title_font.family()}'; font-size: {title_size}pt; font-weight: bold; min-height: {label_height}px; }} #closeButton {{ background-color: transparent; border: none; color: {colors['text_primary']}; font-size: {int(16 * scale)}px; border-radius: 16px; }} #closeButton:hover {{ background-color: {colors['error']}; color: white; }} /* ✅ 중요: QLabel 범위 제한 */ #dialogContainer QLabel {{ color: {colors['text_primary']}; font-family: '{label_font.family()}'; font-size: {label_size}pt; min-height: {label_height}px; }} QLineEdit, QTextEdit, QComboBox, QSpinBox {{ background-color: {colors['input_bg']}; color: {colors['input_text']}; border: 1px solid {colors['input_border']}; border-radius: 6px; padding: {input_height // 4}px 12px; font-family: '{input_font.family()}'; font-size: {input_size}pt; min-height: {input_height}px; }} QLineEdit:focus, QTextEdit:focus, QComboBox:focus, QSpinBox:focus {{ border-color: {colors['input_focus']}; outline: none; }} """) self._on_text_scale_changed(scale, label_font, input_font) def _on_text_scale_changed(self, scale: float, label_font: QFont, input_font: QFont): _ = (scale, label_font, input_font) def get_text_scale_factor(self) -> float: base_width = 400 base_height = 300 scale_x = self.width() / base_width scale_y = self.height() / base_height scale = min(scale_x, scale_y) return max(0.8, min(1.5, scale))