scipion-pyworkflow 3.11.0__py3-none-any.whl → 3.11.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. pyworkflow/apps/__init__.py +29 -0
  2. pyworkflow/apps/pw_manager.py +37 -0
  3. pyworkflow/apps/pw_plot.py +51 -0
  4. pyworkflow/apps/pw_project.py +113 -0
  5. pyworkflow/apps/pw_protocol_list.py +143 -0
  6. pyworkflow/apps/pw_protocol_run.py +51 -0
  7. pyworkflow/apps/pw_run_tests.py +267 -0
  8. pyworkflow/apps/pw_schedule_run.py +322 -0
  9. pyworkflow/apps/pw_sleep.py +37 -0
  10. pyworkflow/apps/pw_sync_data.py +439 -0
  11. pyworkflow/apps/pw_viewer.py +78 -0
  12. pyworkflow/constants.py +1 -1
  13. pyworkflow/gui/__init__.py +36 -0
  14. pyworkflow/gui/browser.py +760 -0
  15. pyworkflow/gui/canvas.py +1190 -0
  16. pyworkflow/gui/dialog.py +979 -0
  17. pyworkflow/gui/form.py +2726 -0
  18. pyworkflow/gui/graph.py +247 -0
  19. pyworkflow/gui/graph_layout.py +271 -0
  20. pyworkflow/gui/gui.py +566 -0
  21. pyworkflow/gui/matplotlib_image.py +233 -0
  22. pyworkflow/gui/plotter.py +247 -0
  23. pyworkflow/gui/project/__init__.py +25 -0
  24. pyworkflow/gui/project/base.py +192 -0
  25. pyworkflow/gui/project/constants.py +139 -0
  26. pyworkflow/gui/project/labels.py +205 -0
  27. pyworkflow/gui/project/project.py +491 -0
  28. pyworkflow/gui/project/searchprotocol.py +238 -0
  29. pyworkflow/gui/project/searchrun.py +181 -0
  30. pyworkflow/gui/project/steps.py +171 -0
  31. pyworkflow/gui/project/utils.py +332 -0
  32. pyworkflow/gui/project/variables.py +179 -0
  33. pyworkflow/gui/project/viewdata.py +472 -0
  34. pyworkflow/gui/project/viewprojects.py +510 -0
  35. pyworkflow/gui/project/viewprotocols.py +2116 -0
  36. pyworkflow/gui/project/viewprotocols_extra.py +562 -0
  37. pyworkflow/gui/text.py +771 -0
  38. pyworkflow/gui/tooltip.py +185 -0
  39. pyworkflow/gui/tree.py +684 -0
  40. pyworkflow/gui/widgets.py +307 -0
  41. pyworkflow/mapper/__init__.py +26 -0
  42. pyworkflow/mapper/mapper.py +222 -0
  43. pyworkflow/mapper/sqlite.py +1581 -0
  44. pyworkflow/mapper/sqlite_db.py +145 -0
  45. pyworkflow/project/__init__.py +31 -0
  46. pyworkflow/project/config.py +454 -0
  47. pyworkflow/project/manager.py +180 -0
  48. pyworkflow/project/project.py +2095 -0
  49. pyworkflow/project/usage.py +165 -0
  50. pyworkflow/protocol/__init__.py +38 -0
  51. pyworkflow/protocol/bibtex.py +48 -0
  52. pyworkflow/protocol/constants.py +87 -0
  53. pyworkflow/protocol/executor.py +483 -0
  54. pyworkflow/protocol/hosts.py +317 -0
  55. pyworkflow/protocol/launch.py +277 -0
  56. pyworkflow/protocol/package.py +42 -0
  57. pyworkflow/protocol/params.py +781 -0
  58. pyworkflow/protocol/protocol.py +2707 -0
  59. pyworkflow/tests/__init__.py +29 -0
  60. pyworkflow/tests/test_utils.py +25 -0
  61. pyworkflow/tests/tests.py +341 -0
  62. pyworkflow/utils/__init__.py +38 -0
  63. pyworkflow/utils/dataset.py +414 -0
  64. pyworkflow/utils/echo.py +104 -0
  65. pyworkflow/utils/graph.py +169 -0
  66. pyworkflow/utils/log.py +293 -0
  67. pyworkflow/utils/path.py +528 -0
  68. pyworkflow/utils/process.py +153 -0
  69. pyworkflow/utils/profiler.py +92 -0
  70. pyworkflow/utils/progressbar.py +154 -0
  71. pyworkflow/utils/properties.py +617 -0
  72. pyworkflow/utils/reflection.py +129 -0
  73. pyworkflow/utils/utils.py +880 -0
  74. pyworkflow/utils/which.py +229 -0
  75. pyworkflow/webservices/__init__.py +8 -0
  76. pyworkflow/webservices/config.py +8 -0
  77. pyworkflow/webservices/notifier.py +152 -0
  78. pyworkflow/webservices/repository.py +59 -0
  79. pyworkflow/webservices/workflowhub.py +74 -0
  80. pyworkflowtests/tests/__init__.py +0 -0
  81. pyworkflowtests/tests/test_canvas.py +72 -0
  82. pyworkflowtests/tests/test_domain.py +45 -0
  83. pyworkflowtests/tests/test_logs.py +74 -0
  84. pyworkflowtests/tests/test_mappers.py +392 -0
  85. pyworkflowtests/tests/test_object.py +507 -0
  86. pyworkflowtests/tests/test_project.py +42 -0
  87. pyworkflowtests/tests/test_protocol_execution.py +146 -0
  88. pyworkflowtests/tests/test_protocol_export.py +78 -0
  89. pyworkflowtests/tests/test_protocol_output.py +158 -0
  90. pyworkflowtests/tests/test_streaming.py +47 -0
  91. pyworkflowtests/tests/test_utils.py +210 -0
  92. {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.1.dist-info}/METADATA +2 -2
  93. scipion_pyworkflow-3.11.1.dist-info/RECORD +161 -0
  94. scipion_pyworkflow-3.11.0.dist-info/RECORD +0 -71
  95. {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.1.dist-info}/WHEEL +0 -0
  96. {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.1.dist-info}/entry_points.txt +0 -0
  97. {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.1.dist-info}/licenses/LICENSE.txt +0 -0
  98. {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,2116 @@
1
+ # -*- coding: utf-8 -*-
2
+ # **************************************************************************
3
+ # *
4
+ # * Authors: J.M. De la Rosa Trevin (delarosatrevin@scilifelab.se) [1]
5
+ # *
6
+ # * [1] SciLifeLab, Stockholm University
7
+ # *
8
+ # * This program is free software: you can redistribute it and/or modify
9
+ # * it under the terms of the GNU General Public License as published by
10
+ # * the Free Software Foundation, either version 3 of the License, or
11
+ # * (at your option) any later version.
12
+ # *
13
+ # * This program is distributed in the hope that it will be useful,
14
+ # * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
+ # * GNU General Public License for more details.
17
+ # *
18
+ # * You should have received a copy of the GNU General Public License
19
+ # * along with this program. If not, see <https://www.gnu.org/licenses/>.
20
+ # *
21
+ # * All comments concerning this program package may be sent to the
22
+ # * e-mail address 'scipion@cnb.csic.es'
23
+ # *
24
+ # **************************************************************************
25
+ import logging
26
+ logger = logging.getLogger(__name__)
27
+
28
+ from pyworkflow import Config, DEFAULT_EXECUTION_ACTION_ASK, DEFAULT_EXECUTION_ACTION_SINGLE, DOCSITEURLS
29
+ from pyworkflow.gui import LIST_TREEVIEW, \
30
+ ShortCut, ToolTip, RESULT_RUN_ALL, RESULT_RUN_SINGLE, RESULT_CANCEL, BORDERLESS_TREEVIEW, showInfo
31
+ from pyworkflow.gui.project.constants import *
32
+ from pyworkflow.protocol import SIZE_1MB, SIZE_1GB, SIZE_1TB, Protocol
33
+
34
+ INIT_REFRESH_SECONDS = Config.SCIPION_GUI_REFRESH_INITIAL_WAIT
35
+
36
+ """
37
+ View with the protocols inside the main project window.
38
+ """
39
+
40
+ import os
41
+ import json
42
+ import re
43
+ import tempfile
44
+ from collections import OrderedDict
45
+ import tkinter as tk
46
+ import tkinter.ttk as ttk
47
+ import datetime as dt
48
+
49
+ from pyworkflow import Config, TK
50
+ import pyworkflow.utils as pwutils
51
+ import pyworkflow.protocol as pwprot
52
+ from pyworkflow.viewer import DESKTOP_TKINTER, ProtocolViewer
53
+ from pyworkflow.utils.properties import Color, KEYSYM, Icon, Message
54
+ from pyworkflow.webservices import WorkflowRepository
55
+
56
+ import pyworkflow.gui as pwgui
57
+ from pyworkflow.gui.form import FormWindow
58
+ from pyworkflow.gui.project.utils import getStatusColorFromNode, inspectObj
59
+ from pyworkflow.gui.project.searchprotocol import SearchProtocolWindow, ProtocolTreeProvider
60
+ from pyworkflow.gui.project.steps import StepsWindow
61
+ from pyworkflow.gui.project.viewprotocols_extra import RunIOTreeProvider, ProtocolTreeConfig
62
+ from pyworkflow.gui.project.searchrun import RunsTreeProvider, SearchRunWindow
63
+
64
+ DEFAULT_BOX_COLOR = '#f8f8f8'
65
+
66
+
67
+ RUNS_TREE = Icon.RUNS_TREE
68
+
69
+ VIEW_LIST = 0
70
+ VIEW_TREE = 1
71
+ VIEW_TREE_SMALL = 2
72
+
73
+
74
+ # noinspection PyAttributeOutsideInit
75
+ class ProtocolsView(tk.Frame):
76
+ """ What you see when the "Protocols" tab is selected.
77
+
78
+ In the main project window there are three tabs: "Protocols | Data | Hosts".
79
+ This extended tk.Frame is what will appear when Protocols is on.
80
+ """
81
+
82
+ RUNS_CANVAS_NAME = "runs_canvas"
83
+
84
+ SIZE_COLORS = {SIZE_1MB: "green",
85
+ SIZE_1GB: "orange",
86
+ SIZE_1TB: "red"}
87
+
88
+ _protocolViews = None
89
+
90
+ def __init__(self, parent, window, **args):
91
+ tk.Frame.__init__(self, parent, **args)
92
+ # Load global configuration
93
+ self.window = window
94
+ self.project = window.project
95
+ self.domain = self.project.getDomain()
96
+ self.root = window.root
97
+ self.getImage = window.getImage
98
+ self.protCfg = self.getCurrentProtocolView()
99
+ self.settings = window.getSettings()
100
+ self.runsView = self.settings.getRunsView()
101
+ self._loadSelection()
102
+ self._items = {}
103
+ self._lastSelectedProtId = None
104
+ self._lastStatus = None
105
+ self.selectingArea = False
106
+ self._lastRightClickPos = None # Keep last right-clicked position
107
+
108
+ self.style = ttk.Style()
109
+ self.root.bind("<Control-a>", self._selectAllProtocols)
110
+ self.root.bind("<Control-t>", self._toggleColorScheme)
111
+ self.root.bind("<Control-D>", self._toggleDebug)
112
+ self.root.bind("<Control-l>", self._locateProtocol)
113
+
114
+ if Config.debugOn():
115
+ self.root.bind("<Control-i>", self._inspectProtocols)
116
+
117
+
118
+ self.__autoRefresh = None
119
+ self.__autoRefreshCounter = INIT_REFRESH_SECONDS # start by 3 secs
120
+
121
+ self.refreshSemaphore = True
122
+ self.repeatRefresh = False
123
+
124
+ c = self.createContent()
125
+ pwgui.configureWeigths(self)
126
+ c.grid(row=0, column=0, sticky='news')
127
+
128
+
129
+ def createContent(self):
130
+ """ Create the Protocols View for the Project.
131
+ It has two panes:
132
+ Left: containing the Protocol classes tree
133
+ Right: containing the Runs list
134
+ """
135
+ p = tk.PanedWindow(self, orient=tk.HORIZONTAL, bg=Config.SCIPION_BG_COLOR)
136
+ bgColor = Color.ALT_COLOR
137
+ # Left pane, contains Protocols Pane
138
+ leftFrame = tk.Frame(p, bg=bgColor)
139
+ leftFrame.columnconfigure(0, weight=1)
140
+ leftFrame.rowconfigure(1, weight=1)
141
+
142
+ # Protocols Tree Pane
143
+ protFrame = tk.Frame(leftFrame, width=300, height=500, bg=bgColor)
144
+ protFrame.grid(row=1, column=0, sticky='news', padx=5, pady=5)
145
+ protFrame.columnconfigure(0, weight=1)
146
+ protFrame.rowconfigure(1, weight=1)
147
+ self._createProtocolsPanel(protFrame, bgColor)
148
+ self.updateProtocolsTree(self.protCfg)
149
+ # Create the right Pane that will be composed by:
150
+ # a Action Buttons TOOLBAR in the top
151
+ # and another vertical Pane with:
152
+ # Runs History (at Top)
153
+
154
+ # Selected run info (at Bottom)
155
+ rightFrame = tk.Frame(p, bg=Config.SCIPION_BG_COLOR)
156
+ rightFrame.columnconfigure(0, weight=1)
157
+ rightFrame.rowconfigure(1, weight=1)
158
+ # rightFrame.rowconfigure(0, minsize=label.winfo_reqheight())
159
+
160
+ # Create the Action Buttons TOOLBAR
161
+ toolbar = tk.Frame(rightFrame, bg=Config.SCIPION_BG_COLOR)
162
+ toolbar.grid(row=0, column=0, sticky='news')
163
+ pwgui.configureWeigths(toolbar)
164
+ # toolbar.columnconfigure(0, weight=1)
165
+ toolbar.columnconfigure(1, weight=1)
166
+
167
+ self.runsToolbar = tk.Frame(toolbar, bg=Config.SCIPION_BG_COLOR)
168
+ self.runsToolbar.grid(row=0, column=0, sticky='sw')
169
+ # On the left of the toolbar will be other
170
+ # actions that can be applied to all runs (refresh, graph view...)
171
+ self.allToolbar = tk.Frame(toolbar, bg=Config.SCIPION_BG_COLOR)
172
+ self.allToolbar.grid(row=0, column=10, sticky='se')
173
+ self.createActionToolbar()
174
+
175
+ # Create the Run History tree
176
+ v = ttk.PanedWindow(rightFrame, orient=tk.VERTICAL)
177
+ # runsFrame = ttk.Labelframe(v, text=' History ', width=500, height=500)
178
+ runsFrame = tk.Frame(v, bg=Config.SCIPION_BG_COLOR)
179
+ # runsFrame.grid(row=1, column=0, sticky='news', pady=5)
180
+ self.runsTree = self.createRunsTree(runsFrame)
181
+ pwgui.configureWeigths(runsFrame)
182
+
183
+ self.createRunsGraph(runsFrame)
184
+
185
+ if self.runsView == VIEW_LIST:
186
+ treeWidget = self.runsTree
187
+ else:
188
+ treeWidget = self.runsGraphCanvas
189
+
190
+ treeWidget.grid(row=0, column=0, sticky='news')
191
+
192
+ # Create the Selected Run Info
193
+ infoFrame = tk.Frame(v)
194
+ infoFrame.columnconfigure(0, weight=1)
195
+ infoFrame.rowconfigure(1, weight=1)
196
+ # Create the info label
197
+ self.infoLabel = tk.Label(infoFrame)
198
+ self.infoLabel.grid(row=0, column=0, sticky='w', padx=3)
199
+ # Create the Analyze results button
200
+ self.btnAnalyze = pwgui.Button(infoFrame, text=Message.LABEL_ANALYZE,
201
+ fg='white', bg=Config.SCIPION_MAIN_COLOR,
202
+ image=self.getImage(Icon.ACTION_VISUALIZE),
203
+ compound=tk.LEFT,
204
+ activeforeground='white',
205
+ activebackground=Config.getActiveColor())
206
+ # command=self._analyzeResultsClicked)
207
+ self.btnAnalyze.bind("<Shift-Button-1>", lambda e: self._analyzeResultsClicked(KEYSYM.SHIFT))
208
+ self.btnAnalyze.bind("<Control-Button-1>", lambda e: self._analyzeResultsClicked(KEYSYM.CONTROL))
209
+ self.btnAnalyze.bind("<Button-1>", lambda e: self._analyzeResultsClicked(None))
210
+
211
+ # self.btnAnalyze.bind("<Button-1>", self._analyzeResultsClicked)
212
+
213
+ self.btnAnalyze.grid(row=0, column=0, sticky='ne', padx=15)
214
+ # self.style.configure("W.TNotebook")#, background='white')
215
+ tab = ttk.Notebook(infoFrame) # , style='W.TNotebook')
216
+
217
+ # Summary tab
218
+ dframe = tk.Frame(tab, bg=Config.SCIPION_BG_COLOR)
219
+ pwgui.configureWeigths(dframe, row=0)
220
+ pwgui.configureWeigths(dframe, row=2)
221
+ # Just configure the provider, later below, in updateSelection, it will be
222
+ # provided with the protocols.
223
+ provider = RunIOTreeProvider(self, None,
224
+ self.project.mapper, self.info)
225
+
226
+ self.infoTree = pwgui.browser.BoundTree(dframe, provider, height=6,
227
+ show='tree',
228
+ style=BORDERLESS_TREEVIEW)
229
+ self.infoTree.grid(row=0, column=0, sticky='news')
230
+ label = tk.Label(dframe, text='SUMMARY', bg=Config.SCIPION_BG_COLOR,
231
+ font=self.window.fontBold)
232
+ label.grid(row=1, column=0, sticky='nw', padx=(15, 0))
233
+
234
+ hView = {'sci-open': self._viewObject,
235
+ 'sci-bib': self._bibExportClicked}
236
+
237
+ self.summaryText = pwgui.text.TaggedText(dframe, width=40, height=5,
238
+ bg=Config.SCIPION_BG_COLOR, bd=0,
239
+ font=self.window.font,
240
+ handlers=hView)
241
+ self.summaryText.grid(row=2, column=0, sticky='news', padx=(30, 0))
242
+
243
+ # Method tab
244
+ mframe = tk.Frame(tab)
245
+ pwgui.configureWeigths(mframe)
246
+ # Methods text box
247
+ self.methodText = pwgui.text.TaggedText(mframe, width=40, height=15,
248
+ bg=Config.SCIPION_BG_COLOR, handlers=hView)
249
+ self.methodText.grid(row=0, column=0, sticky='news')
250
+
251
+ # Output Logs
252
+ ologframe = tk.Frame(tab)
253
+ pwgui.configureWeigths(ologframe)
254
+ self.outputViewer = pwgui.text.TextFileViewer(ologframe, allowOpen=True,
255
+ font=self.window.font)
256
+ self.outputViewer.grid(row=0, column=0, sticky='news')
257
+ self.outputViewer.windows = self.window
258
+
259
+ # Project log
260
+ projLogFrame = tk.Frame(tab)
261
+ pwgui.configureWeigths(projLogFrame)
262
+ self.projLog = pwgui.text.TextFileViewer(projLogFrame, allowOpen=True,
263
+ font=self.window.font)
264
+ self.projLog.grid(row=0, column=0, sticky='news')
265
+ self.projLog.windows = self.window
266
+ self.projLog.addFile(self.project.getProjectLog())
267
+
268
+ # Move to the selected protocol
269
+ if self._isSingleSelection():
270
+ prot = self.getSelectedProtocol()
271
+ node = self.runsGraph.getNode(str(prot.getObjId()))
272
+ self._selectNode(node)
273
+ else:
274
+ self._updateSelection()
275
+
276
+ # Add all tabs
277
+
278
+ tab.add(dframe, text=Message.LABEL_SUMMARY)
279
+ tab.add(mframe, text=Message.LABEL_METHODS)
280
+ tab.add(ologframe, text=Message.LABEL_LOGS_OUTPUT)
281
+ # tab.add(elogframe, text=Message.LABEL_LOGS_ERROR)
282
+ tab.add(projLogFrame, text=Message.LABEL_LOGS_SCIPION)
283
+ tab.grid(row=1, column=0, sticky='news')
284
+
285
+ v.add(runsFrame, weight=1)
286
+ v.add(infoFrame, weight=20)
287
+ v.grid(row=1, column=0, sticky='news')
288
+
289
+ # Add sub-windows to PanedWindows
290
+ p.add(leftFrame, padx=0, pady=0, sticky='news')
291
+ p.add(rightFrame, padx=0, pady=0)
292
+ p.paneconfig(leftFrame, minsize=5)
293
+ leftFrame.config(width=235)
294
+ p.paneconfig(rightFrame, minsize=10)
295
+
296
+ return p
297
+
298
+ def _viewObject(self, objId):
299
+ """ Call appropriate viewer for objId. """
300
+ proj = self.project
301
+ obj = proj.getObject(int(objId))
302
+ viewerClasses = self.domain.findViewers(obj, DESKTOP_TKINTER)
303
+ if not viewerClasses:
304
+ return # TODO: protest nicely
305
+ viewer = viewerClasses[0](project=proj, parent=self.window)
306
+ viewer.visualize(obj)
307
+
308
+ def _loadSelection(self):
309
+ """ Load selected items, remove if some do not exists. """
310
+ self._selection = self.settings.runSelection
311
+ for protId in list(self._selection):
312
+
313
+ if not self.project.doesProtocolExists(protId):
314
+ self._selection.remove(protId)
315
+
316
+ def _isMultipleSelection(self):
317
+ return len(self._selection) > 1
318
+
319
+ def _isSingleSelection(self):
320
+ return len(self._selection) == 1
321
+
322
+ def _noSelection(self):
323
+ return len(self._selection) == 0
324
+
325
+ def info(self, message):
326
+ self.infoLabel.config(text=message)
327
+ self.infoLabel.update_idletasks()
328
+
329
+ def cleanInfo(self):
330
+ self.info("")
331
+
332
+ def refreshRuns(self, e=None, initRefreshCounter=True, checkPids=False, position=None):
333
+ """
334
+ Refresh the protocol runs workflow. If the variable REFRESH_WITH_THREADS
335
+ exits, then use a threads to refresh, i.o.c use normal behavior
336
+ """
337
+ useThreads = Config.refreshInThreads()
338
+ if useThreads:
339
+ import threading
340
+ # Refresh the status of displayed runs.
341
+ if self.refreshSemaphore:
342
+
343
+ threadRefreshRuns = threading.Thread(name="Refreshing runs",
344
+ target=self.refreshDisplayedRuns,
345
+ args=(e, initRefreshCounter,
346
+ checkPids))
347
+ threadRefreshRuns.start()
348
+ else:
349
+ self.repeatRefresh = True
350
+ else:
351
+ self.refreshDisplayedRuns(e, initRefreshCounter, checkPids, position=position)
352
+
353
+ # noinspection PyUnusedLocal
354
+ def refreshDisplayedRuns(self, e=None, initRefreshCounter=True, checkPids=False, position=None):
355
+ """ Refresh the status of displayed runs.
356
+ Params:
357
+ e: Tk event input
358
+ initRefreshCounter: if True the refresh counter will be set to 3 secs
359
+ then only case when False is from _automaticRefreshRuns where the
360
+ refresh time is doubled each time to avoid refreshing too often.
361
+ """
362
+ self.viewButtons[ACTION_REFRESH]['state'] = tk.DISABLED
363
+ self.info('Refreshing...')
364
+ self.refreshSemaphore = False
365
+
366
+ if self.runsView == VIEW_LIST:
367
+ self.updateRunsTree(True)
368
+ else:
369
+ self.updateRunsGraph(True, checkPids=checkPids, position=position)
370
+ self._updateSelection()
371
+
372
+ if initRefreshCounter:
373
+
374
+ self.__autoRefreshCounter = INIT_REFRESH_SECONDS # start by 3 secs
375
+ if self.__autoRefresh:
376
+ self.runsTree.after_cancel(self.__autoRefresh)
377
+ self.__autoRefresh = self.runsTree.after(
378
+ self.__autoRefreshCounter * 1000,
379
+ self._automaticRefreshRuns)
380
+ self.refreshSemaphore = True
381
+ if self.repeatRefresh:
382
+ self.repeatRefresh = False
383
+ self.refreshRuns()
384
+ self.cleanInfo()
385
+ self.viewButtons[ACTION_REFRESH]['state'] = tk.NORMAL
386
+
387
+ # noinspection PyUnusedLocal
388
+ def _automaticRefreshRuns(self, e=None):
389
+ """ Schedule automatic refresh increasing the time between refreshes. """
390
+ if Config.SCIPION_GUI_CANCEL_AUTO_REFRESH:
391
+ return
392
+
393
+ self.refreshRuns(initRefreshCounter=False, checkPids=True)
394
+ secs = self.__autoRefreshCounter
395
+ # double the number of seconds up to 30 min
396
+ self.__autoRefreshCounter = min(2 * secs, 1800)
397
+ self.__autoRefresh = self.runsTree.after(secs * 1000,
398
+ self._automaticRefreshRuns)
399
+
400
+ # noinspection PyUnusedLocal
401
+ def _findProtocol(self, event=None):
402
+ """ Find a desired protocol by typing some keyword. """
403
+
404
+ if event is not None and self._noSelection() and event.widget.widgetName=="canvas" and self:
405
+ position = self.runsGraphCanvas.getCoordinates(event)
406
+ else:
407
+ position = None
408
+
409
+ window = SearchProtocolWindow(self.window, position=position, selectionGetter=self.getSelectedProtocol)
410
+ window.show()
411
+
412
+ def _locateProtocol(self, e=None):
413
+
414
+ window = SearchRunWindow(self.window, self.runsGraph, onDoubleClick=self._onRunClick)
415
+ window.show()
416
+ # self._moveCanvas(0,1)
417
+
418
+ def _onRunClick(self, e=None):
419
+ """ Callback to be called when a click happens o a run in the SearchRunWindow.tree"""
420
+ tree = e.widget
421
+ protId = tree.getFirst()
422
+ node = self.runsGraph.getNode(protId)
423
+ self._selectNode(node)
424
+
425
+ def _selectNode(self, node):
426
+
427
+ x = node.x
428
+ y = node.y
429
+ self._moveCanvas(x, y)
430
+
431
+ # Select the protocol
432
+ self._selectItemProtocol(node.run)
433
+ # We comment the refresh because when the project is loaded,
434
+ # the workflow is traversed twice.
435
+ # self.refreshDisplayedRuns()
436
+
437
+ def _moveCanvas(self, X, Y):
438
+
439
+ self.runsGraphCanvas.moveTo(X, Y)
440
+
441
+ def createActionToolbar(self):
442
+ """ Prepare the buttons that will be available for protocol actions. """
443
+
444
+ self.actionButtons = {}
445
+ actionList = [
446
+ ACTION_NEW, ACTION_EDIT, ACTION_RENAME, ACTION_DUPLICATE, ACTION_COPY, ACTION_PASTE, ACTION_DELETE,
447
+ ACTION_BROWSE,
448
+ ACTION_STOP, ACTION_STOP_WORKFLOW, ACTION_CONTINUE, ACTION_CONTINUE_WORKFLOW, ACTION_RESTART_WORKFLOW, ACTION_RESET_WORKFLOW,
449
+ ACTION_RESULTS,
450
+ ACTION_EXPORT, ACTION_EXPORT_UPLOAD,
451
+ ACTION_COLLAPSE, ACTION_EXPAND,
452
+ ACTION_LABELS, ACTION_SEARCH,
453
+ ACTION_SELECT_FROM, ACTION_SELECT_TO,
454
+ ACTION_STEPS, ACTION_DB
455
+ ]
456
+
457
+ def addButton(action, text, toolbar):
458
+
459
+ labelTxt = text if Config.SCIPION_SHOW_TEXT_IN_TOOLBAR else ""
460
+ btn = tk.Label(toolbar, text=labelTxt,
461
+ image=self.getImage(ActionIcons.get(action, None)),
462
+ compound=tk.TOP, cursor='hand2', bg=Config.SCIPION_BG_COLOR)
463
+
464
+ callback = lambda e: self._runActionClicked(action, event=e)
465
+ btn.bind(TK.LEFT_CLICK, callback)
466
+
467
+ # Shortcuts:
468
+ shortCut = ActionShortCuts.get(action, None)
469
+ if shortCut:
470
+ text += " (%s)" % shortCut
471
+ self.root.bind(shortCut, callback)
472
+
473
+ ToolTip(btn, text, 500)
474
+
475
+ return btn
476
+
477
+ for action in actionList:
478
+ self.actionButtons[action] = addButton(action, action,
479
+ self.runsToolbar)
480
+
481
+ ActionIcons[ACTION_TREE] = RUNS_TREE
482
+
483
+ self.viewButtons = {}
484
+
485
+ # Add combo for switch between views
486
+ viewFrame = tk.Frame(self.allToolbar)
487
+ viewFrame.grid(row=0, column=0)
488
+ self._createViewCombo(viewFrame)
489
+
490
+ # Add refresh Tree button
491
+ btn = addButton(ACTION_TREE, "Organize", self.allToolbar)
492
+ pwgui.tooltip.ToolTip(btn, "Organize the node positions.", 1500)
493
+ self.viewButtons[ACTION_TREE] = btn
494
+ if self.runsView != VIEW_LIST:
495
+ btn.grid(row=0, column=1)
496
+
497
+ # Add refresh button
498
+ btn = addButton(ACTION_REFRESH, ACTION_REFRESH, self.allToolbar)
499
+ btn.grid(row=0, column=2)
500
+ self.viewButtons[ACTION_REFRESH] = btn
501
+
502
+ def _createViewCombo(self, parent):
503
+ """ Create the select-view combobox. """
504
+ label = tk.Label(parent, text='View:', bg=Config.SCIPION_BG_COLOR)
505
+ label.grid(row=0, column=0)
506
+ viewChoices = ['List', 'Tree', 'Tree - small']
507
+ self.switchCombo = pwgui.widgets.ComboBox(parent, width=10,
508
+ choices=viewChoices,
509
+ values=[VIEW_LIST, VIEW_TREE, VIEW_TREE_SMALL],
510
+ initial=viewChoices[self.runsView],
511
+ onChange=lambda e: self._runActionClicked(
512
+ ACTION_SWITCH_VIEW))
513
+ self.switchCombo.grid(row=0, column=1)
514
+
515
+ def _updateActionToolbar(self):
516
+ """ Update which action buttons should be visible. """
517
+
518
+ def displayAction(actionToDisplay, column, condition=True):
519
+
520
+ """ Show/hide the action button if the condition is met. """
521
+
522
+ # If action present (set color is not in the toolbar but in the
523
+ # context menu)
524
+ action = self.actionButtons.get(actionToDisplay, None)
525
+ if action is not None:
526
+ if condition:
527
+ action.grid(row=0, column=column, sticky='sw',
528
+ padx=(0, 5), ipadx=0)
529
+ else:
530
+ action.grid_remove()
531
+
532
+ for i, actionTuple in enumerate(self.provider.getActionsFromSelection()):
533
+ action, cond = actionTuple
534
+ displayAction(action, i, cond)
535
+
536
+ def _createProtocolsTree(self, parent,
537
+ show='tree', columns=None, position=None):
538
+
539
+ t = pwgui.tree.Tree(parent, show=show, style=LIST_TREEVIEW,
540
+ columns=columns)
541
+ t.column('#0', minwidth=300)
542
+
543
+ def configureTag(tag, img):
544
+ # Protocol nodes
545
+ t.tag_configure(tag, image=self.getImage(img))
546
+ t.tag_bind(tag, TK.LEFT_DOUBLE_CLICK, lambda e: self._protocolItemClick(e, position))
547
+ t.tag_bind(tag, TK.RETURN, lambda e: self._protocolItemClick(e, position))
548
+ t.tag_bind(tag, TK.ENTER, lambda e: self._protocolItemClick(e, position))
549
+
550
+ # Protocol nodes
551
+ configureTag(ProtocolTreeConfig.TAG_PROTOCOL, Icon.PRODUCTION)
552
+ # New protocols
553
+ configureTag(ProtocolTreeConfig.TAG_PROTOCOL_NEW, Icon.NEW)
554
+ # Beta protocols
555
+ configureTag(ProtocolTreeConfig.TAG_PROTOCOL_BETA, Icon.BETA)
556
+ # Disable protocols (not installed) are allowed to be added.
557
+ configureTag(ProtocolTreeConfig.TAG_PROTOCOL_DISABLED,
558
+ Icon.PROT_DISABLED)
559
+ # Updated protocols
560
+ configureTag(ProtocolTreeConfig.TAG_PROTOCOL_UPDATED,
561
+ Icon.UPDATED)
562
+ t.tag_configure('protocol_base', image=self.getImage(Icon.GROUP))
563
+ t.tag_configure('protocol_group', image=self.getImage(Icon.GROUP))
564
+ t.tag_configure('section', font=self.window.fontBold)
565
+ return t
566
+
567
+ def _createProtocolsPanel(self, parent, bgColor):
568
+ """Create the protocols Tree displayed in left panel"""
569
+ comboFrame = tk.Frame(parent, bg=bgColor)
570
+ tk.Label(comboFrame, text='View', bg=bgColor).grid(row=0, column=0,
571
+ padx=(0, 5), pady=5)
572
+ choices = self.getProtocolViews()
573
+ initialChoice = self.settings.getProtocolView()
574
+ combo = pwgui.widgets.ComboBox(comboFrame, choices=choices,
575
+ initial=initialChoice)
576
+ combo.setChangeCallback(self._onSelectProtocols)
577
+ combo.grid(row=0, column=1)
578
+ comboFrame.grid(row=0, column=0, padx=5, pady=5, sticky='nw')
579
+
580
+ t = self._createProtocolsTree(parent)
581
+ t.grid(row=1, column=0, sticky='news')
582
+ # Program automatic refresh
583
+ t.after(3000, self._automaticRefreshRuns)
584
+ self.protTree = t
585
+
586
+ def getProtocolViews(self):
587
+
588
+ if self._protocolViews is None:
589
+ self._loadProtocols()
590
+
591
+ return list(self._protocolViews.keys())
592
+
593
+ def getCurrentProtocolView(self):
594
+ """ Select the view that is currently selected.
595
+ Read from the settings the last selected view
596
+ and get the information from the self._protocolViews dict.
597
+ """
598
+ currentView = self.project.getProtocolView()
599
+ if currentView in self.getProtocolViews():
600
+ viewKey = currentView
601
+ else:
602
+ viewKey = self.getProtocolViews()[0]
603
+ self.project.settings.setProtocolView(viewKey)
604
+ if currentView is not None:
605
+ logger.warning("PROJECT: Warning, protocol view '%s' not found. Using '%s' instead." % (currentView, viewKey))
606
+
607
+ return self._protocolViews[viewKey]
608
+
609
+ def _loadProtocols(self):
610
+ """ Load protocol configuration from a .conf file. """
611
+ # If the host file is not passed as argument...
612
+ configProtocols = Config.SCIPION_PROTOCOLS
613
+
614
+ localDir = Config.SCIPION_LOCAL_CONFIG
615
+ protConf = os.path.join(localDir, configProtocols)
616
+ self._protocolViews = ProtocolTreeConfig.load(self.project.getDomain(),
617
+ protConf)
618
+
619
+ def _onSelectProtocols(self, combo):
620
+ """ This function will be called when a protocol menu
621
+ is selected. The index of the new menu is passed.
622
+ """
623
+ protView = combo.getText()
624
+ self.settings.setProtocolView(protView)
625
+ self.protCfg = self.getCurrentProtocolView()
626
+ self.updateProtocolsTree(self.protCfg)
627
+
628
+ def populateTree(self, tree, treeItems, prefix, obj, level=0):
629
+
630
+ # If node does not have leaves (protocols) do not add it
631
+ if not obj.visible:
632
+ return
633
+
634
+ text = obj.text
635
+ if text:
636
+ value = obj.value if obj.value is not None else text
637
+ key = '%s.%s' % (prefix, value)
638
+ img = obj.icon if obj.icon is not None else ''
639
+ tag = obj.tag if obj.tag is not None else ''
640
+
641
+ if img:
642
+ if isinstance(img,str) and "bookmark" in img:
643
+ img = pwutils.Icon.FAVORITE
644
+ img = self.getImage(img)
645
+ # If image is none
646
+ img = img if img is not None else ''
647
+
648
+ protClassName = value.split('.')[-1] # Take last part
649
+ emProtocolsDict = self.domain.getProtocols()
650
+ prot = emProtocolsDict.get(protClassName, None)
651
+
652
+ if tag == 'protocol' and text == 'default':
653
+ if prot is None:
654
+ logger.warning("Protocol className '%s' not found!!!. \n"
655
+ "Fix your config/protocols.conf configuration."
656
+ % protClassName)
657
+ return
658
+
659
+ text = prot.getClassLabel()
660
+
661
+ item = tree.insert(prefix, 'end', key, text=text, image=img, tags=tag)
662
+ treeItems[item] = obj
663
+ # Check if the attribute should be open or close
664
+ openItem = getattr(obj, 'openItem', level < 2)
665
+ if openItem:
666
+ tree.item(item, open=openItem)
667
+
668
+ # I think this mode is deprecated
669
+ if obj.value is not None and tag == 'protocol_base':
670
+ logger.warning('protocol_base tags are deprecated')
671
+ else:
672
+ key = prefix
673
+
674
+ for sub in obj:
675
+ self.populateTree(tree, treeItems, key, sub,
676
+ level + 1)
677
+
678
+ def updateProtocolsTree(self, protCfg):
679
+
680
+ try:
681
+ self.protCfg = protCfg
682
+ self.protTree.clear()
683
+ self.protTree.unbind(TK.TREEVIEW_OPEN)
684
+ self.protTree.unbind(TK.TREEVIEW_CLOSE)
685
+ self.protTreeItems = {}
686
+ self.populateTree(self.protTree, self.protTreeItems, '', self.protCfg)
687
+ self.protTree.bind(TK.TREEVIEW_OPEN,
688
+ lambda e: self._treeViewItemChange(True))
689
+ self.protTree.bind(TK.TREEVIEW_CLOSE,
690
+ lambda e: self._treeViewItemChange(False))
691
+ except Exception as e:
692
+ # Tree can't be loaded report back, but continue
693
+ logger.error("Protocols tree couldn't be loaded.", exc_info=e)
694
+
695
+ def _treeViewItemChange(self, openItem):
696
+ item = self.protTree.focus()
697
+ if item in self.protTreeItems:
698
+ self.protTreeItems[item].openItem = openItem
699
+
700
+ def createRunsTree(self, parent):
701
+ self.provider = RunsTreeProvider(self.project, self._runActionClicked)
702
+
703
+ # This line triggers the getRuns for the first time.
704
+ # Ne need to force the check pids here, temporary
705
+ self.provider._checkPids = True
706
+
707
+ t = pwgui.tree.BoundTree(parent, self.provider, style=LIST_TREEVIEW)
708
+ self.provider._checkPids = False
709
+
710
+ t.itemDoubleClick = self._runItemDoubleClick
711
+ t.itemClick = self._runTreeItemClick
712
+
713
+ return t
714
+
715
+ def updateRunsTree(self, refresh=False):
716
+ self.provider.setRefresh(refresh)
717
+ self.runsTree.update()
718
+ self.updateRunsTreeSelection()
719
+
720
+ def updateRunsTreeSelection(self):
721
+ for prot in self._iterSelectedProtocols():
722
+ treeId = self.provider.getObjectFromId(prot.getObjId())._treeId
723
+ self.runsTree.selection_add(treeId)
724
+
725
+ def createRunsGraph(self, parent):
726
+ self.runsGraphCanvas = pwgui.Canvas(parent, width=400, height=400,
727
+ tooltipCallback=self._runItemTooltip,
728
+ tooltipDelay=1000,
729
+ name=ProtocolsView.RUNS_CANVAS_NAME,
730
+ takefocus=True,
731
+ highlightthickness=0)
732
+
733
+ self.runsGraphCanvas.onClickCallback = self._runItemClick
734
+ self.runsGraphCanvas.onDoubleClickCallback = self._runItemDoubleClick
735
+ self.runsGraphCanvas.onRightClickCallback = self._runItemRightClick
736
+ self.runsGraphCanvas.onControlClickCallback = self._runItemControlClick
737
+ self.runsGraphCanvas.onAreaSelected = self._selectItemsWithinArea
738
+ self.runsGraphCanvas.onMiddleMouseClickCallback = self._runItemMiddleClick
739
+
740
+ parent.grid_columnconfigure(0, weight=1)
741
+ parent.grid_rowconfigure(0, weight=1)
742
+
743
+ self.settings.getNodes().updateDict()
744
+ self.settings.getLabels().updateDict()
745
+
746
+ self.updateRunsGraph()
747
+
748
+ def updateRunsGraph(self, refresh=False, checkPids=False, position=None):
749
+
750
+ self.runsGraph = self.project.getRunsGraph(refresh=refresh,
751
+ checkPids=checkPids)
752
+ self.drawRunsGraph(position=position)
753
+
754
+ def drawRunsGraph(self, reorganize=False, position=None):
755
+
756
+ # Check if there are positions stored
757
+ if reorganize:
758
+ # Create layout to arrange nodes as a level tree
759
+ layout = pwgui.LevelTreeLayout()
760
+ self.runsGraphCanvas.reorganizeGraph(self.runsGraph, layout)
761
+ else:
762
+ self.runsGraphCanvas.clear()
763
+ layout = pwgui.LevelTreeLayout(partial=True)
764
+
765
+ # Create empty nodeInfo for new runs
766
+ for node in self.runsGraph.getNodes():
767
+ nodeId = node.run.getObjId() if node.run else 0
768
+ nodeInfo = self.settings.getNodeById(nodeId)
769
+ if nodeInfo is None:
770
+ if position is None:
771
+ position = (0,0)
772
+ self.settings.addNode(nodeId, x=position[0], y=position[1], expanded=True,
773
+ visible=True)
774
+
775
+ self.runsGraphCanvas.drawGraph(self.runsGraph, layout,
776
+ drawNode=self.createRunItem,
777
+ nodeList=self.settings.nodeList)
778
+
779
+ projectSize = len(self.runsGraph.getNodes())
780
+ settingsNodeSize = len(self.settings.getNodes())
781
+ if projectSize < settingsNodeSize -1:
782
+ logger.info("Settings nodes list (%s) is bigger than current project nodes (%s). "
783
+ "Clean up needed?" % (settingsNodeSize, projectSize) )
784
+ self.settings.cleanUpNodes(self.runsGraph.getNodeNames(), toRemove=False)
785
+
786
+ def createRunItem(self, canvas, node):
787
+
788
+ nodeId = node.run.getObjId() if node.run else 0
789
+ nodeInfo = self.settings.getNodeById(nodeId)
790
+
791
+ # Extend attributes: use some from nodeInfo
792
+ node.expanded = nodeInfo.isExpanded()
793
+ node.x, node.y = nodeInfo.getPosition()
794
+ node.visible = nodeInfo.isVisible()
795
+ nodeText = self._getNodeText(node)
796
+
797
+ # Get status color
798
+ statusColor = getStatusColorFromNode(node)
799
+
800
+ # Get the box color (depends on color mode: label or status)
801
+ boxColor = self._getBoxColor(nodeInfo, statusColor, node)
802
+
803
+ # Draw the box
804
+ item = RunBox(nodeInfo, self.runsGraphCanvas,
805
+ nodeText, node.x, node.y,
806
+ bgColor=boxColor, textColor='black')
807
+ # No border
808
+ item.margin = 0
809
+
810
+ # Paint the oval..if apply.
811
+ #self._paintOval(item, statusColor)
812
+
813
+ # Paint the bottom line (for now only labels are painted there).
814
+ self._paintBottomLine(item)
815
+
816
+ item.setSelected(nodeId in self._selection)
817
+ return item
818
+
819
+ def _getBoxColor(self, nodeInfo, statusColor, node):
820
+
821
+ try:
822
+
823
+ # If the color has to go to the box
824
+ if self.settings.statusColorMode() or self.settings.labelsColorMode():
825
+ boxColor = statusColor
826
+
827
+ elif self.settings.ageColorMode():
828
+
829
+ if node.run:
830
+
831
+ # Project elapsed time
832
+ elapsedTime = node.run.getProject().getElapsedTime()
833
+ creationTime = node.run.getProject().getCreationTime()
834
+
835
+ # Get the latest activity timestamp
836
+ ts = node.run.endTime.datetime()
837
+
838
+ if elapsedTime is None or creationTime is None or ts is None:
839
+ boxColor = DEFAULT_BOX_COLOR
840
+
841
+ else:
842
+
843
+ # tc closer to the end are younger
844
+ protAge = ts - creationTime
845
+
846
+ boxColor = self._ageColor('#6666ff', elapsedTime,
847
+ protAge)
848
+ else:
849
+ boxColor = DEFAULT_BOX_COLOR
850
+
851
+ elif self.settings.sizeColorMode():
852
+
853
+ # Get the protocol size
854
+ protSize = self._getRunSize(node)
855
+
856
+ boxColor = self._sizeColor(protSize)
857
+
858
+ # ... box is for the labels.
859
+ elif self.settings.labelsColorMode():
860
+ # If there is only one label use the box for the color.
861
+ if self._getLabelsCount(nodeInfo) == 1:
862
+
863
+ labelId = nodeInfo.getLabels()[0]
864
+ label = self.settings.getLabels().getLabel(labelId)
865
+
866
+ # If there is no label (it has been deleted)
867
+ if label is None:
868
+ nodeInfo.getLabels().remove(labelId)
869
+ boxColor = DEFAULT_BOX_COLOR
870
+ else:
871
+ boxColor = label.getColor()
872
+
873
+ else:
874
+ boxColor = DEFAULT_BOX_COLOR
875
+ else:
876
+ boxColor = DEFAULT_BOX_COLOR
877
+
878
+ return boxColor
879
+ except Exception as e:
880
+ logger.debug("Can't get color for %s. %s" % (node, e))
881
+ return DEFAULT_BOX_COLOR
882
+
883
+ @staticmethod
884
+ def _getRunSize(node):
885
+ """
886
+ Returns the size "recursively" of a run
887
+
888
+ :param node: node of the graph.
889
+ :return: size in bytes
890
+
891
+ """
892
+
893
+ if not node.run:
894
+ return 0
895
+ else:
896
+ return node.run.getSize()
897
+
898
+ @classmethod
899
+ def _sizeColor(cls, size):
900
+ """
901
+ Returns the color that corresponds to the size
902
+ :param size:
903
+ :return:
904
+ """
905
+
906
+ for threshold, color in cls.SIZE_COLORS.items():
907
+ if size <= threshold:
908
+ return color
909
+
910
+ return "#000000"
911
+ @staticmethod
912
+ def _ageColor(rgbColorStr, projectAge, protocolAge):
913
+
914
+ # Get the ratio
915
+ ratio = protocolAge.seconds / float(projectAge.seconds)
916
+
917
+ # Invert direction: older = white = 100%, newest = rgbColor = 0%
918
+ ratio = 1 - ratio
919
+
920
+ # There are cases coming with protocols older than the project.
921
+ ratio = 0 if ratio < 0 else ratio
922
+
923
+ hexTuple = pwutils.hex_to_rgb(rgbColorStr)
924
+ lighterTuple = pwutils.lighter(hexTuple, ratio)
925
+ return pwutils.rgb_to_hex(lighterTuple)
926
+
927
+ @staticmethod
928
+ def _getLabelsCount(nodeInfo):
929
+
930
+ return 0 if nodeInfo.getLabels() is None else len(nodeInfo.getLabels())
931
+
932
+ def _paintBottomLine(self, item):
933
+
934
+ if self.settings.labelsColorMode() or self.settings.statusColorMode():
935
+ self._addLabels(item)
936
+
937
+ def _paintOval(self, item, statusColor):
938
+ # Show the status as a circle in the top right corner
939
+ if not self.settings.statusColorMode():
940
+ # Option: Status item.
941
+ (topLeftX, topLeftY, bottomRightX,
942
+ bottomRightY) = self.runsGraphCanvas.bbox(item.id)
943
+ statusSize = 10
944
+ statusX = bottomRightX - (statusSize + 3)
945
+ statusY = topLeftY + 3
946
+
947
+ pwgui.Oval(self.runsGraphCanvas, statusX, statusY, statusSize,
948
+ color=statusColor, anchor=item)
949
+
950
+ # in statusColorMode
951
+ else:
952
+ # Show a black circle if there is any label
953
+ if self._getLabelsCount(item.nodeInfo) > 0:
954
+ (topLeftX, topLeftY, bottomRightX,
955
+ bottomRightY) = self.runsGraphCanvas.bbox(item.id)
956
+ statusSize = 10
957
+ statusX = bottomRightX - (statusSize + 3)
958
+ statusY = topLeftY + 3
959
+
960
+ pwgui.Oval(self.runsGraphCanvas, statusX, statusY, statusSize,
961
+ color='black', anchor=item)
962
+
963
+ def _getNodeText(self, node):
964
+ nodeText = node.getLabel()
965
+ # Truncate text to prevent overflow
966
+ if len(nodeText) > 40:
967
+ nodeText = nodeText[:37] + "..."
968
+
969
+ if node.run:
970
+ expandedStr = '' if node.expanded else '\n ➕ %s more' % str(node.countChildren({}))
971
+ if self.runsView == VIEW_TREE_SMALL:
972
+ nodeText = node.getName() + expandedStr
973
+ else:
974
+ nodeText += expandedStr + '\n' + node.run.getStatusMessage() if not expandedStr else expandedStr
975
+ if node.run.summaryWarnings:
976
+ nodeText += u' \u26a0'
977
+ return nodeText
978
+
979
+ def _addLabels(self, item):
980
+ # If there is only one label it should be already used in the box color.
981
+ if self._getLabelsCount(item.nodeInfo) < 1:
982
+ return
983
+ # Get the positions of the box
984
+ (topLeftX, topLeftY, bottomRightX,
985
+ bottomRightY) = self.runsGraphCanvas.bbox(item.id)
986
+
987
+ # Get the width of the box
988
+ boxWidth = bottomRightX - topLeftX
989
+
990
+ # Set the size
991
+ marginV = 3
992
+ marginH = 2
993
+ labelWidth = (boxWidth - (2 * marginH)) / len(item.nodeInfo.getLabels())
994
+ labelHeight = 8
995
+
996
+ # Leave some margin on the right and bottom
997
+ labelX = bottomRightX - marginH
998
+ labelY = bottomRightY - (labelHeight + marginV)
999
+
1000
+ for index, labelId in enumerate(item.nodeInfo.getLabels()):
1001
+
1002
+ # Get the label
1003
+ label = self.settings.getLabels().getLabel(labelId)
1004
+
1005
+ # If not none
1006
+ if label is not None:
1007
+ # Move X one label to the left
1008
+ if index == len(item.nodeInfo.getLabels()) - 1:
1009
+ labelX = topLeftX + marginH
1010
+ else:
1011
+ labelX -= labelWidth
1012
+
1013
+ pwgui.Rectangle(self.runsGraphCanvas, labelX, labelY,
1014
+ labelWidth, labelHeight, color=label.getColor(),
1015
+ anchor=item)
1016
+ else:
1017
+
1018
+ item.nodeInfo.getLabels().remove(labelId)
1019
+
1020
+ def switchRunsView(self):
1021
+ viewValue = self.switchCombo.getValue()
1022
+ self.runsView = viewValue
1023
+ self.settings.setRunsView(viewValue)
1024
+
1025
+ if viewValue == VIEW_LIST:
1026
+ self.runsTree.grid(row=0, column=0, sticky='news')
1027
+ self.runsGraphCanvas.frame.grid_remove()
1028
+ self.updateRunsTree()
1029
+ self.viewButtons[ACTION_TREE].grid_remove()
1030
+ self._lastRightClickPos = None
1031
+ else:
1032
+ self.runsTree.grid_remove()
1033
+ self.updateRunsGraph()
1034
+ self.runsGraphCanvas.frame.grid(row=0, column=0, sticky='news')
1035
+ self.viewButtons[ACTION_TREE].grid(row=0, column=1)
1036
+
1037
+ def _protocolItemClick(self, e=None, position=None):
1038
+ """ Callback for the window to add a new protocol.
1039
+ """
1040
+
1041
+ # Get the tree widget that originated the event
1042
+ # it could be the left panel protocols tree or just
1043
+ # the search protocol dialog tree. In this case now non installed protocols are listed from the suggestions
1044
+ tree = e.widget
1045
+
1046
+ # Get the class name
1047
+ protClassName = tree.getFirst().split('.')[-1]
1048
+
1049
+ # Get the class: Now it may not be installed!!
1050
+ protClass = self.domain.getProtocols().get(protClassName)
1051
+
1052
+ # If found continue to open the protocol form to ask for parameters
1053
+ if protClass is not None:
1054
+ prot = self.project.newProtocol(protClass)
1055
+ self._openProtocolForm(prot, disableRunMode=True, position=position, previousProt=self.getSelectedProtocol())
1056
+ # Missing class: probably not installed. Inform
1057
+ else:
1058
+ # Get the value as populated in pyworkflow.gui.project.searchprotocol.py:235 comming from SearchProtocolWindow.addSuggestions
1059
+ rowValues = tree.item(protClassName)["values"]
1060
+ prot_label = rowValues[0]
1061
+ installedMsg = rowValues[2]
1062
+ msg = ("%s %s To get it use the plugin manager or installation "
1063
+ "command and restart Scipion. See %s .") % (prot_label, installedMsg, DOCSITEURLS.PLUGIN_MANAGER)
1064
+ showInfo("%s protocol is missing." % prot_label,
1065
+ msg, self)
1066
+
1067
+ def _toggleColorScheme(self, e=None):
1068
+
1069
+ currentMode = self.settings.getColorMode()
1070
+
1071
+ if currentMode >= len(self.settings.COLOR_MODES) - 1:
1072
+ currentMode = -1
1073
+
1074
+ nextColorMode = currentMode + 1
1075
+
1076
+ self.settings.setColorMode(nextColorMode)
1077
+ # WHY? self._updateActionToolbar()
1078
+ # self.updateRunsGraph()
1079
+ self.drawRunsGraph()
1080
+ self._infoAboutColorScheme()
1081
+
1082
+ def _infoAboutColorScheme(self):
1083
+ """ Writes in the info widget a brief description abot the color scheme."""
1084
+
1085
+ colorScheme = self.settings.getColorMode()
1086
+
1087
+ msg = "Color mode changed to %s. %s"
1088
+ if colorScheme == self.settings.COLOR_MODE_AGE:
1089
+ msg = msg % ("AGE", "Young boxes will have an darker color.")
1090
+ elif colorScheme == self.settings.COLOR_MODE_SIZE:
1091
+ keys = list(self.SIZE_COLORS.keys())
1092
+ msg = msg % ("SIZE", "Semaphore color scheme. Green <= %s, Orange <=%s, Red <=%s, Dark quite big." %
1093
+ (pwutils.prettySize(keys[0]),
1094
+ pwutils.prettySize(keys[1]),
1095
+ pwutils.prettySize(keys[2]))
1096
+ )
1097
+ elif colorScheme == self.settings.COLOR_MODE_STATUS:
1098
+ msg = msg % ("STATUS", "Color based on the status. A black circle indicates it has labels")
1099
+ elif colorScheme == self.settings.COLOR_MODE_LABELS:
1100
+ msg = msg % ("LABELS", "Color based on custom labels you've assigned. Small circles reflect the protocol status")
1101
+
1102
+ self.info(msg)
1103
+ def _toggleDebug(self, e=None):
1104
+ Config.toggleDebug()
1105
+
1106
+ def _selectAllProtocols(self, e=None):
1107
+ self._selection.clear()
1108
+
1109
+ # WHY GOING TO THE db?
1110
+ # Let's try using in memory data.
1111
+ # for prot in self.project.getRuns():
1112
+ for prot in self.project.runs:
1113
+ self._selection.append(prot.getObjId())
1114
+ self._updateSelection()
1115
+
1116
+ # self.updateRunsGraph()
1117
+ self.drawRunsGraph()
1118
+
1119
+ def _inspectProtocols(self, e=None):
1120
+ objs = self._getSelectedProtocols()
1121
+ # We will inspect the selected objects or
1122
+ # the whole project is no protocol is selected
1123
+ if len(objs) > 0:
1124
+ objs.sort(key=lambda obj: obj._objId, reverse=True)
1125
+ filePath = objs[0]._getLogsPath('inspector.csv')
1126
+ doInspect = True
1127
+ else:
1128
+ proj = self.project
1129
+ filePath = proj.getLogPath('inspector.csv')
1130
+ objs = [proj]
1131
+ doInspect = pwgui.dialog.askYesNo(Message.TITLE_INSPECTOR,
1132
+ Message.LABEL_INSPECTOR, self.root)
1133
+
1134
+ if doInspect:
1135
+ inspectObj(objs, filePath)
1136
+ # we open the resulting CSV file with the OS default software
1137
+ pwgui.text.openTextFileEditor(filePath)
1138
+
1139
+ # NOt used!: pconesa 02/11/2016.
1140
+ # def _deleteSelectedProtocols(self, e=None):
1141
+ #
1142
+ # for selection in self._selection:
1143
+ # self.project.getProtocol(self._selection[0])
1144
+ #
1145
+ #
1146
+ # self._updateSelection()
1147
+ # self.updateRunsGraph()
1148
+
1149
+ def _updateSelection(self):
1150
+ self._fillSummary()
1151
+ self._fillMethod()
1152
+ self._fillLogs()
1153
+ self._showHideAnalyzeResult()
1154
+
1155
+ if self._isSingleSelection():
1156
+ last = self.getSelectedProtocol()
1157
+ self._lastSelectedProtId = last.getObjId() if last else None
1158
+
1159
+ self._updateActionToolbar()
1160
+
1161
+ def _runTreeItemClick(self, item=None):
1162
+ self._selection.clear()
1163
+ for prot in self.runsTree.iterSelectedObjects():
1164
+ self._selection.append(prot.getObjId())
1165
+ self._updateSelection()
1166
+
1167
+ def _selectItemProtocol(self, prot):
1168
+ """ Call this function when a new box (item) of a protocol
1169
+ is selected. It should be called either from itemClick
1170
+ or itemRightClick
1171
+ """
1172
+ self._selection.clear()
1173
+ self.settings.dataSelection.clear()
1174
+ self._selection.append(prot.getObjId())
1175
+
1176
+ # Select output data too
1177
+ self.toggleDataSelection(prot, True)
1178
+
1179
+ self._updateSelection()
1180
+ self.runsGraphCanvas.update_idletasks()
1181
+
1182
+ def _deselectItems(self, exception):
1183
+ """ Deselect all items except the item one. Pass item=None to deselect all
1184
+ """
1185
+ g = self.project.getRunsGraph()
1186
+
1187
+ for node in g.getNodes():
1188
+ if node.run and node.run.getObjId() in self._selection:
1189
+ # This option is only for compatibility with all projects
1190
+ if hasattr(node, 'item'):
1191
+ node.item.setSelected(False)
1192
+
1193
+ # clear the selection
1194
+ self._selection.clear()
1195
+
1196
+ if exception:
1197
+ exception.setSelected(True)
1198
+ self._selection.append(exception.id)
1199
+ def _runItemClick(self, item=None, event=None):
1200
+
1201
+ # If click is in a empty area....start panning
1202
+ if item is None:
1203
+ return
1204
+
1205
+ self.runsGraphCanvas.focus_set()
1206
+
1207
+ # Get last selected item for tree or graph
1208
+ if self.runsView == VIEW_LIST:
1209
+ prot = self.project.mapper.selectById(int(self.runsTree.getFirst()))
1210
+ else:
1211
+ prot = item.node.run
1212
+ if prot is None: # in case it is the main "Project" node
1213
+ return
1214
+ self._deselectItems(item)
1215
+ self._selectItemProtocol(prot)
1216
+
1217
+ def _runItemDoubleClick(self, item=None, e=None):
1218
+ if item.nodeInfo.isExpanded():
1219
+ self._runActionClicked(ACTION_EDIT)
1220
+
1221
+ def _runItemMiddleClick(self, e=None):
1222
+ self._runActionClicked(ACTION_SELECT_TO)
1223
+
1224
+ def _runItemRightClick(self, item=None, e=None):
1225
+ """ Right click on the canvas callback
1226
+
1227
+ :param item: item right-clicked. None if clicked in the void
1228
+ :param e: event object with context information"""
1229
+ prot = None
1230
+ # If there's been a click in a box
1231
+ if item is not None:
1232
+
1233
+ # Get the protocol associated
1234
+ prot = item.node.run
1235
+
1236
+ if prot is None: # in case it is the main "Project" node
1237
+ return
1238
+
1239
+ # Only select item with right-click if there is a single
1240
+ # item selection, not for multiple selection
1241
+ if len(self._selection) == 1:
1242
+ self._deselectItems(item)
1243
+ self._selectItemProtocol(prot)
1244
+ self._lastRightClickPos = self.runsGraphCanvas.eventPos
1245
+ else: # Click on empty area
1246
+ self._deselectItems(None)
1247
+ self._updateSelection()
1248
+
1249
+ return self.provider.getObjectActions(prot,withEvent=True)
1250
+
1251
+ def _runItemControlClick(self, item=None, event=None):
1252
+ # Get last selected item for tree or graph
1253
+ if self.runsView == VIEW_LIST:
1254
+ # TODO: Prot is not used!!
1255
+ prot = self.project.mapper.selectById(int(self.runsTree.getFirst()))
1256
+ else:
1257
+ prot = item.node.run
1258
+ protId = prot.getObjId()
1259
+ if protId in self._selection:
1260
+ item.setSelected(False)
1261
+ self._selection.remove(protId)
1262
+
1263
+ # Remove data selected
1264
+ self.toggleDataSelection(prot, False)
1265
+ else:
1266
+
1267
+ item.setSelected(True)
1268
+ if len(self._selection) == 1: # repaint first selected item
1269
+ firstSelectedNode = self.runsGraph.getNode(str(self._selection[0]))
1270
+ if hasattr(firstSelectedNode, 'item'):
1271
+ firstSelectedNode.item.setSelected(False)
1272
+ firstSelectedNode.item.setSelected(True)
1273
+ self._selection.append(prot.getObjId())
1274
+
1275
+ # Select output data too
1276
+ self.toggleDataSelection(prot, True)
1277
+
1278
+ self._updateSelection()
1279
+
1280
+ def toggleDataSelection(self, prot, append):
1281
+
1282
+ # Go through the data selection
1283
+ for paramName, output in prot.iterOutputAttributes():
1284
+ if append:
1285
+ self.settings.dataSelection.append(output.getObjId())
1286
+ else:
1287
+ self.settings.dataSelection.remove(output.getObjId())
1288
+
1289
+ def _runItemTooltip(self, tw, item):
1290
+ """ Create the contents of the tooltip to be displayed
1291
+ for the given item.
1292
+ Params:
1293
+ tw: a tk.TopLevel instance (ToolTipWindow)
1294
+ item: the selected item.
1295
+ """
1296
+ prot = item.node.run
1297
+
1298
+ if prot:
1299
+ tm = '*%s*\n' % prot.getRunName()
1300
+ tm += 'Identifier :%s\n' % prot.getObjId()
1301
+ tm += 'Status: %s\n' % prot.getStatusMessage()
1302
+ tm += 'Wall time: %s\n' % pwutils.prettyDelta(prot.getElapsedTime())
1303
+ tm += 'CPU time: %s\n' % pwutils.prettyDelta(dt.timedelta(seconds=prot.cpuTime))
1304
+ # tm += 'Folder size: %s\n' % pwutils.prettySize(prot.getSize())
1305
+
1306
+ if not hasattr(tw, 'tooltipText'):
1307
+ frame = tk.Frame(tw)
1308
+ frame.grid(row=0, column=0)
1309
+ tw.tooltipText = pwgui.dialog.createMessageBody(frame, tm, None,
1310
+ textPad=0,
1311
+ textBg=Color.ALT_COLOR_2)
1312
+ tw.tooltipText.config(bd=1, relief=tk.RAISED)
1313
+ else:
1314
+ pwgui.dialog.fillMessageText(tw.tooltipText, tm)
1315
+
1316
+ @staticmethod
1317
+ def _selectItemsWithinArea(x1, y1, x2, y2, enclosed=False):
1318
+ """
1319
+ Parameters
1320
+ ----------
1321
+ x1: x coordinate of first corner of the area
1322
+ y1: y coordinate of first corner of the area
1323
+ x2: x coordinate of second corner of the area
1324
+ y2: y coordinate of second corner of the area
1325
+ enclosed: Default True. Returns enclosed items,
1326
+ overlapping items otherwise.
1327
+ Returns
1328
+ -------
1329
+ Nothing
1330
+
1331
+ """
1332
+
1333
+ return
1334
+ # NOT working properly: Commented for the moment.
1335
+ # if enclosed:
1336
+ # items = self.runsGraphCanvas.find_enclosed(x1, y1, x2, y2)
1337
+ # else:
1338
+ # items = self.runsGraphCanvas.find_overlapping(x1, y1, x2, y2)
1339
+ #
1340
+ # update = False
1341
+ #
1342
+ # for itemId in items:
1343
+ # if itemId in self.runsGraphCanvas.items:
1344
+ #
1345
+ # item = self.runsGraphCanvas.items[itemId]
1346
+ # if not item.node.isRoot():
1347
+ # item.setSelected(True)
1348
+ # self._selection.append(itemId)
1349
+ # update = True
1350
+ #
1351
+ # if update is not None: self._updateSelection()
1352
+
1353
+ def _openProtocolForm(self, prot, disableRunMode=False, position=None, previousProt=None):
1354
+ """Open the Protocol GUI Form given a Protocol instance
1355
+
1356
+ :param prot: protocol to show and edit its parameters
1357
+ :param disableRunMode: to show the form it in read only mode
1358
+ :param position: Optional. Position to add the box once added to the graph.
1359
+ :param previousProt: Optional. If passed it will try to link this protocol to the previous protocol.
1360
+
1361
+ """
1362
+
1363
+ w = FormWindow(Message.TITLE_NAME_RUN + prot.getClassName(),
1364
+ prot, self._executeSaveProtocol, self.window,
1365
+ updateProtocolCallback=self._updateProtocol,
1366
+ disableRunMode=disableRunMode, position=position, previousProt=previousProt)
1367
+ w.adjustSize()
1368
+ w.show(center=True)
1369
+
1370
+ def _browseSteps(self):
1371
+ """ Open a new window with the steps list. """
1372
+ window = StepsWindow(Message.TITLE_BROWSE_DATA, self.window,
1373
+ self.getSelectedProtocol())
1374
+ window.show()
1375
+
1376
+ def _browseRunData(self):
1377
+ provider = ProtocolTreeProvider(self.getSelectedProtocol())
1378
+ window = pwgui.browser.BrowserWindow(Message.TITLE_BROWSE_DATA,
1379
+ self.window)
1380
+ window.setBrowser(pwgui.browser.ObjectBrowser(window.root, provider))
1381
+ window.itemConfig(self.getSelectedProtocol(), open=True)
1382
+ window.show()
1383
+
1384
+ def _browseRunDirectory(self):
1385
+ """ Open a file browser to inspect the files generated by the run. """
1386
+ protocol = self.getSelectedProtocol()
1387
+ workingDir = protocol.getWorkingDir()
1388
+ if os.path.exists(workingDir):
1389
+
1390
+ protFolderShortCut = ShortCut.factory(workingDir,name="Protocol folder", icon=None ,toolTip="Protocol directory")
1391
+ window = pwgui.browser.FileBrowserWindow("Browsing: " + workingDir,
1392
+ master=self.window,
1393
+ path=workingDir,
1394
+ shortCuts=[protFolderShortCut])
1395
+ window.show()
1396
+ else:
1397
+ self.window.showInfo("Protocol working dir does not exists: \n %s"
1398
+ % workingDir)
1399
+
1400
+ def _iterSelectedProtocols(self):
1401
+ for protId in sorted(self._selection):
1402
+ prot = self.project.getRunsGraph().getNode(str(protId)).run
1403
+ if prot:
1404
+ yield prot
1405
+
1406
+ def _getSelectedProtocols(self):
1407
+ return [prot for prot in self._iterSelectedProtocols()]
1408
+
1409
+ def _iterSelectedNodes(self):
1410
+
1411
+ for protId in sorted(self._selection):
1412
+ node = self.settings.getNodeById(protId)
1413
+
1414
+ yield node
1415
+
1416
+ def _getSelectedNodes(self):
1417
+ return [node for node in self._iterSelectedNodes()]
1418
+
1419
+ def getSelectedProtocol(self):
1420
+ if self._selection:
1421
+ return self.project.getProtocol(self._selection[0], fromRuns=True)
1422
+ return None
1423
+
1424
+ def _showHideAnalyzeResult(self):
1425
+
1426
+ if self._selection:
1427
+ self.btnAnalyze.grid()
1428
+ else:
1429
+ self.btnAnalyze.grid_remove()
1430
+
1431
+ def _fillSummary(self):
1432
+ self.summaryText.setReadOnly(False)
1433
+ self.summaryText.clear()
1434
+ self.infoTree.clear()
1435
+ n = len(self._selection)
1436
+
1437
+ if n == 1:
1438
+ prot = self.getSelectedProtocol()
1439
+
1440
+ if prot:
1441
+ provider = RunIOTreeProvider(self, prot, self.project.mapper, self.info)
1442
+ self.infoTree.setProvider(provider)
1443
+ self.infoTree.grid(row=0, column=0, sticky='news')
1444
+ self.infoTree.update_idletasks()
1445
+ # Update summary
1446
+ self.summaryText.addText(prot.summary())
1447
+ else:
1448
+ self.infoTree.clear()
1449
+
1450
+ elif n > 1:
1451
+ self.infoTree.clear()
1452
+ for prot in self._iterSelectedProtocols():
1453
+ self.summaryText.addLine('> _%s_' % prot.getRunName())
1454
+ for line in prot.summary():
1455
+ self.summaryText.addLine(line)
1456
+ self.summaryText.addLine('')
1457
+ self.summaryText.setReadOnly(True)
1458
+
1459
+ def _fillMethod(self):
1460
+
1461
+ try:
1462
+ self.methodText.setReadOnly(False)
1463
+ self.methodText.clear()
1464
+ self.methodText.addLine("*METHODS:*")
1465
+ cites = OrderedDict()
1466
+
1467
+ for prot in self._iterSelectedProtocols():
1468
+ self.methodText.addLine('> _%s_' % prot.getRunName())
1469
+ for line in prot.getParsedMethods():
1470
+ self.methodText.addLine(line)
1471
+ cites.update(prot.getCitations())
1472
+ cites.update(prot.getPackageCitations())
1473
+ self.methodText.addLine('')
1474
+
1475
+ if cites:
1476
+ self.methodText.addLine('*REFERENCES:* '
1477
+ ' [[sci-bib:][<<< Open as bibtex >>>]]')
1478
+ for cite in cites.values():
1479
+ self.methodText.addLine(cite)
1480
+
1481
+ self.methodText.setReadOnly(True)
1482
+ except Exception as e:
1483
+ self.methodText.addLine('Could not load all methods:' + str(e))
1484
+
1485
+ def _fillLogs(self):
1486
+ try:
1487
+ prot = self.getSelectedProtocol()
1488
+
1489
+ if not self._isSingleSelection() or not prot:
1490
+ self.outputViewer.clear()
1491
+ self._lastStatus = None
1492
+ elif prot.getObjId() != self._lastSelectedProtId:
1493
+ self._lastStatus = prot.getStatus()
1494
+ i = self.outputViewer.getIndex()
1495
+ self.outputViewer.clear()
1496
+ # Right now skip the err tab since we are redirecting
1497
+ # stderr to stdout
1498
+ out, err, schedule = prot.getLogPaths()
1499
+ self.outputViewer.addFile(out)
1500
+ self.outputViewer.addFile(err)
1501
+ if os.path.exists(schedule):
1502
+ self.outputViewer.addFile(schedule)
1503
+ elif i == 2:
1504
+ i = 0
1505
+ self.outputViewer.setIndex(i) # Preserve the last selected tab
1506
+ self.outputViewer.selectedText().goEnd()
1507
+ # when there are not logs, force re-load next time
1508
+ if (not os.path.exists(out) or
1509
+ not os.path.exists(err)):
1510
+ self._lastStatus = None
1511
+
1512
+ elif prot.isActive() or prot.getStatus() != self._lastStatus:
1513
+ doClear = self._lastStatus is None
1514
+ self._lastStatus = prot.getStatus()
1515
+ self.outputViewer.refreshAll(clear=doClear, goEnd=doClear)
1516
+ except Exception as e:
1517
+ self.info("Something went wrong filling %s's logs: %s. Check terminal for details" % (prot, e))
1518
+ import traceback
1519
+ traceback.print_exc()
1520
+
1521
+ def _scheduleRunsUpdate(self, secs=1, position=None):
1522
+ # self.runsTree.after(secs*1000, self.refreshRuns)
1523
+ self.window.enqueue(lambda: self.refreshRuns(position=position))
1524
+
1525
+ def executeProtocol(self, prot):
1526
+ """ Function to execute a protocol called not
1527
+ directly from the Form "Execute" button.
1528
+ """
1529
+ # We need to equeue the execute action
1530
+ # to be executed in the same thread
1531
+ self.window.enqueue(lambda: self._executeSaveProtocol(prot))
1532
+
1533
+ def _executeSaveProtocol(self, prot, onlySave=False, doSchedule=False, position=None):
1534
+ if onlySave:
1535
+ self.project.saveProtocol(prot)
1536
+ msg = Message.LABEL_SAVED_FORM
1537
+
1538
+ else:
1539
+ if doSchedule:
1540
+ self.project.scheduleProtocol(prot)
1541
+ else:
1542
+ self.project.launchProtocol(prot)
1543
+ # Select the launched protocol to display its summary, methods..etc
1544
+ self._selection.clear()
1545
+ self._selection.append(prot.getObjId())
1546
+ self._updateSelection()
1547
+ self._lastStatus = None # clear lastStatus to force re-load the logs
1548
+ msg = ""
1549
+
1550
+ # Update runs list display, even in save we
1551
+ # need to get the updated copy of the protocol
1552
+ self._scheduleRunsUpdate(position=position)
1553
+ self._selectItemProtocol(prot)
1554
+
1555
+ return msg
1556
+
1557
+ def _updateProtocol(self, prot):
1558
+ """ Callback to notify about the change of a protocol
1559
+ label or comment.
1560
+ """
1561
+ self._scheduleRunsUpdate()
1562
+
1563
+ def _continueProtocol(self, prot):
1564
+ self.project.continueProtocol(prot)
1565
+ self._scheduleRunsUpdate()
1566
+
1567
+ def _onDelPressed(self):
1568
+ # This function will be connected to the key 'Del' press event
1569
+ # We need to check if the canvas have the focus and then
1570
+ # proceed with the delete action
1571
+
1572
+ # get the widget with the focus
1573
+ widget = self.focus_get()
1574
+
1575
+ # Call the delete action only if the widget is the canvas
1576
+ if str(widget).endswith(ProtocolsView.RUNS_CANVAS_NAME):
1577
+ try:
1578
+ self._deleteProtocol()
1579
+ except Exception as ex:
1580
+ self.window.showError(str(ex))
1581
+
1582
+ def _deleteProtocol(self):
1583
+ protocols = self._getSelectedProtocols()
1584
+
1585
+ if len(protocols) == 0:
1586
+ return
1587
+
1588
+ protStr = '\n - '.join(['*%s*' % p.getRunName() for p in protocols])
1589
+
1590
+ if pwgui.dialog.askYesNo(Message.TITLE_DELETE_FORM,
1591
+ Message.LABEL_DELETE_FORM % protStr,
1592
+ self.root):
1593
+ self.info('Deleting protocols...')
1594
+ self.project.deleteProtocol(*protocols)
1595
+ self.settings.cleanUpNodes([str(prot.getObjId()) for prot in protocols])
1596
+ self._selection.clear()
1597
+ self._updateSelection()
1598
+ self._scheduleRunsUpdate()
1599
+ self.cleanInfo()
1600
+
1601
+
1602
+ def _editProtocol(self, protocol):
1603
+ disableRunMode = False
1604
+ if protocol.isSaved():
1605
+ disableRunMode = True
1606
+ self._openProtocolForm(protocol, disableRunMode=disableRunMode)
1607
+
1608
+ def _pasteProtocolsFromClipboard(self, e=None):
1609
+ """ Pastes the content of the clipboard providing is a json workflow"""
1610
+
1611
+ try:
1612
+
1613
+ self.project.loadProtocols(jsonStr=self.clipboard_get())
1614
+ self.info("Clipboard content pasted successfully.")
1615
+ self.updateRunsGraph(False)
1616
+ except Exception as e:
1617
+ self.info("Paste failed, maybe clipboard content is not valid content? See GUI log for details.")
1618
+ logger.error("Clipboard content couldn't be pasted." , exc_info=e)
1619
+
1620
+ def _copyProtocolsToClipboard(self, e=None):
1621
+
1622
+ protocols = self._getSelectedProtocols()
1623
+ jsonStr = self.project.getProtocolsJson(protocols)
1624
+
1625
+ self.clipboard_clear()
1626
+ self.clipboard_append(jsonStr)
1627
+ self.info("Protocols copied to the clipboard. Now you can paste them here, another project or in a template or ... anywhere!.")
1628
+
1629
+ def _copyProtocols(self, e=None):
1630
+ protocols = self._getSelectedProtocols()
1631
+ if len(protocols) == 1:
1632
+ newProt = self.project.copyProtocol(protocols[0])
1633
+ if newProt is None:
1634
+ self.window.showError("Error copying protocol.!!!")
1635
+ else:
1636
+ self._openProtocolForm(newProt, disableRunMode=True)
1637
+ else:
1638
+ self.info('Copying the protocols...')
1639
+ self.project.copyProtocol(protocols)
1640
+ self.refreshRuns()
1641
+ self.cleanInfo()
1642
+
1643
+ def _stopWorkFlow(self, action):
1644
+
1645
+ protocols = self._getSelectedProtocols()
1646
+
1647
+ # TODO: use filterCallback param and we may not need to return 2 elements
1648
+ workflowProtocolList, activeProtList = self.project._getSubworkflow(protocols[0],
1649
+ fixProtParam=False,
1650
+ getStopped=False)
1651
+ if activeProtList:
1652
+ errorProtList = []
1653
+ if pwgui.dialog.askYesNo(Message.TITLE_STOP_WORKFLOW_FORM,
1654
+ Message.TITLE_STOP_WORKFLOW, self.root):
1655
+ self.info('Stopping the workflow...')
1656
+ errorProtList = self.project.stopWorkFlow(activeProtList)
1657
+ self.cleanInfo()
1658
+ self.refreshRuns()
1659
+ if errorProtList:
1660
+ msg = '\n'
1661
+ for prot in errorProtList:
1662
+ msg += str(prot.getObjLabel()) + '\n'
1663
+ pwgui.dialog.MessageDialog(
1664
+ self, Message.TITLE_STOPPED_WORKFLOW_FAILED,
1665
+ Message.TITLE_STOPPED_WORKFLOW_FAILED + ' with: ' + msg,
1666
+ Icon.ERROR)
1667
+
1668
+ def _resetWorkFlow(self, action):
1669
+
1670
+ protocols = self._getSelectedProtocols()
1671
+ errorProtList = []
1672
+ if pwgui.dialog.askYesNo(Message.TITLE_RESET_WORKFLOW_FORM,
1673
+ Message.TITLE_RESET_WORKFLOW, self.root):
1674
+ self.info('Resetting the workflow...')
1675
+ workflowProtocolList, activeProtList = self.project._getSubworkflow(protocols[0])
1676
+ errorProtList = self.project.resetWorkFlow(workflowProtocolList)
1677
+ self.cleanInfo()
1678
+ self.refreshRuns()
1679
+ if errorProtList:
1680
+ msg = '\n'
1681
+ for prot in errorProtList:
1682
+ msg += str(prot.getObjLabel()) + '\n'
1683
+ pwgui.dialog.MessageDialog(
1684
+ self, Message.TITLE_RESETED_WORKFLOW_FAILED,
1685
+ Message.TITLE_RESETED_WORKFLOW_FAILED + ' with: ' + msg,
1686
+ Icon.ERROR)
1687
+
1688
+ def _launchWorkFlow(self, action):
1689
+ """
1690
+ This function can launch a workflow from a selected protocol in two
1691
+ modes depending on the 'action' value (RESTART, CONTINUE)
1692
+ """
1693
+ protocols = self._getSelectedProtocols()
1694
+ mode = pwprot.MODE_RESTART if action == ACTION_RESTART_WORKFLOW else pwprot.MODE_RESUME
1695
+ errorList, _ = self._launchSubWorkflow(protocols[0], mode, self.root)
1696
+
1697
+ if errorList:
1698
+ msg = ''
1699
+ for errorProt in errorList:
1700
+ msg += str(errorProt) + '\n'
1701
+ pwgui.dialog.MessageDialog(
1702
+ self, Message.TITLE_LAUNCHED_WORKFLOW_FAILED_FORM,
1703
+ Message.TITLE_LAUNCHED_WORKFLOW_FAILED + "\n" + msg,
1704
+ Icon.ERROR)
1705
+ self.refreshRuns()
1706
+
1707
+ @staticmethod
1708
+ def _launchSubWorkflow(protocol, mode, root, askSingleAll=False):
1709
+ """
1710
+ Method to launch a subworkflow
1711
+ mode: mode value (RESTART, CONTINUE)
1712
+ askSingleAll: specify if this method was launched from the form or from the menu
1713
+ """
1714
+ project = protocol.getProject()
1715
+ workflowProtocolList, activeProtList = project._getSubworkflow(protocol)
1716
+
1717
+ # Check if exists active protocols
1718
+ activeProtocols = ""
1719
+ if activeProtList:
1720
+ for protId, activeProt in activeProtList.items():
1721
+ activeProtocols += ("\n* " + activeProt.getRunName())
1722
+
1723
+ # by default, we assume RESTART workflow option
1724
+ title = Message.TITLE_RESTART_WORKFLOW_FORM
1725
+ message = Message.MESSAGE_RESTART_WORKFLOW_WITH_RESULTS % ('%s\n' % activeProtocols) if len(activeProtList) else Message.MESSAGE_RESTART_WORKFLOW
1726
+
1727
+ if mode == pwprot.MODE_RESUME:
1728
+ message = Message.MESSAGE_CONTINUE_WORKFLOW_WITH_RESULTS % ('%s\n' % activeProtocols) if len(activeProtList) else Message.MESSAGE_CONTINUE_WORKFLOW
1729
+ title = Message.TITLE_CONTINUE_WORKFLOW_FORM
1730
+
1731
+ errorList=[]
1732
+
1733
+ if not askSingleAll:
1734
+ if pwgui.dialog.askYesNo(title, message, root):
1735
+ errorList = project.launchWorkflow(workflowProtocolList, mode)
1736
+ return errorList, RESULT_RUN_ALL
1737
+ return [], RESULT_CANCEL
1738
+ else: # launching from a form
1739
+ if len(workflowProtocolList) > 1:
1740
+ if Config.SCIPION_DEFAULT_EXECUTION_ACTION == DEFAULT_EXECUTION_ACTION_ASK:
1741
+ title = Message.TITLE_RESTART_FORM if mode == pwprot.MODE_RESTART else Message.TITLE_CONTINUE_FORM
1742
+ message += Message.MESSAGE_ASK_SINGLE_ALL
1743
+ result = pwgui.dialog.askSingleAllCancel(title, message,
1744
+ root)
1745
+ elif Config.SCIPION_DEFAULT_EXECUTION_ACTION == DEFAULT_EXECUTION_ACTION_SINGLE:
1746
+ result = RESULT_RUN_SINGLE
1747
+ else:
1748
+ result = RESULT_RUN_ALL
1749
+
1750
+ if result == RESULT_RUN_ALL:
1751
+ errorList = []
1752
+ if mode == pwprot.MODE_RESTART:
1753
+ project._restartWorkflow(errorList, workflowProtocolList)
1754
+ else:
1755
+ project._continueWorkflow(errorList, workflowProtocolList)
1756
+
1757
+ return errorList, RESULT_RUN_ALL
1758
+
1759
+ elif result == RESULT_RUN_SINGLE:
1760
+ # If mode resume, we should not reset the "current" protocol
1761
+ if mode == pwprot.MODE_RESUME:
1762
+ workflowProtocolList.pop(protocol.getObjId())
1763
+ errorList = project.resetWorkFlow(workflowProtocolList)
1764
+ return errorList, RESULT_RUN_SINGLE
1765
+
1766
+ elif result == RESULT_CANCEL:
1767
+ return [], RESULT_CANCEL
1768
+
1769
+ else: # is a single protocol
1770
+ if not protocol.isSaved():
1771
+ title = Message.TITLE_RESTART_FORM
1772
+ message = Message.MESSAGE_RESTART_FORM % ('%s\n' % protocol.getRunName())
1773
+ if mode == pwprot.MODE_RESUME:
1774
+ title = Message.TITLE_CONTINUE_FORM
1775
+ message = Message.MESSAGE_CONTINUE_FORM % ('%s\n' % protocol.getRunName())
1776
+
1777
+ result = pwgui.dialog.askYesNo(title, message, root)
1778
+ resultRun = RESULT_RUN_SINGLE if result else RESULT_CANCEL
1779
+ return [], resultRun
1780
+
1781
+ return [], RESULT_RUN_SINGLE
1782
+
1783
+ def _selectLabels(self):
1784
+
1785
+ dlg = self.window.manageLabels()
1786
+
1787
+ selectedNodes = self._getSelectedNodes()
1788
+
1789
+ if dlg.resultYes() and selectedNodes:
1790
+
1791
+ for node in selectedNodes:
1792
+ node.setLabels([label.getName() for label in dlg.values])
1793
+
1794
+ # self.updateRunsGraph()
1795
+ self.drawRunsGraph()
1796
+
1797
+ # Save settings in any case
1798
+ self.window.saveSettings()
1799
+
1800
+ def _selectAncestors(self):
1801
+ self._selectNodes(down=False)
1802
+
1803
+ def _selectDescendants(self):
1804
+ self._selectNodes(down=True)
1805
+
1806
+ def _selectNodes(self, down=True, fromRun=None):
1807
+ """ Selects all nodes in the specified direction, defaults to down."""
1808
+ nodesToSelect = []
1809
+ # If parent param not passed...
1810
+ if fromRun is None:
1811
+ # ..use selection, must be first call
1812
+ for protId in self._selection:
1813
+ run = self.runsGraph.getNode(str(protId))
1814
+ nodesToSelect.append(run)
1815
+ else:
1816
+ name = fromRun.getName()
1817
+
1818
+ if not name.isdigit():
1819
+ return
1820
+ else:
1821
+ name = int(name)
1822
+
1823
+ # If already selected (may be this should be centralized)
1824
+ if name not in self._selection:
1825
+ nodesToSelect = (fromRun,)
1826
+ self._selection.append(name)
1827
+
1828
+ # Go in the direction .
1829
+ for run in nodesToSelect:
1830
+ # Choose the direction: down or up.
1831
+ direction = run.getChildren if down else run.getParents
1832
+
1833
+ # Select himself plus ancestors
1834
+ for parent in direction():
1835
+ self._selectNodes(down, parent)
1836
+
1837
+ # Only update selection at the end, avoid recursion
1838
+ if fromRun is None:
1839
+ self._lastSelectedProtId = None
1840
+ self._updateSelection()
1841
+ self.drawRunsGraph()
1842
+
1843
+
1844
+ def _exportProtocols(self, defaultPath=None, defaultBasename=None):
1845
+ protocols = self._getSelectedProtocols()
1846
+
1847
+ def _export(obj):
1848
+ filename = os.path.join(browser.getCurrentDir(),
1849
+ browser.getEntryValue())
1850
+ try:
1851
+ if (not os.path.exists(filename) or
1852
+ self.window.askYesNo("File already exists",
1853
+ "*%s* already exists, do you want "
1854
+ "to overwrite it?" % filename)):
1855
+ self.project.exportProtocols(protocols, filename)
1856
+ logger.info("Workflow successfully saved to '%s' "
1857
+ % filename)
1858
+ else: # try again
1859
+ self._exportProtocols(defaultPath=browser.getCurrentDir(),
1860
+ defaultBasename=browser.getEntryValue())
1861
+ except Exception as ex:
1862
+ import traceback
1863
+ traceback.print_exc()
1864
+ self.window.showError(str(ex))
1865
+
1866
+ browser = pwgui.browser.FileBrowserWindow(
1867
+ "Choose .json file to save workflow",
1868
+ master=self.window,
1869
+ path=defaultPath or self.project.getPath(''),
1870
+ onSelect=_export,
1871
+ entryLabel='File ', entryValue=defaultBasename or 'workflow.json')
1872
+ browser.show()
1873
+
1874
+ def _exportUploadProtocols(self):
1875
+ try:
1876
+ jsonFn = os.path.join(tempfile.mkdtemp(), 'workflow.json')
1877
+ self.project.exportProtocols(self._getSelectedProtocols(), jsonFn)
1878
+ WorkflowRepository().upload(jsonFn)
1879
+ pwutils.cleanPath(jsonFn)
1880
+ except Exception as ex:
1881
+ self.window.showError("Error connecting to workflow repository:\n"
1882
+ + str(ex))
1883
+
1884
+ def _stopProtocol(self, prot):
1885
+ if pwgui.dialog.askYesNo(Message.TITLE_STOP_FORM,
1886
+ Message.LABEL_STOP_FORM, self.root):
1887
+ self.project.stopProtocol(prot)
1888
+ self._lastStatus = None # force logs to re-load
1889
+ self._scheduleRunsUpdate()
1890
+
1891
+ def _analyzeResults(self, prot:Protocol, keyPressed):
1892
+ viewers = self.domain.findViewers(prot, DESKTOP_TKINTER)
1893
+ if len(viewers):
1894
+ # Instantiate the first available viewer
1895
+ viewer = viewers[0]
1896
+ logger.info("Specific viewer found for protocol %s: %s" % (prot.getRunName, viewer))
1897
+ firstViewer = viewer(project=self.project, protocol=prot,
1898
+ parent=self.window, keyPressed=keyPressed)
1899
+
1900
+ if isinstance(firstViewer, ProtocolViewer):
1901
+ firstViewer.visualize(prot, windows=self.window)
1902
+ else:
1903
+ firstViewer.visualize(prot)
1904
+ else:
1905
+ outputList = []
1906
+ for _, output in prot.iterOutputAttributes():
1907
+ outputList.append(output)
1908
+
1909
+ for output in outputList:
1910
+ viewers = self.domain.findViewers(output, DESKTOP_TKINTER)
1911
+ if len(viewers):
1912
+ # Instantiate the first available viewer
1913
+ # TODO: If there are more than one viewer we should display
1914
+ # TODO: a selection menu
1915
+ viewerclass = viewers[0]
1916
+ firstViewer = viewerclass(project=self.project,
1917
+ protocol=prot,
1918
+ parent=self.window,
1919
+ keyPressed=keyPressed)
1920
+ # FIXME:Probably o longer needed protocol on args, already provided on init
1921
+ firstViewer.visualize(output, windows=self.window,
1922
+ protocol=prot)
1923
+
1924
+ def _analyzeResultsClicked(self, keyPressed=None):
1925
+ """ Function called when button "Analyze results" is called. """
1926
+ prot = self.getSelectedProtocol()
1927
+
1928
+ # Nothing selected
1929
+ if prot is None:
1930
+ return
1931
+
1932
+ if os.path.exists(prot._getPath()):
1933
+ # self.info('"Analyze result" clicked with %s key pressed.' % keyPressed)
1934
+ self._analyzeResults(prot, keyPressed)
1935
+ else:
1936
+ self.window.showInfo("Selected protocol hasn't been run yet.")
1937
+
1938
+ def _bibExportClicked(self, e=None):
1939
+ try:
1940
+ bibTexCites = OrderedDict()
1941
+ for prot in self._iterSelectedProtocols():
1942
+ bibTexCites.update(prot.getCitations(bibTexOutput=True))
1943
+ bibTexCites.update(prot.getPackageCitations(bibTexOutput=True))
1944
+
1945
+ if bibTexCites:
1946
+ with tempfile.NamedTemporaryFile(suffix='.bib') as bibFile:
1947
+ for refId, refDict in bibTexCites.items():
1948
+ # getCitations does not always return a dictionary
1949
+ # if the citation is not found in the bibtex file it adds just
1950
+ # the refId: like "Ramirez-Aportela-2019"
1951
+ # we need to exclude this
1952
+ if isinstance(refDict, dict):
1953
+ refType = refDict['ENTRYTYPE']
1954
+ # remove 'type' and 'id' keys
1955
+ refDict = {k: v for k, v in refDict.items()
1956
+ if k not in ['ENTRYTYPE', 'ID']}
1957
+ jsonStr = json.dumps(refDict, indent=4,
1958
+ ensure_ascii=False)[1:]
1959
+ jsonStr = jsonStr.replace('": "', '"= "')
1960
+ jsonStr = re.sub(r'(?<!= )"(\S*?)"', '\\1', jsonStr)
1961
+ jsonStr = jsonStr.replace('= "', ' = "')
1962
+ refStr = '@%s{%s,%s\n\n' % (refType, refId, jsonStr)
1963
+ bibFile.write(refStr.encode('utf-8'))
1964
+ else:
1965
+ logger.warning("Reference %s not properly defined or unpublished." % refId)
1966
+ # flush so we can see content when opening
1967
+ bibFile.flush()
1968
+ pwgui.text.openTextFileEditor(bibFile.name)
1969
+
1970
+ except Exception as ex:
1971
+ self.window.showError(str(ex))
1972
+
1973
+ return
1974
+
1975
+ def _renameProtocol(self, prot):
1976
+ """ Open the EditObject dialog to edit the protocol name. """
1977
+ kwargs = {}
1978
+ if self._lastRightClickPos:
1979
+ kwargs['position'] = self._lastRightClickPos
1980
+
1981
+ dlg = pwgui.dialog.EditObjectDialog(self.runsGraphCanvas, Message.TITLE_EDIT_OBJECT,
1982
+ prot, self.project.mapper, **kwargs)
1983
+ if dlg.resultYes():
1984
+ self._updateProtocol(prot)
1985
+
1986
+ def _runActionClicked(self, action, event=None):
1987
+
1988
+ if event is not None:
1989
+ # log Search box events are reaching here
1990
+ # Since this method is bound to the window events
1991
+ if event.widget.widgetName == 'entry':
1992
+ return
1993
+
1994
+ # Following actions do not need a select run
1995
+ if action == ACTION_TREE:
1996
+ self.drawRunsGraph(reorganize=True)
1997
+ elif action == ACTION_REFRESH:
1998
+ self.refreshRuns(checkPids=True)
1999
+ elif action == ACTION_PASTE:
2000
+ self._pasteProtocolsFromClipboard()
2001
+
2002
+ elif action == ACTION_SWITCH_VIEW:
2003
+ self.switchRunsView()
2004
+ elif action == ACTION_NEW:
2005
+ self._findProtocol(event)
2006
+ elif action == ACTION_LABELS:
2007
+ self._selectLabels()
2008
+ else:
2009
+ prot = self.getSelectedProtocol()
2010
+ if prot:
2011
+ try:
2012
+ if action == ACTION_DEFAULT:
2013
+ pass
2014
+ elif action == ACTION_EDIT:
2015
+ self._editProtocol(prot)
2016
+ elif action == ACTION_RENAME:
2017
+ self._renameProtocol(prot)
2018
+ elif action == ACTION_DUPLICATE:
2019
+ self._copyProtocols()
2020
+ elif action == ACTION_COPY:
2021
+ self._copyProtocolsToClipboard()
2022
+ elif action == ACTION_DELETE:
2023
+ self._deleteProtocol()
2024
+ elif action == ACTION_STEPS:
2025
+ self._browseSteps()
2026
+ elif action == ACTION_BROWSE:
2027
+ self._browseRunDirectory()
2028
+ elif action == ACTION_DB:
2029
+ self._browseRunData()
2030
+ elif action == ACTION_STOP:
2031
+ self._stopProtocol(prot)
2032
+ elif action == ACTION_CONTINUE:
2033
+ self._continueProtocol(prot)
2034
+ elif action == ACTION_RESULTS:
2035
+ self._analyzeResults(prot, None)
2036
+ elif action == ACTION_EXPORT:
2037
+ self._exportProtocols(defaultPath=pwutils.getHomePath())
2038
+ elif action == ACTION_EXPORT_UPLOAD:
2039
+ self._exportUploadProtocols()
2040
+ elif action == ACTION_COLLAPSE:
2041
+ node = self.runsGraph.getNode(str(prot.getObjId()))
2042
+ nodeInfo = self.settings.getNodeById(prot.getObjId())
2043
+ nodeInfo.setExpanded(False)
2044
+ self.setVisibleNodes(node, visible=False)
2045
+ self.updateRunsGraph(False)
2046
+ self._updateActionToolbar()
2047
+ elif action == ACTION_EXPAND:
2048
+ node = self.runsGraph.getNode(str(prot.getObjId()))
2049
+ nodeInfo = self.settings.getNodeById(prot.getObjId())
2050
+ nodeInfo.setExpanded(True)
2051
+ self.setVisibleNodes(node, visible=True)
2052
+ self.updateRunsGraph(False)
2053
+ self._updateActionToolbar()
2054
+
2055
+ elif action == ACTION_SELECT_FROM:
2056
+ self._selectDescendants()
2057
+ elif action == ACTION_SELECT_TO:
2058
+ self._selectAncestors()
2059
+ elif action == ACTION_RESTART_WORKFLOW:
2060
+ self._launchWorkFlow(action)
2061
+ elif action == ACTION_CONTINUE_WORKFLOW:
2062
+ self._launchWorkFlow(action)
2063
+ elif action == ACTION_STOP_WORKFLOW:
2064
+ self._stopWorkFlow(action)
2065
+ elif action == ACTION_RESET_WORKFLOW:
2066
+ self._resetWorkFlow(action)
2067
+ elif action == ACTION_SEARCH:
2068
+ self._searchProtocol()
2069
+
2070
+ except Exception as ex:
2071
+ self.window.showError(str(ex), exception=ex)
2072
+ if Config.debugOn():
2073
+ import traceback
2074
+ traceback.print_exc()
2075
+ else:
2076
+ self.info("Action '%s' not implemented." % action)
2077
+
2078
+ def setVisibleNodes(self, node, visible=True):
2079
+ hasParentHidden = False
2080
+ for child in node.getChildren():
2081
+ prot = child.run
2082
+ nodeInfo = self.settings.getNodeById(prot.getObjId())
2083
+ if visible:
2084
+ hasParentHidden = self.hasParentHidden(child)
2085
+ if not hasParentHidden:
2086
+ nodeInfo.setVisible(visible)
2087
+ self.setVisibleNodes(child, visible)
2088
+
2089
+ def hasParentHidden(self, node):
2090
+ for parent in node.getParents():
2091
+ prot = parent.run
2092
+ nodeInfo = self.settings.getNodeById(prot.getObjId())
2093
+ if not nodeInfo.isVisible() or not nodeInfo.isExpanded():
2094
+ return True
2095
+ return False
2096
+
2097
+
2098
+ class RunBox(pwgui.TextBox):
2099
+ """ Just override TextBox move method to keep track of
2100
+ position changes in the graph.
2101
+ """
2102
+
2103
+ def __init__(self, nodeInfo, canvas, text, x, y, bgColor, textColor):
2104
+ pwgui.TextBox.__init__(self, canvas, text, x, y, bgColor, textColor)
2105
+ self.nodeInfo = nodeInfo
2106
+ canvas.addItem(self)
2107
+
2108
+ def move(self, dx, dy):
2109
+ pwgui.TextBox.move(self, dx, dy)
2110
+ self.nodeInfo.setPosition(self.x, self.y)
2111
+
2112
+ def moveTo(self, x, y):
2113
+ pwgui.TextBox.moveTo(self, x, y)
2114
+ self.nodeInfo.setPosition(self.x, self.y)
2115
+
2116
+