# -*- coding: utf-8 -*-
# **************************************************************************
# *
# * Authors: J.M. De la Rosa Trevin (jmdelarosa@cnb.csic.es)
# *
# * Unidad de Bioinformatica of Centro Nacional de Biotecnologia , CSIC
# *
# * 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, write to the Free Software
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
# * 02111-1307 USA
# *
# * All comments concerning this program package may be sent to the
# * e-mail address 'scipion@cnb.csic.es'
# *
# **************************************************************************
import os
import tkinter as tk
import tkinter.ttk as ttk
from pyworkflow.mapper import SqliteMapper
from pyworkflow.utils import prettyDelta
from . import gui
from .widgets import Scrollable
FIRST_TREE_COLUMN = '#0'
[docs]class Tree(ttk.Treeview, Scrollable):
""" This widget acts as a wrapper around the ttk.Treeview"""
_images = {}
def __init__(self, master, frame=True, **opts):
"""Create a new Tree, if frame=True, a container
frame will be created and an scrollbar will be added"""
Scrollable.__init__(self, master, ttk.Treeview, frame, **opts)
[docs] def getImage(self, img):
return gui.getImage(img)
[docs] def getFirst(self):
""" Return first selected item or None if selection empty"""
selection = self.selection()
if len(selection):
return selection[0]
return None
def _moveSelection(self, moveFunc):
item = self.selection_first()
if item:
item = moveFunc(item)
if item != '':
self.selection_set(item)
[docs] def moveSelectionUp(self, e=None):
""" change selection to previous item """
self._moveSelection(self.prev)
[docs] def moveSelectionDown(self, e=None):
""" change selection to to next item """
self._moveSelection(self.next)
[docs] def moveItemUp(self, e=None):
"""if selected item is not the first move up one position"""
item = self.selection_first()
if item:
index = self.index(item)
if index > 0:
self.move(item, '', index - 1)
[docs] def moveItemDown(self, e=None):
"""if selected item is not the first move up one position"""
item = self.selection_first()
if item:
index = self.index(item)
if self.next(item) != '':
self.move(item, '', index + 1)
[docs] def clear(self):
""" remove all items """
childs = self.get_children('')
for c in childs:
self.delete(c)
[docs] def selectChildByIndex(self, index):
""" Select the item at the position index """
child = self.get_children('')[index]
self.selection_set(child)
[docs] def selectChild(self, child):
self.selection_set(child)
[docs] def search(self, initial, fromSelected=True):
""" Search the first item starting with "start"
Implemented for Flat tree like FileBrowser...TODO: consider
a proper tree with branches and leaves..
Parameters
----------
initial: String - text to look for in the items. Usually the first initial letter
fromSelected : Boolean, start looking from the selected item"""
# Validate search string, do not allow empty chars
if len(initial) == 0:
return False
# Get all
children = self.get_children('')
# Get the selected item
searchAfter = self.getFirst() if fromSelected else None
# Loop ...
for child in children:
text = self.item(child, 'text')
if searchAfter is not None and searchAfter == text:
searchAfter = None
continue
if searchAfter is None:
# Do a lower case search
text = text.lower()
if text.startswith(initial.lower()):
# Enclose text in "" due to bug
# https://stackoverflow.com/questions/10691257/ttk-treeview-selection-set-cant-accept-spaces
searchText = '"' + child + '"'
self.focus(child)
self.selection_set(searchText)
return True
else:
continue
# If we started from a selected item...start again without selection
if fromSelected:
return self.search(initial, False)
# From: https://stackoverflow.com/questions/1966929/tk-treeview-column-sort
[docs] def sortByColumn(self, col, reverse, casting=str):
"""
Function to sort a treeview
:param self: treview
:param col: column to apply the sorting on
:param reverse: sorting direction
:param casting: optional - casting operation to apply on the column value: str is default. int, float are other options
:return:
"""
l = [(casting(self.set(k, col)), k) for k in self.get_children('')]
l.sort(reverse=reverse)
# rearrange items in sorted positions
for index, (val, k) in enumerate(l):
self.move(k, '', index)
# reverse sort next time
self.heading(col, command=lambda: self.sortByColumn(col, not reverse, casting))
[docs]class TreeProvider:
""" Class class will serve to separate the logic of feed data
from the graphical Tree build. Subclasses should implement
the abstract methods """
def __init__(self, sortingColumnName=None, sortingAscending=True):
self._sortingColumnName = sortingColumnName
self._sortingAscending = sortingAscending
self._sortEnabled = (sortingColumnName is not None)
[docs] def getColumns(self):
"""Return a list of tuples (c, w) where:
c: is the column name and index
w: is the column width
"""
pass
[docs] def getObjects(self):
"""Return the objects that will be inserted in the Tree"""
pass
[docs] def getObjectInfo(self, obj):
""" This function will be called by the Tree with each
object that will be inserted. A dictionary should be
returned with the possible following entries:
'key': the key value to insert in the Tree
'text': text of the object to be displayed
(if not passed the 'key' will be used)
'image': image path to be displayed as icon (optional)
'parent': the object's parent in which insert this object (optional)
'tags': list of tags names (optional)
"""
pass
[docs] def getObjectPreview(self, obj):
""" Should return a tuple (img, desc),
where img is the preview image and
desc the description string.
"""
return None, None
[docs] def getObjectActions(self, obj):
""" Return a list of tuples (key, action)
were keys are the string
options that will be display in the context menu
and the actions are the functions to call when
the specific action is selected.
The first action in the list will be taken
as the default one when the element is double-clicked.
"""
return []
[docs] def setSortingParams(self, columnName, ascending):
"""
Column name to sort by it and sorting direction
Parameters
----------
columnName: Name of the column
ascending: If true sorting will be ascending.
Returns
-------
Nothing
"""
self._sortingColumnName = columnName
self._sortingAscending = ascending
[docs] def getSortingColumnName(self):
return self._sortingColumnName
[docs] def isSortingAscending(self):
return self._sortingAscending
[docs] def sortEnabled(self):
# return self._sortingColumnName is not None
return self._sortEnabled
[docs]class BoundTree(Tree):
""" This class is base on Tree but fetch the
items from a TreeProvider, which provides columns
values for each item and items info to insert into the Tree """
def __init__(self, master, provider, frame=True, **opts):
"""Create a new Tree, if frame=True, a container
frame will be created and a scrollbar will be added"""
# Get columns to display and width
cols = provider.getColumns()
colsTuple = tuple([c[0] for c in cols[1:]])
Tree.__init__(self, master, frame, columns=colsTuple, **opts)
# Set the special case of first tree column
self.heading(FIRST_TREE_COLUMN, text=cols[0][0],
command=lambda: self.sortTree(FIRST_TREE_COLUMN, cols[0][0]))
self.column(FIRST_TREE_COLUMN, width=cols[0][1])
# Set other columns
for c, w in cols[1:]:
self.column(c, width=w)
self.heading(c, text=c, command=lambda _c=c: self.sortTree(_c, _c))
self.grid(row=0, column=0, sticky='news')
self.menu = tk.Menu(self, tearoff=0)
self.setProvider(provider)
self.bind("<Button-3>", self._onRightClick)
# Hide the right-click menu
self.bind('<FocusOut>', self._unpostMenu)
self.bind("<Key>", self._onKeyPress)
self.bind('<Button-1>', self._onClick)
self.bind('<Double-1>', self._onDoubleClick)
self.bind('<<TreeviewSelect>>', self._onSelect)
[docs] def setProvider(self, provider):
""" Set new provider and updated items. """
self.provider = provider
self.update()
def _onClick(self, e=None):
self._unpostMenu()
if hasattr(self, 'itemOnClick'):
self.itemOnClick(e)
def _onKeyPress(self, e=None):
self._unpostMenu()
if hasattr(self, 'itemKeyPressed'):
selected = self.getFirst()
if selected:
obj = self._objDict[selected]
self.itemKeyPressed(obj, e)
def _unpostMenu(self, e=None):
self.menu.unpost()
def _onSelect(self, e=None):
if hasattr(self, 'itemClick'):
selected = self.getFirst()
if selected:
obj = self._objDict[selected]
self.itemClick(obj)
def _onDoubleClick(self, e=None):
selected = self.getFirst()
if selected:
obj = self._objDict[selected]
if hasattr(self, 'itemDoubleClick'):
self.itemDoubleClick(obj)
else: # If not callback, use default action
actions = self.provider.getObjectActions(obj)
if len(actions):
# actions[0] = first Action, [1] = the action callback
actions[0][1]()
def _onRightClick(self, e=None):
item = self.identify('item', e.x, e.y)
unpost = True
if len(item):
self.selection_set(item)
obj = self._objDict[item]
actions = self.provider.getObjectActions(obj)
if len(actions):
self.menu.delete(0, tk.END)
for a in actions:
if a is None:
self.menu.add_separator()
else:
img = ''
if len(a) > 2: # image for the action
img = self.getImage(a[2])
self.menu.add_command(label=a[0], command=a[1],
image=img, compound=tk.LEFT)
self.menu.post(e.x_root, e.y_root)
unpost = False
if unpost:
self._unpostMenu()
[docs] def update(self):
self.clear()
self.provider.configureTags(self)
self._objDict = {} # Store the mapping between Tree ids and objects
self._objects = self.provider.getObjects()
for obj in self._objects:
# If the object is a pointer that has a null value do not show
# if ((not obj.isPointer()) or (obj.isPointer() and obj.get() is not None)):
objDict = self.provider.getObjectInfo(obj)
if objDict is not None:
key = objDict.get('key')
text = objDict.get('text', key)
parent = objDict.get('parent', None)
if parent is None:
parentId = ''
else:
if hasattr(parent, '_treeId'): # This should happens always
parentId = parent._treeId # Previously set
else:
parentId = ''
text += '---> Error: parent not Inserted'
image = objDict.get('image', '')
if len(image):
image = self.getImage(image)
if image is None:
image = ''
values = objDict.get('values', ())
tags = objDict.get('tags', ())
try:
obj._treeId = self.insert(parentId, 'end', key,
text=text, image=image, values=values, tags=tags)
self._objDict[obj._treeId] = obj
if objDict.get('open', False):
self.itemConfig(obj, open=True)
if objDict.get('selected', False):
self.selectChild(obj._treeId)
except Exception as ex:
print("error: ", ex)
if hasattr(obj, "getObjId") and obj.getObjId():
print("error object with id=%d (%s) is duplicated!!!"
% (obj.getObjId(), str(obj)))
else:
print("error, object %s does not have an id. This could"
" be due to the load of old project that does not"
" have recently added attributes "
"(e.g.:datastreaming)" % str(obj))
[docs] def sortTree(self, heading, column):
if not self.provider.sortEnabled():
return
# Calculate the sorting direction. default to true
ascending = True
# Current sorted column
currentSortedColumn = self.provider.getSortingColumnName()
# If its the same column
if column == currentSortedColumn:
ascending = not self.provider.isSortingAscending()
else:
# Remove previous arrow
previousHeading = self.getColumnKeyByColumnName(currentSortedColumn)
self.heading(previousHeading, text=currentSortedColumn)
# Visualize column sorted in the header
if ascending:
self.heading(heading, text=column + ' ▲')
else:
self.heading(heading, text=column + ' ▼')
self.provider.setSortingParams(column, ascending)
self.update()
[docs] def getColumnKeyByColumnName(self, columnName):
try:
self.column(columnName)
return columnName
except Exception as e:
return FIRST_TREE_COLUMN
[docs] def itemConfig(self, obj, **args):
""" Configure inserted items. """
self.item(obj._treeId, **args)
[docs] def iterSelectedObjects(self):
for treeId in self.selection():
yield self.getObjectFromId(treeId)
[docs] def getSelectedObjects(self):
return [obj for obj in self.iterSelectedObjects()]
[docs] def getObjectFromId(self, treeId):
""" Return the corresponding object from a given Tree item id. """
return self._objDict[treeId]
[docs]class ObjectTreeProvider(TreeProvider):
""" Populate Tree from Objects. """
def __init__(self, objList=None):
TreeProvider.__init__(self)
self.objList = objList
self.getColumns = lambda: [('Object', 300), ('Id', 70), ('Class', 150)]
self._parentDict = {}
[docs] def getObjectInfo(self, obj):
# if obj.isPointer() and not obj.hasValue():
# return None
cls = obj.getClassName()
if obj.getName() is None:
t = cls
else:
t = obj.getName().split('.')[-1]
if t.startswith('__item__'):
t = "%s [%s]" % (cls, t.replace('__item__', ''))
value = obj.get()
if value is None:
if obj.isPointer():
t += " = %s" % str(obj.getObjValue())
else:
t += " = None"
else:
t += " = %s" % str(obj)
info = {'key': obj.getObjId(),
'parent': self._parentDict.get(obj.getObjId(), None),
'text': t, 'values': (obj.strId(), cls)}
# This image step.gif is missing, I guess we are not showing Scalars
# if issubclass(obj.__class__, Scalar):
# info['image'] = 'step.gif'
return info
[docs] def getObjectPreview(self, obj):
return None, None
[docs] def getObjectActions(self, obj):
return []
def _getObjectList(self):
"""Retrieve the object list"""
return self.objList
[docs] def getObjects(self):
objList = self._getObjectList()
self._parentDict = {}
childs = []
for obj in objList:
childs += self._getChilds(obj)
objList += childs
return objList
def _getChilds(self, obj):
childs = []
grandchilds = []
for a, v in obj.getAttributesToStore():
childs.append(v)
self._parentDict[v.getObjId()] = obj
grandchilds += self._getChilds(v)
childs += grandchilds
return childs
[docs]class DbTreeProvider(ObjectTreeProvider):
"""Retrieve the elements from the database"""
def __init__(self, dbName, classesDict):
ObjectTreeProvider.__init__(self)
self.mapper = SqliteMapper(dbName, classesDict)
def _getObjectList(self):
return self.mapper.selectAll()
[docs]class ProjectRunsTreeProvider(TreeProvider):
""" Provide run list from a project
to populate a tree.
"""
ID_COLUMN = 'Id'
RUN_COLUMN = 'Run'
STATE_COLUMN = 'State'
TIME_COLUMN = 'Time'
def __init__(self, project, **kwargs):
TreeProvider.__init__(self, sortingColumnName=ProjectRunsTreeProvider.ID_COLUMN)
self.project = project
self._objDict = {}
self._refresh = True
self._checkPids = False
[docs] def setRefresh(self, value):
self._refresh = value
[docs] def getObjects(self):
runs = self.project.getRuns(refresh=self._refresh,
checkPids=self._checkPids)
# Sort objects
runs.sort(key=self.runsKey, reverse=not self.isSortingAscending())
return runs
[docs] def runsKey(self, run):
sortDict = {ProjectRunsTreeProvider.ID_COLUMN: 'getObjId',
ProjectRunsTreeProvider.TIME_COLUMN: 'getElapsedTime',
ProjectRunsTreeProvider.RUN_COLUMN: 'getRunName',
ProjectRunsTreeProvider.STATE_COLUMN: 'getStatusMessage'}
return getattr(run, sortDict.get(self._sortingColumnName))()
[docs] def getColumns(self):
return [(ProjectRunsTreeProvider.ID_COLUMN, 5),
(ProjectRunsTreeProvider.RUN_COLUMN, 300),
(ProjectRunsTreeProvider.STATE_COLUMN, 50),
(ProjectRunsTreeProvider.TIME_COLUMN, 50)]
[docs] def getObjectInfo(self, obj):
objId = obj.getObjId()
self._objDict[objId] = obj
info = {'key': objId, 'text': objId,
'values': (obj.getRunName(), obj.getStatusMessage(),
prettyDelta(obj.getElapsedTime()))
}
objPid = obj.getObjParentId()
if objPid in self._objDict:
info['parent'] = self._objDict[objPid]
return info
[docs] def getObjectFromId(self, objId):
return self._objDict[objId]
[docs]class ListTreeProvider(TreeProvider):
""" Simple list tree provider. """
def __init__(self, objList=None):
TreeProvider.__init__(self)
self.objList = objList
self.getColumns = lambda: [('Object', 150)]
self.getObjects = lambda: self.objList
[docs] def getObjectInfo(self, obj):
info = {'key': obj.getObjId(), 'text': self.getText(obj), 'values': ()}
return info
[docs] def getText(self, obj):
""" Get the text to display for an object. """
index, fn = obj.getLocation()
name = os.path.basename(fn)
if index:
name = "%03d@%s" % (index, name)
return name
[docs] def getObjs(self):
""" Get the objects. """
return self.objList
[docs]class ListTreeProviderString(ListTreeProvider):
[docs] def getText(self, obj):
return obj.get()
[docs]class ListTreeProviderTemplate(ListTreeProviderString):
def __init__(self, objList=None):
TreeProvider.__init__(self)
self.objList = objList
self.getColumns = lambda: [('Template', 250),
('Description', 800)]
self.getObjects = lambda: self.objList
[docs] def getObjectInfo(self, obj):
info = {'key': obj.getObjId(), 'text': self.getText(obj), 'values': self.getValues(obj)}
return info
[docs] def getText(self, obj):
return obj.source + '-' + obj.name
[docs] def getValues(self, obj):
return (obj.description,)
[docs]class AttributesTreeProvider(ListTreeProviderString):
def __init__(self, item):
TreeProvider.__init__(self)
self.objList = self._attributesToObjectList(item)
self.getColumns = lambda: [('attribute', 250),
('value', 125)]
self.getObjects = lambda: self.objList
def _attributesToObjectList(self, item):
""" Returns a list of all available attributes ready as a list of objects for the tree"""
objList = []
for key, attr in item.getAttributesToStore():
clone = attr.clone()
clone.attrName = key
objList.append(clone)
return objList
[docs] def getObjectInfo(self, obj):
info = {'key': obj.attrName, 'text': self.getText(obj), 'values': self.getValues(obj)}
return info
[docs] def getText(self, obj):
return obj.attrName
[docs] def getValues(self, obj):
return (obj.get(),)