Source code for pyworkflow.gui.project.viewdata

#!/usr/bin/env python
# **************************************************************************
# *
# * 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'
# *
# **************************************************************************
"""
Data-oriented view of the project objects.
"""


import tkinter as tk
import tkinter.ttk as ttk
import tkinter.font as tkFont

import pyworkflow.utils as pwutils
import pyworkflow.protocol as pwprot
import pyworkflow.viewer as pwviewer

import pyworkflow.gui as gui
from pyworkflow.gui.tree import Tree
from pyworkflow.gui.dialog import EditObjectDialog
from pyworkflow.gui.text import TaggedText
from pyworkflow.gui import Canvas, RoundedTextBox
from pyworkflow.gui.graph import LevelTree
from pyworkflow.gui.form import getObjectLabel
from pyworkflow.constants import DATA_TAG

ACTION_EDIT = pwutils.Message.LABEL_EDIT
ACTION_COPY = pwutils.Message.LABEL_COPY
ACTION_DELETE = pwutils.Message.LABEL_DELETE
ACTION_REFRESH = pwutils.Message.LABEL_REFRESH
ACTION_STEPS = pwutils.Message.LABEL_BROWSE
ACTION_TREE = pwutils.Message.LABEL_TREE
ACTION_LIST = pwutils.Message.LABEL_LIST
ACTION_STOP = pwutils.Message.LABEL_STOP
ACTION_DEFAULT = pwutils.Message.LABEL_DEFAULT
ACTION_CONTINUE = pwutils.Message.LABEL_CONTINUE
ACTION_RESULTS = pwutils.Message.LABEL_ANALYZE

RUNS_TREE = pwutils.Icon.RUNS_TREE
RUNS_LIST = pwutils.Icon.RUNS_LIST
 
ActionIcons = {
    ACTION_EDIT: pwutils.Icon.ACTION_EDIT,
    ACTION_COPY: pwutils.Icon.ACTION_COPY,
    ACTION_DELETE:  pwutils.Icon.ACTION_DELETE,
    ACTION_REFRESH:  pwutils.Icon.ACTION_REFRESH,
    ACTION_STEPS:  pwutils.Icon.ACTION_STEPS,
    ACTION_TREE:  None,  # should be set
    ACTION_LIST:  pwutils.Icon.ACTION_LIST,
    ACTION_STOP: pwutils.Icon.ACTION_STOP,
    ACTION_CONTINUE: pwutils.Icon.ACTION_CONTINUE,
    ACTION_RESULTS: pwutils.Icon.ACTION_RESULTS,
}

STATUS_COLORS = {
    pwprot.STATUS_SAVED: '#D9F1FA',
    pwprot.STATUS_LAUNCHED: '#D9F1FA',
    pwprot.STATUS_RUNNING: '#FCCE62',
    pwprot.STATUS_FINISHED: '#D2F5CB',
    pwprot.STATUS_FAILED: '#F5CCCB',
    pwprot.STATUS_INTERACTIVE: '#F3F5CB',
    pwprot.STATUS_ABORTED: '#F5CCCB',
    # pwprot.STATUS_SAVED: '#124EB0',
}


