From 335d762fb2433ae2149808a8042a0d57a2ce781f Mon Sep 17 00:00:00 2001 From: Johnny Chan Date: Mon, 4 Sep 2017 00:31:52 +1200 Subject: [PATCH 1/3] rubberband test --- BlueprintNodeGraph/viewer.py | 74 ++++++++++++++++++++++++++++++------ README.md | 5 +-- 2 files changed, 65 insertions(+), 14 deletions(-) diff --git a/BlueprintNodeGraph/viewer.py b/BlueprintNodeGraph/viewer.py index 905932c2..652a5a44 100644 --- a/BlueprintNodeGraph/viewer.py +++ b/BlueprintNodeGraph/viewer.py @@ -9,6 +9,44 @@ from .port import PortItem +class NodeOverlay(QtGui.QWidget): + + def __init__(self, parent=None): + super(NodeOverlay, self).__init__(parent) + self.rubber_band = None + self.origin = None + + def showEvent(self, event): + super(NodeOverlay, self).showEvent(event) + print 'foo' + + def mousePressEvent(self, event): + print 'bar' + self.origin = event.pos() + if not self.rubber_band: + self.rubber_band = QtGui.QRubberBand( + QtGui.QRubberBand.Rectangle, self + ) + self.rubber_band.setGeometry( + QtCore.QRect(self.origin, QtCore.QSize()) + ) + # self.rubber_band.show() + super(NodeOverlay, self).mousePressEvent(event) + + def mouseMoveEvent(self, event): + print 'moveeee' + self.rubber_band.setGeometry( + QtCore.QRect(self.origin, event.pos()).normalized() + ) + super(NodeOverlay, self).mouseMoveEvent(event) + + def mouseReleaseEvent(self, event): + if self.rubber_band: + self.rubber_band.hide() + self.hide() + super(NodeOverlay, self).mouseReleaseEvent(event) + + class NodeViewer(QtGui.QGraphicsView): def __init__(self, parent=None, scene=None): @@ -25,7 +63,8 @@ def __init__(self, parent=None, scene=None): self._connection_pipe = None self._active_pipe = None self._start_port = None - self._cursor_pos = (0.0, 0.0) + self._origin = None + self._rubber_band = QtGui.QRubberBand(QtGui.QRubberBand.Rectangle, self) self.LMB_state = False self.RMB_state = False @@ -36,9 +75,6 @@ def __init__(self, parent=None, scene=None): def __str__(self): return '{}()'.format(self.__class__.__name__) - def _set_cursor_pos(self, event): - self._cursor_pos = (event.x(), event.y()) - def _set_viewer_zoom(self, value): max_zoom = 12 min_zoom = max_zoom * -1 @@ -97,6 +133,10 @@ def _setup_shortcuts(self): self.addAction(sel_all_actn) self.addAction(del_node_actn) + def resizeEvent(self, event): + self._overlay_widget.resize(event.size()) + super(NodeViewer, self).resizeEvent(event) + def mousePressEvent(self, event): if event.button() == QtCore.Qt.LeftButton: self.LMB_state = True @@ -104,7 +144,12 @@ def mousePressEvent(self, event): self.RMB_state = True elif event.button() == QtCore.Qt.MiddleButton: self.MMB_state = True - self._set_cursor_pos(event) + self._origin = event.pos() + if self.LMB_state: + self._rubber_band.setGeometry( + QtCore.QRect(self._origin, QtCore.QSize()) + ) + self._rubber_band.show() super(NodeViewer, self).mousePressEvent(event) def mouseReleaseEvent(self, event): @@ -114,22 +159,29 @@ def mouseReleaseEvent(self, event): self.RMB_state = False elif event.button() == QtCore.Qt.MiddleButton: self.MMB_state = False - self._set_cursor_pos(event) + + self._rubber_band.hide() + super(NodeViewer, self).mouseReleaseEvent(event) def mouseMoveEvent(self, event): modifiers = QtGui.QApplication.keyboardModifiers() alt_pressed = modifiers == QtCore.Qt.AltModifier if self.MMB_state or (self.LMB_state and alt_pressed): - pos_x = (event.x() - self._cursor_pos[0]) - pos_y = (event.y() - self._cursor_pos[1]) + pos_x = (event.x() - self._origin.x()) + pos_y = (event.y() - self._origin.y()) self._set_viewer_pan(pos_x, pos_y) - self._set_cursor_pos(event) elif self.RMB_state: - pos_x = (event.x() - self._cursor_pos[0]) + pos_x = (event.x() - self._origin.x()) zoom = 0.1 if pos_x > 0 else -0.1 self._set_viewer_zoom(zoom) - self._set_cursor_pos(event) + + if self.LMB_state: + self._rubber_band.setGeometry( + QtCore.QRect(self._origin, event.pos()).normalized() + ) + + self._origin = event.pos() super(NodeViewer, self).mouseMoveEvent(event) def wheelEvent(self, event): diff --git a/README.md b/README.md index dc003f42..f04aa2cb 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -### Blueprint Node Graph +### Node Graph This repository contains a work in progress node graph that I'm working on in my spare time, as I wanted to learn how to write a node graph in python using [PySide](http://pyside.github.io/docs/pyside/). -`Blueprint Node Graph` is a node graph widget that can be implemented and repurposed into applications +A node graph widget that can be implemented and repurposed into applications that supports PySide. ![screencap01](https://raw.githubusercontent.com/jchanvfx/bpNodeGraph/master/screenshot.png) @@ -18,4 +18,3 @@ select all nodes : `Ctrl + A`
delete selected node(s) : `Backspace` or `Delete`
save session layout : `Ctrl + S`
open session layout : `Ctrl + O`
->*`(node graph saved in json format with ".ngraph" file extension)`* From 826b22cc6cccf474d1ed6b42418ec002358edd23 Mon Sep 17 00:00:00 2001 From: Johnny Chan Date: Tue, 5 Sep 2017 23:01:24 +1200 Subject: [PATCH 2/3] Selection Square & Pipe - increased pipe selection area. - implemented rubberband square. --- BlueprintNodeGraph/interface.py | 12 ++++ BlueprintNodeGraph/pipe.py | 20 +++--- BlueprintNodeGraph/viewer.py | 111 ++++++++++++++------------------ 3 files changed, 71 insertions(+), 72 deletions(-) diff --git a/BlueprintNodeGraph/interface.py b/BlueprintNodeGraph/interface.py index 0a8080fb..a2cd75a4 100644 --- a/BlueprintNodeGraph/interface.py +++ b/BlueprintNodeGraph/interface.py @@ -362,6 +362,18 @@ def selected_nodes(self): nodes.append(Node(node=node._node_item)) return nodes + def select_all(self): + """ + Select all nodes in the current node graph. + """ + self._viewer.select_all_nodes() + + def clear_selection(self): + """ + Clears the selection in the node graph. + """ + self._viewer.clear_selection() + def get_node(self, name): """ Returns a node object that matches the name. diff --git a/BlueprintNodeGraph/pipe.py b/BlueprintNodeGraph/pipe.py index 9cc3df18..504b02ec 100644 --- a/BlueprintNodeGraph/pipe.py +++ b/BlueprintNodeGraph/pipe.py @@ -41,9 +41,10 @@ def __str__(self): input_node, input_name, output_node, output_name ) - def mousePressEvent(self, event): - super(Pipe, self).mousePressEvent(event) - self.viewer_start_connection(event.scenePos()) + # disabled as pipe selection is done in the viewer. + # def mousePressEvent(self, event): + # super(Pipe, self).mousePressEvent(event) + # self.viewer_start_connection(event.scenePos()) def paint(self, painter, option, widget): color = self._color @@ -124,12 +125,13 @@ def port_from_pos(self, pos, reverse=False): port = self.input_port if reverse else self.output_port return port - def viewer_start_connection(self, pos): - if not self.scene(): - return - start_port = self.port_from_pos(pos, True) - viewer = self.scene().viewer() - viewer.start_connection(start_port) + # disabled as pipe selection is done in the viewer. + # def viewer_start_connection(self, pos): + # if not self.scene(): + # return + # start_port = self.port_from_pos(pos, True) + # viewer = self.scene().viewer() + # viewer.start_connection(start_port) def viewer_pipe_layout(self): if self.scene(): diff --git a/BlueprintNodeGraph/viewer.py b/BlueprintNodeGraph/viewer.py index 652a5a44..2146de91 100644 --- a/BlueprintNodeGraph/viewer.py +++ b/BlueprintNodeGraph/viewer.py @@ -9,44 +9,6 @@ from .port import PortItem -class NodeOverlay(QtGui.QWidget): - - def __init__(self, parent=None): - super(NodeOverlay, self).__init__(parent) - self.rubber_band = None - self.origin = None - - def showEvent(self, event): - super(NodeOverlay, self).showEvent(event) - print 'foo' - - def mousePressEvent(self, event): - print 'bar' - self.origin = event.pos() - if not self.rubber_band: - self.rubber_band = QtGui.QRubberBand( - QtGui.QRubberBand.Rectangle, self - ) - self.rubber_band.setGeometry( - QtCore.QRect(self.origin, QtCore.QSize()) - ) - # self.rubber_band.show() - super(NodeOverlay, self).mousePressEvent(event) - - def mouseMoveEvent(self, event): - print 'moveeee' - self.rubber_band.setGeometry( - QtCore.QRect(self.origin, event.pos()).normalized() - ) - super(NodeOverlay, self).mouseMoveEvent(event) - - def mouseReleaseEvent(self, event): - if self.rubber_band: - self.rubber_band.hide() - self.hide() - super(NodeOverlay, self).mouseReleaseEvent(event) - - class NodeViewer(QtGui.QGraphicsView): def __init__(self, parent=None, scene=None): @@ -63,7 +25,8 @@ def __init__(self, parent=None, scene=None): self._connection_pipe = None self._active_pipe = None self._start_port = None - self._origin = None + self._origin_pos = None + self._previous_pos = None self._rubber_band = QtGui.QRubberBand(QtGui.QRubberBand.Rectangle, self) self.LMB_state = False @@ -106,9 +69,16 @@ def _delete_selection(self): return self.delete_nodes(nodes) - def _select_all(self): - for node in self.all_nodes(): - node.setSelected(True) + def _nearby_items(self, pos, item_type=None, search_area=20): + rect = QtCore.QRect( + pos.x() - (search_area / 2), pos.y() - (search_area / 2), + search_area, search_area + ) + items = [] + for item in self.scene().items(rect): + if not item_type or isinstance(item, item_type): + items.append(item) + return items def _setup_shortcuts(self): open_actn = QtGui.QAction('Open Session Layout', self) @@ -123,7 +93,7 @@ def _setup_shortcuts(self): fit_zoom_actn.triggered.connect(self.center_selection) sel_all_actn = QtGui.QAction('Select All', self) sel_all_actn.setShortcut('Ctrl+A') - sel_all_actn.triggered.connect(self._select_all) + sel_all_actn.triggered.connect(self.select_all_nodes) del_node_actn = QtGui.QAction('Delete Selection', self) del_node_actn.setShortcuts(['Del', 'Backspace']) del_node_actn.triggered.connect(self._delete_selection) @@ -134,7 +104,6 @@ def _setup_shortcuts(self): self.addAction(del_node_actn) def resizeEvent(self, event): - self._overlay_widget.resize(event.size()) super(NodeViewer, self).resizeEvent(event) def mousePressEvent(self, event): @@ -144,10 +113,13 @@ def mousePressEvent(self, event): self.RMB_state = True elif event.button() == QtCore.Qt.MiddleButton: self.MMB_state = True - self._origin = event.pos() - if self.LMB_state: + self._origin_pos = event.pos() + self._previous_pos = event.pos() + + items = self._nearby_items(self.mapToScene(event.pos()), None, 20) + if self.LMB_state and not items: self._rubber_band.setGeometry( - QtCore.QRect(self._origin, QtCore.QSize()) + QtCore.QRect(self._previous_pos, QtCore.QSize()) ) self._rubber_band.show() super(NodeViewer, self).mousePressEvent(event) @@ -160,7 +132,9 @@ def mouseReleaseEvent(self, event): elif event.button() == QtCore.Qt.MiddleButton: self.MMB_state = False - self._rubber_band.hide() + if self._rubber_band.isVisible(): + self._rubber_band.hide() + self.scene().update() super(NodeViewer, self).mouseReleaseEvent(event) @@ -168,20 +142,20 @@ def mouseMoveEvent(self, event): modifiers = QtGui.QApplication.keyboardModifiers() alt_pressed = modifiers == QtCore.Qt.AltModifier if self.MMB_state or (self.LMB_state and alt_pressed): - pos_x = (event.x() - self._origin.x()) - pos_y = (event.y() - self._origin.y()) + pos_x = (event.x() - self._previous_pos.x()) + pos_y = (event.y() - self._previous_pos.y()) self._set_viewer_pan(pos_x, pos_y) elif self.RMB_state: - pos_x = (event.x() - self._origin.x()) + pos_x = (event.x() - self._previous_pos.x()) zoom = 0.1 if pos_x > 0 else -0.1 self._set_viewer_zoom(zoom) - if self.LMB_state: - self._rubber_band.setGeometry( - QtCore.QRect(self._origin, event.pos()).normalized() - ) + if self.LMB_state and self._rubber_band.isVisible(): + rect = QtCore.QRect(self._origin_pos, event.pos()).normalized() + self._rubber_band.setGeometry(rect) + self.scene().update(rect) - self._origin = event.pos() + self._previous_pos = event.pos() super(NodeViewer, self).mouseMoveEvent(event) def wheelEvent(self, event): @@ -268,7 +242,7 @@ def make_pipe_connection(self, start_port, end_port): def sceneMouseMoveEvent(self, event): """ triggered mouse move event for the scene. - redraw the connection pipe. + - redraw the connection pipe. Args: event (QtGui.QGraphicsSceneMouseEvent): @@ -289,6 +263,7 @@ def sceneMouseMoveEvent(self, event): def sceneMousePressEvent(self, event): """ triggered mouse press event for the scene. + - detect selected pipe and start connection. Args: event (QtGui.QGraphicsScenePressEvent): @@ -297,16 +272,18 @@ def sceneMousePressEvent(self, event): if event.modifiers() == QtCore.Qt.ShiftModifier: event.setModifiers(QtCore.Qt.ControlModifier) - for item in self.scene().items(event.scenePos()): - if isinstance(item, Pipe): - self._active_pipe = item - self._active_pipe.activate() - break + pipe_items = self._nearby_items(event.scenePos(), Pipe, 20) + pipe = pipe_items[0] if pipe_items else None + if pipe: + self._active_pipe = pipe + self._active_pipe.activate() + port = self._active_pipe.port_from_pos(event.scenePos(), True) + self.start_connection(port) def sceneMouseReleaseEvent(self, event): """ triggered mouse release event for the scene. - verify to make a the connection Pipe(). + - verify to make a the connection Pipe(). Args: event (QtGui.QGraphicsSceneMouseEvent): @@ -367,6 +344,10 @@ def delete_nodes(self, nodes): if isinstance(node, NodeItem): node.delete() + def select_all_nodes(self): + for node in self.all_nodes(): + node.setSelected(True) + def connect_ports(self, from_port, to_port): if not isinstance(from_port, PortItem): return @@ -404,6 +385,10 @@ def clear_scene(self): for item in self.scene().items(): self.scene().removeItem(item) + def clear_selection(self): + for node in self.selected_nodes(): + node.setSelected(True) + def center_selection(self, nodes=None): if not nodes: if self.selected_nodes(): From d09982e95f6deec37bb652d9888bf5624cae5144 Mon Sep 17 00:00:00 2001 From: Johnny Chan Date: Fri, 8 Sep 2017 00:33:53 +1200 Subject: [PATCH 3/3] implemented selection square, optimized scene update. --- BlueprintNodeGraph/viewer.py | 57 +++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/BlueprintNodeGraph/viewer.py b/BlueprintNodeGraph/viewer.py index 2146de91..7895616a 100644 --- a/BlueprintNodeGraph/viewer.py +++ b/BlueprintNodeGraph/viewer.py @@ -28,7 +28,6 @@ def __init__(self, parent=None, scene=None): self._origin_pos = None self._previous_pos = None self._rubber_band = QtGui.QRubberBand(QtGui.QRubberBand.Rectangle, self) - self.LMB_state = False self.RMB_state = False self.MMB_state = False @@ -63,23 +62,22 @@ def _set_viewer_pan(self, pos_x, pos_y): scroll_x.setValue(scroll_x.value() - pos_x) scroll_y.setValue(scroll_y.value() - pos_y) - def _delete_selection(self): - nodes = self.selected_nodes() - if not nodes: - return - self.delete_nodes(nodes) - - def _nearby_items(self, pos, item_type=None, search_area=20): - rect = QtCore.QRect( - pos.x() - (search_area / 2), pos.y() - (search_area / 2), - search_area, search_area - ) + def _items_near(self, pos, item_type=None, size=20): + x, y = (pos.x() - (size / 2)), (pos.y() - (size / 2)) + rect = QtCore.QRect(x, y, size, size) items = [] for item in self.scene().items(rect): if not item_type or isinstance(item, item_type): items.append(item) return items + def _map_scene_rect(self, rect=None): + map_pos = self.mapToScene(rect.x(), rect.y()) + map_rect = QtCore.QRect( + map_pos.x(), map_pos.y(), rect.width(), rect.height() + ) + return map_rect + def _setup_shortcuts(self): open_actn = QtGui.QAction('Open Session Layout', self) open_actn.setShortcut('Ctrl+o') @@ -96,7 +94,7 @@ def _setup_shortcuts(self): sel_all_actn.triggered.connect(self.select_all_nodes) del_node_actn = QtGui.QAction('Delete Selection', self) del_node_actn.setShortcuts(['Del', 'Backspace']) - del_node_actn.triggered.connect(self._delete_selection) + del_node_actn.triggered.connect(self.delete_selected_nodes) self.addAction(open_actn) self.addAction(save_actn) self.addAction(fit_zoom_actn) @@ -107,6 +105,8 @@ def resizeEvent(self, event): super(NodeViewer, self).resizeEvent(event) def mousePressEvent(self, event): + modifiers = QtGui.QApplication.keyboardModifiers() + alt_pressed = modifiers == QtCore.Qt.AltModifier if event.button() == QtCore.Qt.LeftButton: self.LMB_state = True elif event.button() == QtCore.Qt.RightButton: @@ -116,11 +116,12 @@ def mousePressEvent(self, event): self._origin_pos = event.pos() self._previous_pos = event.pos() - items = self._nearby_items(self.mapToScene(event.pos()), None, 20) - if self.LMB_state and not items: - self._rubber_band.setGeometry( - QtCore.QRect(self._previous_pos, QtCore.QSize()) - ) + items = self._items_near(self.mapToScene(event.pos()), None, 20) + if (self.LMB_state and not alt_pressed) and not items: + rect = QtCore.QRect(self._previous_pos, QtCore.QSize()).normalized() + map_rect = self._map_scene_rect(self._rubber_band.rect()) + self.scene().update(map_rect) + self._rubber_band.setGeometry(rect) self._rubber_band.show() super(NodeViewer, self).mousePressEvent(event) @@ -133,9 +134,9 @@ def mouseReleaseEvent(self, event): self.MMB_state = False if self._rubber_band.isVisible(): + map_rect = self._map_scene_rect(self._rubber_band.rect()) self._rubber_band.hide() - self.scene().update() - + self.scene().update(map_rect) super(NodeViewer, self).mouseReleaseEvent(event) def mouseMoveEvent(self, event): @@ -152,8 +153,12 @@ def mouseMoveEvent(self, event): if self.LMB_state and self._rubber_band.isVisible(): rect = QtCore.QRect(self._origin_pos, event.pos()).normalized() + map_rect = self._map_scene_rect(rect) + path = QtGui.QPainterPath() + path.addRect(map_rect) self._rubber_band.setGeometry(rect) - self.scene().update(rect) + self.scene().setSelectionArea(path, QtCore.Qt.ContainsItemShape) + self.scene().update(map_rect) self._previous_pos = event.pos() super(NodeViewer, self).mouseMoveEvent(event) @@ -272,7 +277,7 @@ def sceneMousePressEvent(self, event): if event.modifiers() == QtCore.Qt.ShiftModifier: event.setModifiers(QtCore.Qt.ControlModifier) - pipe_items = self._nearby_items(event.scenePos(), Pipe, 20) + pipe_items = self._items_near(event.scenePos(), Pipe, 20) pipe = pipe_items[0] if pipe_items else None if pipe: self._active_pipe = pipe @@ -344,6 +349,11 @@ def delete_nodes(self, nodes): if isinstance(node, NodeItem): node.delete() + def delete_selected_nodes(self): + nodes = self.selected_nodes() + if nodes: + self.delete_nodes(nodes) + def select_all_nodes(self): for node in self.all_nodes(): node.setSelected(True) @@ -386,8 +396,7 @@ def clear_scene(self): self.scene().removeItem(item) def clear_selection(self): - for node in self.selected_nodes(): - node.setSelected(True) + self.scene().clearSelection() def center_selection(self, nodes=None): if not nodes: