class DataPanel(qt.QWidget): def __init__(self, parent=None, context=None): qt.QWidget.__init__(self, parent=parent) self.__customNxdataItem = None self.__dataTitle = _HeaderLabel(self) self.__dataTitle.setVisible(False) self.__dataViewer = DataViewerFrame(self) self.__dataViewer.setGlobalHooks(context) layout = qt.QVBoxLayout(self) layout.setSpacing(0) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.__dataTitle) layout.addWidget(self.__dataViewer) def getData(self): return self.__dataViewer.data() def getCustomNxdataItem(self): return self.__customNxdataItem def setData(self, data): self.__customNxdataItem = None self.__dataViewer.setData(data) self.__dataTitle.setVisible(data is not None) if data is not None: self.__dataTitle.setVisible(True) if hasattr(data, "name"): if hasattr(data, "file"): filename = str(data.file.filename) else: filename = "" path = data.name else: filename = "" path = "" self.__dataTitle.setData(filename, path) def setCustomDataItem(self, item): self.__customNxdataItem = item if item is not None: data = item.getVirtualGroup() else: data = None self.__dataViewer.setData(data) self.__dataTitle.setVisible(item is not None) if item is not None: text = item.text() self.__dataTitle.setText(text) def removeDatasetsFrom(self, root): """ Remove all datasets provided by this root .. note:: This function do not update data stored inside customNxdataItem cause in the silx-view context this item is already updated on his own. :param root: The root file of datasets to remove """ data = self.__dataViewer.data() if data is not None: if data.file is not None: # That's an approximation, IS can't be used as h5py generates # To objects for each requests to a node if data.file.filename == root.file.filename: self.__dataViewer.setData(None) def replaceDatasetsFrom(self, removedH5, loadedH5): """ Replace any dataset from any NXdata items using the same dataset name from another root. Usually used when a file was synchronized. .. note:: This function do not update data stored inside customNxdataItem cause in the silx-view context this item is already updated on his own. :param removedRoot: The h5py root file which is replaced (which have to be removed) :param loadedRoot: The new h5py root file which have to be used instread. """ data = self.__dataViewer.data() if data is not None: if data.file is not None: if data.file.filename == removedH5.file.filename: # Try to synchonize the viewed data try: # TODO: It have to update the data without changing the # view which is not so easy newData = loadedH5[data.name] self.__dataViewer.setData(newData) except Exception: _logger.debug("Backtrace", exc_info=True)
class Viewer(qt.QMainWindow): """ This window allows to browse a data file like images or HDF5 and it's content. """ def __init__(self): """ :param files_: List of HDF5 or Spec files (pathes or :class:`silx.io.spech5.SpecH5` or :class:`h5py.File` instances) """ # Import it here to be sure to use the right logging level import silx.gui.hdf5 from silx.gui.data.DataViewerFrame import DataViewerFrame qt.QMainWindow.__init__(self) self.setWindowTitle("Silx viewer") self.__context = ApplicationContext(self) self.__context.restoreLibrarySettings() self.__asyncload = False self.__dialogState = None self.__treeview = silx.gui.hdf5.Hdf5TreeView(self) """Silx HDF5 TreeView""" # Custom the model to be able to manage the life cycle of the files treeModel = silx.gui.hdf5.Hdf5TreeModel(self.__treeview, ownFiles=False) treeModel.sigH5pyObjectLoaded.connect(self.__h5FileLoaded) treeModel.sigH5pyObjectRemoved.connect(self.__h5FileRemoved) treeModel.sigH5pyObjectSynchronized.connect(self.__h5FileSynchonized) treeModel2 = silx.gui.hdf5.NexusSortFilterProxyModel(self.__treeview) treeModel2.setSourceModel(treeModel) self.__treeview.setModel(treeModel2) self.__dataViewer = DataViewerFrame(self) self.__dataViewer.setGlobalHooks(self.__context) vSpliter = qt.QSplitter(qt.Qt.Vertical) vSpliter.addWidget(self.__dataViewer) vSpliter.setSizes([10, 0]) spliter = qt.QSplitter(self) spliter.addWidget(self.__treeview) spliter.addWidget(vSpliter) spliter.setStretchFactor(1, 1) self.__spliter = spliter main_panel = qt.QWidget(self) layout = qt.QVBoxLayout() layout.addWidget(spliter) layout.setStretchFactor(spliter, 1) main_panel.setLayout(layout) self.setCentralWidget(main_panel) model = self.__treeview.selectionModel() model.selectionChanged.connect(self.displayData) self.__treeview.addContextMenuCallback( self.closeAndSyncCustomContextMenu) treeModel = self.__treeview.findHdf5TreeModel() columns = list(treeModel.COLUMN_IDS) columns.remove(treeModel.DESCRIPTION_COLUMN) columns.remove(treeModel.NODE_COLUMN) self.__treeview.header().setSections(columns) self._iconUpward = icons.getQIcon('plot-yup') self._iconDownward = icons.getQIcon('plot-ydown') self.createActions() self.createMenus() self.__context.restoreSettings() def __h5FileLoaded(self, loadedH5): self.__context.pushRecentFile(loadedH5.file.filename) def __h5FileRemoved(self, removedH5): data = self.__dataViewer.data() if data is not None: if data.file.filename == removedH5.file.filename: self.__dataViewer.setData(None) removedH5.close() def __h5FileSynchonized(self, removedH5, loadedH5): data = self.__dataViewer.data() if data is not None: if data.file.filename == removedH5.file.filename: # Try to synchonize the viewed data try: # TODO: It have to update the data without changing the view # which is not so easy newData = loadedH5[data.name] self.__dataViewer.setData(newData) except Exception: _logger.debug("Backtrace", exc_info=True) removedH5.close() def closeEvent(self, event): self.__context.saveSettings() def saveSettings(self, settings): """Save the window settings to this settings object :param qt.QSettings settings: Initialized settings """ isFullScreen = bool(self.windowState() & qt.Qt.WindowFullScreen) if isFullScreen: # show in normal to catch the normal geometry self.showNormal() settings.beginGroup("mainwindow") settings.setValue("size", self.size()) settings.setValue("pos", self.pos()) settings.setValue("full-screen", isFullScreen) settings.endGroup() settings.beginGroup("mainlayout") settings.setValue("spliter", self.__spliter.sizes()) settings.endGroup() if isFullScreen: self.showFullScreen() def restoreSettings(self, settings): """Restore the window settings using this settings object :param qt.QSettings settings: Initialized settings """ settings.beginGroup("mainwindow") size = settings.value("size", qt.QSize(640, 480)) pos = settings.value("pos", qt.QPoint()) isFullScreen = settings.value("full-screen", False) if not isinstance(isFullScreen, bool): isFullScreen = False settings.endGroup() settings.beginGroup("mainlayout") try: data = settings.value("spliter") data = [int(d) for d in data] self.__spliter.setSizes(data) except Exception: _logger.debug("Backtrace", exc_info=True) settings.endGroup() if not pos.isNull(): self.move(pos) if not size.isNull(): self.resize(size) if isFullScreen: self.showFullScreen() def createActions(self): action = qt.QAction("E&xit", self) action.setShortcuts(qt.QKeySequence.Quit) action.setStatusTip("Exit the application") action.triggered.connect(self.close) self._exitAction = action action = qt.QAction("&Open...", self) action.setStatusTip("Open a file") action.triggered.connect(self.open) self._openAction = action action = qt.QAction("Open Recent", self) action.setStatusTip("Open a recently openned file") action.triggered.connect(self.open) self._openRecentAction = action action = qt.QAction("&About", self) action.setStatusTip("Show the application's About box") action.triggered.connect(self.about) self._aboutAction = action # Plot backend action = qt.QAction("Plot rendering backend", self) action.setStatusTip("Select plot rendering backend") self._plotBackendSelection = action menu = qt.QMenu() action.setMenu(menu) group = qt.QActionGroup(self) group.setExclusive(True) action = qt.QAction("matplotlib", self) action.setStatusTip("Plot will be rendered using matplotlib") action.setCheckable(True) action.triggered.connect(self.__forceMatplotlibBackend) group.addAction(action) menu.addAction(action) self._usePlotWithMatplotlib = action action = qt.QAction("OpenGL", self) action.setStatusTip("Plot will be rendered using OpenGL") action.setCheckable(True) action.triggered.connect(self.__forceOpenglBackend) group.addAction(action) menu.addAction(action) self._usePlotWithOpengl = action # Plot image orientation action = qt.QAction("Default plot image y-axis orientation", self) action.setStatusTip( "Select the default y-axis orientation used by plot displaying images" ) self._plotImageOrientation = action menu = qt.QMenu() action.setMenu(menu) group = qt.QActionGroup(self) group.setExclusive(True) action = qt.QAction("Downward, origin on top", self) action.setIcon(self._iconDownward) action.setStatusTip( "Plot images will use a downward Y-axis orientation") action.setCheckable(True) action.triggered.connect(self.__forcePlotImageDownward) group.addAction(action) menu.addAction(action) self._useYAxisOrientationDownward = action action = qt.QAction("Upward, origin on bottom", self) action.setIcon(self._iconUpward) action.setStatusTip("Plot images will use a upward Y-axis orientation") action.setCheckable(True) action.triggered.connect(self.__forcePlotImageUpward) group.addAction(action) menu.addAction(action) self._useYAxisOrientationUpward = action def __updateFileMenu(self): files = self.__context.getRecentFiles() self._openRecentAction.setEnabled(len(files) != 0) menu = None if len(files) != 0: menu = qt.QMenu() for filePath in files: baseName = os.path.basename(filePath) action = qt.QAction(baseName, self) action.setToolTip(filePath) action.triggered.connect( functools.partial(self.__openRecentFile, filePath)) menu.addAction(action) menu.addSeparator() baseName = os.path.basename(filePath) action = qt.QAction("Clear history", self) action.setToolTip("Clear the history of the recent files") action.triggered.connect(self.__clearRecentFile) menu.addAction(action) self._openRecentAction.setMenu(menu) def __clearRecentFile(self): self.__context.clearRencentFiles() def __openRecentFile(self, filePath): self.appendFile(filePath) def __updateOptionMenu(self): """Update the state of the checked options as it is based on global environment values.""" # plot backend action = self._plotBackendSelection title = action.text().split(": ", 1)[0] action.setText("%s: %s" % (title, silx.config.DEFAULT_PLOT_BACKEND)) action = self._usePlotWithMatplotlib action.setChecked( silx.config.DEFAULT_PLOT_BACKEND in ["matplotlib", "mpl"]) title = action.text().split(" (", 1)[0] if not action.isChecked(): title += " (applied after application restart)" action.setText(title) action = self._usePlotWithOpengl action.setChecked(silx.config.DEFAULT_PLOT_BACKEND in ["opengl", "gl"]) title = action.text().split(" (", 1)[0] if not action.isChecked(): title += " (applied after application restart)" action.setText(title) # plot orientation action = self._plotImageOrientation if silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION == "downward": action.setIcon(self._iconDownward) else: action.setIcon(self._iconUpward) action.setIconVisibleInMenu(True) action = self._useYAxisOrientationDownward action.setChecked( silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION == "downward") title = action.text().split(" (", 1)[0] if not action.isChecked(): title += " (applied after application restart)" action.setText(title) action = self._useYAxisOrientationUpward action.setChecked( silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION != "downward") title = action.text().split(" (", 1)[0] if not action.isChecked(): title += " (applied after application restart)" action.setText(title) def createMenus(self): fileMenu = self.menuBar().addMenu("&File") fileMenu.addAction(self._openAction) fileMenu.addAction(self._openRecentAction) fileMenu.addSeparator() fileMenu.addAction(self._exitAction) fileMenu.aboutToShow.connect(self.__updateFileMenu) optionMenu = self.menuBar().addMenu("&Options") optionMenu.addAction(self._plotImageOrientation) optionMenu.addAction(self._plotBackendSelection) optionMenu.aboutToShow.connect(self.__updateOptionMenu) helpMenu = self.menuBar().addMenu("&Help") helpMenu.addAction(self._aboutAction) def open(self): dialog = self.createFileDialog() if self.__dialogState is None: currentDirectory = os.getcwd() dialog.setDirectory(currentDirectory) else: dialog.restoreState(self.__dialogState) result = dialog.exec_() if not result: return self.__dialogState = dialog.saveState() filenames = dialog.selectedFiles() for filename in filenames: self.appendFile(filename) def createFileDialog(self): dialog = qt.QFileDialog(self) dialog.setWindowTitle("Open") dialog.setModal(True) # NOTE: hdf5plugin have to be loaded before import silx.io extensions = collections.OrderedDict() for description, ext in silx.io.supported_extensions().items(): extensions[description] = " ".join(sorted(list(ext))) # NOTE: hdf5plugin have to be loaded before import fabio if fabio is not None: extensions["NeXus layout from EDF files"] = "*.edf" extensions["NeXus layout from TIFF image files"] = "*.tif *.tiff" extensions["NeXus layout from CBF files"] = "*.cbf" extensions["NeXus layout from MarCCD image files"] = "*.mccd" all_supported_extensions = set() for name, exts in extensions.items(): exts = exts.split(" ") all_supported_extensions.update(exts) all_supported_extensions = sorted(list(all_supported_extensions)) filters = [] filters.append("All supported files (%s)" % " ".join(all_supported_extensions)) for name, extension in extensions.items(): filters.append("%s (%s)" % (name, extension)) filters.append("All files (*)") dialog.setNameFilters(filters) dialog.setFileMode(qt.QFileDialog.ExistingFiles) return dialog def about(self): from .About import About About.about(self, "Silx viewer") def __forcePlotImageDownward(self): silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION = "downward" def __forcePlotImageUpward(self): silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION = "upward" def __forceMatplotlibBackend(self): silx.config.DEFAULT_PLOT_BACKEND = "matplotlib" def __forceOpenglBackend(self): silx.config.DEFAULT_PLOT_BACKEND = "opengl" def appendFile(self, filename): self.__treeview.findHdf5TreeModel().appendFile(filename) def displayData(self): """Called to update the dataviewer with the selected data. """ selected = list( self.__treeview.selectedH5Nodes(ignoreBrokenLinks=False)) if len(selected) == 1: # Update the viewer for a single selection data = selected[0] self.__dataViewer.setData(data) def useAsyncLoad(self, useAsync): self.__asyncload = useAsync def closeAndSyncCustomContextMenu(self, event): """Called to populate the context menu :param silx.gui.hdf5.Hdf5ContextMenuEvent event: Event containing expected information to populate the context menu """ selectedObjects = event.source().selectedH5Nodes( ignoreBrokenLinks=False) menu = event.menu() if not menu.isEmpty(): menu.addSeparator() # Import it here to be sure to use the right logging level import h5py for obj in selectedObjects: if obj.ntype is h5py.File: action = qt.QAction("Remove %s" % obj.local_filename, event.source()) action.triggered.connect( lambda: self.__treeview.findHdf5TreeModel( ).removeH5pyObject(obj.h5py_object)) menu.addAction(action) action = qt.QAction("Synchronize %s" % obj.local_filename, event.source()) action.triggered.connect( lambda: self.__treeview.findHdf5TreeModel( ).synchronizeH5pyObject(obj.h5py_object)) menu.addAction(action)