handOver2/ui/base/base_dialog.py

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))