# -*- coding: utf-8 -*-
# **************************************************************************
# *
# * Authors: J.M. De la Rosa Trevin (delarosatrevin@scilifelab.se) [1]
# *
# * [1] SciLifeLab, Stockholm University
# *
# * This program is free software: you can redistribute it and/or modify
# * it under the terms of the GNU General Public License as published by
# * the Free Software Foundation, either version 3 of the License, or
# * (at your option) any later version.
# *
# * This program is distributed in the hope that it will be useful,
# * but WITHOUT ANY WARRANTY; without even the implied warranty of
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# * GNU General Public License for more details.
# *
# * You should have received a copy of the GNU General Public License
# * along with this program. If not, see <https://www.gnu.org/licenses/>.
# *
# * All comments concerning this program package may be sent to the
# * e-mail address 'scipion@cnb.csic.es'
# *
# **************************************************************************
import logging
logger = logging.getLogger(__name__)
from pyworkflow import Config, DEFAULT_EXECUTION_ACTION_ASK, DEFAULT_EXECUTION_ACTION_SINGLE
from pyworkflow.gui import LIST_TREEVIEW, \
ShortCut, ToolTip, RESULT_RUN_ALL, RESULT_RUN_SINGLE, RESULT_CANCEL, BORDERLESS_TREEVIEW
from pyworkflow.gui.project.constants import *
from pyworkflow.protocol import SIZE_1MB, SIZE_1GB, SIZE_1TB
INIT_REFRESH_SECONDS = Config.SCIPION_GUI_REFRESH_INITIAL_WAIT
"""
View with the protocols inside the main project window.
"""
import os
import json
import re
import tempfile
from collections import OrderedDict
import tkinter as tk
import tkinter.ttk as ttk
import datetime as dt
from pyworkflow import Config, TK
import pyworkflow.utils as pwutils
import pyworkflow.protocol as pwprot
from pyworkflow.viewer import DESKTOP_TKINTER, ProtocolViewer
from pyworkflow.utils.properties import Color, KEYSYM, Icon, Message
from pyworkflow.webservices import WorkflowRepository
import pyworkflow.gui as pwgui
from pyworkflow.gui.form import FormWindow
from pyworkflow.gui.project.utils import getStatusColorFromNode, inspectObj
from pyworkflow.gui.project.searchprotocol import SearchProtocolWindow, ProtocolTreeProvider
from pyworkflow.gui.project.steps import StepsWindow
from pyworkflow.gui.project.viewprotocols_extra import RunIOTreeProvider, ProtocolTreeConfig
from pyworkflow.gui.project.searchrun import RunsTreeProvider, SearchRunWindow
DEFAULT_BOX_COLOR = '#f8f8f8'
RUNS_TREE = Icon.RUNS_TREE
VIEW_LIST = 0
VIEW_TREE = 1
VIEW_TREE_SMALL = 2
# noinspection PyAttributeOutsideInit
[docs]class ProtocolsView(tk.Frame):
""" What you see when the "Protocols" tab is selected.
In the main project window there are three tabs: "Protocols | Data | Hosts".
This extended tk.Frame is what will appear when Protocols is on.
"""
RUNS_CANVAS_NAME = "runs_canvas"
SIZE_COLORS = {SIZE_1MB: "green",
SIZE_1GB: "orange",
SIZE_1TB: "red"}
_protocolViews = None
def __init__(self, parent, window, **args):
tk.Frame.__init__(self, parent, **args)
# Load global configuration
self.window = window
self.project = window.project
self.domain = self.project.getDomain()
self.root = window.root
self.getImage = window.getImage
self.protCfg = self.getCurrentProtocolView()
self.settings = window.getSettings()
self.runsView = self.settings.getRunsView()
self._loadSelection()
self._items = {}
self._lastSelectedProtId = None
self._lastStatus = None
self.selectingArea = False
self._lastRightClickPos = None # Keep last right-clicked position
self.style = ttk.Style()
self.root.bind("<Control-a>", self._selectAllProtocols)
self.root.bind("<Control-t>", self._toggleColorScheme)
self.root.bind("<Control-D>", self._toggleDebug)
self.root.bind("<Control-l>", self._locateProtocol)
if Config.debugOn():
self.root.bind("<Control-i>", self._inspectProtocols)
self.__autoRefresh = None
self.__autoRefreshCounter = INIT_REFRESH_SECONDS # start by 3 secs
self.refreshSemaphore = True
self.repeatRefresh = False
c = self.createContent()
pwgui.configureWeigths(self)
c.grid(row=0, column=0, sticky='news')
[docs] def createContent(self):
""" Create the Protocols View for the Project.
It has two panes:
Left: containing the Protocol classes tree
Right: containing the Runs list
"""
p = tk.PanedWindow(self, orient=tk.HORIZONTAL, bg=Config.SCIPION_BG_COLOR)
bgColor = Color.ALT_COLOR
# Left pane, contains Protocols Pane
leftFrame = tk.Frame(p, bg=bgColor)
leftFrame.columnconfigure(0, weight=1)
leftFrame.rowconfigure(1, weight=1)
# Protocols Tree Pane
protFrame = tk.Frame(leftFrame, width=300, height=500, bg=bgColor)
protFrame.grid(row=1, column=0, sticky='news', padx=5, pady=5)
protFrame.columnconfigure(0, weight=1)
protFrame.rowconfigure(1, weight=1)
self._createProtocolsPanel(protFrame, bgColor)
self.updateProtocolsTree(self.protCfg)
# Create the right Pane that will be composed by:
# a Action Buttons TOOLBAR in the top
# and another vertical Pane with:
# Runs History (at Top)
# Selected run info (at Bottom)
rightFrame = tk.Frame(p, bg=Config.SCIPION_BG_COLOR)
rightFrame.columnconfigure(0, weight=1)
rightFrame.rowconfigure(1, weight=1)
# rightFrame.rowconfigure(0, minsize=label.winfo_reqheight())
# Create the Action Buttons TOOLBAR
toolbar = tk.Frame(rightFrame, bg=Config.SCIPION_BG_COLOR)
toolbar.grid(row=0, column=0, sticky='news')
pwgui.configureWeigths(toolbar)
# toolbar.columnconfigure(0, weight=1)
toolbar.columnconfigure(1, weight=1)
self.runsToolbar = tk.Frame(toolbar, bg=Config.SCIPION_BG_COLOR)
self.runsToolbar.grid(row=0, column=0, sticky='sw')
# On the left of the toolbar will be other
# actions that can be applied to all runs (refresh, graph view...)
self.allToolbar = tk.Frame(toolbar, bg=Config.SCIPION_BG_COLOR)
self.allToolbar.grid(row=0, column=10, sticky='se')
self.createActionToolbar()
# Create the Run History tree
v = ttk.PanedWindow(rightFrame, orient=tk.VERTICAL)
# runsFrame = ttk.Labelframe(v, text=' History ', width=500, height=500)
runsFrame = tk.Frame(v, bg=Config.SCIPION_BG_COLOR)
# runsFrame.grid(row=1, column=0, sticky='news', pady=5)
self.runsTree = self.createRunsTree(runsFrame)
pwgui.configureWeigths(runsFrame)
self.createRunsGraph(runsFrame)
if self.runsView == VIEW_LIST:
treeWidget = self.runsTree
else:
treeWidget = self.runsGraphCanvas
treeWidget.grid(row=0, column=0, sticky='news')
# Create the Selected Run Info
infoFrame = tk.Frame(v)
infoFrame.columnconfigure(0, weight=1)
infoFrame.rowconfigure(1, weight=1)
# Create the info label
self.infoLabel = tk.Label(infoFrame)
self.infoLabel.grid(row=0, column=0, sticky='w', padx=3)
# Create the Analyze results button
self.btnAnalyze = pwgui.Button(infoFrame, text=Message.LABEL_ANALYZE,
fg='white', bg=Config.SCIPION_MAIN_COLOR,
image=self.getImage(Icon.ACTION_VISUALIZE),
compound=tk.LEFT,
activeforeground='white',
activebackground=Config.getActiveColor())
# command=self._analyzeResultsClicked)
self.btnAnalyze.bind("<Shift-Button-1>", lambda e: self._analyzeResultsClicked(KEYSYM.SHIFT))
self.btnAnalyze.bind("<Control-Button-1>", lambda e: self._analyzeResultsClicked(KEYSYM.CONTROL))
self.btnAnalyze.bind("<Button-1>", lambda e: self._analyzeResultsClicked(None))
# self.btnAnalyze.bind("<Button-1>", self._analyzeResultsClicked)
self.btnAnalyze.grid(row=0, column=0, sticky='ne', padx=15)
# self.style.configure("W.TNotebook")#, background='white')
tab = ttk.Notebook(infoFrame) # , style='W.TNotebook')
# Summary tab
dframe = tk.Frame(tab, bg=Config.SCIPION_BG_COLOR)
pwgui.configureWeigths(dframe, row=0)
pwgui.configureWeigths(dframe, row=2)
# Just configure the provider, later below, in updateSelection, it will be
# provided with the protocols.
provider = RunIOTreeProvider(self, None,
self.project.mapper, self.info)
self.infoTree = pwgui.browser.BoundTree(dframe, provider, height=6,
show='tree',
style=BORDERLESS_TREEVIEW)
self.infoTree.grid(row=0, column=0, sticky='news')
label = tk.Label(dframe, text='SUMMARY', bg=Config.SCIPION_BG_COLOR,
font=self.window.fontBold)
label.grid(row=1, column=0, sticky='nw', padx=(15, 0))
hView = {'sci-open': self._viewObject,
'sci-bib': self._bibExportClicked}
self.summaryText = pwgui.text.TaggedText(dframe, width=40, height=5,
bg=Config.SCIPION_BG_COLOR, bd=0,
font=self.window.font,
handlers=hView)
self.summaryText.grid(row=2, column=0, sticky='news', padx=(30, 0))
# Method tab
mframe = tk.Frame(tab)
pwgui.configureWeigths(mframe)
# Methods text box
self.methodText = pwgui.text.TaggedText(mframe, width=40, height=15,
bg=Config.SCIPION_BG_COLOR, handlers=hView)
self.methodText.grid(row=0, column=0, sticky='news')
# Output Logs
ologframe = tk.Frame(tab)
pwgui.configureWeigths(ologframe)
self.outputViewer = pwgui.text.TextFileViewer(ologframe, allowOpen=True,
font=self.window.font)
self.outputViewer.grid(row=0, column=0, sticky='news')
self.outputViewer.windows = self.window
# Project log
projLogFrame = tk.Frame(tab)
pwgui.configureWeigths(projLogFrame)
self.projLog = pwgui.text.TextFileViewer(projLogFrame, allowOpen=True,
font=self.window.font)
self.projLog.grid(row=0, column=0, sticky='news')
self.projLog.windows = self.window
self.projLog.addFile(self.project.getProjectLog())
# Move to the selected protocol
if self._isSingleSelection():
prot = self.getSelectedProtocol()
node = self.runsGraph.getNode(str(prot.getObjId()))
self._selectNode(node)
else:
self._updateSelection()
# Add all tabs
tab.add(dframe, text=Message.LABEL_SUMMARY)
tab.add(mframe, text=Message.LABEL_METHODS)
tab.add(ologframe, text=Message.LABEL_LOGS_OUTPUT)
# tab.add(elogframe, text=Message.LABEL_LOGS_ERROR)
tab.add(projLogFrame, text=Message.LABEL_LOGS_SCIPION)
tab.grid(row=1, column=0, sticky='news')
v.add(runsFrame, weight=1)
v.add(infoFrame, weight=20)
v.grid(row=1, column=0, sticky='news')
# Add sub-windows to PanedWindows
p.add(leftFrame, padx=0, pady=0, sticky='news')
p.add(rightFrame, padx=0, pady=0)
p.paneconfig(leftFrame, minsize=5)
leftFrame.config(width=235)
p.paneconfig(rightFrame, minsize=10)
return p
def _viewObject(self, objId):
""" Call appropriate viewer for objId. """
proj = self.project
obj = proj.getObject(int(objId))
viewerClasses = self.domain.findViewers(obj.getClassName(), DESKTOP_TKINTER)
if not viewerClasses:
return # TODO: protest nicely
viewer = viewerClasses[0](project=proj, parent=self.window)
viewer.visualize(obj)
def _loadSelection(self):
""" Load selected items, remove if some do not exists. """
self._selection = self.settings.runSelection
for protId in list(self._selection):
if not self.project.doesProtocolExists(protId):
self._selection.remove(protId)
def _isMultipleSelection(self):
return len(self._selection) > 1
def _isSingleSelection(self):
return len(self._selection) == 1
def _noSelection(self):
return len(self._selection) == 0
[docs] def info(self, message):
self.infoLabel.config(text=message)
self.infoLabel.update_idletasks()
[docs] def cleanInfo(self):
self.info("")
[docs] def refreshRuns(self, e=None, initRefreshCounter=True, checkPids=False, position=None):
"""
Refresh the protocol runs workflow. If the variable REFRESH_WITH_THREADS
exits, then use a threads to refresh, i.o.c use normal behavior
"""
useThreads = Config.refreshInThreads()
if useThreads:
import threading
# Refresh the status of displayed runs.
if self.refreshSemaphore:
threadRefreshRuns = threading.Thread(name="Refreshing runs",
target=self.refreshDisplayedRuns,
args=(e, initRefreshCounter,
checkPids))
threadRefreshRuns.start()
else:
self.repeatRefresh = True
else:
self.refreshDisplayedRuns(e, initRefreshCounter, checkPids, position=position)
# noinspection PyUnusedLocal
[docs] def refreshDisplayedRuns(self, e=None, initRefreshCounter=True, checkPids=False, position=None):
""" Refresh the status of displayed runs.
Params:
e: Tk event input
initRefreshCounter: if True the refresh counter will be set to 3 secs
then only case when False is from _automaticRefreshRuns where the
refresh time is doubled each time to avoid refreshing too often.
"""
self.viewButtons[ACTION_REFRESH]['state'] = tk.DISABLED
self.info('Refreshing...')
self.refreshSemaphore = False
if self.runsView == VIEW_LIST:
self.updateRunsTree(True)
else:
self.updateRunsGraph(True, checkPids=checkPids, position=position)
self._updateSelection()
if initRefreshCounter:
self.__autoRefreshCounter = INIT_REFRESH_SECONDS # start by 3 secs
if self.__autoRefresh:
self.runsTree.after_cancel(self.__autoRefresh)
self.__autoRefresh = self.runsTree.after(
self.__autoRefreshCounter * 1000,
self._automaticRefreshRuns)
self.refreshSemaphore = True
if self.repeatRefresh:
self.repeatRefresh = False
self.refreshRuns()
self.cleanInfo()
self.viewButtons[ACTION_REFRESH]['state'] = tk.NORMAL
# noinspection PyUnusedLocal
def _automaticRefreshRuns(self, e=None):
""" Schedule automatic refresh increasing the time between refreshes. """
if Config.SCIPION_GUI_CANCEL_AUTO_REFRESH:
return
self.refreshRuns(initRefreshCounter=False, checkPids=True)
secs = self.__autoRefreshCounter
# double the number of seconds up to 30 min
self.__autoRefreshCounter = min(2 * secs, 1800)
self.__autoRefresh = self.runsTree.after(secs * 1000,
self._automaticRefreshRuns)
# noinspection PyUnusedLocal
def _findProtocol(self, event=None):
""" Find a desired protocol by typing some keyword. """
if event is not None and event.widget.widgetName=="canvas":
position = self.runsGraphCanvas.getCoordinates(event)
else:
position = None
window = SearchProtocolWindow(self.window, position=position)
window.show()
def _locateProtocol(self, e=None):
window = SearchRunWindow(self.window, self.runsGraph, onDoubleClick=self._onRunClick)
window.show()
# self._moveCanvas(0,1)
def _onRunClick(self, e=None):
""" Callback to be called when a click happens o a run in the SearchRunWindow.tree"""
tree = e.widget
protId = tree.getFirst()
node = self.runsGraph.getNode(protId)
self._selectNode(node)
def _selectNode(self, node):
x = node.x
y = node.y
self._moveCanvas(x, y)
# Select the protocol
self._selectItemProtocol(node.run)
# We comment the refresh because when the project is loaded,
# the workflow is traversed twice.
# self.refreshDisplayedRuns()
def _moveCanvas(self, X, Y):
self.runsGraphCanvas.moveTo(X, Y)
def _createViewCombo(self, parent):
""" Create the select-view combobox. """
label = tk.Label(parent, text='View:', bg=Config.SCIPION_BG_COLOR)
label.grid(row=0, column=0)
viewChoices = ['List', 'Tree', 'Tree - small']
self.switchCombo = pwgui.widgets.ComboBox(parent, width=10,
choices=viewChoices,
values=[VIEW_LIST, VIEW_TREE, VIEW_TREE_SMALL],
initial=viewChoices[self.runsView],
onChange=lambda e: self._runActionClicked(
ACTION_SWITCH_VIEW))
self.switchCombo.grid(row=0, column=1)
def _updateActionToolbar(self):
""" Update which action buttons should be visible. """
def displayAction(actionToDisplay, column, condition=True):
""" Show/hide the action button if the condition is met. """
# If action present (set color is not in the toolbar but in the
# context menu)
action = self.actionButtons.get(actionToDisplay, None)
if action is not None:
if condition:
action.grid(row=0, column=column, sticky='sw',
padx=(0, 5), ipadx=0)
else:
action.grid_remove()
for i, actionTuple in enumerate(self.provider.getActionsFromSelection()):
action, cond = actionTuple
displayAction(action, i, cond)
def _createProtocolsTree(self, parent,
show='tree', columns=None, position=None):
t = pwgui.tree.Tree(parent, show=show, style=LIST_TREEVIEW,
columns=columns)
t.column('#0', minwidth=300)
def configureTag(tag, img):
# Protocol nodes
t.tag_configure(tag, image=self.getImage(img))
t.tag_bind(tag, TK.LEFT_DOUBLE_CLICK, lambda e: self._protocolItemClick(e, position))
t.tag_bind(tag, TK.RETURN, lambda e: self._protocolItemClick(e, position))
t.tag_bind(tag, TK.ENTER, lambda e: self._protocolItemClick(e, position))
# Protocol nodes
configureTag(ProtocolTreeConfig.TAG_PROTOCOL, Icon.PRODUCTION)
# New protocols
configureTag(ProtocolTreeConfig.TAG_PROTOCOL_NEW, Icon.NEW)
# Beta protocols
configureTag(ProtocolTreeConfig.TAG_PROTOCOL_BETA, Icon.BETA)
# Disable protocols (not installed) are allowed to be added.
configureTag(ProtocolTreeConfig.TAG_PROTOCOL_DISABLED,
Icon.PROT_DISABLED)
# Updated protocols
configureTag(ProtocolTreeConfig.TAG_PROTOCOL_UPDATED,
Icon.UPDATED)
t.tag_configure('protocol_base', image=self.getImage(Icon.GROUP))
t.tag_configure('protocol_group', image=self.getImage(Icon.GROUP))
t.tag_configure('section', font=self.window.fontBold)
return t
def _createProtocolsPanel(self, parent, bgColor):
"""Create the protocols Tree displayed in left panel"""
comboFrame = tk.Frame(parent, bg=bgColor)
tk.Label(comboFrame, text='View', bg=bgColor).grid(row=0, column=0,
padx=(0, 5), pady=5)
choices = self.getProtocolViews()
initialChoice = self.settings.getProtocolView()
combo = pwgui.widgets.ComboBox(comboFrame, choices=choices,
initial=initialChoice)
combo.setChangeCallback(self._onSelectProtocols)
combo.grid(row=0, column=1)
comboFrame.grid(row=0, column=0, padx=5, pady=5, sticky='nw')
t = self._createProtocolsTree(parent)
t.grid(row=1, column=0, sticky='news')
# Program automatic refresh
t.after(3000, self._automaticRefreshRuns)
self.protTree = t
[docs] def getProtocolViews(self):
if self._protocolViews is None:
self._loadProtocols()
return list(self._protocolViews.keys())
[docs] def getCurrentProtocolView(self):
""" Select the view that is currently selected.
Read from the settings the last selected view
and get the information from the self._protocolViews dict.
"""
currentView = self.project.getProtocolView()
if currentView in self.getProtocolViews():
viewKey = currentView
else:
viewKey = self.getProtocolViews()[0]
self.project.settings.setProtocolView(viewKey)
if currentView is not None:
logger.warning("PROJECT: Warning, protocol view '%s' not found. Using '%s' instead." % (currentView, viewKey))
return self._protocolViews[viewKey]
def _loadProtocols(self):
""" Load protocol configuration from a .conf file. """
# If the host file is not passed as argument...
configProtocols = Config.SCIPION_PROTOCOLS
localDir = Config.SCIPION_LOCAL_CONFIG
protConf = os.path.join(localDir, configProtocols)
self._protocolViews = ProtocolTreeConfig.load(self.project.getDomain(),
protConf)
def _onSelectProtocols(self, combo):
""" This function will be called when a protocol menu
is selected. The index of the new menu is passed.
"""
protView = combo.getText()
self.settings.setProtocolView(protView)
self.protCfg = self.getCurrentProtocolView()
self.updateProtocolsTree(self.protCfg)
[docs] def populateTree(self, tree, treeItems, prefix, obj, level=0):
# If node does not have leaves (protocols) do not add it
if not obj.visible:
return
text = obj.text
if text:
value = obj.value if obj.value is not None else text
key = '%s.%s' % (prefix, value)
img = obj.icon if obj.icon is not None else ''
tag = obj.tag if obj.tag is not None else ''
if img:
if isinstance(img,str) and "bookmark" in img:
img = pwutils.Icon.FAVORITE
img = self.getImage(img)
# If image is none
img = img if img is not None else ''
protClassName = value.split('.')[-1] # Take last part
emProtocolsDict = self.domain.getProtocols()
prot = emProtocolsDict.get(protClassName, None)
if tag == 'protocol' and text == 'default':
if prot is None:
logger.warning("Protocol className '%s' not found!!!. \n"
"Fix your config/protocols.conf configuration."
% protClassName)
return
text = prot.getClassLabel()
item = tree.insert(prefix, 'end', key, text=text, image=img, tags=tag)
treeItems[item] = obj
# Check if the attribute should be open or close
openItem = getattr(obj, 'openItem', level < 2)
if openItem:
tree.item(item, open=openItem)
# I think this mode is deprecated
if obj.value is not None and tag == 'protocol_base':
logger.warning('protocol_base tags are deprecated')
else:
key = prefix
for sub in obj:
self.populateTree(tree, treeItems, key, sub,
level + 1)
[docs] def updateProtocolsTree(self, protCfg):
try:
self.protCfg = protCfg
self.protTree.clear()
self.protTree.unbind(TK.TREEVIEW_OPEN)
self.protTree.unbind(TK.TREEVIEW_CLOSE)
self.protTreeItems = {}
self.populateTree(self.protTree, self.protTreeItems, '', self.protCfg)
self.protTree.bind(TK.TREEVIEW_OPEN,
lambda e: self._treeViewItemChange(True))
self.protTree.bind(TK.TREEVIEW_CLOSE,
lambda e: self._treeViewItemChange(False))
except Exception as e:
# Tree can't be loaded report back, but continue
logger.error("Protocols tree couldn't be loaded.", exc_info=e)
def _treeViewItemChange(self, openItem):
item = self.protTree.focus()
if item in self.protTreeItems:
self.protTreeItems[item].openItem = openItem
[docs] def createRunsTree(self, parent):
self.provider = RunsTreeProvider(self.project, self._runActionClicked)
# This line triggers the getRuns for the first time.
# Ne need to force the check pids here, temporary
self.provider._checkPids = True
t = pwgui.tree.BoundTree(parent, self.provider, style=LIST_TREEVIEW)
self.provider._checkPids = False
t.itemDoubleClick = self._runItemDoubleClick
t.itemClick = self._runTreeItemClick
return t
[docs] def updateRunsTree(self, refresh=False):
self.provider.setRefresh(refresh)
self.runsTree.update()
self.updateRunsTreeSelection()
[docs] def updateRunsTreeSelection(self):
for prot in self._iterSelectedProtocols():
treeId = self.provider.getObjectFromId(prot.getObjId())._treeId
self.runsTree.selection_add(treeId)
[docs] def createRunsGraph(self, parent):
self.runsGraphCanvas = pwgui.Canvas(parent, width=400, height=400,
tooltipCallback=self._runItemTooltip,
tooltipDelay=1000,
name=ProtocolsView.RUNS_CANVAS_NAME,
takefocus=True,
highlightthickness=0)
self.runsGraphCanvas.onClickCallback = self._runItemClick
self.runsGraphCanvas.onDoubleClickCallback = self._runItemDoubleClick
self.runsGraphCanvas.onRightClickCallback = self._runItemRightClick
self.runsGraphCanvas.onControlClickCallback = self._runItemControlClick
self.runsGraphCanvas.onAreaSelected = self._selectItemsWithinArea
self.runsGraphCanvas.onMiddleMouseClickCallback = self._runItemMiddleClick
parent.grid_columnconfigure(0, weight=1)
parent.grid_rowconfigure(0, weight=1)
self.settings.getNodes().updateDict()
self.settings.getLabels().updateDict()
self.updateRunsGraph()
[docs] def updateRunsGraph(self, refresh=False, checkPids=False, position=None):
self.runsGraph = self.project.getRunsGraph(refresh=refresh,
checkPids=checkPids)
self.drawRunsGraph(position=position)
[docs] def drawRunsGraph(self, reorganize=False, position=None):
# Check if there are positions stored
if reorganize:
# Create layout to arrange nodes as a level tree
layout = pwgui.LevelTreeLayout()
self.runsGraphCanvas.reorganizeGraph(self.runsGraph, layout)
else:
self.runsGraphCanvas.clear()
layout = pwgui.LevelTreeLayout(partial=True)
# Create empty nodeInfo for new runs
for node in self.runsGraph.getNodes():
nodeId = node.run.getObjId() if node.run else 0
nodeInfo = self.settings.getNodeById(nodeId)
if nodeInfo is None:
if position is None:
position = (0,0)
self.settings.addNode(nodeId, x=position[0], y=position[1], expanded=True,
visible=True)
self.runsGraphCanvas.drawGraph(self.runsGraph, layout,
drawNode=self.createRunItem,
nodeList=self.settings.nodeList)
projectSize = len(self.runsGraph.getNodes())
settingsNodeSize = len(self.settings.getNodes())
if projectSize < settingsNodeSize -1:
logger.info("Settings nodes list (%s) is bigger than current project nodes (%s). "
"Clean up needed?" % (settingsNodeSize, projectSize) )
self.settings.cleanUpNodes(self.runsGraph.getNodeNames(), toRemove=False)
[docs] def createRunItem(self, canvas, node):
nodeId = node.run.getObjId() if node.run else 0
nodeInfo = self.settings.getNodeById(nodeId)
# Extend attributes: use some from nodeInfo
node.expanded = nodeInfo.isExpanded()
node.x, node.y = nodeInfo.getPosition()
node.visible = nodeInfo.isVisible()
nodeText = self._getNodeText(node)
# Get status color
statusColor = getStatusColorFromNode(node)
# Get the box color (depends on color mode: label or status)
boxColor = self._getBoxColor(nodeInfo, statusColor, node)
# Draw the box
item = RunBox(nodeInfo, self.runsGraphCanvas,
nodeText, node.x, node.y,
bgColor=boxColor, textColor='black')
# No border
item.margin = 0
# Paint the oval..if apply.
#self._paintOval(item, statusColor)
# Paint the bottom line (for now only labels are painted there).
self._paintBottomLine(item)
item.setSelected(nodeId in self._selection)
return item
def _getBoxColor(self, nodeInfo, statusColor, node):
try:
# If the color has to go to the box
if self.settings.statusColorMode() or self.settings.labelsColorMode():
boxColor = statusColor
elif self.settings.ageColorMode():
if node.run:
# Project elapsed time
elapsedTime = node.run.getProject().getElapsedTime()
creationTime = node.run.getProject().getCreationTime()
# Get the latest activity timestamp
ts = node.run.endTime.datetime()
if elapsedTime is None or creationTime is None or ts is None:
boxColor = DEFAULT_BOX_COLOR
else:
# tc closer to the end are younger
protAge = ts - creationTime
boxColor = self._ageColor('#6666ff', elapsedTime,
protAge)
else:
boxColor = DEFAULT_BOX_COLOR
elif self.settings.sizeColorMode():
# Get the protocol size
protSize = self._getRunSize(node)
boxColor = self._sizeColor(protSize)
# ... box is for the labels.
elif self.settings.labelsColorMode():
# If there is only one label use the box for the color.
if self._getLabelsCount(nodeInfo) == 1:
labelId = nodeInfo.getLabels()[0]
label = self.settings.getLabels().getLabel(labelId)
# If there is no label (it has been deleted)
if label is None:
nodeInfo.getLabels().remove(labelId)
boxColor = DEFAULT_BOX_COLOR
else:
boxColor = label.getColor()
else:
boxColor = DEFAULT_BOX_COLOR
else:
boxColor = DEFAULT_BOX_COLOR
return boxColor
except Exception as e:
logger.debug("Can't get color for %s. %s" % (node, e))
return DEFAULT_BOX_COLOR
@staticmethod
def _getRunSize(node):
"""
Returns the size "recursively" of a run
:param node: node of the graph.
:return: size in bytes
"""
if not node.run:
return 0
else:
return node.run.getSize()
@classmethod
def _sizeColor(cls, size):
"""
Returns the color that corresponds to the size
:param size:
:return:
"""
for threshold, color in cls.SIZE_COLORS.items():
if size <= threshold:
return color
return "#000000"
@staticmethod
def _ageColor(rgbColorStr, projectAge, protocolAge):
# Get the ratio
ratio = protocolAge.seconds / float(projectAge.seconds)
# Invert direction: older = white = 100%, newest = rgbColor = 0%
ratio = 1 - ratio
# There are cases coming with protocols older than the project.
ratio = 0 if ratio < 0 else ratio
hexTuple = pwutils.hex_to_rgb(rgbColorStr)
lighterTuple = pwutils.lighter(hexTuple, ratio)
return pwutils.rgb_to_hex(lighterTuple)
@staticmethod
def _getLabelsCount(nodeInfo):
return 0 if nodeInfo.getLabels() is None else len(nodeInfo.getLabels())
def _paintBottomLine(self, item):
if self.settings.labelsColorMode() or self.settings.statusColorMode():
self._addLabels(item)
def _paintOval(self, item, statusColor):
# Show the status as a circle in the top right corner
if not self.settings.statusColorMode():
# Option: Status item.
(topLeftX, topLeftY, bottomRightX,
bottomRightY) = self.runsGraphCanvas.bbox(item.id)
statusSize = 10
statusX = bottomRightX - (statusSize + 3)
statusY = topLeftY + 3
pwgui.Oval(self.runsGraphCanvas, statusX, statusY, statusSize,
color=statusColor, anchor=item)
# in statusColorMode
else:
# Show a black circle if there is any label
if self._getLabelsCount(item.nodeInfo) > 0:
(topLeftX, topLeftY, bottomRightX,
bottomRightY) = self.runsGraphCanvas.bbox(item.id)
statusSize = 10
statusX = bottomRightX - (statusSize + 3)
statusY = topLeftY + 3
pwgui.Oval(self.runsGraphCanvas, statusX, statusY, statusSize,
color='black', anchor=item)
def _getNodeText(self, node):
nodeText = node.getLabel()
# Truncate text to prevent overflow
if len(nodeText) > 40:
nodeText = nodeText[:37] + "..."
if node.run:
expandedStr = '' if node.expanded else '\n ➕ %s more' % str(node.countChildren({}))
if self.runsView == VIEW_TREE_SMALL:
nodeText = node.getName() + expandedStr
else:
nodeText += expandedStr + '\n' + node.run.getStatusMessage() if not expandedStr else expandedStr
if node.run.summaryWarnings:
nodeText += u' \u26a0'
return nodeText
def _addLabels(self, item):
# If there is only one label it should be already used in the box color.
if self._getLabelsCount(item.nodeInfo) < 1:
return
# Get the positions of the box
(topLeftX, topLeftY, bottomRightX,
bottomRightY) = self.runsGraphCanvas.bbox(item.id)
# Get the width of the box
boxWidth = bottomRightX - topLeftX
# Set the size
marginV = 3
marginH = 2
labelWidth = (boxWidth - (2 * marginH)) / len(item.nodeInfo.getLabels())
labelHeight = 8
# Leave some margin on the right and bottom
labelX = bottomRightX - marginH
labelY = bottomRightY - (labelHeight + marginV)
for index, labelId in enumerate(item.nodeInfo.getLabels()):
# Get the label
label = self.settings.getLabels().getLabel(labelId)
# If not none
if label is not None:
# Move X one label to the left
if index == len(item.nodeInfo.getLabels()) - 1:
labelX = topLeftX + marginH
else:
labelX -= labelWidth
pwgui.Rectangle(self.runsGraphCanvas, labelX, labelY,
labelWidth, labelHeight, color=label.getColor(),
anchor=item)
else:
item.nodeInfo.getLabels().remove(labelId)
[docs] def switchRunsView(self):
viewValue = self.switchCombo.getValue()
self.runsView = viewValue
self.settings.setRunsView(viewValue)
if viewValue == VIEW_LIST:
self.runsTree.grid(row=0, column=0, sticky='news')
self.runsGraphCanvas.frame.grid_remove()
self.updateRunsTree()
self.viewButtons[ACTION_TREE].grid_remove()
self._lastRightClickPos = None
else:
self.runsTree.grid_remove()
self.updateRunsGraph()
self.runsGraphCanvas.frame.grid(row=0, column=0, sticky='news')
self.viewButtons[ACTION_TREE].grid(row=0, column=1)
def _protocolItemClick(self, e=None, position=None):
""" Callback for the window to add a new protocol."""
# Get the tree widget that originated the event
# it could be the left panel protocols tree or just
# the search protocol dialog tree
tree = e.widget
protClassName = tree.getFirst().split('.')[-1]
protClass = self.domain.getProtocols().get(protClassName)
prot = self.project.newProtocol(protClass)
self._openProtocolForm(prot, disableRunMode=True, position=position)
def _toggleColorScheme(self, e=None):
currentMode = self.settings.getColorMode()
if currentMode >= len(self.settings.COLOR_MODES) - 1:
currentMode = -1
nextColorMode = currentMode + 1
self.settings.setColorMode(nextColorMode)
# WHY? self._updateActionToolbar()
# self.updateRunsGraph()
self.drawRunsGraph()
self._infoAboutColorScheme()
def _infoAboutColorScheme(self):
""" Writes in the info widget a brief description abot the color scheme."""
colorScheme = self.settings.getColorMode()
msg = "Color mode changed to %s. %s"
if colorScheme == self.settings.COLOR_MODE_AGE:
msg = msg % ("AGE", "Young boxes will have an darker color.")
elif colorScheme == self.settings.COLOR_MODE_SIZE:
keys = list(self.SIZE_COLORS.keys())
msg = msg % ("SIZE", "Semaphore color scheme. Green <= %s, Orange <=%s, Red <=%s, Dark quite big." %
(pwutils.prettySize(keys[0]),
pwutils.prettySize(keys[1]),
pwutils.prettySize(keys[2]))
)
elif colorScheme == self.settings.COLOR_MODE_STATUS:
msg = msg % ("STATUS", "Color based on the status. A black circle indicates it has labels")
elif colorScheme == self.settings.COLOR_MODE_LABELS:
msg = msg % ("LABELS", "Color based on custom labels you've assigned. Small circles reflect the protocol status")
self.info(msg)
def _toggleDebug(self, e=None):
Config.toggleDebug()
def _selectAllProtocols(self, e=None):
self._selection.clear()
# WHY GOING TO THE db?
# Let's try using in memory data.
# for prot in self.project.getRuns():
for prot in self.project.runs:
self._selection.append(prot.getObjId())
self._updateSelection()
# self.updateRunsGraph()
self.drawRunsGraph()
def _inspectProtocols(self, e=None):
objs = self._getSelectedProtocols()
# We will inspect the selected objects or
# the whole project is no protocol is selected
if len(objs) > 0:
objs.sort(key=lambda obj: obj._objId, reverse=True)
filePath = objs[0]._getLogsPath('inspector.csv')
doInspect = True
else:
proj = self.project
filePath = proj.getLogPath('inspector.csv')
objs = [proj]
doInspect = pwgui.dialog.askYesNo(Message.TITLE_INSPECTOR,
Message.LABEL_INSPECTOR, self.root)
if doInspect:
inspectObj(objs, filePath)
# we open the resulting CSV file with the OS default software
pwgui.text.openTextFileEditor(filePath)
# NOt used!: pconesa 02/11/2016.
# def _deleteSelectedProtocols(self, e=None):
#
# for selection in self._selection:
# self.project.getProtocol(self._selection[0])
#
#
# self._updateSelection()
# self.updateRunsGraph()
def _updateSelection(self):
self._fillSummary()
self._fillMethod()
self._fillLogs()
self._showHideAnalyzeResult()
if self._isSingleSelection():
last = self.getSelectedProtocol()
self._lastSelectedProtId = last.getObjId() if last else None
self._updateActionToolbar()
def _runTreeItemClick(self, item=None):
self._selection.clear()
for prot in self.runsTree.iterSelectedObjects():
self._selection.append(prot.getObjId())
self._updateSelection()
def _selectItemProtocol(self, prot):
""" Call this function when a new box (item) of a protocol
is selected. It should be called either from itemClick
or itemRightClick
"""
self._selection.clear()
self.settings.dataSelection.clear()
self._selection.append(prot.getObjId())
# Select output data too
self.toggleDataSelection(prot, True)
self._updateSelection()
self.runsGraphCanvas.update_idletasks()
def _deselectItems(self, exception):
""" Deselect all items except the item one. Pass item=None to deselect all
"""
g = self.project.getRunsGraph()
for node in g.getNodes():
if node.run and node.run.getObjId() in self._selection:
# This option is only for compatibility with all projects
if hasattr(node, 'item'):
node.item.setSelected(False)
# clear the selection
self._selection.clear()
if exception:
exception.setSelected(True)
self._selection.append(exception.id)
def _runItemClick(self, item=None, event=None):
# If click is in a empty area....start panning
if item is None:
return
self.runsGraphCanvas.focus_set()
# Get last selected item for tree or graph
if self.runsView == VIEW_LIST:
prot = self.project.mapper.selectById(int(self.runsTree.getFirst()))
else:
prot = item.node.run
if prot is None: # in case it is the main "Project" node
return
self._deselectItems(item)
self._selectItemProtocol(prot)
def _runItemDoubleClick(self, item=None, e=None):
if item.nodeInfo.isExpanded():
self._runActionClicked(ACTION_EDIT)
def _runItemMiddleClick(self, e=None):
self._runActionClicked(ACTION_SELECT_TO)
def _runItemRightClick(self, item=None, e=None):
""" Right click on the canvas callback
:param item: item right-clicked. None if clicked in the void
:param e: event object with context information"""
prot = None
# If there's been a click in a box
if item is not None:
# Get the protocol associated
prot = item.node.run
if prot is None: # in case it is the main "Project" node
return
# Only select item with right-click if there is a single
# item selection, not for multiple selection
if len(self._selection) == 1:
self._deselectItems(item)
self._selectItemProtocol(prot)
self._lastRightClickPos = self.runsGraphCanvas.eventPos
else: # Click on empty area
self._deselectItems(None)
self._updateSelection()
return self.provider.getObjectActions(prot,withEvent=True)
def _runItemControlClick(self, item=None, event=None):
# Get last selected item for tree or graph
if self.runsView == VIEW_LIST:
# TODO: Prot is not used!!
prot = self.project.mapper.selectById(int(self.runsTree.getFirst()))
else:
prot = item.node.run
protId = prot.getObjId()
if protId in self._selection:
item.setSelected(False)
self._selection.remove(protId)
# Remove data selected
self.toggleDataSelection(prot, False)
else:
item.setSelected(True)
if len(self._selection) == 1: # repaint first selected item
firstSelectedNode = self.runsGraph.getNode(str(self._selection[0]))
if hasattr(firstSelectedNode, 'item'):
firstSelectedNode.item.setSelected(False)
firstSelectedNode.item.setSelected(True)
self._selection.append(prot.getObjId())
# Select output data too
self.toggleDataSelection(prot, True)
self._updateSelection()
[docs] def toggleDataSelection(self, prot, append):
# Go through the data selection
for paramName, output in prot.iterOutputAttributes():
if append:
self.settings.dataSelection.append(output.getObjId())
else:
self.settings.dataSelection.remove(output.getObjId())
def _runItemTooltip(self, tw, item):
""" Create the contents of the tooltip to be displayed
for the given item.
Params:
tw: a tk.TopLevel instance (ToolTipWindow)
item: the selected item.
"""
prot = item.node.run
if prot:
tm = '*%s*\n' % prot.getRunName()
tm += 'Identifier :%s\n' % prot.getObjId()
tm += 'Status: %s\n' % prot.getStatusMessage()
tm += 'Wall time: %s\n' % pwutils.prettyDelta(prot.getElapsedTime())
tm += 'CPU time: %s\n' % pwutils.prettyDelta(dt.timedelta(seconds=prot.cpuTime))
# tm += 'Folder size: %s\n' % pwutils.prettySize(prot.getSize())
if not hasattr(tw, 'tooltipText'):
frame = tk.Frame(tw)
frame.grid(row=0, column=0)
tw.tooltipText = pwgui.dialog.createMessageBody(frame, tm, None,
textPad=0,
textBg=Color.ALT_COLOR_2)
tw.tooltipText.config(bd=1, relief=tk.RAISED)
else:
pwgui.dialog.fillMessageText(tw.tooltipText, tm)
@staticmethod
def _selectItemsWithinArea(x1, y1, x2, y2, enclosed=False):
"""
Parameters
----------
x1: x coordinate of first corner of the area
y1: y coordinate of first corner of the area
x2: x coordinate of second corner of the area
y2: y coordinate of second corner of the area
enclosed: Default True. Returns enclosed items,
overlapping items otherwise.
Returns
-------
Nothing
"""
return
# NOT working properly: Commented for the moment.
# if enclosed:
# items = self.runsGraphCanvas.find_enclosed(x1, y1, x2, y2)
# else:
# items = self.runsGraphCanvas.find_overlapping(x1, y1, x2, y2)
#
# update = False
#
# for itemId in items:
# if itemId in self.runsGraphCanvas.items:
#
# item = self.runsGraphCanvas.items[itemId]
# if not item.node.isRoot():
# item.setSelected(True)
# self._selection.append(itemId)
# update = True
#
# if update is not None: self._updateSelection()
def _openProtocolForm(self, prot, disableRunMode=False, position=None):
"""Open the Protocol GUI Form given a Protocol instance"""
w = FormWindow(Message.TITLE_NAME_RUN + prot.getClassName(),
prot, self._executeSaveProtocol, self.window,
updateProtocolCallback=self._updateProtocol,
disableRunMode=disableRunMode, position=position)
w.adjustSize()
w.show(center=True)
def _browseSteps(self):
""" Open a new window with the steps list. """
window = StepsWindow(Message.TITLE_BROWSE_DATA, self.window,
self.getSelectedProtocol())
window.show()
def _browseRunData(self):
provider = ProtocolTreeProvider(self.getSelectedProtocol())
window = pwgui.browser.BrowserWindow(Message.TITLE_BROWSE_DATA,
self.window)
window.setBrowser(pwgui.browser.ObjectBrowser(window.root, provider))
window.itemConfig(self.getSelectedProtocol(), open=True)
window.show()
def _browseRunDirectory(self):
""" Open a file browser to inspect the files generated by the run. """
protocol = self.getSelectedProtocol()
workingDir = protocol.getWorkingDir()
if os.path.exists(workingDir):
protFolderShortCut = ShortCut.factory(workingDir,name="Protocol folder", icon=None ,toolTip="Protocol directory")
window = pwgui.browser.FileBrowserWindow("Browsing: " + workingDir,
master=self.window,
path=workingDir,
shortCuts=[protFolderShortCut])
window.show()
else:
self.window.showInfo("Protocol working dir does not exists: \n %s"
% workingDir)
def _iterSelectedProtocols(self):
for protId in sorted(self._selection):
prot = self.project.getRunsGraph().getNode(str(protId)).run
if prot:
yield prot
def _getSelectedProtocols(self):
return [prot for prot in self._iterSelectedProtocols()]
def _iterSelectedNodes(self):
for protId in sorted(self._selection):
node = self.settings.getNodeById(protId)
yield node
def _getSelectedNodes(self):
return [node for node in self._iterSelectedNodes()]
[docs] def getSelectedProtocol(self):
if self._selection:
return self.project.getProtocol(self._selection[0], fromRuns=True)
return None
def _showHideAnalyzeResult(self):
if self._selection:
self.btnAnalyze.grid()
else:
self.btnAnalyze.grid_remove()
def _fillSummary(self):
self.summaryText.setReadOnly(False)
self.summaryText.clear()
self.infoTree.clear()
n = len(self._selection)
if n == 1:
prot = self.getSelectedProtocol()
if prot:
provider = RunIOTreeProvider(self, prot, self.project.mapper, self.info)
self.infoTree.setProvider(provider)
self.infoTree.grid(row=0, column=0, sticky='news')
self.infoTree.update_idletasks()
# Update summary
self.summaryText.addText(prot.summary())
else:
self.infoTree.clear()
elif n > 1:
self.infoTree.clear()
for prot in self._iterSelectedProtocols():
self.summaryText.addLine('> _%s_' % prot.getRunName())
for line in prot.summary():
self.summaryText.addLine(line)
self.summaryText.addLine('')
self.summaryText.setReadOnly(True)
def _fillMethod(self):
try:
self.methodText.setReadOnly(False)
self.methodText.clear()
self.methodText.addLine("*METHODS:*")
cites = OrderedDict()
for prot in self._iterSelectedProtocols():
self.methodText.addLine('> _%s_' % prot.getRunName())
for line in prot.getParsedMethods():
self.methodText.addLine(line)
cites.update(prot.getCitations())
cites.update(prot.getPackageCitations())
self.methodText.addLine('')
if cites:
self.methodText.addLine('*REFERENCES:* '
' [[sci-bib:][<<< Open as bibtex >>>]]')
for cite in cites.values():
self.methodText.addLine(cite)
self.methodText.setReadOnly(True)
except Exception as e:
self.methodText.addLine('Could not load all methods:' + str(e))
def _fillLogs(self):
try:
prot = self.getSelectedProtocol()
if not self._isSingleSelection() or not prot:
self.outputViewer.clear()
self._lastStatus = None
elif prot.getObjId() != self._lastSelectedProtId:
self._lastStatus = prot.getStatus()
i = self.outputViewer.getIndex()
self.outputViewer.clear()
# Right now skip the err tab since we are redirecting
# stderr to stdout
out, err, schedule = prot.getLogPaths()
self.outputViewer.addFile(out)
self.outputViewer.addFile(err)
if os.path.exists(schedule):
self.outputViewer.addFile(schedule)
elif i == 2:
i = 0
self.outputViewer.setIndex(i) # Preserve the last selected tab
self.outputViewer.selectedText().goEnd()
# when there are not logs, force re-load next time
if (not os.path.exists(out) or
not os.path.exists(err)):
self._lastStatus = None
elif prot.isActive() or prot.getStatus() != self._lastStatus:
doClear = self._lastStatus is None
self._lastStatus = prot.getStatus()
self.outputViewer.refreshAll(clear=doClear, goEnd=doClear)
except Exception as e:
self.info("Something went wrong filling %s's logs: %s. Check terminal for details" % (prot, e))
import traceback
traceback.print_exc()
def _scheduleRunsUpdate(self, secs=1, position=None):
# self.runsTree.after(secs*1000, self.refreshRuns)
self.window.enqueue(lambda : self.refreshRuns(position=position))
[docs] def executeProtocol(self, prot):
""" Function to execute a protocol called not
directly from the Form "Execute" button.
"""
# We need to equeue the execute action
# to be executed in the same thread
self.window.enqueue(lambda: self._executeSaveProtocol(prot))
def _executeSaveProtocol(self, prot, onlySave=False, doSchedule=False, position=None):
if onlySave:
self.project.saveProtocol(prot)
msg = Message.LABEL_SAVED_FORM
# msg = "Protocol successfully saved."
else:
if doSchedule:
self.project.scheduleProtocol(prot)
else:
self.project.launchProtocol(prot)
# Select the launched protocol to display its summary, methods..etc
self._selection.clear()
self._selection.append(prot.getObjId())
self._updateSelection()
self._lastStatus = None # clear lastStatus to force re-load the logs
msg = ""
# Update runs list display, even in save we
# need to get the updated copy of the protocol
self._scheduleRunsUpdate(position=position)
self._selectItemProtocol(prot)
return msg
def _updateProtocol(self, prot):
""" Callback to notify about the change of a protocol
label or comment.
"""
self._scheduleRunsUpdate()
def _continueProtocol(self, prot):
self.project.continueProtocol(prot)
self._scheduleRunsUpdate()
def _onDelPressed(self):
# This function will be connected to the key 'Del' press event
# We need to check if the canvas have the focus and then
# proceed with the delete action
# get the widget with the focus
widget = self.focus_get()
# Call the delete action only if the widget is the canvas
if str(widget).endswith(ProtocolsView.RUNS_CANVAS_NAME):
try:
self._deleteProtocol()
except Exception as ex:
self.window.showError(str(ex))
def _deleteProtocol(self):
protocols = self._getSelectedProtocols()
if len(protocols) == 0:
return
protStr = '\n - '.join(['*%s*' % p.getRunName() for p in protocols])
if pwgui.dialog.askYesNo(Message.TITLE_DELETE_FORM,
Message.LABEL_DELETE_FORM % protStr,
self.root):
self.info('Deleting protocols...')
self.project.deleteProtocol(*protocols)
self.settings.cleanUpNodes([str(prot.getObjId()) for prot in protocols])
self._selection.clear()
self._updateSelection()
self._scheduleRunsUpdate()
self.cleanInfo()
def _editProtocol(self, protocol):
disableRunMode = False
if protocol.isSaved():
disableRunMode = True
self._openProtocolForm(protocol, disableRunMode=disableRunMode)
def _pasteProtocolsFromClipboard(self, e=None):
""" Pastes the content of the clipboard providing is a json workflow"""
try:
self.project.loadProtocols(jsonStr=self.clipboard_get())
self.info("Clipboard content pasted successfully.")
self.updateRunsGraph(False)
except Exception as e:
self.info("Paste failed, maybe clipboard content is not valid content? See GUI log for details.")
logger.error("Clipboard content couldn't be pasted." , exc_info=e)
def _copyProtocolsToClipboard(self, e=None):
protocols = self._getSelectedProtocols()
jsonStr = self.project.getProtocolsJson(protocols)
self.clipboard_clear()
self.clipboard_append(jsonStr)
self.info("Protocols copied to the clipboard. Now you can paste them here, another project or in a template or ... anywhere!.")
def _copyProtocols(self, e=None):
protocols = self._getSelectedProtocols()
if len(protocols) == 1:
newProt = self.project.copyProtocol(protocols[0])
if newProt is None:
self.window.showError("Error copying protocol.!!!")
else:
self._openProtocolForm(newProt, disableRunMode=True)
else:
self.info('Copying the protocols...')
self.project.copyProtocol(protocols)
self.refreshRuns()
self.cleanInfo()
def _stopWorkFlow(self, action):
protocols = self._getSelectedProtocols()
# TODO: use filterCallback param and we may not need to return 2 elements
workflowProtocolList, activeProtList = self.project._getSubworkflow(protocols[0],
fixProtParam=False,
getStopped=False)
if activeProtList:
errorProtList = []
if pwgui.dialog.askYesNo(Message.TITLE_STOP_WORKFLOW_FORM,
Message.TITLE_STOP_WORKFLOW, self.root):
self.info('Stopping the workflow...')
errorProtList = self.project.stopWorkFlow(activeProtList)
self.cleanInfo()
self.refreshRuns()
if errorProtList:
msg = '\n'
for prot in errorProtList:
msg += str(prot.getObjLabel()) + '\n'
pwgui.dialog.MessageDialog(
self, Message.TITLE_STOPPED_WORKFLOW_FAILED,
Message.TITLE_STOPPED_WORKFLOW_FAILED + ' with: ' + msg,
Icon.ERROR)
def _resetWorkFlow(self, action):
protocols = self._getSelectedProtocols()
errorProtList = []
if pwgui.dialog.askYesNo(Message.TITLE_RESET_WORKFLOW_FORM,
Message.TITLE_RESET_WORKFLOW, self.root):
self.info('Resetting the workflow...')
workflowProtocolList, activeProtList = self.project._getSubworkflow(protocols[0])
errorProtList = self.project.resetWorkFlow(workflowProtocolList)
self.cleanInfo()
self.refreshRuns()
if errorProtList:
msg = '\n'
for prot in errorProtList:
msg += str(prot.getObjLabel()) + '\n'
pwgui.dialog.MessageDialog(
self, Message.TITLE_RESETED_WORKFLOW_FAILED,
Message.TITLE_RESETED_WORKFLOW_FAILED + ' with: ' + msg,
Icon.ERROR)
def _launchWorkFlow(self, action):
"""
This function can launch a workflow from a selected protocol in two
modes depending on the 'action' value (RESTART, CONTINUE)
"""
protocols = self._getSelectedProtocols()
mode = pwprot.MODE_RESTART if action == ACTION_RESTART_WORKFLOW else pwprot.MODE_RESUME
errorList, _ = self._launchSubWorkflow(protocols[0], mode, self.root)
if errorList:
msg = ''
for errorProt in errorList:
msg += str(errorProt) + '\n'
pwgui.dialog.MessageDialog(
self, Message.TITLE_LAUNCHED_WORKFLOW_FAILED_FORM,
Message.TITLE_LAUNCHED_WORKFLOW_FAILED + "\n" + msg,
Icon.ERROR)
self.refreshRuns()
@staticmethod
def _launchSubWorkflow(protocol, mode, root, askSingleAll=False):
"""
Method to launch a subworkflow
mode: mode value (RESTART, CONTINUE)
askSingleAll: specify if this method was launched from the form or from the menu
"""
project = protocol.getProject()
workflowProtocolList, activeProtList = project._getSubworkflow(protocol)
# Check if exists active protocols
activeProtocols = ""
if activeProtList:
for protId, activeProt in activeProtList.items():
activeProtocols += ("\n* " + activeProt.getRunName())
# by default, we assume RESTART workflow option
title = Message.TITLE_RESTART_WORKFLOW_FORM
message = Message.MESSAGE_RESTART_WORKFLOW_WITH_RESULTS % ('%s\n' % activeProtocols) if len(activeProtList) else Message.MESSAGE_RESTART_WORKFLOW
if mode == pwprot.MODE_RESUME:
message = Message.MESSAGE_CONTINUE_WORKFLOW_WITH_RESULTS % ('%s\n' % activeProtocols) if len(activeProtList) else Message.MESSAGE_CONTINUE_WORKFLOW
title = Message.TITLE_CONTINUE_WORKFLOW_FORM
errorList=[]
if not askSingleAll:
if pwgui.dialog.askYesNo(title, message, root):
errorList = project.launchWorkflow(workflowProtocolList, mode)
return errorList, RESULT_RUN_ALL
return [], RESULT_CANCEL
else: # launching from a form
if len(workflowProtocolList) > 1:
if Config.SCIPION_DEFAULT_EXECUTION_ACTION == DEFAULT_EXECUTION_ACTION_ASK:
title = Message.TITLE_RESTART_FORM if mode == pwprot.MODE_RESTART else Message.TITLE_CONTINUE_FORM
message += Message.MESSAGE_ASK_SINGLE_ALL
result = pwgui.dialog.askSingleAllCancel(title, message,
root)
elif Config.SCIPION_DEFAULT_EXECUTION_ACTION == DEFAULT_EXECUTION_ACTION_SINGLE:
result = RESULT_RUN_SINGLE
else:
result = RESULT_RUN_ALL
if result == RESULT_RUN_ALL:
errorList = []
if mode == pwprot.MODE_RESTART:
project._restartWorkflow(errorList, workflowProtocolList)
else:
project._continueWorkflow(errorList, workflowProtocolList)
return errorList, RESULT_RUN_ALL
elif result == RESULT_RUN_SINGLE:
# If mode resume, we should not reset the "current" protocol
if mode == pwprot.MODE_RESUME:
workflowProtocolList.pop(protocol.getObjId())
errorList = project.resetWorkFlow(workflowProtocolList)
return errorList, RESULT_RUN_SINGLE
elif result == RESULT_CANCEL:
return [], RESULT_CANCEL
else: # is a single protocol
if not protocol.isSaved():
title = Message.TITLE_RESTART_FORM
message = Message.MESSAGE_RESTART_FORM % ('%s\n' % protocol.getRunName())
if mode == pwprot.MODE_RESUME:
title = Message.TITLE_CONTINUE_FORM
message = Message.MESSAGE_CONTINUE_FORM % ('%s\n' % protocol.getRunName())
result = pwgui.dialog.askYesNo(title, message, root)
resultRun = RESULT_RUN_SINGLE if result else RESULT_CANCEL
return [], resultRun
return [], RESULT_RUN_SINGLE
def _selectLabels(self):
dlg = self.window.manageLabels()
selectedNodes = self._getSelectedNodes()
if dlg.resultYes() and selectedNodes:
for node in selectedNodes:
node.setLabels([label.getName() for label in dlg.values])
# self.updateRunsGraph()
self.drawRunsGraph()
# Save settings in any case
self.window.saveSettings()
def _selectAncestors(self):
self._selectNodes(down=False)
def _selectDescendants(self):
self._selectNodes(down=True)
def _selectNodes(self, down=True, fromRun=None):
""" Selects all nodes in the specified direction, defaults to down."""
nodesToSelect = []
# If parent param not passed...
if fromRun is None:
# ..use selection, must be first call
for protId in self._selection:
run = self.runsGraph.getNode(str(protId))
nodesToSelect.append(run)
else:
name = fromRun.getName()
if not name.isdigit():
return
else:
name = int(name)
# If already selected (may be this should be centralized)
if name not in self._selection:
nodesToSelect = (fromRun,)
self._selection.append(name)
# Go in the direction .
for run in nodesToSelect:
# Choose the direction: down or up.
direction = run.getChildren if down else run.getParents
# Select himself plus ancestors
for parent in direction():
self._selectNodes(down, parent)
# Only update selection at the end, avoid recursion
if fromRun is None:
self._lastSelectedProtId = None
self._updateSelection()
self.drawRunsGraph()
def _exportProtocols(self, defaultPath=None, defaultBasename=None):
protocols = self._getSelectedProtocols()
def _export(obj):
filename = os.path.join(browser.getCurrentDir(),
browser.getEntryValue())
try:
if (not os.path.exists(filename) or
self.window.askYesNo("File already exists",
"*%s* already exists, do you want "
"to overwrite it?" % filename)):
self.project.exportProtocols(protocols, filename)
logger.info("Workflow successfully saved to '%s' "
% filename)
else: # try again
self._exportProtocols(defaultPath=browser.getCurrentDir(),
defaultBasename=browser.getEntryValue())
except Exception as ex:
import traceback
traceback.print_exc()
self.window.showError(str(ex))
browser = pwgui.browser.FileBrowserWindow(
"Choose .json file to save workflow",
master=self.window,
path=defaultPath or self.project.getPath(''),
onSelect=_export,
entryLabel='File ', entryValue=defaultBasename or 'workflow.json')
browser.show()
def _exportUploadProtocols(self):
try:
jsonFn = os.path.join(tempfile.mkdtemp(), 'workflow.json')
self.project.exportProtocols(self._getSelectedProtocols(), jsonFn)
WorkflowRepository().upload(jsonFn)
pwutils.cleanPath(jsonFn)
except Exception as ex:
self.window.showError("Error connecting to workflow repository:\n"
+ str(ex))
def _stopProtocol(self, prot):
if pwgui.dialog.askYesNo(Message.TITLE_STOP_FORM,
Message.LABEL_STOP_FORM, self.root):
self.project.stopProtocol(prot)
self._lastStatus = None # force logs to re-load
self._scheduleRunsUpdate()
def _analyzeResults(self, prot, keyPressed):
viewers = self.domain.findViewers(prot.getClassName(), DESKTOP_TKINTER)
if len(viewers):
# Instantiate the first available viewer
# TODO: If there are more than one viewer we should display
# TODO: a selection menu
firstViewer = viewers[0](project=self.project, protocol=prot,
parent=self.window, keyPressed=keyPressed)
if isinstance(firstViewer, ProtocolViewer):
firstViewer.visualize(prot, windows=self.window)
else:
firstViewer.visualize(prot)
else:
outputList = []
for _, output in prot.iterOutputAttributes():
outputList.append(output)
for output in outputList:
viewers = self.domain.findViewers(output.getClassName(), DESKTOP_TKINTER)
if len(viewers):
# Instantiate the first available viewer
# TODO: If there are more than one viewer we should display
# TODO: a selection menu
viewerclass = viewers[0]
firstViewer = viewerclass(project=self.project,
protocol=prot,
parent=self.window,
keyPressed=keyPressed)
# FIXME:Probably o longer needed protocol on args, already provided on init
firstViewer.visualize(output, windows=self.window,
protocol=prot)
def _analyzeResultsClicked(self, keyPressed=None):
""" Function called when button "Analyze results" is called. """
prot = self.getSelectedProtocol()
# Nothing selected
if prot is None:
return
if os.path.exists(prot._getPath()):
# self.info('"Analyze result" clicked with %s key pressed.' % keyPressed)
self._analyzeResults(prot, keyPressed)
else:
self.window.showInfo("Selected protocol hasn't been run yet.")
def _bibExportClicked(self, e=None):
try:
bibTexCites = OrderedDict()
for prot in self._iterSelectedProtocols():
bibTexCites.update(prot.getCitations(bibTexOutput=True))
bibTexCites.update(prot.getPackageCitations(bibTexOutput=True))
if bibTexCites:
with tempfile.NamedTemporaryFile(suffix='.bib') as bibFile:
for refId, refDict in bibTexCites.items():
# getCitations does not always return a dictionary
# if the citation is not found in the bibtex file it adds just
# the refId: like "Ramirez-Aportela-2019"
# we need to exclude this
if isinstance(refDict, dict):
refType = refDict['ENTRYTYPE']
# remove 'type' and 'id' keys
refDict = {k: v for k, v in refDict.items()
if k not in ['ENTRYTYPE', 'ID']}
jsonStr = json.dumps(refDict, indent=4,
ensure_ascii=False)[1:]
jsonStr = jsonStr.replace('": "', '"= "')
jsonStr = re.sub(r'(?<!= )"(\S*?)"', '\\1', jsonStr)
jsonStr = jsonStr.replace('= "', ' = "')
refStr = '@%s{%s,%s\n\n' % (refType, refId, jsonStr)
bibFile.write(refStr.encode('utf-8'))
else:
logger.warning("Reference %s not properly defined or unpublished." % refId)
# flush so we can see content when opening
bibFile.flush()
pwgui.text.openTextFileEditor(bibFile.name)
except Exception as ex:
self.window.showError(str(ex))
return
def _renameProtocol(self, prot):
""" Open the EditObject dialog to edit the protocol name. """
kwargs = {}
if self._lastRightClickPos:
kwargs['position'] = self._lastRightClickPos
dlg = pwgui.dialog.EditObjectDialog(self.runsGraphCanvas, Message.TITLE_EDIT_OBJECT,
prot, self.project.mapper, **kwargs)
if dlg.resultYes():
self._updateProtocol(prot)
def _runActionClicked(self, action, event=None):
if event is not None:
# log Search box events are reaching here
# Since this method is bound to the window events
if event.widget.widgetName == 'entry':
return
# Following actions do not need a select run
if action == ACTION_TREE:
self.drawRunsGraph(reorganize=True)
elif action == ACTION_REFRESH:
self.refreshRuns(checkPids=True)
elif action == ACTION_PASTE:
self._pasteProtocolsFromClipboard()
elif action == ACTION_SWITCH_VIEW:
self.switchRunsView()
elif action == ACTION_NEW:
self._findProtocol(event)
elif action == ACTION_LABELS:
self._selectLabels()
else:
prot = self.getSelectedProtocol()
if prot:
try:
if action == ACTION_DEFAULT:
pass
elif action == ACTION_EDIT:
self._editProtocol(prot)
elif action == ACTION_RENAME:
self._renameProtocol(prot)
elif action == ACTION_DUPLICATE:
self._copyProtocols()
elif action == ACTION_COPY:
self._copyProtocolsToClipboard()
elif action == ACTION_DELETE:
self._deleteProtocol()
elif action == ACTION_STEPS:
self._browseSteps()
elif action == ACTION_BROWSE:
self._browseRunDirectory()
elif action == ACTION_DB:
self._browseRunData()
elif action == ACTION_STOP:
self._stopProtocol(prot)
elif action == ACTION_CONTINUE:
self._continueProtocol(prot)
elif action == ACTION_RESULTS:
self._analyzeResults(prot, None)
elif action == ACTION_EXPORT:
self._exportProtocols(defaultPath=pwutils.getHomePath())
elif action == ACTION_EXPORT_UPLOAD:
self._exportUploadProtocols()
elif action == ACTION_COLLAPSE:
node = self.runsGraph.getNode(str(prot.getObjId()))
nodeInfo = self.settings.getNodeById(prot.getObjId())
nodeInfo.setExpanded(False)
self.setVisibleNodes(node, visible=False)
self.updateRunsGraph(False)
self._updateActionToolbar()
elif action == ACTION_EXPAND:
node = self.runsGraph.getNode(str(prot.getObjId()))
nodeInfo = self.settings.getNodeById(prot.getObjId())
nodeInfo.setExpanded(True)
self.setVisibleNodes(node, visible=True)
self.updateRunsGraph(False)
self._updateActionToolbar()
elif action == ACTION_SELECT_FROM:
self._selectDescendants()
elif action == ACTION_SELECT_TO:
self._selectAncestors()
elif action == ACTION_RESTART_WORKFLOW:
self._launchWorkFlow(action)
elif action == ACTION_CONTINUE_WORKFLOW:
self._launchWorkFlow(action)
elif action == ACTION_STOP_WORKFLOW:
self._stopWorkFlow(action)
elif action == ACTION_RESET_WORKFLOW:
self._resetWorkFlow(action)
elif action == ACTION_SEARCH:
self._searchProtocol()
except Exception as ex:
self.window.showError(str(ex), exception=ex)
if Config.debugOn():
import traceback
traceback.print_exc()
else:
self.info("Action '%s' not implemented." % action)
[docs] def setVisibleNodes(self, node, visible=True):
hasParentHidden = False
for child in node.getChildren():
prot = child.run
nodeInfo = self.settings.getNodeById(prot.getObjId())
if visible:
hasParentHidden = self.hasParentHidden(child)
if not hasParentHidden:
nodeInfo.setVisible(visible)
self.setVisibleNodes(child, visible)
[docs] def hasParentHidden(self, node):
for parent in node.getParents():
prot = parent.run
nodeInfo = self.settings.getNodeById(prot.getObjId())
if not nodeInfo.isVisible() or not nodeInfo.isExpanded():
return True
return False
[docs]class RunBox(pwgui.TextBox):
""" Just override TextBox move method to keep track of
position changes in the graph.
"""
def __init__(self, nodeInfo, canvas, text, x, y, bgColor, textColor):
pwgui.TextBox.__init__(self, canvas, text, x, y, bgColor, textColor)
self.nodeInfo = nodeInfo
canvas.addItem(self)
[docs] def move(self, dx, dy):
pwgui.TextBox.move(self, dx, dy)
self.nodeInfo.setPosition(self.x, self.y)
[docs] def moveTo(self, x, y):
pwgui.TextBox.moveTo(self, x, y)
self.nodeInfo.setPosition(self.x, self.y)