Skip to content

Commit

Permalink
Extract cutline from graphics view
Browse files Browse the repository at this point in the history
  • Loading branch information
don4get committed Jun 19, 2020
1 parent 3f6a900 commit 2da65f2
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 92 deletions.
42 changes: 41 additions & 1 deletion nodedge/edge.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import logging
from collections import OrderedDict
from enum import IntEnum
from typing import Optional
from typing import Callable, List, Optional

from nodedge.graphics_edge import (
GraphicsEdge,
Expand Down Expand Up @@ -37,6 +37,9 @@ class Edge(Serializable):
[NODE 1]------EDGE------[NODE 2]
"""

edgeValidators: List[Callable] = [] #: class variable containing list of
# registered edge validators

def __init__(
self,
scene: "Scene", # type: ignore
Expand Down Expand Up @@ -303,6 +306,43 @@ def remove(self, silentForSocket: Optional[Socket] = None, silent: bool = False)
except Exception as e:
dumpException(e)

@classmethod
def getEdgeValidators(cls):
"""Return the list of Edge Validator Callbacks"""
return cls.edgeValidators

@classmethod
def registerEdgeValidator(cls, validatorCallback: Callable):
"""Register Edge Validator Callback
:param validatorCallback: A function handle to validate Edge
:type validatorCallback: `function`
"""
cls.edgeValidators.append(validatorCallback)

@classmethod
def validateEdge(cls, startSocket: Socket, endSocket: Socket) -> bool:
"""Validate Edge against all registered `Edge Validator Callbacks`
:param startSocket: Starting :class:`~nodedge.socket.Socket` of Edge to check
:type startSocket: :class:`~nodedge.socket.Socket`
:param endSocket: Target/End :class:`~nodedge.socket.Socket` of Edge to check
:type endSocket: :class:`~nodedge.socket.Socket`
:return: ``True`` if the Edge is valid, ``False`` otherwise
:rtype: ``bool``
"""
for validator in cls.getEdgeValidators():
if not validator(startSocket, endSocket):
return False
return True

def reconnect(self, sourceSocket: Socket, targetSocket: Socket):
"""Helper function which reconnects edge `sourceSocket` to `targetSocket`"""
if self.sourceSocket == sourceSocket:
self.sourceSocket = targetSocket
elif self.targetSocket == sourceSocket:
self.targetSocket = targetSocket

def serialize(self):
return OrderedDict(
[
Expand Down
26 changes: 25 additions & 1 deletion nodedge/edge_dragging.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from enum import IntEnum
from typing import Optional

from PyQt5.QtWidgets import QGraphicsItem

from nodedge.edge import Edge, EdgeType
from nodedge.graphics_socket import GraphicsSocket
from nodedge.socket import Socket
Expand Down Expand Up @@ -35,6 +37,25 @@ def __init__(self, graphicsView: "GraphicsView") -> None: # type: ignore
self.__logger = logging.getLogger(__name__)
self.__logger.setLevel(logging.INFO)

def update(self, item: Optional[QGraphicsItem]):
if isinstance(item, GraphicsSocket):
graphicsSocket: GraphicsSocket = item
if self.mode == EdgeDraggingMode.NOOP:
self.mode = EdgeDraggingMode.EDGE_DRAG
self.__logger.debug(f"Drag mode: {self.mode}")
self.startEdgeDragging(graphicsSocket)
return
elif self.mode == EdgeDraggingMode.EDGE_DRAG:
ret = self.endEdgeDragging(graphicsSocket)
if ret:
self.__logger.debug(f"Drag mode: {self.mode}")
return
else:
if self.mode == EdgeDraggingMode.EDGE_DRAG:
self.mode = EdgeDraggingMode.NOOP
self.endEdgeDragging(None)
self.__logger.debug("End dragging edge early")

def startEdgeDragging(self, graphicsSocket: GraphicsSocket):
"""
Handle the start of dragging an :class:`~nodedge.edge.Edge` operation.
Expand All @@ -55,7 +76,7 @@ def startEdgeDragging(self, graphicsSocket: GraphicsSocket):
except Exception as e:
dumpException(e)

def endEdgeDragging(self, graphicsSocket: GraphicsSocket):
def endEdgeDragging(self, graphicsSocket: Optional[GraphicsSocket]):
"""
Handle the end of dragging an :class:`~nodedge.edge.Edge` operation.
Expand All @@ -81,6 +102,9 @@ def endEdgeDragging(self, graphicsSocket: GraphicsSocket):
if not self.dragStartSocket.allowMultiEdges:
self.dragStartSocket.removeAllEdges()

if graphicsSocket is None:
return

if not graphicsSocket.socket.allowMultiEdges:
graphicsSocket.socket.removeAllEdges()

Expand Down
18 changes: 17 additions & 1 deletion nodedge/editor_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,14 @@
from nodedge.graphics_view import GraphicsView
from nodedge.node import Node
from nodedge.scene import InvalidFile, Scene
from nodedge.utils import dumpException


class EditorWidget(QWidget):
""":class:`~nodedge.editor_widget.EditorWidget` class"""

SceneClass = Scene
GraphicsViewClass = GraphicsView
"""
:class:`~nodedge.editor_widget.EditorWidget` class
Expand Down Expand Up @@ -65,7 +69,9 @@ def initUI(self):

self.setLayout(self.layout)
self.scene: Scene = self.__class__.SceneClass()
self.view: GraphicsView = GraphicsView(self.scene.graphicsScene, self)
self.view: GraphicsView = self.__class__.GraphicsViewClass(
self.scene.graphicsScene, self
)
self.layout.addWidget(self.view)

@property
Expand Down Expand Up @@ -180,12 +186,22 @@ def loadFile(self, filename: str) -> bool:
QApplication.restoreOverrideCursor()
self.evalNodes()
return True
except FileNotFoundError as e:
self.__logger.warning(f"File {filename} not found: {e}")
dumpException(e)
QMessageBox.warning(
self,
"Error loading %s" % os.path.basename(filename),
str(e).replace("[Errno 2]", ""),
)
return False
except InvalidFile as e:
self.__logger.warning(f"Error loading {filename}: {e}")
QApplication.restoreOverrideCursor()
QMessageBox.warning(
self, f"Error loading {os.path.basename(filename)}", str(e)
)
dumpException(e)
return False

def saveFile(self, filename: Optional[str] = None) -> bool:
Expand Down
18 changes: 18 additions & 0 deletions nodedge/editor_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ def saveFileAs(self):
if filename == "":
return

self.beforeSaveFileAs(self.currentEditorWidget, filename)
self.currentEditorWidget.saveFile(filename)
self.statusBar().showMessage(
f"Successfully saved to {self.currentEditorWidget.shortName}", 5000
Expand Down Expand Up @@ -481,3 +482,20 @@ def writeSettings(self):
settings = QSettings(self.companyName, self.productName)
settings.setValue("pos", self.pos())
settings.setValue("size", self.size())

def beforeSaveFileAs(
self, currentEditorWidget: EditorWidget, filename: str
) -> None:
"""
Event triggered after choosing filename and before actual fileSave(). Current
:class:`~nodedge.editor_widget.EditorWidget` is passed because focus is lost
after asking with ``QFileDialog`` and therefore `getCurrentNodeEditorWidget`
will return ``None``.
:param currentEditorWidget: :class:`~nodedge.editor_widget.EditorWidget`
currently focused
:type currentEditorWidget: :class:`~nodedge.editor_widget.EditorWidget`
:param filename: name of the file to be saved
:type filename: ``str``
"""
pass
116 changes: 106 additions & 10 deletions nodedge/graphics_cut_line.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,127 @@
# -*- coding: utf-8 -*-
"""
Graphics cut line module containing :class:`~nodedge.graphics_cut_line.GraphicsCutLine` class.
"""
"""Graphics cut line module containing
:class:`~nodedge.graphics_cut_line.GraphicsCutLine` class. """

from typing import List, Optional, cast
from enum import IntEnum
from typing import List, Optional

from PyQt5.QtCore import QPointF, QRectF, Qt
from PyQt5.QtGui import QPainter, QPainterPath, QPen, QPolygonF
from PyQt5.QtWidgets import QGraphicsItem, QStyleOptionGraphicsItem, QWidget
from PyQt5.QtCore import QEvent, QPointF, QRectF, Qt
from PyQt5.QtGui import QMouseEvent, QPainter, QPainterPath, QPen, QPolygonF
from PyQt5.QtWidgets import (
QApplication,
QGraphicsItem,
QStyleOptionGraphicsItem,
QWidget,
)


class CutLineMode(IntEnum):
"""
:class:`~nodedge.graphics_cut_line.CutLineMode` class.
"""

NOOP = 1 #: Mode representing ready state
CUTTING = 2 #: Mode representing when we draw a cutting edge


class CutLine:
"""
:class:`~nodedge.graphics_cut_line.CutLine` class.
"""

def __init__(self, graphicsView: "GraphicsView"): # type: ignore
self.mode: CutLineMode = CutLineMode.NOOP
self.graphicsCutLine: GraphicsCutLine = GraphicsCutLine()
self.graphicsView = graphicsView
self.graphicsView.graphicsScene.addItem(self.graphicsCutLine)

def update(self, event: QMouseEvent) -> Optional[QMouseEvent]:
"""
Update the state machine of the cut line as well as the graphics cut line.
:param event: Event triggering the update
:type event: ``QMouseEvent``
:return: Optional modified event needed by
:class:`~nodedge.graphics_view.GraphicsView`
:rtype: Optional[QMouseEvent]
"""
eventButton: Qt.MouseButton = event.button()
eventType: QEvent.Type = event.type()
eventScenePos = self.graphicsView.mapToScene(event.pos())
eventModifiers: Qt.KeyboardModifiers = event.modifiers()
if self.mode == CutLineMode.NOOP:
if (
eventType == QEvent.MouseButtonPress
and eventButton == Qt.LeftButton
and int(eventModifiers) & Qt.ControlModifier
):

self.mode = CutLineMode.CUTTING
QApplication.setOverrideCursor(Qt.CrossCursor)

return QMouseEvent(
QEvent.MouseButtonRelease,
event.localPos(),
event.screenPos(),
Qt.LeftButton,
Qt.NoButton,
event.modifiers(),
)

if self.mode == CutLineMode.CUTTING:
if event.type() == QEvent.MouseMove:
self.graphicsCutLine.linePoints.append(eventScenePos)
self.graphicsCutLine.update()
elif (
eventType == QEvent.MouseButtonRelease and eventButton == Qt.LeftButton
):
self.cutIntersectingEdges()
self.graphicsCutLine.linePoints = []
self.graphicsCutLine.update()
QApplication.setOverrideCursor(Qt.ArrowCursor)
self.mode = CutLineMode.NOOP

return None

def cutIntersectingEdges(self) -> None:
"""
Compare which :class:`~nodedge.edge.Edge`s intersect with current
:class:`~nodedge.graphics_cut_line.GraphicsCutLine` and delete them safely.
"""
scene: "Scene" = self.graphicsView.graphicsScene.scene # type: ignore

for ix in range(len(self.graphicsCutLine.linePoints) - 1):
p1 = self.graphicsCutLine.linePoints[ix]
p2 = self.graphicsCutLine.linePoints[ix + 1]

# @TODO: Notify intersecting edges once.
# we could collect all touched nodes, and notify them once after
# all edges removed we could cut 3 edges leading to a single editor
# this will notify it 3x maybe we could use some Notifier class with
# methods collect() and dispatch()
for edge in scene.edges:
if edge.graphicsEdge.intersectsWith(p1, p2):
edge.remove()

scene.history.store("Delete cut edges.")


class GraphicsCutLine(QGraphicsItem):
""":class:`~nodedge.graphics_cut_line.GraphicsCutLine` class
Cutting Line used for cutting multiple `Edges` with one stroke"""

def __init__(self, parent=None):
def __init__(self, parent: Optional[QGraphicsItem] = None) -> None:
"""
:param parent: parent widget
:type parent: ``QWidget``
:type parent: ``Optional[QGraphicsItem]``
"""

super().__init__(parent)

self.linePoints: List[QPointF] = []
self._pen: QPen = QPen(Qt.gray)
self._pen.setWidth(2.0)
self._pen.setWidth(2)
self._pen.setDashPattern([3, 3])

self.setZValue(2)
Expand Down
2 changes: 1 addition & 1 deletion nodedge/graphics_scene.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:
item is not None
and item not in self.selectedItems()
and item.parentItem() not in self.selectedItems()
and not event.modifiers() & Qt.ShiftModifier # type: ignore
and not int(event.modifiers()) & Qt.ShiftModifier
):
self.__logger.debug(f"Pressed item: {item}")
self.__logger.debug(f"Pressed parent item: {item.parentItem()}")
Expand Down

0 comments on commit 2da65f2

Please sign in to comment.