503 lines
17 KiB
Python
503 lines
17 KiB
Python
# -*- 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))
|