Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UrlList: allow removal of url from the list #3913

Merged
merged 30 commits into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
9f5d57b
ImagStack: upgrade some call to super()
payno Nov 27, 2023
5338afc
UrlList: make it inherit directly from qt.QListWidget instead of QWid…
payno Jul 24, 2023
e0b8b3a
gui: UrlList: add an 'editable mode' to remove some url from the list
payno Nov 27, 2023
6e3c8fc
UrlList: rename 'remove' action to 'Remove'
payno Jul 25, 2023
76deca4
UrlList: add a shortcut to the remove action
payno Jul 25, 2023
cd85c0c
Update src/silx/gui/plot/ImageStack.py
payno Oct 23, 2023
5e488e7
Update src/silx/gui/plot/ImageStack.py
payno Oct 23, 2023
f2cf974
Update src/silx/gui/plot/ImageStack.py
payno Oct 23, 2023
53748ca
Update src/silx/gui/plot/ImageStack.py
payno Oct 23, 2023
d29ce51
Update src/silx/gui/plot/ImageStack.py
payno Oct 23, 2023
0b54d43
Update src/silx/gui/plot/ImageStack.py
payno Oct 23, 2023
7e4dd64
Update src/silx/gui/plot/ImageStack.py
payno Oct 23, 2023
379f4e3
silx.gui.plot.ImageStack.UrlList: fix some doc indentation
payno Oct 24, 2023
c1dc352
silx.gui.plot.ImageStack.UrlList: remove setting selection mode from …
payno Oct 24, 2023
b7a989b
_ToggleableUrlSelectionTable: remove some unused functions
payno Oct 24, 2023
4c72413
silx.gui.plot.Imagestack.UrlList: replace `sigUrlsRemoved` by `sigUrl…
payno Oct 24, 2023
c5c9ddb
silx.gui.plot.ImageStack: adapt to remove a single url.
payno Oct 24, 2023
a47c922
move silx.gui.plot.ImageStack.UrlList to silx.gui.widgets.UrlList
payno Oct 24, 2023
f0d6464
Update src/silx/gui/plot/ImageStack.py
payno Oct 25, 2023
2787f96
Update src/silx/gui/widgets/UrlList.py
payno Oct 25, 2023
09631cd
Update src/silx/gui/widgets/UrlList.py
payno Oct 25, 2023
522a6c0
Update src/silx/gui/widgets/UrlList.py
payno Oct 25, 2023
2ceb86e
Update src/silx/gui/widgets/UrlList.py
payno Oct 25, 2023
2580f59
Update src/silx/gui/widgets/UrlList.py
payno Oct 25, 2023
01d2104
gui: ImageStack/ UrlList: fix typo: logger is used in UrlList now
payno Nov 27, 2023
14a8cc8
fix doc indentation typos
payno Oct 25, 2023
6d7cd50
gui: UrlList: deprecate 'setUrls' in favor of 'addUrls'
payno Oct 25, 2023
688c8bc
_ToggleableUrlSelectionTable: improve '_urlsTable' exposition and dep…
payno Nov 27, 2023
eef4caa
UrlList: remove empty line
payno Oct 25, 2023
31fe5be
silx.gui.plot.ImageStack: add import of annotation from __future__
payno Nov 27, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/imageStack.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ def main():
widget.setNPrefetch(1)
urls = create_datasets(folder=dataset_folder)
widget.setUrls(urls=urls)
widget.setUrlsEditable(True) # allow the user to remove some url from the list
widget.show()
qapp.exec()
widget.close()
Expand Down
209 changes: 113 additions & 96 deletions src/silx/gui/plot/ImageStack.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
# ###########################################################################*/
"""Image stack view with data prefetch capabilty."""

from __future__ import annotations

__authors__ = ["H. Payno"]
__license__ = "MIT"
__date__ = "04/03/2019"
Expand All @@ -33,9 +35,14 @@
from silx.io.url import DataUrl
from silx.io.utils import get_data
from silx.gui.widgets.FrameBrowser import HorizontalSliderWithBrowser
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__)

Expand All @@ -45,7 +52,7 @@ 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)

Expand All @@ -57,80 +64,34 @@ def _urlChanged(self, value):
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._urlsTable = UrlList(parent=self)

self.layout().addWidget(self._urlsTable, 1, 1, 1, 2)

# set up
self._setButtonIcon(show=True)

# 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()
Expand All @@ -149,19 +110,33 @@ def _setButtonIcon(self, show):
def urlSelectionTableIsVisible(self):
return self._urlsTable.isVisibleTo(self)

def _propagateSignal(self, url):
self.sigCurrentUrlChanged.emit(url)

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
Expand All @@ -188,7 +163,7 @@ 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)
Expand Down Expand Up @@ -222,13 +197,14 @@ def __init__(self, parent=None) -> None:

# 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:
"""
Expand Down Expand Up @@ -337,6 +313,27 @@ def getNPrefetch(self) -> int:
"""
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
Expand All @@ -345,26 +342,16 @@ def setUrls(self, urls: list) -> None:
(position in the stack), value is the DataUrl
:type: list
"""
def createUrlIndexes():
indexes = {}
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())
Expand All @@ -373,6 +360,35 @@ def createUrlIndexes():
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:
"""

Expand Down Expand Up @@ -475,39 +491,40 @@ def setCurrentUrlIndex(self, index: int):
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._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))
self._urlsTable.blockSignals(old_url_table)
self._slider.blockSignals(old_slider)
with blockSignals(self._urlsTable):
with blockSignals(self._slider):

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._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]:
"""
Expand Down
Loading