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 905932c2..7895616a 100644
--- a/BlueprintNodeGraph/viewer.py
+++ b/BlueprintNodeGraph/viewer.py
@@ -25,8 +25,9 @@ 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_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
@@ -36,9 +37,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
@@ -64,15 +62,21 @@ 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 _select_all(self):
- for node in self.all_nodes():
- node.setSelected(True)
+ 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)
@@ -87,24 +91,38 @@ 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)
+ del_node_actn.triggered.connect(self.delete_selected_nodes)
self.addAction(open_actn)
self.addAction(save_actn)
self.addAction(fit_zoom_actn)
self.addAction(sel_all_actn)
self.addAction(del_node_actn)
+ 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:
self.RMB_state = True
elif event.button() == QtCore.Qt.MiddleButton:
self.MMB_state = True
- self._set_cursor_pos(event)
+ self._origin_pos = event.pos()
+ self._previous_pos = event.pos()
+
+ 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)
def mouseReleaseEvent(self, event):
@@ -114,22 +132,35 @@ def mouseReleaseEvent(self, event):
self.RMB_state = False
elif event.button() == QtCore.Qt.MiddleButton:
self.MMB_state = False
- self._set_cursor_pos(event)
+
+ if self._rubber_band.isVisible():
+ map_rect = self._map_scene_rect(self._rubber_band.rect())
+ self._rubber_band.hide()
+ self.scene().update(map_rect)
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._previous_pos.x())
+ pos_y = (event.y() - self._previous_pos.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._previous_pos.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 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().setSelectionArea(path, QtCore.Qt.ContainsItemShape)
+ self.scene().update(map_rect)
+
+ self._previous_pos = event.pos()
super(NodeViewer, self).mouseMoveEvent(event)
def wheelEvent(self, event):
@@ -216,7 +247,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):
@@ -237,6 +268,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):
@@ -245,16 +277,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._items_near(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):
@@ -315,6 +349,15 @@ 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)
+
def connect_ports(self, from_port, to_port):
if not isinstance(from_port, PortItem):
return
@@ -352,6 +395,9 @@ def clear_scene(self):
for item in self.scene().items():
self.scene().removeItem(item)
+ def clear_selection(self):
+ self.scene().clearSelection()
+
def center_selection(self, nodes=None):
if not nodes:
if self.selected_nodes():
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.

@@ -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)`*