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.
- pyworkflow/apps/__init__.py +29 -0
- pyworkflow/apps/pw_manager.py +37 -0
- pyworkflow/apps/pw_plot.py +51 -0
- pyworkflow/apps/pw_project.py +113 -0
- pyworkflow/apps/pw_protocol_list.py +143 -0
- pyworkflow/apps/pw_protocol_run.py +51 -0
- pyworkflow/apps/pw_run_tests.py +267 -0
- pyworkflow/apps/pw_schedule_run.py +322 -0
- pyworkflow/apps/pw_sleep.py +37 -0
- pyworkflow/apps/pw_sync_data.py +439 -0
- pyworkflow/apps/pw_viewer.py +78 -0
- pyworkflow/constants.py +1 -1
- pyworkflow/gui/__init__.py +36 -0
- pyworkflow/gui/browser.py +760 -0
- pyworkflow/gui/canvas.py +1190 -0
- pyworkflow/gui/dialog.py +979 -0
- pyworkflow/gui/form.py +2726 -0
- pyworkflow/gui/graph.py +247 -0
- pyworkflow/gui/graph_layout.py +271 -0
- pyworkflow/gui/gui.py +566 -0
- pyworkflow/gui/matplotlib_image.py +233 -0
- pyworkflow/gui/plotter.py +247 -0
- pyworkflow/gui/project/__init__.py +25 -0
- pyworkflow/gui/project/base.py +192 -0
- pyworkflow/gui/project/constants.py +139 -0
- pyworkflow/gui/project/labels.py +205 -0
- pyworkflow/gui/project/project.py +491 -0
- pyworkflow/gui/project/searchprotocol.py +238 -0
- pyworkflow/gui/project/searchrun.py +181 -0
- pyworkflow/gui/project/steps.py +171 -0
- pyworkflow/gui/project/utils.py +332 -0
- pyworkflow/gui/project/variables.py +179 -0
- pyworkflow/gui/project/viewdata.py +472 -0
- pyworkflow/gui/project/viewprojects.py +510 -0
- pyworkflow/gui/project/viewprotocols.py +2116 -0
- pyworkflow/gui/project/viewprotocols_extra.py +562 -0
- pyworkflow/gui/text.py +771 -0
- pyworkflow/gui/tooltip.py +185 -0
- pyworkflow/gui/tree.py +684 -0
- pyworkflow/gui/widgets.py +307 -0
- pyworkflow/mapper/__init__.py +26 -0
- pyworkflow/mapper/mapper.py +222 -0
- pyworkflow/mapper/sqlite.py +1581 -0
- pyworkflow/mapper/sqlite_db.py +145 -0
- pyworkflow/project/__init__.py +31 -0
- pyworkflow/project/config.py +454 -0
- pyworkflow/project/manager.py +180 -0
- pyworkflow/project/project.py +2095 -0
- pyworkflow/project/usage.py +165 -0
- pyworkflow/protocol/__init__.py +38 -0
- pyworkflow/protocol/bibtex.py +48 -0
- pyworkflow/protocol/constants.py +87 -0
- pyworkflow/protocol/executor.py +483 -0
- pyworkflow/protocol/hosts.py +317 -0
- pyworkflow/protocol/launch.py +277 -0
- pyworkflow/protocol/package.py +42 -0
- pyworkflow/protocol/params.py +781 -0
- pyworkflow/protocol/protocol.py +2707 -0
- pyworkflow/tests/__init__.py +29 -0
- pyworkflow/tests/test_utils.py +25 -0
- pyworkflow/tests/tests.py +341 -0
- pyworkflow/utils/__init__.py +38 -0
- pyworkflow/utils/dataset.py +414 -0
- pyworkflow/utils/echo.py +104 -0
- pyworkflow/utils/graph.py +169 -0
- pyworkflow/utils/log.py +293 -0
- pyworkflow/utils/path.py +528 -0
- pyworkflow/utils/process.py +153 -0
- pyworkflow/utils/profiler.py +92 -0
- pyworkflow/utils/progressbar.py +154 -0
- pyworkflow/utils/properties.py +617 -0
- pyworkflow/utils/reflection.py +129 -0
- pyworkflow/utils/utils.py +880 -0
- pyworkflow/utils/which.py +229 -0
- pyworkflow/webservices/__init__.py +8 -0
- pyworkflow/webservices/config.py +8 -0
- pyworkflow/webservices/notifier.py +152 -0
- pyworkflow/webservices/repository.py +59 -0
- pyworkflow/webservices/workflowhub.py +74 -0
- pyworkflowtests/tests/__init__.py +0 -0
- pyworkflowtests/tests/test_canvas.py +72 -0
- pyworkflowtests/tests/test_domain.py +45 -0
- pyworkflowtests/tests/test_logs.py +74 -0
- pyworkflowtests/tests/test_mappers.py +392 -0
- pyworkflowtests/tests/test_object.py +507 -0
- pyworkflowtests/tests/test_project.py +42 -0
- pyworkflowtests/tests/test_protocol_execution.py +146 -0
- pyworkflowtests/tests/test_protocol_export.py +78 -0
- pyworkflowtests/tests/test_protocol_output.py +158 -0
- pyworkflowtests/tests/test_streaming.py +47 -0
- pyworkflowtests/tests/test_utils.py +210 -0
- {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.1.dist-info}/METADATA +2 -2
- scipion_pyworkflow-3.11.1.dist-info/RECORD +161 -0
- scipion_pyworkflow-3.11.0.dist-info/RECORD +0 -71
- {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.1.dist-info}/WHEEL +0 -0
- {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.1.dist-info}/entry_points.txt +0 -0
- {scipion_pyworkflow-3.11.0.dist-info → scipion_pyworkflow-3.11.1.dist-info}/licenses/LICENSE.txt +0 -0
- {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
|
+
|