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