# -*- coding: utf-8 -*-
# gui/bases/datalist.py
from __future__ import absolute_import # PEP328
from __future__ import division
from past.utils import old_div
from builtins import str
from builtins import range
import os.path
import logging
import collections
import sys
try:
# make all modifications to sys (below) local to this module
# Python 2 needed this for some reason ...
reload(sys)
sys.setdefaultencoding('utf8')
except NameError:
pass
from time import time as timestamp
from gui.utils.signal import Signal
from utils.error import EmptySelection
from gui.utils.displayexception import DisplayException
from bases.dataset import DataSet, DisplayMixin
from utils import isList, isMap, isString
from utils.lastpath import LastPath
from gui.utils.translate import tr
from gui.qt import QtCore, QtGui
from QtCore import Qt, QMetaObject
from QtGui import (QWidget, QAction, QTreeWidget, QTreeWidgetItem,
QVBoxLayout, QPushButton, QAbstractItemView, QKeySequence)
from gui.bases.mixins.dropwidget import DropWidget
from gui.bases.mixins.contextmenuwidget import ContextMenuWidget
from gui.bases.mixins.titlehandler import TitleHandler
# alternative implementation to DataItem._store which fails
class __ItemStore__(object):
"""Actually stores the data objects whereas the UI widgets store the key
(object ID) of the data objects only.
This will be reworked or removed once we implement an non-UI queue."""
_store = None
@classmethod
def store(cls, key, value):
if cls._store is None:
cls._store = dict()
cls._store[key] = value
@classmethod
def clear(cls, key):
if cls._store is None:
return
del cls._store[key]
@classmethod
def get(cls, key):
return cls._store.get(key, None)
[docs]class DataItem(QTreeWidgetItem):
"""Generates a QTreeWidgetItem from arbitrary python objects.
Storing those objects separately."""
_isRemovable = None
@staticmethod
[docs] def hash32(data):
"""Avoids OverFlowError at setData() with PySide on MacOS."""
return hash(data) & 0x7fffffff
@property
def isRemovable(self):
return bool(self._isRemovable)
def __init__(self, data):
super(DataItem, self).__init__()
self.setChildIndicatorPolicy(
QTreeWidgetItem.DontShowIndicatorWhenChildless)
assert isinstance(data, DisplayMixin)
self._isRemovable = data.isRemovable
self.setData(0, Qt.UserRole, self.hash32(data))
# this fails magically with Python 3.4 on Ubuntu 14.04, default packages
# print(0, DataItem._store)
# DataItem._store = 2314
# print(1, DataItem._store) # seems it is set later on, some delay?
__ItemStore__.store(self.dataId(), data)
self.update()
[docs] def update(self):
"""Updates this item according to eventually changed data object"""
data = self.data() # forced to be DataSet in __init__
columnCount = len(data.displayData)
for column in range(0, columnCount):
columnData = data.displayData[column]
if not isList(columnData): # columnData is supposed to be tuple
columnData = (columnData, )
for attrname in columnData: # set attributes of columns if avail
if not isString(attrname):
continue
value = getattr(data, attrname, None)
if value is None:
continue
getProperty, setProperty, value = self.getItemProperty(value)
# set it always, regardless if needed
setProperty(column, value)
# adjust #table columns
treeWidget = self.treeWidget()
if treeWidget is not None and treeWidget.columnCount() < columnCount:
treeWidget.setColumnCount(columnCount)
# update children
for item in self.takeChildren(): # remove all children
item.remove()
del item
[docs] def getItemProperty(self, value):
"""For a value, returns this items getter/setter methods according
to value type."""
getProperty, setProperty = self.text, self.setText
if isinstance(value, bool):
getProperty, setProperty = self.checkState, self.setCheckState
if value:
value = Qt.Checked
else:
value = Qt.Unchecked
elif value is None: # allows not set attrib., removes text from gui
value = ""
else:
value = str(value) # convert numbers eventually
return getProperty, setProperty, value
[docs] def dataId(self):
value = self.data(0, Qt.UserRole)
try:
return value.toPyObject()
except:
return value
[docs] def data(self, *args):
if len(args) > 1:
return super(DataItem, self).data(*args)
return __ItemStore__.get(self.dataId())
[docs] def listIndex(self):
"""
Index of this items top most parent in the treewidget.
"""
item = self
while item.parent() is not None:
item = item.parent()
return self.treeWidget().indexOfTopLevelItem(item)
[docs] def isTopLevelItem(self):
return self.parent() is None
[docs] def remove(self):
"""Removes the item from its treewidget or parent item."""
if self.isTopLevelItem() and self.treeWidget():
self.treeWidget().takeTopLevelItem(self.listIndex())
elif self.parent():
self.parent().removeChild(self)
__ItemStore__.clear(self.dataId()) # remove data object from store
[docs] def setClicked(self, column):
self.clicked = column, timestamp()
[docs] def setChanged(self, column):
self.changed = column, timestamp()
[docs] def wasClickedAndChanged(self):
"""Tests if this item was previously clicked and changed in the UI."""
try:
deltaTs = abs(self.clicked[1] - self.changed[1])
return (self.clicked[0] == self.changed[0] and
deltaTs < 0.1)
except Exception:
pass
return False
[docs] def setAlignment(self, alignment):
if alignment is None:
return
if not isList(alignment):
alignment = [alignment]
for c in range(0, self.columnCount()):
self.setTextAlignment(c, alignment[min(c, len(alignment)-1)])
[docs]class DataList(QWidget, DropWidget, ContextMenuWidget):
"""
Manages all loaded spectra.
>>> from utilsgui import DialogInteraction, DisplayException
>>> from spectralist import SpectraList
>>> sl = DialogInteraction.instance(SpectraList)
Test available actions
>>> [str(action.text()) for action in sl.listWidget.actions()]
['load spectra', 'remove', '', 'save matrices', 'select all']
>>> sl.listWidget.count()
0
Test methods on empty list
>>> sl.updateSpectra()
>>> sl.removeSelectedSpectra()
>>> [sl.getMatrix(i) for i in -1,0,1]
[None, None, None]
>>> DialogInteraction.query(DisplayException, sl.saveMatrix,
... slot = 'accept')
>>> sl.selectionChangedSlot()
"""
sigSelectedData = Signal((object,))
sigUpdatedData = Signal((object,))
sigRemovedData = Signal(list)
sigEmpty = Signal()
sigReceivedUrls = Signal(list)
sigEditingFinished = Signal()
_nestedItems = None # are nested items allowed? (plain list behaviour)
def __init__(self, parent = None, title = None, withBtn = True,
nestedItems = True):
QWidget.__init__(self, parent)
ContextMenuWidget.__init__(self)
self.title = TitleHandler.setup(self, title)
self._nestedItems = nestedItems
self._setupUi(withBtn)
self._setupActions()
self.setupUi()
QMetaObject.connectSlotsByName(self)
def _setupUi(self, withBtn):
self.setObjectName("DataList")
self.setAcceptDrops(True)
self.verticalLayout = QVBoxLayout(self)
self.verticalLayout.setObjectName("verticalLayout")
if withBtn:
self.loadBtn = QPushButton(self)
self.loadBtn.setText(tr("load"))
self.loadBtn.setObjectName("loadBtn")
self.loadBtn.released.connect(self.loadData)
self.verticalLayout.addWidget(self.loadBtn)
self.listWidget = QTreeWidget(self)
self.listWidget.setHeaderHidden(True)
self.listWidget.setContextMenuPolicy(Qt.ActionsContextMenu)
self.listWidget.setEditTriggers(QAbstractItemView.NoEditTriggers)
self.listWidget.setDragEnabled(True)
self.listWidget.setDragDropMode(QAbstractItemView.InternalMove)
self.listWidget.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.listWidget.setObjectName("listWidget")
self.listWidget.itemSelectionChanged.connect(self.selectionChanged)
self.listWidget.itemClicked.connect(self._itemClicked)
self.listWidget.itemChanged.connect(self._itemChanged)
self.listWidget.itemDoubleClicked.connect(self.itemDoubleClicked)
self.listWidget.itemExpanded.connect(self.fitColumnsToContents)
self.listWidget.itemCollapsed.connect(self.fitColumnsToContents)
self.verticalLayout.addWidget(self.listWidget)
self.sigReceivedUrls.connect(self.loadData)
self.clearSelection = self.listWidget.clearSelection
def _setupActions(self):
self.addMenuEntry(
name = "load", text = tr("load %1"), menuStates = "*",
toolTip = tr("Add one or more %1."),
callbacks = self.loadData)
self.addMenuSeparator()
self.addMenuEntry(
name = "remove", text = tr("remove"),
toolTip = tr("Remove selected %1."),
shortCut = QKeySequence.Delete, menuStates = "isRemovableSelected",
callbacks = self.removeSelected)
self.addMenuSeparator("hasSelection")
self.addMenuSeparator("isNotEmpty")
self.addMenuEntry(
name = "selectall", text = tr("select all"),
shortCut = QKeySequence.SelectAll, menuStates = "isNotEmpty",
callbacks = self.selectAll)
self.addMenuEntry(
name = "expandall", text = tr("expand all"),
toolTip = tr("Show nested items of this %1. (double-click)"),
callbacks = self.expandAll,
menuStates = "itemsHaveChildren")
self._updateContextMenu()
def _updateContextMenu(self):
self.updateMenu(self.listWidget)
[docs] def fitColumnsToContents(self, *args):
for c in range(0, self.listWidget.columnCount()):
self.listWidget.resizeColumnToContents(c)
[docs] def setupUi(self):
"""Reimplement this in child classes for custom UI configuration."""
pass
[docs] def leaveEvent(self, event):
self.sigEditingFinished.emit()
[docs] def clear(self):
self.listWidget.clear()
[docs] def selectAll(self):
"""Selects all items in the list if not all are selected.
Clears the selection if all items in the list already are selected.
"""
if (old_div(len(self.listWidget.selectedIndexes()), self.listWidget.columnCount())) == len(self):
self.listWidget.clearSelection()
else:
self.listWidget.selectAll()
[docs] def expandAll(self):
self.listWidget.expandAll()
self.fitColumnsToContents()
def __len__(self):
return self.listWidget.topLevelItemCount()
[docs] def isEmpty(self):
return len(self) <= 0
[docs] def isNotEmpty(self):
return not self.isEmpty()
[docs] def hasSelection(self):
return len(self.listWidget.selectedItems()) > 0
[docs] def isRemovableSelected(self):
"""True, if there is at least one item selected which may be removed"""
return self.hasSelection()
[docs] def itemsHaveChildren(self):
return any([item.childCount() > 0 for item in self.topLevelItems()])
[docs] def updateItems(self):
for item in self.topLevelItems():
item.update()
self.fitColumnsToContents()
def _itemClicked(self, item, column):
item.setClicked(column)
if item.wasClickedAndChanged():
self.itemUpdate(item, column)
def _itemChanged(self, item, column):
item.setChanged(column)
if item.wasClickedAndChanged():
self.itemUpdate(item, column)
[docs] def itemUpdate(self, item, column):
"""Reimplement to update item if changed by user in GUI"""
pass
[docs] def itemDoubleClicked(self, item, column):
pass
[docs] def currentSelection(self):
selected = self.listWidget.selectedItems()
index, data = -1, None
if len(selected) > 0:
index = selected[0].listIndex()
data = selected[0].data()
return index, data
[docs] def selectionChanged(self):
index, data = self.currentSelection()
self.sigSelectedData.emit(data)
self._updateContextMenu()
if self.isEmpty():
self.sigEmpty.emit()
[docs] def removeItems(self, indexList):
"""Deletes items specified in the given list of indices."""
if not isList(indexList) or not len(indexList):
return
removedItems = []
for i in reversed(sorted(indexList)):
item = self.listWidget.topLevelItem(i)
if not item.isRemovable:
continue
item.remove()
removedItems.append(item.data())
self.sigRemovedData.emit(removedItems)
self.selectionChanged()
[docs] def removeSelected(self):
selected = self.listWidget.selectedItems()
index = 0
self.sigRemovedData.emit(self.data(selected))
for item in selected:
if not item.isRemovable:
continue
index = item.listIndex()
item.remove()
# select the next item after the removed ones
self.listWidget.clearSelection()
if index >= len(self):
index = len(self) - 1
if index >= 0:
self.listWidget.setCurrentIndex(
self.listWidget.indexFromItem(
self.listWidget.topLevelItem(index)))
self.selectionChanged()
[docs] def setCurrentIndex(self, index):
if index < 0 or index >= len(self):
return
item = self.listWidget.topLevelItem(index)
index = self.listWidget.indexFromItem(item)
self.listWidget.setCurrentIndex(index)
self.selectionChanged()
[docs] def add(self, data):
if self.isEmpty():
self.setHeader(data.displayDataDescr)
DataItem(data) # WTF? w/o it returns QTreeWidgetItem instead of DataItem below!
self.listWidget.addTopLevelItem(DataItem(data))
item = self.listWidget.topLevelItem(len(self)-1)
if not self._nestedItems:
item.setFlags(int(item.flags()) - int(Qt.ItemIsDropEnabled))
return item
[docs] def topLevelItems(self):
return [self.listWidget.topLevelItem(i)
for i in range(0, len(self))]
[docs] def data(self, indexOrItem = None, selectedOnly = False):
"""
Returns the list of data for a given list index or list widget item.
If none is specified return the data of all items or the data of
selected items only, if desired.
"""
if type(indexOrItem) is int:
if indexOrItem < 0 or indexOrItem >= len(self):
raise IndexError
items = [self.listWidget.topLevelItem(indexOrItem)]
elif type(indexOrItem) is DataItem:
items = [indexOrItem]
elif (isList(indexOrItem) and
len(indexOrItem) > 0 and
type(indexOrItem[0]) is DataItem):
items = indexOrItem
else: # no indexOrItem given
if selectedOnly:
items = self.listWidget.selectedItems()
else:
items = self.topLevelItems()
return [item.data() for item in items]
[docs] def updateData(self, selectedOnly = False, showProgress = True,
updateFunc = None, prepareFunc = None, stopFunc = None,
**kwargs):
"""
Calls the provided function on all data items.
The object returned by prepareFunc() is forwarded as optional argument
to updateFunc(dataItem, optionalArguments = None).
"""
data = self.data(selectedOnly = selectedOnly)
if data is None or len(data) <= 0:
self.sigUpdatedData.emit(None)
return
progress = None
if showProgress:
from gui.utils.progressdialog import ProgressDialog
progress = ProgressDialog(self, count = len(data))
updateResult = []
# check provided stop function
if (stopFunc is not None and
not isinstance(stopFunc, collections.Callable)):
stopFunc = None
# call provided functions which can raise exceptions
errorOccured = False # raise error after processing all items
try:
# call prepare function
prepareResult = None
if prepareFunc is not None:
prepareResult = prepareFunc(**kwargs)
if prepareResult is None:
prepareResult = []
if not isList(prepareResult):
prepareResult = [prepareResult,]
# call update function on each data object
for item in data:
try:
updateResult.append(
updateFunc(item, *prepareResult, **kwargs))
if progress is not None and progress.update():
break
if stopFunc is not None and stopFunc():
break
except Exception as e:
errorOccured = True
import traceback
logging.error(traceback.format_exc())
itemName = str(item)
try:
itemName = item.filename
except AttributeError:
pass
logging.error("Skipping '{}'".format(itemName))
continue
if progress is not None:
progress.close()
except Exception as e:
# progress.cancel()
# catch and display _all_ exceptions in user friendly manner
# DisplayException(e)
errorOccured = True
import traceback
logging.error(traceback.format_exc())
pass
if errorOccured:
self.reraiseLast()
# self.selectionChanged()
self.sigUpdatedData.emit(self.currentSelection()[1])
return updateResult
[docs] def loadData(self, sourceList = None, processSourceFunc = None,
showProgress = True, alignment = None, **kwargs):
"""
Loads a list of data source items.
processSourceFunc is expected to be a function which gets individual
elements of sourceList as argument. It returns an arbitrary data item
which is then added to this data list widget.
Reimplement it in child classes and it will be called on load button
and add action signal.
This method handles exceptions and progress indication.
Test loading a single spectra
>>> import utils
>>> from tests import TestData
>>> from utilsgui import DialogInteraction, UiSettings, fileDialogType
>>> from chemsettings import ChemSettings
>>> from datafiltersgui import DataFiltersGui
>>> from spectralist import SpectraList
>>> cs = DialogInteraction.instance(ChemSettings)
>>> dfg = DialogInteraction.instance(DataFiltersGui)
>>> sl = DialogInteraction.instance(SpectraList, settings = cs)
>>> utils.LastPath.path = TestData.spectra(0)
>>> DialogInteraction.query(fileDialogType(), sl.loadData,
... slot = 'accept')
>>> sl.updateSpectra()
>>> utils.LastPath.path = utils.getTempFileName()
>>> matrixfiles = DialogInteraction.query(fileDialogType(), sl.saveMatrix,
... slot = 'accept')
>>> len(matrixfiles)
1
>>> matrixfiles
Verify written matrix data with existent matrix export
>>> TestData.verifyMatrix(TestData.spectra(0),
... matrixfiles[0])
True
"""
assert processSourceFunc is not None
assert isList(sourceList)
progress = None
if showProgress:
from gui.utils.progressdialog import ProgressDialog
progress = ProgressDialog(self, count = len(sourceList))
self.listWidget.clearSelection()
errorOccured = False
lastItem = None
for sourceItem in sourceList:
data = None
try:
data = processSourceFunc(sourceItem, **kwargs)
except Exception as e:
# progress.cancel()
# DisplayException(e)
# on error, skip the current file
errorOccured = True
logging.error(str(e).replace("\n"," ") + " ... skipping")
continue
if data is not None:
lastItem = self.add(data)
try:
lastItem.setAlignment(alignment)
# sometime PySide return a QTreeWidgetItem instead of a DataItem
except AttributeError: pass
if progress is not None and progress.update():
break
if progress is not None:
progress.close()
self.fitColumnsToContents()
# notify interested widgets about changes
if lastItem is not None:
self.listWidget.setCurrentItem(lastItem)
if errorOccured:
self.reraiseLast()
[docs] def reraiseLast(self):
"""Reraise the last error if any and display an error message dialog.
"""
try:
if sys.exc_info()[0] is not None:
raise
except Exception as e:
DisplayException(e)
if __name__ == "__main__":
import doctest
doctest.testmod()
# vim: set ts=4 sw=4 sts=4 tw=0: