Skip to content

Commit 46b7207

Browse files
authored
Merge pull request #116 from jchanvfx/slice_pipes
pipe connection slicer tool
2 parents d36393e + 6e9bfef commit 46b7207

File tree

9 files changed

+157
-6
lines changed

9 files changed

+157
-6
lines changed

NodeGraphQt/base/graph.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ def __repr__(self):
5959
def _wire_signals(self):
6060
# internal signals.
6161
self._viewer.search_triggered.connect(self._on_search_triggered)
62+
self._viewer.connection_sliced.connect(self._on_connection_sliced)
6263
self._viewer.connection_changed.connect(self._on_connection_changed)
6364
self._viewer.moved_nodes.connect(self._on_nodes_moved)
6465
self._viewer.node_double_clicked.connect(self._on_node_double_clicked)
@@ -188,6 +189,26 @@ def _on_connection_changed(self, disconnected, connected):
188189
port1.connect_to(port2)
189190
self._undo_stack.endMacro()
190191

192+
def _on_connection_sliced(self, ports):
193+
"""
194+
slot when connection pipes have been sliced.
195+
196+
Args:
197+
ports (list[list[widgets.port.PortItem]]):
198+
pair list of port connections (in port, out port)
199+
"""
200+
if not ports:
201+
return
202+
ptypes = {'in': 'inputs', 'out': 'outputs'}
203+
self._undo_stack.beginMacro('slice connections')
204+
for p1_view, p2_view in ports:
205+
node1 = self._model.nodes[p1_view.node.id]
206+
node2 = self._model.nodes[p2_view.node.id]
207+
port1 = getattr(node1, ptypes[p1_view.port_type])()[p1_view.name]
208+
port2 = getattr(node2, ptypes[p2_view.port_type])()[p2_view.name]
209+
port1.disconnect_from(port2)
210+
self._undo_stack.endMacro()
211+
191212
@property
192213
def model(self):
193214
"""

NodeGraphQt/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
PIPE_DISABLED_COLOR = (190, 20, 20, 255)
1414
PIPE_ACTIVE_COLOR = (70, 255, 220, 255)
1515
PIPE_HIGHLIGHT_COLOR = (232, 184, 13, 255)
16+
PIPE_SLICER_COLOR = (255, 50, 75)
1617
#: The draw the connection pipes as straight lines.
1718
PIPE_LAYOUT_STRAIGHT = 0
1819
#: The draw the connection pipes as curved lines.

NodeGraphQt/qgraphics/slicer.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#!/usr/bin/python
2+
from NodeGraphQt import QtCore, QtGui, QtWidgets
3+
from NodeGraphQt.constants import Z_VAL_NODE_WIDGET, PIPE_SLICER_COLOR
4+
5+
6+
class SlicerPipe(QtWidgets.QGraphicsPathItem):
7+
"""
8+
Base item used for drawing the pipe connection slicer.
9+
"""
10+
11+
def __init__(self):
12+
super(SlicerPipe, self).__init__()
13+
self.setZValue(Z_VAL_NODE_WIDGET + 2)
14+
15+
def paint(self, painter, option, widget):
16+
"""
17+
Draws the slicer pipe.
18+
19+
Args:
20+
painter (QtGui.QPainter): painter used for drawing the item.
21+
option (QtGui.QStyleOptionGraphicsItem):
22+
used to describe the parameters needed to draw.
23+
widget (QtWidgets.QWidget): not used.
24+
"""
25+
color = QtGui.QColor(*PIPE_SLICER_COLOR)
26+
p1 = self.path().pointAtPercent(0)
27+
p2 = self.path().pointAtPercent(1)
28+
size = 6.0
29+
offset = size / 2
30+
31+
painter.save()
32+
painter.setRenderHint(painter.Antialiasing, True)
33+
34+
font = painter.font()
35+
font.setPointSize(12)
36+
painter.setFont(font)
37+
text = 'slice'
38+
text_x = painter.fontMetrics().width(text) / 2
39+
text_y = painter.fontMetrics().height() / 1.5
40+
text_pos = QtCore.QPointF(p1.x() - text_x, p1.y() - text_y)
41+
text_color = QtGui.QColor(*PIPE_SLICER_COLOR)
42+
text_color.setAlpha(80)
43+
painter.setPen(QtGui.QPen(text_color, 1.5, QtCore.Qt.SolidLine))
44+
painter.drawText(text_pos, text)
45+
46+
painter.setPen(QtGui.QPen(color, 1.5, QtCore.Qt.DashLine))
47+
painter.drawPath(self.path())
48+
49+
painter.setPen(QtGui.QPen(color, 1.5, QtCore.Qt.SolidLine))
50+
painter.setBrush(color)
51+
52+
rect = QtCore.QRectF(p1.x() - offset, p1.y() - offset, size, size)
53+
painter.drawEllipse(rect)
54+
55+
rect = QtCore.QRectF(p2.x() - offset, p2.y() - offset, size, size)
56+
painter.drawEllipse(rect)
57+
painter.restore()
58+
59+
def draw_path(self, p1, p2):
60+
path = QtGui.QPainterPath()
61+
path.moveTo(p1)
62+
path.lineTo(p2)
63+
self.setPath(path)

NodeGraphQt/widgets/viewer.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from NodeGraphQt.qgraphics.node_backdrop import BackdropNodeItem
1313
from NodeGraphQt.qgraphics.pipe import Pipe
1414
from NodeGraphQt.qgraphics.port import PortItem
15+
from NodeGraphQt.qgraphics.slicer import SlicerPipe
1516
from NodeGraphQt.widgets.scene import NodeScene
1617
from NodeGraphQt.widgets.stylesheet import STYLE_QMENU
1718
from NodeGraphQt.widgets.tab_search import TabSearchWidget
@@ -30,6 +31,7 @@ class NodeViewer(QtWidgets.QGraphicsView):
3031

3132
moved_nodes = QtCore.Signal(dict)
3233
search_triggered = QtCore.Signal(str, tuple)
34+
connection_sliced = QtCore.Signal(list)
3335
connection_changed = QtCore.Signal(list, list)
3436

3537
# pass through signals
@@ -63,6 +65,10 @@ def __init__(self, parent=None):
6365
self._rubber_band = QtWidgets.QRubberBand(
6466
QtWidgets.QRubberBand.Rectangle, self
6567
)
68+
self._pipe_slicer = SlicerPipe()
69+
self._pipe_slicer.setVisible(False)
70+
self.scene().addItem(self._pipe_slicer)
71+
6672
self._undo_stack = QtWidgets.QUndoStack(self)
6773
self._context_menu = QtWidgets.QMenu('main', self)
6874
self._context_menu.setStyleSheet(STYLE_QMENU)
@@ -126,6 +132,12 @@ def _on_search_submitted(self, node_type):
126132
pos = self.mapToScene(self._previous_pos)
127133
self.search_triggered.emit(node_type, (pos.x(), pos.y()))
128134

135+
def _on_pipes_sliced(self, path):
136+
self.connection_sliced.emit([
137+
[i.input_port, i.output_port]
138+
for i in self.scene().items(path) if isinstance(i, Pipe)
139+
])
140+
129141
# --- reimplemented events ---
130142

131143
def resizeEvent(self, event):
@@ -138,6 +150,7 @@ def contextMenuEvent(self, event):
138150
def mousePressEvent(self, event):
139151
alt_modifier = event.modifiers() == QtCore.Qt.AltModifier
140152
shift_modifier = event.modifiers() == QtCore.Qt.ShiftModifier
153+
141154
if event.button() == QtCore.Qt.LeftButton:
142155
self.LMB_state = True
143156
elif event.button() == QtCore.Qt.RightButton:
@@ -152,10 +165,19 @@ def mousePressEvent(self, event):
152165
if self._search_widget.isVisible():
153166
self.tab_search_toggle()
154167

168+
# cursor pos.
169+
map_pos = self.mapToScene(event.pos())
170+
171+
# pipe slicer enabled.
172+
if event.modifiers() == (QtCore.Qt.AltModifier | QtCore.Qt.ShiftModifier):
173+
self._pipe_slicer.draw_path(map_pos, map_pos)
174+
self._pipe_slicer.setVisible(True)
175+
return
176+
155177
if alt_modifier:
156178
return
157179

158-
items = self._items_near(self.mapToScene(event.pos()), None, 20, 20)
180+
items = self._items_near(map_pos, None, 20, 20)
159181
nodes = [i for i in items if isinstance(i, AbstractNodeItem)]
160182

161183
# toggle extend node selection.
@@ -188,6 +210,13 @@ def mouseReleaseEvent(self, event):
188210
elif event.button() == QtCore.Qt.MiddleButton:
189211
self.MMB_state = False
190212

213+
# hide pipe slicer.
214+
if self._pipe_slicer.isVisible():
215+
self._on_pipes_sliced(self._pipe_slicer.path())
216+
p = QtCore.QPointF(0.0, 0.0)
217+
self._pipe_slicer.draw_path(p, p)
218+
self._pipe_slicer.setVisible(False)
219+
191220
# hide selection marquee
192221
if self._rubber_band.isVisible():
193222
rect = self._rubber_band.rect()
@@ -211,6 +240,15 @@ def mouseReleaseEvent(self, event):
211240
def mouseMoveEvent(self, event):
212241
alt_modifier = event.modifiers() == QtCore.Qt.AltModifier
213242
shift_modifier = event.modifiers() == QtCore.Qt.ShiftModifier
243+
if event.modifiers() == (QtCore.Qt.AltModifier | QtCore.Qt.ShiftModifier):
244+
if self.LMB_state:
245+
p1 = self._pipe_slicer.path().pointAtPercent(0)
246+
p2 = self.mapToScene(self._previous_pos)
247+
self._pipe_slicer.draw_path(p1, p2)
248+
self._previous_pos = event.pos()
249+
super(NodeViewer, self).mouseMoveEvent(event)
250+
return
251+
214252
if self.MMB_state and alt_modifier:
215253
pos_x = (event.x() - self._previous_pos.x())
216254
zoom = 0.1 if pos_x > 0 else -0.1
@@ -296,6 +334,10 @@ def sceneMousePressEvent(self, event):
296334
event (QtWidgets.QGraphicsScenePressEvent):
297335
The event handler from the QtWidgets.QGraphicsScene
298336
"""
337+
# pipe slicer enabled.
338+
if event.modifiers() == (QtCore.Qt.AltModifier | QtCore.Qt.ShiftModifier):
339+
return
340+
# viewer pan mode.
299341
if event.modifiers() == QtCore.Qt.AltModifier:
300342
return
301343

