Add Dispatch_V0.1.1
This commit is contained in:
@@ -0,0 +1,323 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# gui/components/model_view/_interaction_scenario.py
|
||||
"""Базовый сценарий взаимодействия и менеджер сценариев.
|
||||
|
||||
Архитектура:
|
||||
- InteractionScenario — базовый класс с методами-хуками для событий.
|
||||
- CameraPolicy — декларативная политика камеры для сценария.
|
||||
- InteractionManager — стек сценариев + делегация событий.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum, auto
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from PySide6.QtCore import Qt, QEvent
|
||||
from PySide6.QtGui import QGuiApplication
|
||||
from error_logger import log_exception
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from gui.components.model_view_widget import ModelViewWidget
|
||||
|
||||
|
||||
# ── CameraPolicy ────────────────────────────────────────────────────────────
|
||||
|
||||
class CameraPolicy(Enum):
|
||||
"""Политика камеры для сценария."""
|
||||
FREE = auto() # ПКМ вращение, СКМ панорамирование
|
||||
TOP_VIEW = auto() # Только pan/zoom, без вращения
|
||||
LOCKED = auto() # Камера заблокирована
|
||||
|
||||
|
||||
# ── InteractionScenario ─────────────────────────────────────────────────────
|
||||
|
||||
class InteractionScenario:
|
||||
"""Базовый класс сценария взаимодействия.
|
||||
|
||||
Каждый конкретный сценарий переопределяет нужные хуки.
|
||||
Менеджер передаёт события текущему сценарию через эти методы.
|
||||
"""
|
||||
|
||||
name: str = ""
|
||||
camera_policy: CameraPolicy = CameraPolicy.FREE
|
||||
|
||||
def on_activate(self, mv: "ModelViewWidget") -> None:
|
||||
"""Вызывается при входе в сценарий (push/replace)."""
|
||||
|
||||
def on_deactivate(self, mv: "ModelViewWidget") -> None:
|
||||
"""Вызывается при выходе из сценария (pop/replace/reset)."""
|
||||
|
||||
def on_mouse_press(self, mv: "ModelViewWidget", event) -> bool:
|
||||
"""ЛКМ/ПКМ/СКМ нажатие. Вернуть True если событие поглощено."""
|
||||
return False
|
||||
|
||||
def on_mouse_move(self, mv: "ModelViewWidget", event) -> bool:
|
||||
"""Движение мыши (с нажатой кнопкой). Вернуть True если поглощено."""
|
||||
return False
|
||||
|
||||
def on_mouse_release(self, mv: "ModelViewWidget", event) -> bool:
|
||||
"""Отпускание кнопки мыши. Вернуть True если поглощено."""
|
||||
return False
|
||||
|
||||
def on_hover(self, mv: "ModelViewWidget", event) -> None:
|
||||
"""Движение мыши без нажатых кнопок (hover)."""
|
||||
|
||||
def on_key_press(self, mv: "ModelViewWidget", event) -> bool:
|
||||
"""Нажатие клавиши. Вернуть True если поглощено."""
|
||||
return False
|
||||
|
||||
def on_resume(self, mv: "ModelViewWidget") -> None:
|
||||
"""Вызывается когда сценарий снова стал верхним (после pop вышестоящего).
|
||||
|
||||
По умолчанию вызывает on_activate повторно, чтобы восстановить обработчики.
|
||||
"""
|
||||
self.on_activate(mv)
|
||||
|
||||
def on_widget_mouse_press(self, mv: "ModelViewWidget", event) -> bool:
|
||||
"""mousePressEvent виджета (не eventFilter). Вернуть True если поглощено."""
|
||||
return False
|
||||
|
||||
def on_widget_mouse_move(self, mv: "ModelViewWidget", event) -> bool:
|
||||
"""mouseMoveEvent виджета. Вернуть True если поглощено."""
|
||||
return False
|
||||
|
||||
def on_widget_mouse_release(self, mv: "ModelViewWidget", event) -> bool:
|
||||
"""mouseReleaseEvent виджета. Вернуть True если поглощено."""
|
||||
return False
|
||||
|
||||
|
||||
# ── InteractionManager ──────────────────────────────────────────────────────
|
||||
|
||||
class InteractionManager:
|
||||
"""Стек сценариев взаимодействия.
|
||||
|
||||
- Всегда есть base-layer (камерный сценарий) — он не в стеке.
|
||||
- Стек содержит доменные сценарии (contour_edit, rack_placement...).
|
||||
- Верхний элемент стека — текущий сценарий.
|
||||
- ESC → pop() → автоматический on_deactivate().
|
||||
- reset() → очищает весь стек.
|
||||
"""
|
||||
|
||||
def __init__(self, mv: "ModelViewWidget") -> None:
|
||||
self._mv = mv
|
||||
self._stack: list[InteractionScenario] = []
|
||||
self._camera: InteractionScenario | None = None
|
||||
|
||||
# -- Публичный API --------------------------------------------------------
|
||||
|
||||
def set_camera_scenario(self, scenario: InteractionScenario) -> None:
|
||||
"""Установить базовый камерный сценарий (всегда активен)."""
|
||||
self._camera = scenario
|
||||
|
||||
@property
|
||||
def current(self) -> InteractionScenario | None:
|
||||
"""Текущий (верхний) доменный сценарий, или None."""
|
||||
return self._stack[-1] if self._stack else None
|
||||
|
||||
@property
|
||||
def current_name(self) -> str:
|
||||
"""Имя текущего сценария."""
|
||||
top = self.current
|
||||
return top.name if top else ""
|
||||
|
||||
def is_active(self, name: str) -> bool:
|
||||
"""Проверить, есть ли сценарий с данным именем в стеке."""
|
||||
key = str(name)
|
||||
return any(s.name == key for s in self._stack)
|
||||
|
||||
def push(self, scenario: InteractionScenario) -> None:
|
||||
"""Войти в новый сценарий (добавить на вершину стека)."""
|
||||
scenario.on_activate(self._mv)
|
||||
self._stack.append(scenario)
|
||||
|
||||
def pop(self) -> InteractionScenario | None:
|
||||
"""Выйти из текущего сценария, вернуть его."""
|
||||
if not self._stack:
|
||||
return None
|
||||
scenario = self._stack.pop()
|
||||
scenario.on_deactivate(self._mv)
|
||||
# Восстановить обработчики нижележащего сценария
|
||||
new_top = self.current
|
||||
if new_top is not None:
|
||||
new_top.on_resume(self._mv)
|
||||
return scenario
|
||||
|
||||
def pop_by_name(self, name: str) -> InteractionScenario | None:
|
||||
"""Удалить сценарий по имени (из любого места стека)."""
|
||||
key = str(name)
|
||||
for i, s in enumerate(self._stack):
|
||||
if s.name == key:
|
||||
was_top = (i == len(self._stack) - 1)
|
||||
scenario = self._stack.pop(i)
|
||||
scenario.on_deactivate(self._mv)
|
||||
if was_top:
|
||||
new_top = self.current
|
||||
if new_top is not None:
|
||||
new_top.on_resume(self._mv)
|
||||
return scenario
|
||||
return None
|
||||
|
||||
def replace(self, scenario: InteractionScenario) -> InteractionScenario | None:
|
||||
"""Заменить текущий сценарий новым (без промежуточного on_resume)."""
|
||||
old = None
|
||||
if self._stack:
|
||||
old = self._stack.pop()
|
||||
old.on_deactivate(self._mv)
|
||||
self.push(scenario)
|
||||
return old
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Сбросить весь стек (ESC-уровень)."""
|
||||
while self._stack:
|
||||
scenario = self._stack.pop()
|
||||
try:
|
||||
scenario.on_deactivate(self._mv)
|
||||
except Exception as e:
|
||||
log_exception(__name__, "reset", e)
|
||||
|
||||
# -- Диспетчеризация событий eventFilter ----------------------------------
|
||||
|
||||
def dispatch_event_filter(self, watched, event) -> bool | None:
|
||||
"""Маршрутизация события из eventFilter.
|
||||
|
||||
Returns:
|
||||
True — событие поглощено.
|
||||
False — событие НЕ должно обрабатываться дальше.
|
||||
None — передать в super().eventFilter().
|
||||
"""
|
||||
top = self.current
|
||||
etype = event.type()
|
||||
|
||||
# 1. Нажатие клавиши → текущий сценарий
|
||||
if etype == QEvent.Type.KeyPress:
|
||||
if top and top.on_key_press(self._mv, event):
|
||||
return True
|
||||
|
||||
# 2. Mouse press/move/release → текущий сценарий, затем камера
|
||||
if etype == QEvent.Type.MouseButtonPress:
|
||||
if top and top.on_mouse_press(self._mv, event):
|
||||
return True
|
||||
if self._camera and self._dispatch_camera_press(event):
|
||||
return True
|
||||
|
||||
if etype == QEvent.Type.MouseMove:
|
||||
# robust-классификация drag vs hover:
|
||||
# учитываем Qt event buttons, глобальные кнопки и внутренние drag-флаги камеры.
|
||||
ev_buttons = Qt.MouseButton.NoButton
|
||||
global_buttons = Qt.MouseButton.NoButton
|
||||
try:
|
||||
if hasattr(event, "buttons"):
|
||||
ev_buttons = event.buttons()
|
||||
except Exception as e:
|
||||
log_exception(__name__, "dispatch_event_filter", e)
|
||||
ev_buttons = Qt.MouseButton.NoButton
|
||||
try:
|
||||
global_buttons = QGuiApplication.mouseButtons()
|
||||
except Exception as e:
|
||||
log_exception(__name__, "dispatch_event_filter", e)
|
||||
global_buttons = Qt.MouseButton.NoButton
|
||||
has_buttons = bool(
|
||||
ev_buttons
|
||||
or global_buttons
|
||||
or getattr(self._mv, "_cam_rotate_active", False)
|
||||
or getattr(self._mv, "_cam_pan_active", False)
|
||||
)
|
||||
if has_buttons:
|
||||
# Движение с зажатой кнопкой
|
||||
if top and top.on_mouse_move(self._mv, event):
|
||||
return True
|
||||
cam_result = self._dispatch_camera_move(event)
|
||||
if cam_result is not None:
|
||||
return cam_result
|
||||
# Не отдаём drag-события в VTK-style, чтобы исключить побочный захват камеры.
|
||||
return False
|
||||
else:
|
||||
# Hover (без кнопок)
|
||||
if top:
|
||||
top.on_hover(self._mv, event)
|
||||
# В origin-flow hover нужен для preview-маркера,
|
||||
# но не должен уходить в VTK interactor style.
|
||||
if top.name == "origin_point":
|
||||
# Защита от ложного swallow после завершения origin-сценария:
|
||||
# поглощаем hover только пока реально подключены origin-hover handlers.
|
||||
has_hover_handlers = bool(
|
||||
getattr(self._mv, "_hover_screen_handler", None)
|
||||
or getattr(self._mv, "_hover_handler", None)
|
||||
)
|
||||
if has_hover_handlers:
|
||||
return True
|
||||
# Камера не обрабатывает hover
|
||||
|
||||
if etype == QEvent.Type.MouseButtonRelease:
|
||||
if top and top.on_mouse_release(self._mv, event):
|
||||
return True
|
||||
cam_result = self._dispatch_camera_release(event)
|
||||
if cam_result is not None:
|
||||
return cam_result
|
||||
|
||||
return None
|
||||
|
||||
# -- Диспетчеризация mousePressEvent/Move/Release виджета -----------------
|
||||
|
||||
def dispatch_widget_mouse_press(self, event) -> bool:
|
||||
"""Делегация mousePressEvent виджета текущему сценарию."""
|
||||
top = self.current
|
||||
if top and top.on_widget_mouse_press(self._mv, event):
|
||||
return True
|
||||
return False
|
||||
|
||||
def dispatch_widget_mouse_move(self, event) -> bool:
|
||||
"""Делегация mouseMoveEvent виджета текущему сценарию."""
|
||||
top = self.current
|
||||
if top and top.on_widget_mouse_move(self._mv, event):
|
||||
return True
|
||||
return False
|
||||
|
||||
def dispatch_widget_mouse_release(self, event) -> bool:
|
||||
"""Делегация mouseReleaseEvent виджета текущему сценарию."""
|
||||
top = self.current
|
||||
if top and top.on_widget_mouse_release(self._mv, event):
|
||||
return True
|
||||
return False
|
||||
|
||||
# -- Камерные делегации ---------------------------------------------------
|
||||
|
||||
def _dispatch_camera_press(self, event) -> bool:
|
||||
if not self._camera:
|
||||
return False
|
||||
if getattr(self._mv, "_camera_locked", False):
|
||||
return False
|
||||
top = self.current
|
||||
policy = top.camera_policy if top else CameraPolicy.FREE
|
||||
if policy == CameraPolicy.LOCKED:
|
||||
return False
|
||||
# TOP_VIEW: блокировать вращение (ПКМ), разрешить панорамирование (СКМ)
|
||||
if policy == CameraPolicy.TOP_VIEW:
|
||||
if hasattr(event, "button") and event.button() == Qt.MouseButton.RightButton:
|
||||
# Поглощаем ПКМ, чтобы событие не ушло в VTK-style (иначе возможен zoom/pan).
|
||||
return True
|
||||
return self._camera.on_mouse_press(self._mv, event)
|
||||
|
||||
def _dispatch_camera_move(self, event) -> bool | None:
|
||||
if not self._camera:
|
||||
return None
|
||||
if getattr(self._mv, "_camera_locked", False):
|
||||
return None
|
||||
top = self.current
|
||||
policy = top.camera_policy if top else CameraPolicy.FREE
|
||||
if policy == CameraPolicy.LOCKED:
|
||||
return None
|
||||
return self._camera.on_mouse_move(self._mv, event) or None
|
||||
|
||||
def _dispatch_camera_release(self, event) -> bool | None:
|
||||
if not self._camera:
|
||||
return None
|
||||
if getattr(self._mv, "_camera_locked", False):
|
||||
return None
|
||||
top = self.current
|
||||
policy = top.camera_policy if top else CameraPolicy.FREE
|
||||
if policy == CameraPolicy.TOP_VIEW:
|
||||
if hasattr(event, "button") and event.button() == Qt.MouseButton.RightButton:
|
||||
return True
|
||||
return self._camera.on_mouse_release(self._mv, event) or None
|
||||
Reference in New Issue
Block a user