Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions BlueprintNodeGraph/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
20 changes: 11 additions & 9 deletions BlueprintNodeGraph/pipe.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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():
Expand Down
106 changes: 76 additions & 30 deletions BlueprintNodeGraph/viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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):
Expand All @@ -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):
Expand Down Expand Up @@ -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):
Expand All @@ -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):
Expand All @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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():
Expand Down
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -18,4 +18,3 @@ select all nodes : `Ctrl + A`<br/>
delete selected node(s) : `Backspace` or `Delete`<br/>
save session layout : `Ctrl + S` <br/>
open session layout : `Ctrl + O` <br/>
>*`(node graph saved in json format with ".ngraph" file extension)`*