summaryrefslogtreecommitdiff
path: root/src/silx/gui/plot/ImageStack.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/silx/gui/plot/ImageStack.py')
-rw-r--r--src/silx/gui/plot/ImageStack.py348
1 files changed, 146 insertions, 202 deletions
diff --git a/src/silx/gui/plot/ImageStack.py b/src/silx/gui/plot/ImageStack.py
index e2bed9d..175d6e4 100644
--- a/src/silx/gui/plot/ImageStack.py
+++ b/src/silx/gui/plot/ImageStack.py
@@ -1,6 +1,6 @@
# /*##########################################################################
#
-# Copyright (c) 2020-2021 European Synchrotron Radiation Facility
+# Copyright (c) 2020-2023 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
@@ -23,118 +23,35 @@
# ###########################################################################*/
"""Image stack view with data prefetch capabilty."""
+from __future__ import annotations
+
__authors__ = ["H. Payno"]
__license__ = "MIT"
__date__ = "04/03/2019"
-from silx.gui import icons, qt
+from silx.gui import qt
from silx.gui.plot import Plot2D
-from silx.gui.utils import concurrent
from silx.io.url import DataUrl
from silx.io.utils import get_data
-from collections import OrderedDict
from silx.gui.widgets.FrameBrowser import HorizontalSliderWithBrowser
-import time
-import threading
+from silx.gui.widgets.UrlList import UrlList
+from silx.gui.utils import blockSignals
+from silx.utils.deprecation import deprecated
+
import typing
import logging
+from silx.gui.widgets.WaitingOverlay import WaitingOverlay
+from collections.abc import Iterable
_logger = logging.getLogger(__name__)
-class _PlotWithWaitingLabel(qt.QWidget):
- """Image plot widget with an overlay 'waiting' status.
- """
-
- class AnimationThread(threading.Thread):
- def __init__(self, label):
- self.running = True
- self._label = label
- self.animated_icon = icons.getWaitIcon()
- self.animated_icon.register(self._label)
- super(_PlotWithWaitingLabel.AnimationThread, self).__init__()
-
- def run(self):
- while self.running:
- time.sleep(0.05)
- icon = self.animated_icon.currentIcon()
- self.future_result = concurrent.submitToQtMainThread(
- self._label.setPixmap, icon.pixmap(30, state=qt.QIcon.On))
-
- def stop(self):
- """Stop the update thread"""
- if self.running:
- self.animated_icon.unregister(self._label)
- self.running = False
- self.join(2)
-
- def __init__(self, parent):
- super(_PlotWithWaitingLabel, self).__init__(parent=parent)
- self._autoResetZoom = True
- layout = qt.QStackedLayout(self)
- layout.setStackingMode(qt.QStackedLayout.StackAll)
-
- self._waiting_label = qt.QLabel(parent=self)
- self._waiting_label.setAlignment(qt.Qt.AlignHCenter | qt.Qt.AlignVCenter)
- layout.addWidget(self._waiting_label)
-
- self._plot = Plot2D(parent=self)
- layout.addWidget(self._plot)
-
- self.updateThread = _PlotWithWaitingLabel.AnimationThread(self._waiting_label)
- self.updateThread.start()
-
- def close(self) -> bool:
- super(_PlotWithWaitingLabel, self).close()
- self.stopUpdateThread()
-
- def stopUpdateThread(self):
- self.updateThread.stop()
-
- def setAutoResetZoom(self, reset):
- """
- Should we reset the zoom when adding an image (eq. when browsing)
-
- :param bool reset:
- """
- self._autoResetZoom = reset
- if self._autoResetZoom:
- self._plot.resetZoom()
-
- def isAutoResetZoom(self):
- """
-
- :return: True if a reset is done when the image change
- :rtype: bool
- """
- return self._autoResetZoom
-
- def setWaiting(self, activate=True):
- if activate is True:
- self._plot.clear()
- self._waiting_label.show()
- else:
- self._waiting_label.hide()
-
- def setData(self, data):
- self.setWaiting(activate=False)
- self._plot.addImage(data=data, resetzoom=self._autoResetZoom)
-
- def clear(self):
- self._plot.clear()
- self.setWaiting(False)
-
- def getPlotWidget(self):
- return self._plot
-
-
class _HorizontalSlider(HorizontalSliderWithBrowser):
-
sigCurrentUrlIndexChanged = qt.Signal(int)
def __init__(self, parent):
- super(_HorizontalSlider, self).__init__(parent=parent)
+ super().__init__(parent=parent)
# connect signal / slot
self.valueChanged.connect(self._urlChanged)
@@ -146,67 +63,23 @@ class _HorizontalSlider(HorizontalSliderWithBrowser):
self.sigCurrentUrlIndexChanged.emit(value)
-class UrlList(qt.QWidget):
- """List of URLs the user to select an URL"""
-
- sigCurrentUrlChanged = qt.Signal(str)
- """Signal emitted when the active/current url change"""
-
- def __init__(self, parent=None):
- super(UrlList, self).__init__(parent)
- self.setLayout(qt.QVBoxLayout())
- self.layout().setSpacing(0)
- self.layout().setContentsMargins(0, 0, 0, 0)
- self._listWidget = qt.QListWidget(parent=self)
- self.layout().addWidget(self._listWidget)
-
- # connect signal / Slot
- self._listWidget.currentItemChanged.connect(self._notifyCurrentUrlChanged)
-
- # expose API
- self.currentItem = self._listWidget.currentItem
-
- def setUrls(self, urls: list) -> None:
- url_names = []
- [url_names.append(url.path()) for url in urls]
- self._listWidget.addItems(url_names)
-
- def _notifyCurrentUrlChanged(self, current, previous):
- if current is None:
- pass
- else:
- self.sigCurrentUrlChanged.emit(current.text())
-
- def setUrl(self, url: DataUrl) -> None:
- assert isinstance(url, DataUrl)
- sel_items = self._listWidget.findItems(url.path(), qt.Qt.MatchExactly)
- if sel_items is None:
- _logger.warning(url.path(), ' is not registered in the list.')
- elif len(sel_items) > 0:
- item = sel_items[0]
- self._listWidget.setCurrentItem(item)
- self.sigCurrentUrlChanged.emit(item.text())
-
- def clear(self):
- self._listWidget.clear()
-
-
class _ToggleableUrlSelectionTable(qt.QWidget):
-
_BUTTON_ICON = qt.QStyle.SP_ToolBarHorizontalExtensionButton # noqa
sigCurrentUrlChanged = qt.Signal(str)
"""Signal emitted when the active/current url change"""
+ sigUrlRemoved = qt.Signal(str)
+
def __init__(self, parent=None) -> None:
- qt.QWidget.__init__(self, parent)
+ super().__init__(parent)
self.setLayout(qt.QGridLayout())
self._toggleButton = qt.QPushButton(parent=self)
self.layout().addWidget(self._toggleButton, 0, 2, 1, 1)
- self._toggleButton.setSizePolicy(qt.QSizePolicy.Fixed,
- qt.QSizePolicy.Fixed)
+ self._toggleButton.setSizePolicy(qt.QSizePolicy.Fixed, qt.QSizePolicy.Fixed)
self._urlsTable = UrlList(parent=self)
+
self.layout().addWidget(self._urlsTable, 1, 1, 1, 2)
# set up
@@ -214,12 +87,8 @@ class _ToggleableUrlSelectionTable(qt.QWidget):
# Signal / slot connection
self._toggleButton.clicked.connect(self.toggleUrlSelectionTable)
- self._urlsTable.sigCurrentUrlChanged.connect(self._propagateSignal)
-
- # expose API
- self.setUrls = self._urlsTable.setUrls
- self.setUrl = self._urlsTable.setUrl
- self.currentItem = self._urlsTable.currentItem
+ self._urlsTable.sigCurrentUrlChanged.connect(self.sigCurrentUrlChanged)
+ self._urlsTable.sigUrlRemoved.connect(self.sigUrlRemoved)
def toggleUrlSelectionTable(self):
visible = not self.urlSelectionTableIsVisible()
@@ -236,21 +105,36 @@ class _ToggleableUrlSelectionTable(qt.QWidget):
self._toggleButton.setIcon(icon)
def urlSelectionTableIsVisible(self):
- return self._urlsTable.isVisible()
-
- def _propagateSignal(self, url):
- self.sigCurrentUrlChanged.emit(url)
+ return self._urlsTable.isVisibleTo(self)
def clear(self):
self._urlsTable.clear()
+ # expose UrlList API
+ @deprecated(replacement="addUrls", since_version="2.0")
+ def setUrls(self, urls: Iterable[DataUrl]):
+ self._urlsTable.addUrls(urls=urls)
+
+ def addUrls(self, urls: Iterable[DataUrl]):
+ self._urlsTable.addUrls(urls=urls)
+
+ def setUrl(self, url: typing.Optional[DataUrl]):
+ self._urlsTable.setUrl(url=url)
+
+ def removeUrl(self, url: str):
+ self._urlsTable.removeUrl(url)
+
+ def currentItem(self):
+ return self._urlsTable.currentItem()
+
class UrlLoader(qt.QThread):
"""
Thread use to load DataUrl
"""
+
def __init__(self, parent, url):
- super(UrlLoader, self).__init__(parent=parent)
+ super().__init__(parent=parent)
assert isinstance(url, DataUrl)
self.url = url
self.data = None
@@ -277,17 +161,21 @@ class ImageStack(qt.QMainWindow):
"""Signal emitted when the current url change"""
def __init__(self, parent=None) -> None:
- super(ImageStack, self).__init__(parent)
+ super().__init__(parent)
self.__n_prefetch = ImageStack.N_PRELOAD
self._loadingThreads = []
self.setWindowFlags(qt.Qt.Widget)
self._current_url = None
self._url_loader = UrlLoader
"class to instantiate for loading urls"
+ self._autoResetZoom = True
# main widget
- self._plot = _PlotWithWaitingLabel(parent=self)
+ self._plot = Plot2D(parent=self)
self._plot.setAttribute(qt.Qt.WA_DeleteOnClose, True)
+ self._waitingOverlay = WaitingOverlay(self._plot)
+ self._waitingOverlay.setIconSize(qt.QSize(30, 30))
+ self._waitingOverlay.hide()
self.setWindowTitle("Image stack")
self.setCentralWidget(self._plot)
@@ -308,12 +196,14 @@ class ImageStack(qt.QMainWindow):
# connect signal / slot
self._urlsTable.sigCurrentUrlChanged.connect(self.setCurrentUrl)
+ self._urlsTable.sigUrlRemoved.connect(self.removeUrl)
self._slider.sigCurrentUrlIndexChanged.connect(self.setCurrentUrlIndex)
def close(self) -> bool:
self._freeLoadingThreads()
+ self._waitingOverlay.close()
self._plot.close()
- super(ImageStack, self).close()
+ super().close()
def setUrlLoaderClass(self, urlLoader: typing.Type[UrlLoader]) -> None:
"""
@@ -346,14 +236,14 @@ class ImageStack(qt.QMainWindow):
:return: PlotWidget contained in this window
:rtype: Plot2D
"""
- return self._plot.getPlotWidget()
+ return self._plot
def reset(self) -> None:
"""Clear the plot and remove any link to url"""
self._freeLoadingThreads()
self._urls = None
self._urlIndexes = None
- self._urlData = OrderedDict({})
+ self._urlData = {}
self._current_url = None
self._plot.clear()
self._urlsTable.clear()
@@ -396,7 +286,8 @@ class ImageStack(qt.QMainWindow):
if url in self._urlIndexes:
self._urlData[url] = sender.data
if self.getCurrentUrl().path() == url:
- self._plot.setData(self._urlData[url])
+ self._waitingOverlay.setVisible(False)
+ self._plot.addImage(self._urlData[url], resetzoom=self._autoResetZoom)
if sender in self._loadingThreads:
self._loadingThreads.remove(sender)
self.sigLoaded.emit(url)
@@ -421,6 +312,29 @@ class ImageStack(qt.QMainWindow):
"""
return self.__n_prefetch
+ def setUrlsEditable(self, editable: bool):
+ self._urlsTable._urlsTable.setEditable(editable)
+ if editable:
+ selection_mode = qt.QAbstractItemView.ExtendedSelection
+ else:
+ selection_mode = qt.QAbstractItemView.SingleSelection
+ self._urlsTable._urlsTable.setSelectionMode(selection_mode)
+
+ @staticmethod
+ def createUrlIndexes(urls: tuple):
+ indexes = {}
+ for index, url in enumerate(urls):
+ assert isinstance(
+ url, DataUrl
+ ), f"url is expected to be a DataUrl. Get {type(url)}"
+ indexes[index] = url
+ return indexes
+
+ def _resetSlider(self):
+ with blockSignals(self._slider):
+ self._slider.setMinimum(0)
+ self._slider.setMaximum(len(self._urls) - 1)
+
def setUrls(self, urls: list) -> None:
"""list of urls within an index. Warning: urls should contain an image
compatible with the silx.gui.plot.Plot class
@@ -429,26 +343,16 @@ class ImageStack(qt.QMainWindow):
(position in the stack), value is the DataUrl
:type: list
"""
- def createUrlIndexes():
- indexes = OrderedDict()
- for index, url in enumerate(urls):
- indexes[index] = url
- return indexes
-
- urls_with_indexes = createUrlIndexes()
+ urls_with_indexes = self.createUrlIndexes(urls=urls)
urlsToIndex = self._urlsToIndex(urls_with_indexes)
self.reset()
self._urls = urls_with_indexes
self._urlIndexes = urlsToIndex
- old_url_table = self._urlsTable.blockSignals(True)
- self._urlsTable.setUrls(urls=list(self._urls.values()))
- self._urlsTable.blockSignals(old_url_table)
+ with blockSignals(self._urlsTable):
+ self._urlsTable.addUrls(urls=list(self._urls.values()))
- old_slider = self._slider.blockSignals(True)
- self._slider.setMinimum(0)
- self._slider.setMaximum(len(self._urls) - 1)
- self._slider.blockSignals(old_slider)
+ self._resetSlider()
if self.getCurrentUrl() in self._urls:
self.setCurrentUrl(self.getCurrentUrl())
@@ -457,6 +361,35 @@ class ImageStack(qt.QMainWindow):
first_url = self._urls[list(self._urls.keys())[0]]
self.setCurrentUrl(first_url)
+ def removeUrl(self, url: str) -> None:
+ """
+ Remove provided URL from the table
+
+ :param url: URL as str
+ """
+ # remove the given urls from self._urls and self._urlIndexes
+ if not isinstance(url, str):
+ raise TypeError("url is expected to be the str representation of the url")
+
+ # try to get reset the url displayed
+ current_url = self.getCurrentUrl()
+ with blockSignals(self._urlsTable):
+ self._urlsTable.removeUrl(url)
+ # update urls
+ urls_with_indexes = self.createUrlIndexes(
+ filter(
+ lambda a: a.path() != url,
+ self._urls.values(),
+ )
+ )
+ urlsToIndex = self._urlsToIndex(urls_with_indexes)
+ self._urls = urls_with_indexes
+ self._urlIndexes = urlsToIndex
+ self._resetSlider()
+
+ if current_url != url:
+ self.setCurrentUrl(current_url)
+
def getUrls(self) -> tuple:
"""
@@ -555,41 +488,46 @@ class ImageStack(qt.QMainWindow):
if self._urls is None:
return
elif index >= len(self._urls):
- raise ValueError('requested index out of bounds')
+ raise ValueError("requested index out of bounds")
else:
return self.setCurrentUrl(self._urls[index])
- def setCurrentUrl(self, url: typing.Union[DataUrl, str]) -> None:
+ def setCurrentUrl(self, url: typing.Optional[typing.Union[DataUrl, str]]) -> None:
"""
Define the url to be displayed
:param url: url to be displayed
:type: DataUrl
+ :raises KeyError: raised if the url is not know
"""
- assert isinstance(url, (DataUrl, str))
- if isinstance(url, str):
+ assert isinstance(url, (DataUrl, str, type(None)))
+ if url == "":
+ url = None
+ elif isinstance(url, str):
url = DataUrl(path=url)
- if url != self._current_url:
+ if url is not None and url != self._current_url:
self._current_url = url
self.sigCurrentUrlChanged.emit(url.path())
- old_url_table = self._urlsTable.blockSignals(True)
- old_slider = self._slider.blockSignals(True)
-
- self._urlsTable.setUrl(url)
- self._slider.setUrlIndex(self._urlIndexes[url.path()])
- if self._current_url is None:
- self._plot.clear()
- else:
- if self._current_url.path() in self._urlData:
- self._plot.setData(self._urlData[url.path()])
- else:
- self._load(url)
- self._notifyLoading()
- self._preFetch(self._getNNextUrls(self.__n_prefetch, url))
- self._preFetch(self._getNPreviousUrls(self.__n_prefetch, url))
- self._urlsTable.blockSignals(old_url_table)
- self._slider.blockSignals(old_slider)
+ with blockSignals(self._urlsTable):
+ with blockSignals(self._slider):
+ self._urlsTable.setUrl(url)
+ if url is not None:
+ self._slider.setUrlIndex(self._urlIndexes[url.path()])
+ if self._current_url is None:
+ self._plot.clear()
+ else:
+ if self._current_url.path() in self._urlData:
+ self._waitingOverlay.setVisible(False)
+ self._plot.addImage(
+ self._urlData[url.path()], resetzoom=self._autoResetZoom
+ )
+ else:
+ self._plot.clear()
+ self._load(url)
+ self._waitingOverlay.setVisible(True)
+ self._preFetch(self._getNNextUrls(self.__n_prefetch, url))
+ self._preFetch(self._getNPreviousUrls(self.__n_prefetch, url))
def getCurrentUrl(self) -> typing.Union[None, DataUrl]:
"""
@@ -618,17 +556,15 @@ class ImageStack(qt.QMainWindow):
res[url.path()] = index
return res
- def _notifyLoading(self):
- """display a simple image of loading..."""
- self._plot.setWaiting(activate=True)
-
def setAutoResetZoom(self, reset):
"""
Should we reset the zoom when adding an image (eq. when browsing)
:param bool reset:
"""
- self._plot.setAutoResetZoom(reset)
+ self._autoResetZoom = reset
+ if self._autoResetZoom:
+ self._plot.resetZoom()
def isAutoResetZoom(self) -> bool:
"""
@@ -636,4 +572,12 @@ class ImageStack(qt.QMainWindow):
:return: True if a reset is done when the image change
:rtype: bool
"""
- return self._plot.isAutoResetZoom()
+ return self._autoResetZoom
+
+ def getWaiterOverlay(self):
+ """
+
+ :return: Return the instance of `WaitingOverlay` used to display if processing or not
+ :rtype: WaitingOverlay
+ """
+ return self._waitingOverlay