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. ![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)`*