handOver2/ui/base/base_table.py

401 lines
12 KiB
Python

# -*- coding: utf-8 -*-
"""
기본 테이블 클래스 모듈
커스텀 테이블 위젯의 기반 클래스를 정의합니다.
이 모듈은 다음 기능을 제공합니다:
- 커스텀 스타일링
- 팀 확인 체크박스
- 편성 팝업 지원
- 드래그 앤 드롭
"""
from typing import List, Dict, Any, Optional
from PySide6.QtWidgets import (
QTableWidget, QTableWidgetItem, QHeaderView,
QWidget, QHBoxLayout, QCheckBox, QPushButton,
QAbstractItemView, QStyledItemDelegate
)
from PySide6.QtCore import Qt, Signal, QPoint, QRect
from PySide6.QtGui import QFont, QColor, QPainter, QBrush, QPen
from core.logger import get_logger
from core.config import ConfigManager
from core.signals import GlobalSignals
from core.constants import TEAMS
# 로거 설정
logger = get_logger(__name__)
class TeamConfirmationWidget(QWidget):
"""
팀 확인 체크박스 위젯
각 팀의 확인 상태를 표시하는 체크박스 그룹입니다.
"""
confirmation_changed = Signal(str, bool) # 팀명, 확인여부
def __init__(self, confirmations: Dict[str, bool] = None, parent=None):
super().__init__(parent)
self.confirmations = confirmations or {team: False for team in TEAMS}
self.checkboxes: Dict[str, QCheckBox] = {}
self._setup_ui()
def _setup_ui(self):
"""UI 설정"""
layout = QHBoxLayout(self)
layout.setContentsMargins(4, 2, 4, 2)
layout.setSpacing(2)
for team in TEAMS:
checkbox = QCheckBox()
checkbox.setChecked(self.confirmations.get(team, False))
checkbox.setToolTip(team)
checkbox.stateChanged.connect(
lambda state, t=team: self._on_state_changed(t, state)
)
# 팀 이니셜 라벨
checkbox.setStyleSheet(f"""
QCheckBox {{
spacing: 2px;
}}
QCheckBox::indicator {{
width: 16px;
height: 16px;
}}
QCheckBox::indicator:checked {{
background-color: #22c55e;
border-radius: 3px;
}}
QCheckBox::indicator:unchecked {{
background-color: #64748b;
border-radius: 3px;
}}
""")
layout.addWidget(checkbox)
self.checkboxes[team] = checkbox
def _on_state_changed(self, team: str, state: int):
"""체크박스 상태 변경"""
checked = state == Qt.Checked
self.confirmations[team] = checked
self.confirmation_changed.emit(team, checked)
def set_confirmations(self, confirmations: Dict[str, bool]):
"""확인 상태 설정"""
self.confirmations = confirmations
for team, checkbox in self.checkboxes.items():
checkbox.blockSignals(True)
checkbox.setChecked(confirmations.get(team, False))
checkbox.blockSignals(False)
def get_confirmations(self) -> Dict[str, bool]:
"""확인 상태 반환"""
return self.confirmations.copy()
def all_confirmed(self) -> bool:
"""모든 팀 확인 여부"""
return all(self.confirmations.values())
class TrainNumberDelegate(QStyledItemDelegate):
"""
편성번호 셀 델리게이트
편성번호 위에 마우스를 올리면 최근 고장 목록 팝업을 표시합니다.
"""
train_hovered = Signal(str, int, int) # 편성번호, x, y
train_left = Signal()
def __init__(self, parent=None):
super().__init__(parent)
self.signals = GlobalSignals()
def paint(self, painter: QPainter, option, index):
"""셀 렌더링"""
super().paint(painter, option, index)
# 값 가져오기
value = index.data(Qt.DisplayRole)
if not value:
return
# 작업 여부 확인 (노란 느낌표)
has_work = index.data(Qt.UserRole + 1)
if has_work:
self._draw_work_indicator(painter, option.rect)
# 청소 유형 확인
cleaning_type = index.data(Qt.UserRole + 2)
if cleaning_type == "중청소":
self._draw_cleaning_indicator(painter, option.rect, "medium")
elif cleaning_type == "대청소":
self._draw_cleaning_indicator(painter, option.rect, "large")
def _draw_work_indicator(self, painter: QPainter, rect: QRect):
"""작업 표시 (노란 느낌표)"""
painter.save()
# 느낌표 위치 (오른쪽 상단)
x = rect.right() - 12
y = rect.top() + 4
painter.setPen(QPen(QColor("#f59e0b"), 2))
painter.setFont(QFont("Arial", 10, QFont.Bold))
painter.drawText(x, y + 10, "!")
painter.restore()
def _draw_cleaning_indicator(self, painter: QPainter, rect: QRect, cleaning_type: str):
"""청소 표시 (파란 네모 / 빨간 동그라미)"""
painter.save()
# 표시 위치 (왼쪽 상단)
x = rect.left() + 2
y = rect.top() + 2
size = 12
if cleaning_type == "medium":
# 파란 네모
painter.setPen(QPen(QColor("#3b82f6"), 2))
painter.setBrush(Qt.NoBrush)
painter.drawRect(x, y, size, size)
elif cleaning_type == "large":
# 빨간 동그라미
painter.setPen(QPen(QColor("#ef4444"), 2))
painter.setBrush(Qt.NoBrush)
painter.drawEllipse(x, y, size, size)
painter.restore()
def editorEvent(self, event, model, option, index):
"""에디터 이벤트 처리"""
from PySide6.QtCore import QEvent
if event.type() == QEvent.MouseMove:
value = index.data(Qt.DisplayRole)
if value:
pos = event.globalPosition().toPoint()
self.train_hovered.emit(value, pos.x(), pos.y())
return super().editorEvent(event, model, option, index)
class BaseTable(QTableWidget):
"""
기본 테이블 클래스
모든 커스텀 테이블이 상속받는 기반 클래스입니다.
Attributes:
config: 설정 관리자
signals: 전역 시그널
"""
# 시그널
row_selected = Signal(int)
row_double_clicked = Signal(int)
cell_edited = Signal(int, str, str) # row, column_name, new_value
def __init__(self, parent=None):
super().__init__(parent)
self.config = ConfigManager()
self.signals = GlobalSignals()
self._setup_table()
self._apply_style()
def _setup_table(self):
"""테이블 설정"""
# 기본 설정
self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setSelectionMode(QAbstractItemView.SingleSelection)
self.setAlternatingRowColors(True)
self.setShowGrid(False)
self.verticalHeader().setVisible(False)
# 헤더 설정
header = self.horizontalHeader()
header.setStretchLastSection(True)
header.setSectionResizeMode(QHeaderView.Interactive)
header.setDefaultAlignment(Qt.AlignCenter)
# 행 높이
self.verticalHeader().setDefaultSectionSize(40)
# 시그널 연결
self.itemSelectionChanged.connect(self._on_selection_changed)
self.itemDoubleClicked.connect(self._on_double_clicked)
self.itemChanged.connect(self._on_item_changed)
def _apply_style(self):
"""스타일 적용"""
theme = self.config.theme
if theme == 'dark':
bg_color = "#0f172a"
alt_bg = "#1e293b"
header_bg = "#334155"
text_color = "#f8fafc"
selected_bg = "#3b82f6"
border_color = "#334155"
hover_bg = "#1e3a5f"
else:
bg_color = "#ffffff"
alt_bg = "#f8fafc"
header_bg = "#e2e8f0"
text_color = "#1e293b"
selected_bg = "#3b82f6"
border_color = "#e2e8f0"
hover_bg = "#dbeafe"
self.setStyleSheet(f"""
QTableWidget {{
background-color: {bg_color};
color: {text_color};
border: 1px solid {border_color};
border-radius: 8px;
font-family: 'GmarketSans';
font-size: 13px;
}}
QTableWidget::item {{
padding: 8px;
border-bottom: 1px solid {border_color};
}}
QTableWidget::item:selected {{
background-color: {selected_bg};
color: white;
}}
QTableWidget::item:hover {{
background-color: {hover_bg};
}}
QTableWidget::item:alternate {{
background-color: {alt_bg};
}}
QHeaderView::section {{
background-color: {header_bg};
color: {text_color};
padding: 12px 8px;
border: none;
border-bottom: 2px solid {border_color};
font-weight: bold;
font-size: 13px;
}}
QScrollBar:vertical {{
background-color: {bg_color};
width: 10px;
border-radius: 5px;
}}
QScrollBar::handle:vertical {{
background-color: {border_color};
border-radius: 5px;
min-height: 20px;
}}
QScrollBar::handle:vertical:hover {{
background-color: {selected_bg};
}}
""")
def _on_selection_changed(self):
"""선택 변경 이벤트"""
selected = self.selectedItems()
if selected:
self.row_selected.emit(selected[0].row())
def _on_double_clicked(self, item: QTableWidgetItem):
"""더블클릭 이벤트"""
self.row_double_clicked.emit(item.row())
def _on_item_changed(self, item: QTableWidgetItem):
"""아이템 변경 이벤트"""
column_name = self.horizontalHeaderItem(item.column()).text()
self.cell_edited.emit(item.row(), column_name, item.text())
def set_columns(self, columns: List[Dict[str, Any]]):
"""
컬럼 설정
Args:
columns: 컬럼 설정 리스트
[{"name": "id", "label": "ID", "width": 50}, ...]
"""
self.setColumnCount(len(columns))
for i, col in enumerate(columns):
header = QTableWidgetItem(col.get("label", col["name"]))
self.setHorizontalHeaderItem(i, header)
if "width" in col:
self.setColumnWidth(i, col["width"])
def add_row(self, data: List[Any], row_data: Any = None):
"""
행 추가
Args:
data: 셀 데이터 리스트
row_data: 행에 저장할 추가 데이터 (레코드 ID 등)
"""
row = self.rowCount()
self.insertRow(row)
for col, value in enumerate(data):
item = QTableWidgetItem(str(value) if value else "")
item.setTextAlignment(Qt.AlignCenter)
if row_data is not None:
item.setData(Qt.UserRole, row_data)
self.setItem(row, col, item)
def get_row_data(self, row: int) -> Any:
"""
행의 추가 데이터 반환
Args:
row: 행 인덱스
Returns:
저장된 데이터
"""
item = self.item(row, 0)
if item:
return item.data(Qt.UserRole)
return None
def clear_rows(self):
"""모든 행 삭제"""
self.setRowCount(0)
def get_selected_row(self) -> int:
"""선택된 행 인덱스 반환"""
selected = self.selectedItems()
if selected:
return selected[0].row()
return -1
def get_selected_data(self) -> Any:
"""선택된 행의 데이터 반환"""
row = self.get_selected_row()
if row >= 0:
return self.get_row_data(row)
return None