[docs]def populateTree(tree, elements, parentId=''): for node in elements: if hasattr(node, 'count'): t = node.getName() if node.count: t += ' (%d)' % node.count node.nodeId = tree.insert(parentId, 'end', node.getName(), text=t, tags=DATA_TAG) populateTree(tree, node.getChilds(), node.nodeId) if node.count: tree.see(node.nodeId) tree.item(node.nodeId, tags=('non-empty', DATA_TAG))
[docs]class ProjectDataView(tk.Frame): def __init__(self, parent, windows, **args): tk.Frame.__init__(self, parent, **args) # Load global configuration self.windows = windows self.project = windows.project self.root = windows.root self.getImage = windows.getImage self.settings = windows.getSettings() self.showGraph = self.settings.getRunsView() self.style = ttk.Style() self.root.bind("<F5>", self.refreshData) self.__autoRefresh = None self.__autoRefreshCounter = 3 # start by 3 secs self._dataGraph = windows.project.getSourceGraph(True) c = self._createContent() gui.configureWeigths(self) c.grid(row=0, column=0, sticky='news') def _createContent(self): """ Create the Data View for the Project. It has two panes: Left: containing the Protocol classes tree Right: containing the Data list """ p = tk.PanedWindow(self, orient=tk.HORIZONTAL, bg='white') # Left pane, contains Data tree leftFrame = tk.Frame(p, bg='white') bgColor = '#eaebec' self._createDataTree(leftFrame, bgColor) gui.configureWeigths(leftFrame) # Right pane rightFrame = tk.Frame(p, bg='white') rightFrame.columnconfigure(0, weight=1) rightFrame.rowconfigure(1, weight=1) # rightFrame.rowconfigure(0, minsize=label.winfo_reqheight()) self._fillRightPane(rightFrame) # Add sub-windows to PanedWindows p.add(leftFrame, padx=5, pady=5) p.add(rightFrame, padx=5, pady=5) p.paneconfig(leftFrame, minsize=300) p.paneconfig(rightFrame, minsize=400) return p def _createDataTree(self, parent, bgColor): """Create a tree on the left panel to store how many object are from each type and the hierarchy. """ defaultFont = gui.getDefaultFont() self.style.configure("W.Treeview", background=pwutils.Color.LIGHT_GREY_COLOR, borderwidth=0, rowheight=defaultFont.metrics()['linespace']) self.dataTree = Tree(parent, show='tree', style='W.Treeview') self.dataTree.column('#0', minwidth=300) self.dataTree.tag_configure('protocol', image=self.getImage('python_file.gif')) self.dataTree.tag_configure('protocol_base', image=self.getImage('class_obj.gif')) f = tkFont.Font(family='helvetica', size='10', weight='bold') self.dataTree.tag_configure('non-empty', font=f) self.dataTree.grid(row=0, column=0, sticky='news') # bind click events self.dataTree.tag_bind(DATA_TAG, '<Double-1>', self._dataItemClick) self.dataTree.tag_bind(DATA_TAG, '<Return>', self._dataItemClick) self.dataTree.tag_bind(DATA_TAG, '<KP_Enter>', self._dataItemClick) # Program automatic refresh self.dataTree.after(3000, self._automaticRefreshData) self._updateDataTree() def _updateDataTree(self): def createClassNode(classObj): """ Add the object class to hierarchy and any needed subclass. """ className = classObj.__name__ classNode = classesGraph.getNode(className) if not classNode: classNode = classesGraph.createNode(className) if className != 'EMObject' and classObj.__bases__: baseClass = classObj.__bases__[0] for b in classObj.__bases__: if b.__name__ == 'EMObject': baseClass = b break parent = createClassNode(baseClass) parent.addChild(classNode) classNode.count = 0 return classNode classesGraph = pwutils.Graph() self.dataTree.clear() for node in self._dataGraph.getNodes(): if node.pointer: classNode = createClassNode(node.pointer.get().getClass()) classNode.count += 1 populateTree(self.dataTree, classesGraph.getRootNodes()) def _fillRightPane(self, parent): """ # 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) """ # Create the Action Buttons TOOLBAR toolbar = tk.Frame(parent, bg='white') toolbar.grid(row=0, column=0, sticky='news') gui.configureWeigths(toolbar) # toolbar.columnconfigure(0, weight=1) toolbar.columnconfigure(1, weight=1) self.runsToolbar = tk.Frame(toolbar, bg='white') 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='white') self.allToolbar.grid(row=0, column=10, sticky='se') # self.createActionToolbar() # Create the Run History tree v = ttk.PanedWindow(parent, orient=tk.VERTICAL) # runsFrame = ttk.Labelframe(v, text=' History ', width=500, height=500) runsFrame = tk.Frame(v, bg='white') self._createDataGraph(runsFrame) gui.configureWeigths(runsFrame) # Create the Selected Run Info infoFrame = tk.Frame(v) gui.configureWeigths(infoFrame) self._infoText = TaggedText(infoFrame, bg='white', handlers={'sci-open': self._openProtocolFormFromId}) self._infoText.grid(row=0, column=0, sticky='news') v.add(runsFrame, weight=3) v.add(infoFrame, weight=1) v.grid(row=1, column=0, sticky='news') # TODO(josemi): check that the call to RoundedTextBox is correct # It looks suspicious because RoundedTextBox() needs 5 arguments, not 3 def _createNode(self, canvas, node, y): """ Create Data node to be painted in the graph. """ node.selected = False node.box = RoundedTextBox(canvas, node, y) return node.box def _createDataItem(self, canvas, node, y): if node.pointer is None: nodeText = "Project" else: label = getObjectLabel(node.pointer, self.project.mapper) nodeText = (label[:25]+'...') if len(label) > 25 else label textColor = 'black' color = '#ADD8E6' # Lightblue item = self._dataCanvas.createTextbox(nodeText, 100, y, bgColor=color, textColor=textColor) # Get the dataId if not node.isRoot(): dataId = node.pointer.get().getObjId() if dataId in self.settings.dataSelection: item.setSelected(True) return item def _createDataGraph(self, parent): """ This will be the upper part of the right panel. It will contain the Data Graph with their relations. """ self._dataCanvas = Canvas(parent, width=600, height=500) self._dataCanvas.grid(row=0, column=0, sticky='nsew') self._dataCanvas.onClickCallback = self._onClick self._dataCanvas.onDoubleClickCallback = self._onDoubleClick self._dataCanvas.onRightClickCallback = self._onRightClick self._updateDataGraph() def _updateDataGraph(self): lt = LevelTree(self._dataGraph) self._dataCanvas.clear() lt.setCanvas(self._dataCanvas) lt.paint(self._createDataItem) self._dataCanvas.updateScrollRegion() def _dataItemClick(self, e=None): # Get the tree widget that originated the event # from the left pane data tree tree = e.widget dataClassName = tree.getFirst() if dataClassName is not None: self._loopData(lambda item: self._selectItemByClass(item, dataClassName)) def _selectObject(self, pobj): obj = pobj.get() objValue = pobj.getObjValue() self._selected = obj self._infoText.setReadOnly(False) self._infoText.setText('*Label:* ' + getObjectLabel(pobj, self.project.mapper)) self._infoText.addText('*Info:* ' + str(obj)) self._infoText.addText('*Created by:*') self._infoText.addText(' - %s (%s)' % (objValue.getObjectTag(objValue), obj.getObjCreation())) # Get the consumers (this will get the data!!) derivedData = self.project.getSourceChilds(objValue) if derivedData is not None and len(derivedData) > 0: self._infoText.addText('*Consumed by:*') for data in derivedData: # Get the protocol protocol = self.project.getObject(data.getObjParentId()) self._infoText.addText(' - ' + protocol.getObjectTag(protocol)) if obj.getObjComment(): self._infoText.addText('*Comments:* ' + obj.getObjComment()) self._infoText.setReadOnly(True) def _onClick(self, e=None): self._deselectAll() if e.node.pointer: self.toggleItemSelection(e, True) self._selectObject(e.node.pointer) def _invertSelection(self): self._loopData(self._invertAction) def _deselectAll(self): self._loopData(self._deselectItemAction) def _selectAll(self): self._loopData(self._selectItemAction) def _selectItemAction(self, item): self.toggleItemSelection(item, True) def _selectItemByClass(self, item, className): if not item.node.isRoot(): data = item.node.pointer.get() self.toggleItemSelection( item, isinstance(data, self.domain.getObjects()[className])) def _invertAction(self, item): self.toggleItemSelection(item, not item.getSelected()) def _deselectItemAction(self, item): self.toggleItemSelection(item, False)
[docs] def toggleItemSelection(self, item, select): if item.node.isRoot(): return selection = self.settings.dataSelection runSelection = self.settings.runSelection dataId = item.node.pointer.get().getObjId() protocolId = item.node.pointer.getObjValue().getObjId() if not select: try: if dataId in selection: selection.remove(dataId) if protocolId in runSelection: runSelection.remove(protocolId) except ValueError: print("id not in selection") else: selection.append(dataId) runSelection.append(protocolId) item.setSelected(select)
def _loopData(self, action): results = [] # Loop through all the items for key, item in self._dataCanvas.items.items(): result = action(item) if result is not None: results.append(result) return results def _onDoubleClick(self, e=None): if e.node.pointer: self._selectObject(e.node.pointer) self._viewObject(e.node.pointer.get().getObjId()) return # self._selectObject(e.node.pointer) # # Graph the first viewer available for this type of object # ViewerClass = findViewers(self._selected.getClassName(), DESKTOP_TKINTER)[0] # # viewer = ViewerClass(project=self.project) # viewer.visualize(self._selected) def _viewObject(self, objId): """ Call appropriate viewer for objId. """ obj = self.project.getObject(int(objId)) viewerClasses = self.domain.findViewers(obj.getClassName(), pwviewer.DESKTOP_TKINTER) if not viewerClasses: return # TODO: protest nicely viewer = viewerClasses[0](project=self.project, parent=self.windows) viewer.visualize(obj) def _openScipionLink(self, id): """ So far only protocols are coming through links""" self._openProtocolFormFromId(id) def _openProtocolFormFromId(self, protId): prot = self.project.getObject(int(protId)) self._openProtocolForm(prot) def _openProtocolForm(self, prot): """Open the Protocol GUI Form given a Protocol instance""" title = pwutils.Message.TITLE_NAME_RUN + prot.getClassName() w = gui.form.FormWindow(title, prot, self._executeSaveProtocol, self.windows, hostList=self.project.getHostNames()) w.adjustSize() w.show(center=True) def _executeSaveProtocol(self, prot, onlySave=False): if onlySave: self.project.saveProtocol(prot) msg = pwutils.Message.LABEL_SAVED_FORM # msg = "Protocol successfully saved." 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() # clear lastStatus to force re-load the logs # self._lastStatus = None msg = "" return msg def _onRightClick(self, e=None): return [ (pwutils.Message.LABEL_EDIT, self._editObject, pwutils.Icon.ACTION_EDIT), ('Go to protocol', self._goToProtocol, pwutils.Icon.ACTION_SEARCH) ] def _editObject(self): """Open the Edit GUI Form given an instance""" EditObjectDialog(self, pwutils.Message.TITLE_EDIT_OBJECT, self._selected, self.project.mapper) def _goToProtocol(self): """Switch to protocols view selecting the correspondent protocol"""
[docs] def refreshData(self, e=None, initRefreshCounter=True): """ Refresh the status of displayed data. Params: e: Tk event input initRefreshCounter: if True the refresh counter will be set to 3 secs then only case when False is from _automaticRefreshData where the refresh time is doubled each time to avoid refreshing too often. """ self._dataGraph = self.windows.project.getSourceGraph(True) self._updateDataTree() self._updateDataGraph() if initRefreshCounter: self.__autoRefreshCounter = 3 # start by 3 secs if self.__autoRefresh: self.dataTree.after_cancel(self.__autoRefresh) self.__autoRefresh = self.dataTree.after( self.__autoRefreshCounter*1000, self._automaticRefreshData)
def _automaticRefreshData(self, e=None): """ Schedule automatic refresh increasing the time between refreshes. """ self.refreshData(initRefreshCounter=False) secs = self.__autoRefreshCounter # double the number of seconds up to 30 min self.__autoRefreshCounter = min(2*secs, 1800) self.__autoRefresh = self.dataTree.after(secs*1000, self._automaticRefreshData)
[docs]class DataTextBox(RoundedTextBox): def __init__(self, canvas, text, x, y, bgColor, textColor='black'): RoundedTextBox.__init__(self, canvas, text, x, y, bgColor, textColor)