forked from chris-gardner/derp
-
Notifications
You must be signed in to change notification settings - Fork 0
/
depends_main_window.py
1009 lines (807 loc) · 41.5 KB
/
depends_main_window.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
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#
# Depends
# Copyright (C) 2014 by Andrew Gardner & Jonas Unger. All rights reserved.
# BSD license (LICENSE.txt for details).
#
import os
import sys
import json
import tempfile
import itertools
from functools import partial
from PySide import QtCore, QtGui
import depends_dag
import depends_node
import depends_util
import depends_variables
import depends_data_packet
import depends_undo_commands
import depends_property_widget
import depends_variable_widget
import depends_graphics_widgets
"""
The main window which contains the dependency graph view widget and a dock for
additional windows. Executing the program from the commandline interface
creates one of these windows (and therefore all startup routines execute), but
does not display it.
"""
###############################################################################
###############################################################################
class MainWindow(QtGui.QMainWindow):
"""
This class constructs the UI, consisting of the many windows, menu items,
undo managers, plugin systems, workflow variables, etc. It also holds
functions to manage what happens when dag nodes change (see "DAG management"
section), loading and saving of DAG snapshots, and much
"""
def __init__(self, startFile="", parent=None):
"""
"""
QtGui.QMainWindow.__init__(self, parent)
# Add the DAG widget
self.graphicsViewWidget = depends_graphics_widgets.GraphicsViewWidget(self)
self.graphicsScene = self.graphicsViewWidget.scene()
self.setCentralWidget(self.graphicsViewWidget)
# Create the docking widget for the properties dialog
self.propDock = QtGui.QDockWidget()
self.propDock.setObjectName('propDock')
self.propDock.setAllowedAreas(QtCore.Qt.RightDockWidgetArea | QtCore.Qt.LeftDockWidgetArea)
self.propDock.setWindowTitle("Properties")
self.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.propDock)
# Create and add the properties dialog to the dock widget
self.propWidget = depends_property_widget.PropWidget(self)
self.propDock.setWidget(self.propWidget)
# Create the docking widget for the variable dialog
self.variableDock = QtGui.QDockWidget()
self.variableDock.setObjectName('variableDock')
self.variableDock.setAllowedAreas(QtCore.Qt.RightDockWidgetArea | QtCore.Qt.LeftDockWidgetArea)
self.variableDock.setWindowTitle("Variables")
self.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.variableDock)
# Create and add the variable dialog to the dock widget
self.variableWidget = depends_variable_widget.VariableWidget(self)
self.variableDock.setWidget(self.variableWidget)
self.variableWidget.rebuild(depends_variables.variableSubstitutions)
# self.variableDock.hide()
# Set some locals
self.dag = None
self.undoStack = QtGui.QUndoStack(self)
# Undo and Redo have built-in ways to create their menus
undoAction = self.undoStack.createUndoAction(self, "&Undo")
undoAction.setShortcuts(QtGui.QKeySequence.Undo)
redoAction = self.undoStack.createRedoAction(self, "&Redo")
redoAction.setShortcuts(QtGui.QKeySequence.Redo)
# Application settings
self.settings = QtCore.QSettings('vcl', 'depends', self)
self.restoreSettings()
# Create the menu bar
fileMenu = self.menuBar().addMenu("&File")
fileMenu.addAction(QtGui.QAction("&Open DAG...", self, shortcut="Ctrl+O", triggered=self.openDialog))
self.recentMenu = fileMenu.addMenu('Recent Files')
self.rebuildRecentMenu()
fileMenu.addAction(QtGui.QAction("&Save DAG", self, shortcut="Ctrl+S", triggered=lambda: self.save(self.workingFilename)))
#fileMenu.addAction(QtGui.QAction("Save DAG &Version Up", self, shortcut="Ctrl+Space", triggered=self.saveVersionUp))
fileMenu.addAction(QtGui.QAction("Save DAG &As...", self, shortcut="Ctrl+Shift+S", triggered=self.saveAs))
fileMenu.addAction(QtGui.QAction("&Quit...", self, shortcut="Ctrl+Q", triggered=self.close))
editMenu = self.menuBar().addMenu("&Edit")
editMenu.addAction(undoAction)
editMenu.addAction(redoAction)
editMenu.addSeparator()
createMenu = self.menuBar().addMenu("&Nodes")
editMenu.addAction(QtGui.QAction("&Delete Node(s)", self, shortcut="Delete", triggered=self.deleteSelectedNodes))
editMenu.addAction(QtGui.QAction("&Shake Node(s)", self, shortcut="Backspace", triggered=self.shakeSelectedNodes))
editMenu.addAction(QtGui.QAction("D&uplicate Node", self, shortcut="Ctrl+D", triggered=self.duplicateSelectedNodes))
editMenu.addSeparator()
editMenu.addAction(QtGui.QAction("&Group Nodes", self, shortcut="Ctrl+G", triggered=self.groupSelectedNodes))
editMenu.addAction(QtGui.QAction("&Ungroup Nodes", self, shortcut="Ctrl+Shift+G", triggered=self.ungroupSelectedNodes))
executeMenu = self.menuBar().addMenu("E&xecute")
executeMenu.addAction(QtGui.QAction("Execute &Selected Node", self, shortcut= "Ctrl+Shift+E", triggered=lambda: self.executeSelected(executeImmediately=True)))
recipeMenu = executeMenu.addMenu("&Output Recipe")
executeMenu.addSeparator()
executeMenu.addAction(QtGui.QAction("Version &Up outputs", self, shortcut= "Ctrl+U", triggered=self.versionUpSelectedOutputFilenames))
#executeMenu.addAction(QtGui.QAction("&Test Menu Item", self, shortcut= "Ctrl+T", triggered=self.testMenuItem))
executeMenu.addSeparator()
executeMenu.addAction(QtGui.QAction("&Reload plugins", self, shortcut= "Ctrl+0", triggered=self.reloadPlugins))
windowMenu = self.menuBar().addMenu("&Window")
windowMenu.addAction(self.propDock.toggleViewAction())
windowMenu.addAction(self.variableDock.toggleViewAction())
# Setup the variables, load the plugins, and auto-generate the read dag nodes
self.setupStartupVariables()
depends_node.loadChildNodesFromPaths(depends_variables.value('NODE_PATH').split(':'))
# Generate the Create menu. Must be done after plugins are loaded.
menuActions = self.createCreateMenuActions()
for action in menuActions:
cat = None
for eAct in createMenu.actions():
if eAct.menu():
if eAct.text() == action.category:
cat = eAct.menu()
break
if not cat:
cat = QtGui.QMenu(createMenu)
cat.setTitle(action.category)
createMenu.addMenu(cat)
cat.addAction(action)
# Load the starting filename or create a new DAG
self.workingFilename = startFile
self.dag = depends_dag.DAG()
self.graphicsScene.setDag(self.dag)
if not self.open(self.workingFilename):
self.setWindowTitle("Depends")
self.undoStack.setClean()
# This is a small workaround to insure the properties dialog doesn't
# try to redraw twice when two nodes are rapidly selected
# (within a frame of eachother). There's a good chance the way I
# construct a property dialog is strange, but a twice-at-once redraw
# was making the entire UI destroy itself and spawn a temporary child
# window that had the same name as the program 'binary'.
self.selectionTimer = QtCore.QTimer(self)
self.selectionTimer.setInterval(0)
self.selectionTimer.timeout.connect(self.selectionRefresh)
# Hook up some signals
self.graphicsViewWidget.createNode.connect(self.createNode)
self.graphicsScene.selectionChanged.connect(self.selectionChanged)
self.graphicsScene.nodesDisconnected.connect(self.nodesDisconnected)
self.graphicsScene.nodesConnected.connect(self.nodesConnected)
self.propWidget.attrChanged.connect(self.propertyEdited)
self.variableWidget.addVariable.connect(depends_variables.add)
self.variableWidget.setVariable.connect(depends_variables.setx)
self.variableWidget.removeVariable.connect(depends_variables.remove)
self.undoStack.cleanChanged.connect(self.setWindowTitleClean)
###########################################################################
## Event overrides
###########################################################################
def closeEvent(self, event):
"""
Save program settings and ask "are you sure" if there are unsaved changes.
"""
if not self.undoStack.isClean():
if self.yesNoDialog("Current workflow is not saved. Save it before quitting?"):
if self.workingFilename:
self.save(self.workingFilename)
else:
self.saveAs()
self.saveSettings()
QtGui.QMainWindow.closeEvent(self, event)
###########################################################################
## Internal functionality
###########################################################################
def selectedDagNodes(self):
"""
Return a list of all selected DagNodes in the scene.
"""
selectedDrawNodes = self.graphicsScene.selectedItems()
return [sdn.dagNode for sdn in selectedDrawNodes]
def clearSelection(self):
selectedDrawNodes = self.graphicsScene.selectedItems()
for dagNode in selectedDrawNodes:
dagNode.setSelected(False)
self.selectionChanged()
def setupStartupVariables(self):
"""
Each program starts with a set of workflow variables that are defined
by where the program is executed from and potentially a set of
environment variables.
"""
# The current session gets a "binary directory" variable
depends_variables.add('DEPENDS_DIR')
depends_variables.setx('DEPENDS_DIR', os.path.dirname(os.path.realpath(__file__)), readOnly=True)
# ...And a path that points to where the nodes are loaded from
depends_variables.add('NODE_PATH')
if not os.environ.get('DEPENDS_NODE_PATH'):
depends_variables.setx('NODE_PATH', os.path.join(depends_variables.value('DEPENDS_DIR'), 'nodes'), readOnly=True)
else:
depends_variables.setx('NODE_PATH', os.environ.get('DEPENDS_NODE_PATH'), readOnly=True)
def clearVariableDictionary(self):
"""
Clear all variables from the 'global' variable dictionary that aren't
"built-in" to the current session.
"""
for key in depends_variables.names():
if key == 'DEPENDS_DIR':
continue
if key == 'NODE_PATH':
continue
def saveSettings(self):
"""
Register the software's general settings with the QSettings object
and force a save with sync().
"""
self.settings.setValue("mainWindowGeometry", self.saveGeometry())
self.settings.setValue("mainWindowState", self.saveState())
self.settings.sync()
def restoreSettings(self):
"""
Restore the software's general settings by pulling data out of the
current settings object.
"""
self.restoreGeometry(self.settings.value('mainWindowGeometry'))
self.restoreState(self.settings.value('mainWindowState'))
###########################################################################
## Complex message handling
###########################################################################
def createNode(self, nodeType, nodeLocation):
"""
Create a new dag node with a safe name, add it to the dag, and register it with the QGraphicsScene.
"""
preSnap = self.dag.snapshot(nodeMetaDict=self.graphicsScene.nodeMetaDict(), connectionMetaDict=self.graphicsScene.connectionMetaDict())
newDagNode = nodeType()
nodeName = depends_node.cleanNodeName(newDagNode.typeStr())
nodeName = self.dag.safeNodeName(nodeName)
newDagNode.setName(nodeName)
self.dag.addNode(newDagNode)
self.graphicsScene.addExistingDagNode(newDagNode, nodeLocation)
currentSnap = self.dag.snapshot(nodeMetaDict=self.graphicsScene.nodeMetaDict(), connectionMetaDict=self.graphicsScene.connectionMetaDict())
self.undoStack.push(depends_undo_commands.DagAndSceneUndoCommand(preSnap, currentSnap, self.dag, self.graphicsScene))
self.clearSelection()
drawNode = self.graphicsScene.drawNode(newDagNode)
self.selectNode(drawNode)
def deleteNodes(self, dagNodesToDelete):
"""
Delete an existing dag node and its edges, and make sure the QGraphicsScene cleans up as well.
"""
nodesAffected = list()
preSnap = self.dag.snapshot(nodeMetaDict=self.graphicsScene.nodeMetaDict(), connectionMetaDict=self.graphicsScene.connectionMetaDict())
# Clean up the graphics scene
# TODO: Should be a signal that tells the scene what to do
for dagNode in dagNodesToDelete:
drawNode = self.graphicsScene.drawNode(dagNode)
drawEdges = drawNode.drawEdges()
for edge in drawEdges:
edge.sourceDrawNode().removeDrawEdge(edge)
edge.destDrawNode().removeDrawEdge(edge)
self.graphicsScene.removeItem(edge)
self.graphicsScene.removeItem(drawNode)
# Remove the nodes from the dag
for delNode in dagNodesToDelete:
nodesAffected = nodesAffected + self.dagNodeDisconnected(delNode)
nodesAffected.remove(delNode)
self.dag.removeNode(delNode)
currentSnap = self.dag.snapshot(nodeMetaDict=self.graphicsScene.nodeMetaDict(), connectionMetaDict=self.graphicsScene.connectionMetaDict())
self.undoStack.push(depends_undo_commands.DagAndSceneUndoCommand(preSnap, currentSnap, self.dag, self.graphicsScene))
# Updates the drawNodes for each of the affected dagNodes
self.graphicsScene.refreshDrawNodes(nodesAffected)
def shakeNodes(self, dagNodesToShake):
"""
Pull a node out of the dependency chain without deleting it and without
losing downstream information.
"""
nodesAffected = list()
preSnap = self.dag.snapshot(nodeMetaDict=self.graphicsScene.nodeMetaDict(), connectionMetaDict=self.graphicsScene.connectionMetaDict())
for dagNode in dagNodesToShake:
inNodes = self.dag.nodeConnectionsIn(dagNode)
outNodes = self.dag.nodeConnectionsOut(dagNode)
drawNode = self.graphicsScene.drawNode(dagNode)
# Connect all previous dag nodes to all next nodes & add the draw edges
for inputDagNode in inNodes:
inputDrawNode = self.graphicsScene.drawNode(inputDagNode)
for outputDagNode in outNodes:
outputDrawNode = self.graphicsScene.drawNode(outputDagNode)
self.dag.connectNodes(inputDagNode, outputDagNode)
newDrawEdge = self.graphicsScene.addExistingConnection(inputDagNode, outputDagNode)
newDrawEdge.horizontalConnectionOffset = self.graphicsScene.drawEdge(drawNode, outputDrawNode).horizontalConnectionOffset
newDrawEdge.adjust()
# Disconnect this dag node from everything
for inputDagNode in inNodes:
self.dag.disconnectNodes(inputDagNode, dagNode)
for outputDagNode in outNodes:
nodesAffected = nodesAffected + self.dagNodeDisconnected(dagNode)
self.dag.disconnectNodes(dagNode, outputDagNode)
# Remove all draw edges
for edge in drawNode.drawEdges():
edge.sourceDrawNode().removeDrawEdge(edge)
edge.destDrawNode().removeDrawEdge(edge)
self.graphicsScene.removeItem(edge)
# Nullify all our inputs
for input in dagNode.inputs():
dagNode.setInputValue(input.name, "")
currentSnap = self.dag.snapshot(nodeMetaDict=self.graphicsScene.nodeMetaDict(), connectionMetaDict=self.graphicsScene.connectionMetaDict())
self.undoStack.push(depends_undo_commands.DagAndSceneUndoCommand(preSnap, currentSnap, self.dag, self.graphicsScene))
# A few refreshes
self.propWidget.refresh()
self.graphicsScene.refreshDrawNodes(nodesAffected)
def duplicateNodes(self, dagNodesToDupe):
"""
Create identical copies of the given dag nodes, but drop their
incoming and outgoing connections.
"""
preSnap = self.dag.snapshot(nodeMetaDict=self.graphicsScene.nodeMetaDict(), connectionMetaDict=self.graphicsScene.connectionMetaDict())
for dagNode in dagNodesToDupe:
dupedNode = dagNode.duplicate("_Dupe")
newLocation = self.graphicsScene.drawNode(dagNode).pos() + QtCore.QPointF(20, 20)
self.dag.addNode(dupedNode)
self.graphicsScene.addExistingDagNode(dupedNode, newLocation)
currentSnap = self.dag.snapshot(nodeMetaDict=self.graphicsScene.nodeMetaDict(), connectionMetaDict=self.graphicsScene.connectionMetaDict())
self.undoStack.push(depends_undo_commands.DagAndSceneUndoCommand(preSnap, currentSnap, self.dag, self.graphicsScene))
def versionUpOutputFilenames(self, dagNodesToVersionUp):
"""
Increment the filename version of all output filenames in a given
list of dag nodes.
"""
preSnap = self.dag.snapshot(nodeMetaDict=self.graphicsScene.nodeMetaDict(), connectionMetaDict=self.graphicsScene.connectionMetaDict())
nodesAffected = list()
for dagNode in self.selectedDagNodes():
for output in dagNode.outputs():
for soName in output.subOutputNames():
if not output.value[soName]:
continue
currentValue = output.value[soName]
updatedValue = depends_util.nextFilenameVersion(currentValue)
dagNode.setOutputValue(output.name, soName, updatedValue)
self.dag.setNodeStale(dagNode, False)
nodesAffected = nodesAffected + self.dagNodeOutputChanged(dagNode, dagNode.outputNamed(output.name))
currentSnap = self.dag.snapshot(nodeMetaDict=self.graphicsScene.nodeMetaDict(), connectionMetaDict=self.graphicsScene.connectionMetaDict())
self.undoStack.push(depends_undo_commands.DagAndSceneUndoCommand(preSnap, currentSnap, self.dag, self.graphicsScene))
# Updates the drawNodes for each of the affected dagNodes
self.propWidget.refresh()
self.graphicsScene.refreshDrawNodes(nodesAffected)
def nodesDisconnected(self, fromDagNode, toDagNode):
"""
When the user interface disconnects two nodes, tell the in-flight
dag about it.
"""
print 'disconnect', fromDagNode, toDagNode
nodesAffected = list()
nodesAffected = nodesAffected + self.dagNodeDisconnected(fromDagNode)
self.dag.disconnectNodes(fromDagNode, toDagNode)
# A few refreshes
self.propWidget.refresh()
self.graphicsScene.refreshDrawNodes(nodesAffected)
def nodesConnected(self, fromDagNode, toDagNode, sourcePort, destPort):
"""
When the user interface connects two nodes, tell the in-flight dag
about it.
"""
print 'connecting nodes', fromDagNode, toDagNode
print 'ports', sourcePort, destPort
self.dag.connectNodes(fromDagNode, toDagNode, sourcePort=sourcePort, destPort=destPort )
def propertyEdited(self, dagNode, propName, newValue, propertyType=None):
"""
When the user interface edits a property of a node (meaning an attribute,
input, or output), communicate this information to the in-flight dag
and nodes, and handle the repercussions.
"""
somethingChanged = False
preSnap = self.dag.snapshot(nodeMetaDict=self.graphicsScene.nodeMetaDict(), connectionMetaDict=self.graphicsScene.connectionMetaDict())
nodesAffected = list()
if propName == "Name" and propertyType is depends_node.DagNodeAttribute:
if newValue != dagNode.name:
dagNode.setName(newValue)
nodesAffected = nodesAffected + [dagNode]
somethingChanged = True
else:
if propertyType is depends_node.DagNodeAttribute:
if newValue != dagNode.attributeValue(propName):
dagNode.setAttributeValue(propName, newValue)
nodesAffected = nodesAffected + [dagNode]
somethingChanged = True
# Undos aren't registered when the value doesn't actually change
if somethingChanged:
currentSnap = self.dag.snapshot(nodeMetaDict=self.graphicsScene.nodeMetaDict(), connectionMetaDict=self.graphicsScene.connectionMetaDict())
self.undoStack.push(depends_undo_commands.DagAndSceneUndoCommand(preSnap, currentSnap, self.dag, self.graphicsScene, self.propWidget))
# Updates the drawNodes for each of the affected dagNodes
self.graphicsScene.refreshDrawNodes(nodesAffected)
def selectNode(self, dagNode):
"""
Select a node
"""
dagNode.setSelected(True)
self.selectionChanged()
return dagNode
def selectUpstreamNodes(self, dagNode):
"""
Selects all nodes that feed into this node
"""
self.clearSelection()
orderedDependencies = self.dag.buildExecutionList(dagNode)
# include ourselves at the end
orderedDependencies.append(dagNode)
drawNodes = self.graphicsScene.drawNodes()
for node in orderedDependencies:
for dNode in drawNodes:
if dNode.dagNode == node:
dNode.setSelected(True)
self.selectionChanged()
return orderedDependencies
def selectionRefresh(self):
"""
A small workaround for a property dialog refresh issue. See comments
in constructor ("This is a small workaround...") for more details.
"""
selectedDagNodes = self.selectedDagNodes()
self.propWidget.rebuild(self.dag, selectedDagNodes)
self.selectionTimer.stop()
def selectionChanged(self):
"""
Fires off an instantaneous timer that rebuilds the scenegraph and
property widget based on a change of selection in the SceneWidget.
Works in conjunction with self.selectionRefresh().
"""
if self.selectionTimer.isActive():
return
self.selectionTimer.start(0)
def setWindowTitleClean(self, isClean):
"""
Adjust the window title's clean state based on an incoming parameter.
"""
if isClean:
self.setWindowTitle("%s" % self.windowTitle()[:-1])
else:
self.setWindowTitle("%s*" % self.windowTitle())
###########################################################################
## DAG management
###########################################################################
def dagNodeDisconnected(self, fromDagNode):
"""
When a node is disconnected, all nodes after it lose their inputs to
nodes that come before it. This function is to be called before the DAG
removes any connections or deletes any nodes from its collection.
"""
nodesAffected = [fromDagNode]
allNodesAfter = self.dag.allNodesAfter(fromDagNode)
allNodesBefore = self.dag.allNodesBefore(fromDagNode) + [fromDagNode]
for afterNode in allNodesAfter:
for input in afterNode.inputs():
inputNode = self.dag.nodeInputComesFromNode(afterNode, input)[0]
if inputNode in allNodesBefore:
afterNode.setInputValue(input.name, "")
afterNode.setInputRange(input.name, None)
nodesAffected.append(afterNode)
nodesAffected = nodesAffected + self.dagNodeInputChanged(afterNode, input)
return nodesAffected
def dagNodeInputChanged(self, dagNode, input):
"""
When a node's input is changed, its range should be set to the same as
the output connected to it. Also, if there is an output that is affected
by this input, its range should be changed as well.
"""
nodesAffected = list()
# Gather the output that corresponds to the changed input
affectedOutput = dagNode.outputAffectedByInput(input)
# Notify the system that there has been a changed output - this covers the case of the output range
# change (loop just above this one) and the input type being changed, and thus cascading down.
if affectedOutput:
nodesAffected = nodesAffected + self.dagNodeOutputChanged(dagNode, affectedOutput)
return nodesAffected
def dagNodeOutputChanged(self, dagNode, output):
"""
When a node output's type or range is changed, inputs that connect to it
may need to be adjusted. Incompatible types must be disconnected and
ranges should be clamped.
"""
nodesAffected = list()
# Input types downstream may no longer be able to connect to this node. Handle recursively.
allAffectedNodes = self.dag.allNodesDependingOnNode(dagNode, recursion=False)
for affectedNode in allAffectedNodes:
for input in affectedNode.inputs():
nodeComingInOutputType = self.dag.nodeOutputType(*self.dag.nodeInputComesFromNode(affectedNode, input))
nodesAffected = nodesAffected + self.dagNodeInputChanged(affectedNode, input)
if nodeComingInOutputType not in input.allPossibleInputTypes():
affectedNode.setInputValue(input.name, "")
affectedNode.setInputRange(input.name, None)
nodesAffected.append(affectedNode)
# The range of directly-connected inputs may need to be adjusted
directlyAffectedNodes = self.dag.allNodesDependingOnNode(dagNode, recursion=False)
for affectedNode in directlyAffectedNodes:
for input in affectedNode.inputs():
(incomingNode, incomingOutput) = self.dag.nodeInputComesFromNode(affectedNode, input)
if input.seqRange != incomingOutput.getSeqRange():
input.seqRange = incomingOutput.getSeqRange()
nodesAffected.append(affectedNode)
# Data that used to exist may no longer exist. Therefore all directly affected nodes should refresh.
nodesAffected = nodesAffected + [dagNode] + directlyAffectedNodes
return nodesAffected
def dagNodeVariablesUsed(self, dagNode):
"""
Returns a tuple containing a list of all the single-dollar and a list
of all the double-dollar variables used in the current DAG.
"""
singleDollarList = list()
doubleDollarList = list()
for attribute in dagNode.attributes():
vps = depends_variables.present(dagNode.attributeValue(attribute.name, variableSubstitution=False))
vss = (list(), list())
vss2 = (list(), list())
if dagNode.attributeRange(attribute.name, variableSubstitution=False):
vss = depends_variables.present(dagNode.attributeRange(attribute.name, variableSubstitution=False)[0])
vss2 = depends_variables.present(dagNode.attributeRange(attribute.name, variableSubstitution=False)[1])
singleDollarList += vps[0] + vss[0] + vss2[0]
doubleDollarList += vps[1] + vss[1] + vss2[1]
return (list(set(singleDollarList)), list(set(doubleDollarList)))
def dagNodesSanityCheck(self, dagNodes):
"""
Runs a series of sanity tests on the given dag nodes to make sure they
are fit to be executed in their current state.
"""
#
# Full DAG validation
#
# Insure all $ variables that are used, exist
for dagNode in dagNodes:
(singleDollarVariables, doubleDollarVariables) = self.dagNodeVariablesUsed(dagNode)
for sdVariable in singleDollarVariables:
if sdVariable not in depends_variables.names():
raise RuntimeError("Depends variable $%s used in node '%s' does not exist in current environment." % (sdVariable, dagNode.name))
# Insure all $$ variables that are used, are present in the current environment
for dagNode in dagNodes:
(singleDollarVariables, doubleDollarVariables) = self.dagNodeVariablesUsed(dagNode)
for ddVariable in doubleDollarVariables:
if ddVariable not in os.environ:
raise RuntimeError("Environment variable $%s used in node '%s' does not exist in current environment." % (ddVariable, dagNode.name))
#
# Individual node validation
#
for dagNode in dagNodes:
# Insure all the inputs are connected
if not self.dag.nodeAllInputsConnected(dagNode):
raise RuntimeError("Node '%s' is missing a required input." % (dagNode.name))
# Insure the validation function passes for each node.
try:
dagNode.validate()
except Exception, err:
raise RuntimeError("Dag node '%s' did not pass its validation test with the error:\n%s" % (dagNode.name, err))
# Insure no node is in two groups at once
for dagNode in dagNodes:
if self.dag.nodeGroupCount(dagNode) > 1:
raise RuntimeError("Node '%s' is present in multiple groups." % (dagNode.name))
def dagExecuteNode(self, dagNode):
"""
Generate an execution script using a output recipe for the given node.
Takes a path for where to write the execution script, and offers the
ability to evaluate the script immediately.
"""
print 'executing dag nodes'.center(120, '#')
# get the list of nodes to execute
orderedDependencies = self.dag.buildExecutionList(dagNode)
# include ourselves at the end
orderedDependencies.append(dagNode)
print orderedDependencies
# try:
# self.dagNodesSanityCheck(orderedDependencies)
# except Exception, err:
# print err
# print "Aborting Dag execution."
# return
executionList = list()
for dagNode in orderedDependencies:
print 'executing ', dagNode
# Command execution
nodesBefore = self.dag.nodeConnectionsByPort(dagNode)
dagNode.setPortValues(nodesBefore)
commandList = dagNode.executePython()
executionList.append((dagNode.name, dagNode.outVal))
print 'this is what i executed:'
print executionList
###########################################################################
## Menu operations
###########################################################################
def open(self, filename):
"""
Loads a snapshot, in the form of a json, file off disk and applies the
values it pulls to the currently active dependency graph. Cleans up
the UI accordingly.
"""
if not os.path.exists(filename):
return False
# Load the snapshot off disk
with open(filename, 'rb') as fp:
snapshot = json.loads(fp.read())
# Apply the data to the in-flight Dag
self.dag.restoreSnapshot(snapshot["DAG"])
# Initialize the objects inside the graphWidget & restore the scene
self.graphicsScene.restoreSnapshot(snapshot["DAG"])
# Variable substitutions
self.clearVariableDictionary()
for v in snapshot["DAG"]["VARIABLE_SUBSTITIONS"]:
depends_variables.variableSubstitutions[v["NAME"]] = (v["VALUE"], False)
# The current session gets a variable representing the location of the current workflow
if 'WORKFLOW_DIR' not in depends_variables.names():
depends_variables.add('WORKFLOW_DIR')
depends_variables.setx('WORKFLOW_DIR', os.path.dirname(filename), readOnly=True)
# Additional meta-data loading
if "RELOAD_PLUGINS_FILENAME_TEMP" in snapshot:
filename = snapshot["RELOAD_PLUGINS_FILENAME_TEMP"]
# UI tidies
self.undoStack.clear()
self.undoStack.setClean()
self.workingFilename = filename
self.setWindowTitle("Depends (%s)" % self.workingFilename)
self.variableWidget.rebuild(depends_variables.variableSubstitutions)
self.addRecentItem(filename)
self.rebuildRecentMenu()
return True
def openDialog(self):
"""
Pops open a file dialog, recovers a filename from it, and calls self.open()
on the results.
"""
# TODO: This code is used twice almost identically. Can it go into yesNoDialog?
if not self.undoStack.isClean():
if self.yesNoDialog("Current workflow is not saved. Save it before opening?"):
if self.workingFilename:
self.save(self.workingFilename)
else:
self.saveAs()
filename, throwaway = QtGui.QFileDialog.getOpenFileName(self, caption='Open Workflow', filter="Workflow files (*.json)")
if not filename:
return
self.open(filename)
def save(self, filename, additionalFileDictionary=None):
"""
Functionality for writing snapshots of the software's running state
to a json file. Modifies the UI accordingly.
"""
if not filename:
return
currentFileName = self.workingFilename
# Create a nested meta dict for saving node locations
nodeMetaDict = self.graphicsScene.nodeMetaDict()
# Create a nested meta dict for saving connection offsets
connectionMetaDict = self.graphicsScene.connectionMetaDict()
# Store all but read-only variables to the state
varDicts = depends_variables.changeableList()
# TODO: A generic way to pass additional dicts into the snapshot function might be good.
# The parameter list is getting long and specific! (also, variableList->variableDict)
# Or maybe we should just merge it all right here?
# Concoct a full snapshot from the current DAG and additional information fed into the function
snapshot = self.dag.snapshot(nodeMetaDict=nodeMetaDict, connectionMetaDict=connectionMetaDict, variableMetaList=varDicts)
fullSnap = {"DAG":snapshot}
if additionalFileDictionary:
fullSnap = dict({"DAG":snapshot}.items() + additionalFileDictionary.items())
# Serialize to disk
fp = open(filename, 'wb')
fp.write(json.dumps(fullSnap, sort_keys=True, indent=4))
fp.close()
# UI tidies
self.undoStack.setClean()
if currentFileName != filename:
# file name has changed
self.addRecentItem(filename)
self.rebuildRecentMenu()
self.workingFilename = filename
self.setWindowTitle("Depends (%s)" % self.workingFilename)
def saveAs(self):
"""
Save the DAG to a filename pulled out of a file dialog.
"""
currentDir = os.path.dirname(self.workingFilename)
filename, throwaway = QtGui.QFileDialog.getSaveFileName(self, caption='Save Workflow As', filter="Workflow files (*.json)", dir=currentDir)
if not filename:
return
self.save(filename)
def saveVersionUp(self):
"""
Save the next version of the current file.
"""
nextVersionFilename = depends_util.nextFilenameVersion(self.workingFilename)
self.save(nextVersionFilename)
def yesNoDialog(self, text):
"""
A simple yes/no dialog box.
"""
reply = QtGui.QMessageBox.question(self, "Notice", text, QtGui.QMessageBox.Yes|QtGui.QMessageBox.No)
if (reply == QtGui.QMessageBox.Yes):
return True
return False
def reloadPlugins(self):
"""
This menu item reloads all the plugin files off disk by restarting
depends in-place. If the current workflow has been modified, save
a temporary copy and reload it on startup.
"""
self.saveSettings()
args = QtGui.qApp.arguments()
if not self.undoStack.isClean():
(osJunk, filename) = tempfile.mkstemp(prefix="dependsreload_", suffix=".json")
metaDict = {"RELOAD_PLUGINS_FILENAME_TEMP":self.workingFilename}
self.save(filename, metaDict)
filenameIndexMinusOne = args.index('-workflow')
args[filenameIndexMinusOne+1] = filename
depends_util.restartProgram(args)
def executeSelected(self, executeImmediately=False):
"""
Execute the selected node using self.dagExecuteNode().
"""
selectedDagNodes = self.selectedDagNodes()
if len(selectedDagNodes) > 1 or not selectedDagNodes:
# TODO: Status bar
return
selectedNode = selectedDagNodes[0]
if selectedNode.__class__.__name__ == 'DagNodeExecute':
self.dagExecuteNode(selectedDagNodes[0])
else:
print 'can only execute execute nodes'
def deleteSelectedNodes(self):
"""
Delete selected nodes using self.deleteNodes().
"""
dagNodesToDelete = self.selectedDagNodes()
if not dagNodesToDelete:
return
self.deleteNodes(dagNodesToDelete)
def shakeSelectedNodes(self):
"""
"Shake" selected nodes from the dag using self.shakeNodes().
"""
dagNodesToShake = self.selectedDagNodes()
if not dagNodesToShake:
return
self.shakeNodes(dagNodesToShake)
def duplicateSelectedNodes(self):
"""
Duplicate selected nodes using self.duplicateNodes().
"""
dagNodesToDupe = self.selectedDagNodes()
if not dagNodesToDupe:
return
self.duplicateNodes(dagNodesToDupe)
def groupSelectedNodes(self):
"""
Group selected nodes into an execution collection.
"""
selDagNodes = self.selectedDagNodes()
for dagNode in selDagNodes:
if self.dag.nodeGroupCount(dagNode) > 0:
raise RuntimeError("Nodes cannot currently be in more than one group.")
groupName = depends_util.generateUniqueNameSimiarToExisting('group', self.dag.nodeGroupDict.keys())
self.dag.addNodeGroup(groupName, selDagNodes)
self.graphicsScene.addExistingGroupBox(groupName, selDagNodes)
def ungroupSelectedNodes(self):
"""
If all selected nodes are in a group together, remove their node group
from the in-flight dependency graph.
"""
selDagNodes = self.selectedDagNodes()
groupNameInDag = self.dag.nodeGroupName(selDagNodes)
if not groupNameInDag:
return
self.graphicsScene.removeExistingGroupBox(groupNameInDag)
self.dag.removeNodeGroup(nodeListToRemove=selDagNodes)
def versionUpSelectedOutputFilenames(self):
"""
Version up the output filenames for each of the selected nodes using
self.versionUpOutputFilenames().
"""
dagNodesToVersionUp = self.selectedDagNodes()
if not dagNodesToVersionUp:
return
self.versionUpOutputFilenames(dagNodesToVersionUp)
def createNodeFromMenuStub(self):
"""
Create a new node from scratch. The stub funciton is theoretically
temporary until I can figure out how to make a deep copy of a type.
"""
nodeType = self.sender().data()[0]
# No nodelocation is supplied for actions that come from the menu, so compute now and apply.
nodeLocation = self.sender().data()[1]
if nodeLocation is None:
nodeLocation = self.graphicsViewWidget.centerCoordinates()
self.createNode(nodeType, nodeLocation)
def createCreateMenuActions(self):
"""
Creates all the menu commands that create nodes from all the dag node
types present in the current session.
"""
actionList = list()
for tipe in depends_node.dagNodeTypes():
menuAction = QtGui.QAction(tipe().typeStr(), self, triggered=self.createNodeFromMenuStub)
menuAction.setData((tipe, None))
menuAction.category = tipe.category
actionList.append(menuAction)
return actionList
def addRecentItem(self, item):
for x in reversed(range(1, 4)):
pref = 'recent_%d' % x
nextpref = 'recent_%d' % (x + 1)
if self.settings.value(pref) is not None:
if self.settings.value(pref) != self.settings.value(nextpref):
self.settings.setValue(nextpref, self.settings.value(pref))
self.settings.setValue('recent_1', item)
def rebuildRecentMenu(self):
self.recentMenu.clear()