docs/_images/slicer.png

103 KB
Loading

docs/_static/ngqt.css

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ code span.pre {
4343
}
4444

4545
/* tables */
46-
table.docutils td, table.docutils th {
46+
table.docutils td,
47+
table.docutils th {
4748
padding: 4px 8px;
4849
border-top: 0;
4950
border-left: 0;
@@ -52,6 +53,10 @@ table.docutils td, table.docutils th {
5253
background: #24272b;
5354
}
5455

56+
table.align-center {
57+
margin-left: unset;
58+
}
59+
5560
/*-----------------------------------------*/
5661

5762
/* modules index */

docs/overview.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,21 @@ Navigation
1717
| Pan | *Alt + LMB + Drag* or *MMB + Drag* |
1818
+---------------+----------------------------------------------+
1919

20+
Port Connections
21+
================
22+
23+
.. image:: _images/slicer.png
24+
:width: 600px
25+
26+
Connection pipes can be disconnected easily with the built in slice tool.
27+
28+
+---------------------+----------------------------+
29+
| action | controls |
30+
+=====================+============================+
31+
| Slice connections | *Alt + Shift + LMB + Drag* |
32+
+---------------------+----------------------------+
33+
34+
2035
Node Search
2136
===========
2237

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
PySide2>=5.12
22
Qt.py>=1.2.0.b2
3-
python>=3.6
3+
python>=3.6

setup.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22
# -*- coding: utf-8 -*-
33
import setuptools
44

5-
from NodeGraphQt import __version__ as version
5+
import NodeGraphQt
66

77
with open('README.md', 'r') as fh:
88
long_description = fh.read()
99

10+
with open('requirements.txt') as f:
11+
requirements = f.read().splitlines()
12+
1013
description = (
1114
'Node graph framework that can be re-implemented into applications that '
1215
'supports PySide & PySide2'
@@ -19,7 +22,8 @@
1922

2023
setuptools.setup(
2124
name='NodeGraphQt',
22-
version=version,
25+
version=NodeGraphQt.__version__,
26+
install_requires=requirements,
2327
author='Johnny Chan',
2428
author_email='[email protected]',
2529
description=description,
@@ -28,7 +32,7 @@
2832
url='https://github.com/jchanvfx/NodeGraphQt',
2933
packages=setuptools.find_packages(exclude=["example_nodes"]),
3034
classifiers=classifiers,
31-
include_package_data=True,
35+
include_package_data=True
3236
)
3337

3438

0 commit comments

Comments
 (0)