# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # # ###########################################################################*/ """ This module define model and widget to display 1D slices from numpy array using compound data types or hdf5 databases. """ from __future__ import division __authors__ = ["V. Valls"] __license__ = "MIT" __date__ = "07/04/2017" import functools import os.path import logging from silx.gui import qt import silx.io from .TextFormatter import TextFormatter import silx.gui.hdf5 from silx.gui.widgets import HierarchicalTableView _logger = logging.getLogger(__name__) class _CellData(object): """Store a table item """ def __init__(self, value=None, isHeader=False, span=None): """ Constructor :param str value: Label of this property :param bool isHeader: True if the cell is an header :param tuple span: Tuple of row, column span """ self.__value = value self.__isHeader = isHeader self.__span = span def isHeader(self): """Returns true if the property is a sub-header title. :rtype: bool """ return self.__isHeader def value(self): """Returns the value of the item. """ return self.__value def span(self): """Returns the span size of the cell. :rtype: tuple """ return self.__span class _TableData(object): """Modelize a table with header, row and column span. It is mostly defined as a row based table. """ def __init__(self, columnCount): """Constructor. :param int columnCount: Define the number of column of the table """ self.__colCount = columnCount self.__data = [] def rowCount(self): """Returns the number of rows. :rtype: int """ return len(self.__data) def columnCount(self): """Returns the number of columns. :rtype: int """ return self.__colCount def clear(self): """Remove all the cells of the table""" self.__data = [] def cellAt(self, row, column): """Returns the cell at the row column location. Else None if there is nothing. :rtype: _CellData """ if row < 0: return None if column < 0: return None if row >= len(self.__data): return None cells = self.__data[row] if column >= len(cells): return None return cells[column] def addHeaderRow(self, headerLabel): """Append the table with header on the full row. :param str headerLabel: label of the header. """ item = _CellData(value=headerLabel, isHeader=True, span=(1, self.__colCount)) self.__data.append([item]) def addHeaderValueRow(self, headerLabel, value): """Append the table with a row using the first column as an header and other cells as a single cell for the value. :param str headerLabel: label of the header. :param object value: value to store. """ header = _CellData(value=headerLabel, isHeader=True) value = _CellData(value=value, span=(1, self.__colCount)) self.__data.append([header, value]) def addRow(self, *args): """Append the table with a row using arguments for each cells :param list[object] args: List of cell values for the row """ row = [] for value in args: if not isinstance(value, _CellData): value = _CellData(value=value) row.append(value) self.__data.append(row) class Hdf5TableModel(HierarchicalTableView.HierarchicalTableModel): """This data model provides access to HDF5 node content (File, Group, Dataset). Main info, like name, file, attributes... are displayed """ def __init__(self, parent=None, data=None): """ Constructor :param qt.QObject parent: Parent object :param object data: An h5py-like object (file, group or dataset) """ super(Hdf5TableModel, self).__init__(parent) self.__obj = None self.__data = _TableData(columnCount=4) self.__formatter = None formatter = TextFormatter(self) self.setFormatter(formatter) self.setObject(data) def rowCount(self, parent_idx=None): """Returns number of rows to be displayed in table""" return self.__data.rowCount() def columnCount(self, parent_idx=None): """Returns number of columns to be displayed in table""" return self.__data.columnCount() def data(self, index, role=qt.Qt.DisplayRole): """QAbstractTableModel method to access data values in the format ready to be displayed""" if not index.isValid(): return None cell = self.__data.cellAt(index.row(), index.column()) if cell is None: return None if role == self.SpanRole: return cell.span() elif role == self.IsHeaderRole: return cell.isHeader() elif role == qt.Qt.DisplayRole: value = cell.value() if callable(value): value = value(self.__obj) return str(value) return None def flags(self, index): """QAbstractTableModel method to inform the view whether data is editable or not. """ return qt.QAbstractTableModel.flags(self, index) def isSupportedObject(self, h5pyObject): """ Returns true if the provided object can be modelized using this model. """ isSupported = False isSupported = isSupported or silx.io.is_group(h5pyObject) isSupported = isSupported or silx.io.is_dataset(h5pyObject) isSupported = isSupported or isinstance(h5pyObject, silx.gui.hdf5.H5Node) return isSupported def setObject(self, h5pyObject): """Set the h5py-like object exposed by the model :param h5pyObject: A h5py-like object. It can be a `h5py.Dataset`, a `h5py.File`, a `h5py.Group`. It also can be a, `silx.gui.hdf5.H5Node` which is needed to display some local path information. """ if qt.qVersion() > "4.6": self.beginResetModel() if h5pyObject is None or self.isSupportedObject(h5pyObject): self.__obj = h5pyObject else: _logger.warning("Object class %s unsupported. Object ignored.", type(h5pyObject)) self.__initProperties() if qt.qVersion() > "4.6": self.endResetModel() else: self.reset() def __initProperties(self): """Initialize the list of available properties according to the defined h5py-like object.""" self.__data.clear() if self.__obj is None: return obj = self.__obj hdf5obj = obj if isinstance(obj, silx.gui.hdf5.H5Node): hdf5obj = obj.h5py_object if silx.io.is_file(hdf5obj): objectType = "File" elif silx.io.is_group(hdf5obj): objectType = "Group" elif silx.io.is_dataset(hdf5obj): objectType = "Dataset" else: objectType = obj.__class__.__name__ self.__data.addHeaderRow(headerLabel="HDF5 %s" % objectType) self.__data.addHeaderRow(headerLabel="Path info") self.__data.addHeaderValueRow("basename", lambda x: os.path.basename(x.name)) self.__data.addHeaderValueRow("name", lambda x: x.name) if silx.io.is_file(obj): self.__data.addHeaderValueRow("filename", lambda x: x.filename) if isinstance(obj, silx.gui.hdf5.H5Node): # helpful informations if the object come from an HDF5 tree self.__data.addHeaderValueRow("local_basename", lambda x: x.local_basename) self.__data.addHeaderValueRow("local_name", lambda x: x.local_name) self.__data.addHeaderValueRow("local_filename", lambda x: x.local_file.filename) if hasattr(obj, "dtype"): self.__data.addHeaderRow(headerLabel="Data info") self.__data.addHeaderValueRow("dtype", lambda x: x.dtype) if hasattr(obj, "shape"): self.__data.addHeaderValueRow("shape", lambda x: x.shape) if hasattr(obj, "size"): self.__data.addHeaderValueRow("size", lambda x: x.size) if hasattr(obj, "chunks") and obj.chunks is not None: self.__data.addHeaderValueRow("chunks", lambda x: x.chunks) # relative to compression # h5py expose compression, compression_opts but are not initialized # for external plugins, then we use id # h5py also expose fletcher32 and shuffle attributes, but it is also # part of the filters if hasattr(obj, "shape") and hasattr(obj, "id"): dcpl = obj.id.get_create_plist() if dcpl.get_nfilters() > 0: self.__data.addHeaderRow(headerLabel="Compression info") pos = _CellData(value="Position", isHeader=True) hdf5id = _CellData(value="HDF5 ID", isHeader=True) name = _CellData(value="Name", isHeader=True) options = _CellData(value="Options", isHeader=True) self.__data.addRow(pos, hdf5id, name, options) for index in range(dcpl.get_nfilters()): callback = lambda index, dataIndex, x: self.__get_filter_info(x, index)[dataIndex] pos = _CellData(value=functools.partial(callback, index, 0)) hdf5id = _CellData(value=functools.partial(callback, index, 1)) name = _CellData(value=functools.partial(callback, index, 2)) options = _CellData(value=functools.partial(callback, index, 3)) self.__data.addRow(pos, hdf5id, name, options) if hasattr(obj, "attrs"): if len(obj.attrs) > 0: self.__data.addHeaderRow(headerLabel="Attributes") for key in sorted(obj.attrs.keys()): callback = lambda key, x: self.__formatter.toString(x.attrs[key]) self.__data.addHeaderValueRow(headerLabel=key, value=functools.partial(callback, key)) def __get_filter_info(self, dataset, filterIndex): """Get a tuple of readable info from dataset filters :param h5py.Dataset dataset: A h5py dataset :param int filterId: """ try: dcpl = dataset.id.get_create_plist() info = dcpl.get_filter(filterIndex) filterId, _flags, cdValues, name = info name = self.__formatter.toString(name) options = " ".join([self.__formatter.toString(i) for i in cdValues]) return (filterIndex, filterId, name, options) except Exception: _logger.debug("Backtrace", exc_info=True) return [filterIndex, None, None, None] def object(self): """Returns the internal object modelized. :rtype: An h5py-like object """ return self.__obj def setFormatter(self, formatter): """Set the formatter object to be used to display data from the model :param TextFormatter formatter: Formatter to use """ if formatter is self.__formatter: return if qt.qVersion() > "4.6": self.beginResetModel() if self.__formatter is not None: self.__formatter.formatChanged.disconnect(self.__formatChanged) self.__formatter = formatter if self.__formatter is not None: self.__formatter.formatChanged.connect(self.__formatChanged) if qt.qVersion() > "4.6": self.endResetModel() else: self.reset() def getFormatter(self): """Returns the text formatter used. :rtype: TextFormatter """ return self.__formatter def __formatChanged(self): """Called when the format changed. """ self.reset() class Hdf5TableView(HierarchicalTableView.HierarchicalTableView): """A widget to display metadata about a HDF5 node using a table.""" def __init__(self, parent=None): super(Hdf5TableView, self).__init__(parent) self.setModel(Hdf5TableModel(self)) def isSupportedData(self, data): """ Returns true if the provided object can be modelized using this model. """ return self.model().isSupportedObject(data) def setData(self, data): """Set the h5py-like object exposed by the model :param h5pyObject: A h5py-like object. It can be a `h5py.Dataset`, a `h5py.File`, a `h5py.Group`. It also can be a, `silx.gui.hdf5.H5Node` which is needed to display some local path information. """ self.model().setObject(data) header = self.horizontalHeader() if qt.qVersion() < "5.0": setResizeMode = header.setResizeMode else: setResizeMode = header.setSectionResizeMode setResizeMode(0, qt.QHeaderView.Fixed) setResizeMode(1, qt.QHeaderView.Stretch) header.setStretchLastSection(True)