handOver2/ui/widgets/clickableLabel.py

445 lines
14 KiB
Python

from PySide6.QtWidgets import QLabel, QMenu, QVBoxLayout, QWidget
from PySide6.QtCore import Qt,Signal, QPoint
from PySide6.QtGui import QMouseEvent, QEnterEvent, QDragLeaveEvent, QCursor
from core.logger import get_logger
logger = get_logger(__name__)
class ClickableLabel(QLabel):
"""
클릭 / 더블클릭 / 우클릭 컨텍스트 메뉴 / 마우스 호버링 / 드래그 / 그리드 스냅 지원 QLabel
[Signals]
- clicked(): 좌클릭
- double_clicked(): 좌측 더블클릭
- right_clicked(pos: QPoint): 우클릭 (로컬 좌표)
- hover_entered(): 마우스 진입
- hover_left(): 마우스 이탈
- drag_started(pos: QPoint): 드래그 시작
- dragging(delta: QPoint): 드래그 중 (이동량)
- drag_finished(): 드래그 종료
/사용예시
label = ClickableLabel("클릭해보세요")
label.clicked.connect(lambda: logger.info("원클릭"))
label.double_clicked.connect(lambda: logger.info("더블클릭"))
label.hover_entered.connect(lambda: logger.info("마우스 진입"))
label.hover_left.connect(lambda: logger.info("마우스 이탈"))
/우클릭 컨텍스트
label.right_clicked.connect(
lambda pos: logger.info(f"우클릭 위치: {pos}")
)
/드래그
label.drag_started.connect(
lambda pos: logger.info(f"드래그 시작 위치: {pos}")
)
label.dragging.connect(
lambda delta: logger.info(f"드래그 이동량: {delta}")
)
label.drag_finished.connect(lambda: logger.info("드래그 종료"))
def on_drag(delta: QPoint):
label.move(label.pos() + delta)
label.dragging.connect(on_drag)
/드래그 시작 시 UI 반응 정의 : 배경색 변경
label.drag_started.connect(lambda _: label.setStyleSheet("background:#d0ebff"))
label.drag_finished.connect(lambda: label.setStyleSheet(""))
호버링 (호버링 사용시 setMouseTracking(True) 필요)
def on_hover_enter():
label.setStyleSheet("background-color: #f0f0f0;")
def on_hover_leave():
label.setStyleSheet("background-color: none;")
label.hover_entered.connect(on_hover_enter)
label.hover_left.connect(on_hover_leave)
"""
clicked = Signal() # 좌클릭
double_clicked = Signal() # 더블클릭
right_clicked = Signal(QPoint) # 우클릭
hover_entered = Signal() # 마우스 진입
hover_left = Signal() # 마우스 이탈
drag_started = Signal(QPoint) # 드래그 시작 위치
dragging = Signal(QPoint) # 드래그 이동량
drag_finished = Signal() # 드래그 종료
def __init__(
self,
text: str = "",
parent=None,
hover_text=None,
*,
enable_click: bool = True,
enable_double_click: bool = True,
enable_right_click: bool = False,
enable_hover: bool = True,
enable_drag: bool = False,
enable_context_menu: bool = False,
context_menu_builder=None,
enable_grid_snap: bool = False,
grid_size: int = 20,
**kwargs
):
super().__init__(text, parent)
# -----------------------------
# 기능 활성화 플래그
# -----------------------------
self.enable_click = enable_click
self.enable_double_click = enable_double_click
self.enable_right_click = enable_right_click
self.enable_hover = enable_hover
self.enable_drag = enable_drag
self.enable_context_menu = enable_context_menu
self._context_menu_builder = context_menu_builder
self.enable_grid_snap = enable_grid_snap
# -----------------------------
# 드래그 / 그리드 설정
# -----------------------------
self.grid_size = grid_size
self._drag_threshold = 5 # px (클릭 vs 드래그 구분)
self._press_pos: QPoint | None = None
self._dragging = False
# -----------------------------
# UI 기본 설정
# -----------------------------
self.setCursor(Qt.PointingHandCursor)
# 텍스트 중앙 정렬
self.setAlignment(Qt.AlignCenter)
self._hover_text = hover_text
self._hover_text_provider: callable | None = None
self._hover_popup = None
# hover는 mouse tracking 필수
if self.enable_hover:
self.setMouseTracking(True)
else:
self.setMouseTracking(False)
# -------------------------------------------------
# 마우스 누름
# -------------------------------------------------
def mousePressEvent(self, event: QMouseEvent):
if event.button() == Qt.LeftButton:
if self.enable_click:
self.clicked.emit()
if self.enable_drag:
self._press_pos = event.pos()
self._dragging = False
elif event.button() == Qt.RightButton:
if self.enable_right_click:
self.right_clicked.emit(event.pos())
if self.enable_context_menu:
self._show_context_menu(event.globalPos())
super().mousePressEvent(event)
# -------------------------------------------------
# 마우스 이동 (드래그)
# -------------------------------------------------
def mouseMoveEvent(self, event: QMouseEvent):
if not self.enable_drag or self._press_pos is None:
super().mouseMoveEvent(event)
return
delta = event.pos() - self._press_pos
if not self._dragging and delta.manhattanLength() >= self._drag_threshold:
self._dragging = True
self.drag_started.emit(self._press_pos)
if self._dragging:
self.dragging.emit(delta)
self._move_with_grid_snap(delta)
super().mouseMoveEvent(event)
# -------------------------------------------------
# 마우스 해제
# -------------------------------------------------
def mouseReleaseEvent(self, event: QMouseEvent):
if event.button() == Qt.LeftButton and self.enable_drag:
if self._dragging:
self.drag_finished.emit()
self._dragging = False
self._press_pos = None
super().mouseReleaseEvent(event)
# -------------------------------------------------
# 더블클릭
# -------------------------------------------------
def mouseDoubleClickEvent(self, event: QMouseEvent):
if event.button() == Qt.LeftButton and self.enable_double_click:
self.double_clicked.emit()
super().mouseDoubleClickEvent(event)
# -------------------------------------------------
# 호버
# 사용법 (주의:provider 함수에서 DB 조회, 네트워크 요청, 긴 계산 등을 포함하면 안됨,필요한 경우 캐시+갱신 이벤트구조로 구현)
# label.set_hover_text_provider(
# lambda: f"""
# 상태: {node.status}
# 전압: {node.voltage}V
# 최근 점검: {node.last_check}
# """.strip()
# )
# -------------------------------------------------
def enterEvent(self, event: QEnterEvent):
if self.enable_hover:
self.hover_entered.emit()
text = self._get_hover_text()
if text:
self._hover_popup = HoverInfoPopup(text)
pos = QCursor.pos() + QPoint(10, 10) # 마우스 커서 위치에서 약간 오프셋
self._hover_popup.move(pos)
self._hover_popup.show()
super().enterEvent(event)
def leaveEvent(self, event: QDragLeaveEvent):
if not self.enable_hover:
super().leaveEvent(event)
return
# popup이 있고, 마우스가 popup 안에 있으면 닫지 않음
if self._hover_popup and self._hover_popup.is_mouse_inside():
return
self.hover_left.emit()
if self.enable_hover:
if self._hover_popup:
self._hover_popup.close()
self._hover_popup = None
super().leaveEvent(event)
def set_hover_text(self, text: str):
"""
hover 정보 텍스트 설정 (즉시 반영)
"""
self._hover_text = text
self._hover_text_provider = None
if self._hover_popup:
self._hover_popup.set_text(text)
def _get_hover_text(self) -> str | None:
if self._hover_text_provider:
return self._hover_text_provider()
return self._hover_text
def clear_hover_text(self):
"""
hover 정보 제거
"""
self._hover_text = None
self._hover_text_provider = None
if self._hover_popup:
self._hover_popup.close()
self._hover_popup = None
def set_hover_text_provider(self, provider: callable):
"""
hover 시점에 호출되는 함수 등록
"""
self._hover_text_provider = provider
self._hover_text = None
if self._hover_popup:
self._hover_popup.set_text(provider())
# =================================================
# 내부 기능
# =================================================
def _show_context_menu(self, global_pos):
if not self._context_menu_builder:
return
menu = QMenu(self)
self._context_menu_builder(menu, self) # 컨텍스트 메뉴 빌더 호출
menu.exec(global_pos)
''' 외부에서 컨텍스트 메뉴 빌더 정의
def build_label_menu(menu: QMenu, label: ClickableLabel):
menu.addAction("편집", lambda: edit_label(label))
menu.addAction("삭제", lambda: delete_label(label))
menu.addSeparator()
menu.addAction("정보", lambda: show_info(label))
def edit_label(label):
logger.info(f"편집: {label.text()}")
def delete_label(label):
label.deleteLater()
def show_info(label):
logger.info(f"위치: {label.pos()}")
객체 생성시 컨텍스트 메뉴 빌더 전달
label = ClickableLabel(
"NODE-1",
context_menu_builder=build_label_menu
)
'''
def _move_with_grid_snap(self, delta: QPoint):
new_pos = self.pos() + delta
if self.enable_grid_snap:
x = round(new_pos.x() / self.grid_size) * self.grid_size
y = round(new_pos.y() / self.grid_size) * self.grid_size
self.move(QPoint(x, y))
else:
self.move(new_pos)
class PopupTheme:
LIGHT = "light"
DARK = "dark"
class HoverInfoPopup(QWidget):
"""
마우스 호버 시 정보 팝업 표시 QWidget
사용예시
popup = HoverInfoPopup("호버 정보")
popup.show()
"""
def __init__(self, text: str, theme=PopupTheme.DARK, parent=None):
super().__init__(parent)
# -----------------------------
# 테마 설정
# -----------------------------
self._theme = theme
self.setWindowFlags(
Qt.ToolTip |
Qt.FramelessWindowHint |
Qt.BypassWindowManagerHint
)
self.setAttribute(Qt.WA_ShowWithoutActivating)
self.apply_theme(theme)
# -----------------------------
# 윈도우 설정
# -----------------------------
self.setAttribute(Qt.WA_TransparentForMouseEvents, False)
# -----------------------------
# 드래그 설정
# -----------------------------
self._drag_start_pos = None
# -----------------------------
# UI 설정
# -----------------------------
layout = QVBoxLayout(self)
layout.setContentsMargins(8, 6, 8, 6)
# -----------------------------
# 라벨 설정
# -----------------------------
self.label = QLabel(text)
self.label.setTextInteractionFlags(
Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard
)
layout.addWidget(self.label)
self.setStyleSheet("""
QWidget {
background-color: #ffffe1;
border: 1px solid #c8c8a9;
border-radius: 4px;
font-size: 12px;
}
""")
# -----------------------------
# 드래그 이동
# -----------------------------
def mousePressEvent(self, event: QMouseEvent):
if event.button() == Qt.LeftButton:
self._drag_start_pos = event.globalPos() - self.frameGeometry().topLeft()
def mouseMoveEvent(self, event: QMouseEvent):
if self._drag_start_pos:
self.move(event.globalPos() - self._drag_start_pos)
def mouseReleaseEvent(self, event: QMouseEvent):
self._drag_start_pos = None
# -------------------------------------------------
# 텍스트 설정
# -------------------------------------------------
def set_text(self, text: str):
self.label.setText(text)
self.adjustSize() # 내용 바뀌면 크기 자동 조정
# -------------------------------------------------
# 마우스 위치 확인
# -------------------------------------------------
def is_mouse_inside(self) -> bool:
pos = QCursor.pos()
return self.geometry().contains(pos)
# -------------------------------------------------
# 마우스 이탈 시 팝업 닫기
# -------------------------------------------------
def leaveEvent(self, event):
self.close()
super().leaveEvent(event)
# -------------------------------------------------
# 테마 적용
# -------------------------------------------------
def apply_theme(self, theme: str):
if theme == PopupTheme.DARK:
self.setStyleSheet("""
QWidget {
background-color: #2b2b2b;
color: #eaeaea;
border: 1px solid #555;
border-radius: 6px;
}
""")
else:
self.setStyleSheet("""
QWidget {
background-color: #ffffe1;
color: #202020;
border: 1px solid #c8c8a9;
border-radius: 6px;
}
""")