445 lines
14 KiB
Python
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;
|
|
}
|
|
""") |