/
node.py
519 lines (412 loc) · 19.9 KB
/
node.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
#!/usr/bin/python3
# -*- coding: utf-8 -*-
"""
Widget to display simulation data of a CSDF graph.
author: Sander Giesselink
"""
import sys
from PyQt5.QtWidgets import QWidget, QGraphicsItem, QPushButton, QVBoxLayout, QMenu, QAction, QInputDialog, QMessageBox
from PyQt5.QtCore import QRectF, QPointF, QPoint, Qt, QVariant
from PyQt5.QtGui import QColor, QPainter, QBrush, QPainterPath, QLinearGradient, QFont, QContextMenuEvent
from collections import Counter
from log import Log
class Node(QGraphicsItem):
NODE_COLOR = QColor(210, 210, 210)
def __init__(self, widget, view, nodeName, function, clashCode, color):
super().__init__()
self.ioWidth = 15
self.ioHeight = 10
self.ioHeightDifference = 10
self.nodeBodyWidth = 80
self.maxNameLength = 6
self.calculateNodeColors(color)
self.lastPos = QPointF(0, 0)
self.yTranslationLeftIO = 0
self.yTranslationRightIO = 0
self.snappingIsOn = True
self.showNeutralIO = False
self.nodeFunction = function
self.clashCode = clashCode
self.widget = widget
self.view = view
self.nodeName = nodeName
self.nodeNameDisplayed = ''
self.edgeList = []
self.ioList = []
#Add 2x IO ('left' = left, 'right' = right /,/ 0 = neutral, 1 = input, 2 is output)
self.addNewIO('left', 0)
self.addNewIO('right', 0)
self.setNodeAction = QAction('Edit node function', self.widget)
self.setNodeAction.triggered.connect(self.setNodeActiontriggered)
self.setClashCodeAction = QAction('Edit CLaSH code', self.widget)
self.setClashCodeAction.triggered.connect(self.setClashCodeActionTriggered)
self.nodeMenu = QMenu()
self.nodeMenu.addAction(self.setNodeAction)
self.nodeMenu.addAction(self.setClashCodeAction)
self.setYTranslationLeftIO()
self.setYTranslationRightIO()
#Set flags for selecting, moving and enabling a position change event
self.setFlags(QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemSendsGeometryChanges)
self.setAcceptHoverEvents(True)
self.hover = False
def boundingRect(self):
#Used for collision detection and repaint
return QRectF(0, 0, self.nodeBodyWidth, self.nodeBodyHeight)
def shape(self):
# Determines the collision area
path = QPainterPath()
path.addRect(0, 0, self.nodeBodyWidth, self.nodeBodyHeight)
return path
def paint(self, painter, option, widget):
lod = option.levelOfDetailFromTransform(painter.worldTransform())
self.paintNodeBody(painter, lod)
if lod > 0.2:
self.paintNodeIO(painter, lod)
if lod > 0.4:
self.paintNodeName(painter)
def paintNodeBody(self, painter, lod):
painter.setPen(Qt.black)
#Subtle gradient
if lod > 0.2:
gradient = QLinearGradient(0, 0, self.nodeBodyWidth, self.nodeBodyHeight)
gradient.setColorAt(0, self.nodeBodyColor)
gradient.setColorAt(1, self.nodeBodyColorGradient)
brush = QBrush(gradient)
else:
brush = QBrush(self.nodeBodyColor)
if self.hover:
brush = QBrush(self.nodeBodyColorHover)
if QGraphicsItem.isSelected(self):
brush = QBrush(self.nodeBodyColorSelected)
painter.setBrush(brush)
if lod > 0.1:
painter.drawRoundedRect(0, 0, self.nodeBodyWidth, self.nodeBodyHeight, 10, 5)
else:
painter.drawRect(0, 0, self.nodeBodyWidth, self.nodeBodyHeight)
def paintNodeIO(self, painter, lod):
#Draw all IO
for i in range(0, len(self.ioList)):
#Center io if one side contains less io
yTranslation = 0
if self.ioList[i][3] == 'left':
yTranslation = self.yTranslationLeftIO
else:
yTranslation = self.yTranslationRightIO
#Determine io color based on IO TYPE and if mouse is HOVERING
painter.setPen(QColor(0, 0, 0, 200))
if self.ioList[i][5]:
painter.setPen(Qt.black)
if self.ioList[i][4] == 0:
#neutral
if self.ioList[i][5]:
painter.setPen(Qt.black)
brush = QBrush(self.nodeNeutralColorHover)
else:
painter.setPen(QColor(0, 0, 0, 60))
brush = QBrush(self.nodeNeutralColor)
elif self.ioList[i][4] == 1:
#input
if self.ioList[i][5]:
brush = QBrush(self.nodeInputColorHover)
else:
brush = QBrush(self.nodeInputColor)
else:
#output
if self.ioList[i][5]:
brush = QBrush(self.nodeOutputColorHover)
else:
brush = QBrush(self.nodeOutputColor)
#Don't paint neutral IO if disabled
if self.ioList[i][4] == 0 and self.showNeutralIO:
painter.setBrush(brush)
path = self.getRoundedRectPath(i, yTranslation, self.ioList[i][3])
painter.drawPath(path.simplified())
elif self.ioList[i][4] != 0:
painter.setBrush(brush)
path = self.getRoundedRectPath(i, yTranslation, self.ioList[i][3])
painter.drawPath(path.simplified())
#Paint IO name
if lod > 0.4:
painter.setFont(QFont("Arial", 6))
if self.ioList[i][3] == 'left':
painter.drawText(self.getIONameRect(i, yTranslation, self.ioList[i][3]), Qt.AlignLeft, str(self.ioList[i][6]))
else:
painter.drawText(self.getIONameRect(i, yTranslation, self.ioList[i][3]), Qt.AlignRight, str(self.ioList[i][6]))
painter.setPen(Qt.black)
def paintNodeName(self, painter):
if self.nodeNameDisplayed == '':
self.setNodeName()
font = QFont("Arial", 12)
font.setItalic(True)
painter.setFont(font)
rect = QRectF(0, 0, self.nodeBodyWidth, self.nodeBodyHeight)
painter.drawText(rect, Qt.AlignCenter, self.nodeNameDisplayed)
def mousePressEvent(self, event):
self.mouseIsOnIO(event.pos(), True)
super().mousePressEvent(event)
self.update()
#Must be done after super().mousePressEvent(event) in order to
#flag the node again after clicking on an input/output
self.setFlag(QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QGraphicsItem.ItemIsMovable, True)
def mouseMoveEvent(self, event):
#Code for ShiftModifier goes here
self.update()
super().mouseMoveEvent(event)
def itemChange(self, change, value):
# Move selected nodes and edges to the front, untill unselected
if change == QGraphicsItem.ItemSelectedChange:
if QGraphicsItem.isSelected(self):
#Unselected (since the flag is not updated yet)
self.setZValue(0)
self.setZValueEdges(1)
else:
#Selected
self.setZValue(4)
self.setZValueEdges(5)
#If the position of the node changes -> calculate position change
#and move edges with the node
newPos = value
if change == QGraphicsItem.ItemPositionChange:
if self.snappingIsOn:
newPos = self.snapToGrid(newPos)
posChange = newPos - self.lastPos
# Due to the grid snapping, only process when node actually moved
if not posChange.isNull():
self.moveEdges(posChange)
self.lastPos = newPos
self.widget.editNodePosition(self.nodeName, newPos)
return super(Node, self).itemChange(change, newPos)
def snapToGrid(self, position):
#Return position of closest grid point
gridSizeX = 40
gridSizeY = 20
curPos = QPoint(position.x(), position.y())
gridPos = QPoint(round(curPos.x() / gridSizeX) * gridSizeX, round(curPos.y() / gridSizeY) * gridSizeY)
return gridPos
def mouseReleaseEvent(self, event):
self.setFlag(QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QGraphicsItem.ItemIsMovable, True)
super().mouseReleaseEvent(event)
self.update()
def hoverMoveEvent(self, event):
#Don't execute when the nodeBody is selected in order to prevent unselecting the nodeBody
if not QGraphicsItem.isSelected(self):
self.mouseIsOnIO(event.pos())
super().hoverMoveEvent(event)
self.update()
#Must be done after super().mousePressEvent(event) in order to
#flag the node again after clicking on an input/output
self.setFlag(QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QGraphicsItem.ItemIsMovable, True)
def hoverEnterEvent(self, event):
self.setCursor(Qt.PointingHandCursor)
super().hoverEnterEvent(event)
self.update()
def hoverLeaveEvent(self, event):
self.hover = False
self.setHoveringToFalse()
self.setCursor(Qt.ArrowCursor)
super().hoverLeaveEvent(event)
self.update()
def contextMenuEvent(self, event):
pos = event.scenePos()
point = self.view.mapFromScene(pos)
point = self.view.mapToGlobal(point)
self.nodeMenu.exec(point)
def setNodeActiontriggered(self):
functionStr = str(self.nodeFunction)
newFunctionStr, ok = QInputDialog.getText(self.widget, 'Edit node function', 'Function:', text = functionStr)
if ok:
try:
newFunction = eval(newFunctionStr)
self.nodeFunction = newFunctionStr
self.widget.editNodeFunction(self.nodeName, newFunctionStr)
except:
Log.addLogMessage(Log.ERROR, 'Invalid node function')
QMessageBox.critical(self.widget, 'Error', 'Invalid node function')
def setClashCodeActionTriggered(self):
clashCodeStr = self.clashCode
newClashCode, ok = QInputDialog.getMultiLineText(self.widget, 'CLaSH code for ' + self.nodeName, 'CLaSH code:', text=clashCodeStr)
if ok:
try:
# TODO add validation of code
self.clashCode = newClashCode
self.widget.editClashCode(self.nodeName, newClashCode)
except:
Log.addLogMessage(Log.ERROR, 'Invalid CLaSH code')
QMessageBox.critical(self.widget, 'Error', 'Invalid CLaSH code')
def getIOPoint(self, sideIndex, side):
#Gets the point from where the IO rectangle is drawn
addWidthForRightSide = 0
if side == 'right':
addWidthForRightSide = self.nodeBodyWidth - self.ioWidth
ioPoint = QPointF(addWidthForRightSide, sideIndex * (self.ioHeightDifference + self.ioHeight) + self.ioHeight / 2)
return ioPoint
def getIOPointForEdge(self, side, ioType):
#Gets the point where an edge can connect to the IO
addedX = 0
addedY = 0
if side == 'right':
#Add x translation if the IO lies on the right side of the node
addedX = self.nodeBodyWidth
#Add y translation for the exact IO position relative to the node
ioIndex = self.getLengthRightSide()
addedY = (ioIndex - 1) * (self.ioHeightDifference + self.ioHeight) + self.ioHeight / 2 + (self.ioHeight / 2) + self.yTranslationRightIO
else:
ioIndex = self.getLengthLeftSide()
addedY = (ioIndex - 1) * (self.ioHeightDifference + self.ioHeight) + self.ioHeight / 2 + (self.ioHeight / 2) + self.yTranslationLeftIO
#Returns the calculated point of the IO
ioPoint = QPointF(self.pos().x() + addedX, self.pos().y() + addedY)
return ioPoint
def addNewIO(self, side, ioType):
if side == 'left':
i = self.getLengthLeftSide()
else:
i = self.getLengthRightSide()
#---newIO = (ioPoint.x, ioPoint.y, hasEdge, side, ioType, mouseHover, name)---
newIO = (self.getIOPoint(i, side).x(), self.getIOPoint(i, side).y(), False, side, ioType, False, '')
self.ioList.append(newIO)
#Update the nodeBodyHeight
self.updateNode()
def setIOType(self, side, ioType, name):
#Update the type paramater of the IO
i = self.getLastIOSide(side)
self.ioList.insert(i, (self.ioList[i][0], self.ioList[i][1], self.ioList[i][2], self.ioList[i][3], ioType, self.ioList[i][5], name))
del self.ioList[i + 1]
def mouseIsOnIO(self, mousePos, click = False):
#Returns the IO that the mouse is on
for i in range(0, len(self.ioList)):
#Adjust if IO is centered on a side
if self.ioList[i][3] == 'left':
yTranslation = self.yTranslationLeftIO
else:
yTranslation = self.yTranslationRightIO
#Get point of IO
IOPoint = QPointF(self.ioList[i][0], self.ioList[i][1] + yTranslation)
#If mouse is over IO -> return IO
if mousePos.x() > IOPoint.x() and mousePos.x() < IOPoint.x() + self.ioWidth:
if mousePos.y() > IOPoint.y() and mousePos.y() < IOPoint.y() + self.ioHeight:
# entry point for drawing graphs.......
# if click:
# print('mouse on IO: ' + str(i) + ' (' + str(self.ioList[i][3]) + ', ' + str(self.ioList[i][4]) + ')')
#Update the hover paramater of the IO
self.ioList.insert(i, (self.ioList[i][0], self.ioList[i][1], self.ioList[i][2], self.ioList[i][3], self.ioList[i][4], True, self.ioList[i][6]))
del self.ioList[i + 1]
self.setFlag(QGraphicsItem.ItemIsSelectable, False)
self.setFlag(QGraphicsItem.ItemIsMovable, False)
self.hover = False
return i
#If no IO is found under the mouse -> make sure hovering is enabled and return -1
self.hover = True
self.setHoveringToFalse()
return -1
def setHoveringToFalse(self):
for i in range(0, len(self.ioList)):
#Set all hover parameters to false
self.ioList.insert(i, (self.ioList[i][0], self.ioList[i][1], self.ioList[i][2], self.ioList[i][3], self.ioList[i][4], False, self.ioList[i][6]))
del self.ioList[i + 1]
def setYTranslationLeftIO(self):
i = 1
def setYTranslationRightIO(self):
i = 1
def updateNode(self):
#Update the dimentional values of the node and its IO
self.calculateNodeBodyHeight()
self.setYTranslationLeftIO()
self.setYTranslationRightIO()
def calculateNodeBodyHeight(self):
#Get how many inputs/outputs are on each side
ioOnLeftSide = self.getLengthLeftSide()
ioOnRightSide = self.getLengthRightSide()
#Pick the longest side
if ioOnLeftSide > ioOnRightSide:
longestSide = ioOnLeftSide
else:
longestSide = ioOnRightSide
#Make node smaller when the neutral IO is hidden
if not self.showNeutralIO:
longestSide = longestSide - 1
#Set nodeBodyHeight based on longest io side
self.nodeBodyHeight = (longestSide * (self.ioHeightDifference + self.ioHeight))
def getNodeBodyHeigth(self):
return self.nodeBodyHeight
def getLengthLeftSide(self):
countSides = Counter(elem[3] for elem in self.ioList)
return countSides['left']
def getLengthRightSide(self):
countSides = Counter(elem[3] for elem in self.ioList)
return countSides['right']
def getLastIOSide(self, side):
#Returns the index of the last IO on a side
ioIndex = 0
for i in reversed(range(len(self.ioList))):
if side in self.ioList[i]:
ioIndex = i
break
return ioIndex
def setNodeName(self):
#Determine the displayed name of the node and its location once
self.nodeNameDisplayed = self.nodeName
if len(self.nodeName) > self.maxNameLength:
#Cutoff text if the name is too long
self.nodeNameDisplayed = self.nodeName[:self.maxNameLength]
self.nodeNameDisplayed += '..'
def getRoundedRectPath(self, i, yTranslation, side):
path = QPainterPath();
path.setFillRule(Qt.WindingFill);
path.addRoundedRect(self.ioList[i][0], self.ioList[i][1] + yTranslation, self.ioWidth, self.ioHeight, 2, 2)
#Remove rounded edges on left or right side
if side == 'left':
path.addRect(self.ioList[i][0], self.ioList[i][1] + yTranslation, 2, 2)
path.addRect(self.ioList[i][0], self.ioList[i][1] + yTranslation + self.ioHeight - 2, 2, 2)
else:
path.addRect(self.ioList[i][0] + self.ioWidth - 2, self.ioList[i][1] + yTranslation, 2, 2)
path.addRect(self.ioList[i][0] + self.ioWidth - 2, self.ioList[i][1] + yTranslation + self.ioHeight - 2, 2, 2)
return path
def getIONameRect(self, i, yTranslation, side):
if side == 'left':
rect = QRectF(self.ioList[i][0] + self.ioWidth + 2, self.ioList[i][1] + yTranslation, self.ioWidth, self.ioHeight)
else:
rect = QRectF(self.ioList[i][0] - self.ioWidth - 2, self.ioList[i][1] + yTranslation, self.ioWidth, self.ioHeight)
return rect
def addEdge(self, edge, edgeSide):
#Add new edge with: (reference to edge, 'begin' or 'end')
newEdge = (edge, edgeSide)
self.edgeList.append(newEdge)
def moveEdges(self, posChange, side = 'both'):
#Move edges connected to node
if len(self.edgeList) > 0:
for i in range(len(self.edgeList)):
if 'begin' in self.edgeList[i]:
#Only move edge side if the entire edge is moved or the specified side is moved
if side == 'both' or side == self.ioList[i][3]:
self.edgeList[i][0].moveEdge(posChange, 'begin')
else:
if side == 'both' or side == self.ioList[i][3]:
self.edgeList[i][0].moveEdge(posChange, 'end')
def setZValueEdges(self, zValue):
for i in range(len(self.edgeList)):
self.edgeList[i][0].setZValueEdge(zValue)
def calculateNodeColors(self, color):
#Calculate all node colors based on a given color
r = color.red()
g = color.green()
b = color.blue()
if r < 60:
r = 60
if g < 60:
g = 60
if b < 60:
b = 60
self.nodeBodyColor = QColor(r, g, b)
self.nodeBodyColorGradient = QColor(r - 30, g - 30, b - 30)
self.nodeBodyColorSelected = QColor(r - 60, g - 60, b - 60)
self.nodeBodyColorHover = QColor(r - 30, g - 30, b - 30)
#Colors of IO (fixed colors)
self.nodeInputColor = QColor(230, 230, 230)
self.nodeInputColorHover = QColor(255, 255, 255)
self.nodeOutputColor = QColor(120, 120, 120)
self.nodeOutputColorHover = QColor(80, 80, 80)
self.nodeNeutralColor = QColor(180, 180, 180, 100)
self.nodeNeutralColorHover = QColor(180, 180, 180)