Add left node-library dock (grouped, searchable, drag-and-drop vector symbols), right property dock with type-aware grouped editors (incl. geometry, catalogue single/multi, color, value list), zoom/pan/fit canvas view accepting node drops, flow animator, configuration dialog, and the MainWindow wiring menus/toolbar, undo/redo, delete/duplicate, calculate flow, save/open (JSON), and PNG/SVG export. Includes a demo sample network, entry points, and panel + app integration tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
100 lines
3.7 KiB
Python
100 lines
3.7 KiB
Python
"""CanvasView: zoomable/pannable QGraphicsView that accepts node drops."""
|
|
from __future__ import annotations
|
|
|
|
from PyQt6.QtCore import Qt
|
|
from PyQt6.QtGui import QPainter
|
|
from PyQt6.QtWidgets import QGraphicsView
|
|
|
|
from ..panels.node_panel import NODE_MIME
|
|
|
|
MIN_SCALE = 0.15
|
|
MAX_SCALE = 6.0
|
|
|
|
|
|
class CanvasView(QGraphicsView):
|
|
def __init__(self, scene, parent=None):
|
|
super().__init__(scene, parent)
|
|
self.setRenderHints(
|
|
QPainter.RenderHint.Antialiasing | QPainter.RenderHint.SmoothPixmapTransform)
|
|
self.setDragMode(QGraphicsView.DragMode.RubberBandDrag)
|
|
self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
|
|
self.setResizeAnchor(QGraphicsView.ViewportAnchor.AnchorViewCenter)
|
|
self.setAcceptDrops(True)
|
|
self._scale = 1.0
|
|
self._panning = False
|
|
|
|
# -- zoom -------------------------------------------------------------
|
|
def wheelEvent(self, event):
|
|
factor = 1.2 if event.angleDelta().y() > 0 else 1 / 1.2
|
|
new_scale = self._scale * factor
|
|
if new_scale < MIN_SCALE or new_scale > MAX_SCALE:
|
|
return
|
|
self._scale = new_scale
|
|
self.scale(factor, factor)
|
|
|
|
def reset_zoom(self):
|
|
self.resetTransform()
|
|
self._scale = 1.0
|
|
|
|
def fit_to_contents(self):
|
|
rect = self.scene().itemsBoundingRect()
|
|
if rect.isNull():
|
|
return
|
|
self.fitInView(rect.adjusted(-40, -40, 40, 40), Qt.AspectRatioMode.KeepAspectRatio)
|
|
self._scale = self.transform().m11()
|
|
|
|
# -- panning (space or middle mouse) ----------------------------------
|
|
def keyPressEvent(self, event):
|
|
if event.key() == Qt.Key.Key_Space and not self._panning:
|
|
self._panning = True
|
|
self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
|
|
super().keyPressEvent(event)
|
|
|
|
def keyReleaseEvent(self, event):
|
|
if event.key() == Qt.Key.Key_Space:
|
|
self._panning = False
|
|
self.setDragMode(QGraphicsView.DragMode.RubberBandDrag)
|
|
super().keyReleaseEvent(event)
|
|
|
|
def mousePressEvent(self, event):
|
|
if event.button() == Qt.MouseButton.MiddleButton:
|
|
self._panning = True
|
|
self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
|
|
# forward as a left press so ScrollHandDrag engages
|
|
from PyQt6.QtGui import QMouseEvent
|
|
fake = QMouseEvent(event.type(), event.position(), event.globalPosition(),
|
|
Qt.MouseButton.LeftButton, Qt.MouseButton.LeftButton,
|
|
event.modifiers())
|
|
super().mousePressEvent(fake)
|
|
return
|
|
super().mousePressEvent(event)
|
|
|
|
def mouseReleaseEvent(self, event):
|
|
if event.button() == Qt.MouseButton.MiddleButton and self._panning:
|
|
self._panning = False
|
|
self.setDragMode(QGraphicsView.DragMode.RubberBandDrag)
|
|
return
|
|
super().mouseReleaseEvent(event)
|
|
|
|
# -- drops from the node panel ---------------------------------------
|
|
def dragEnterEvent(self, event):
|
|
if event.mimeData().hasFormat(NODE_MIME):
|
|
event.acceptProposedAction()
|
|
else:
|
|
super().dragEnterEvent(event)
|
|
|
|
def dragMoveEvent(self, event):
|
|
if event.mimeData().hasFormat(NODE_MIME):
|
|
event.acceptProposedAction()
|
|
else:
|
|
super().dragMoveEvent(event)
|
|
|
|
def dropEvent(self, event):
|
|
if event.mimeData().hasFormat(NODE_MIME):
|
|
key = bytes(event.mimeData().data(NODE_MIME)).decode("utf-8")
|
|
scene_pos = self.mapToScene(event.position().toPoint())
|
|
self.scene().create_node(key, scene_pos.x(), scene_pos.y())
|
|
event.acceptProposedAction()
|
|
else:
|
|
super().dropEvent(event)
|