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

gui: add WaiterOverlay. To display processing wheel on top of another widget #3876

Merged
merged 36 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
973cda5
gui: add WaiterOverlay. To dispaly processing wheel on top of another…
payno Jun 22, 2023
3c09d9f
rework WaiterOverlay with @vallsv.
payno Jun 23, 2023
7a17112
_PlotWithWaitingLabel: remove deprecation as this class is still conv…
payno Jun 26, 2023
1615ac9
WaiterOverlay: stop waiting before closing
payno Jul 10, 2023
120bff1
clean
payno Jul 10, 2023
7e5d120
remove _PlotWithWaitingLabel and incorporate it in `ImageStack`
payno Jul 10, 2023
bd552ce
WaiterOverlay: replace `underlying_widget` by object parent
payno Jul 10, 2023
be0bef8
make WaiterOverlay inherit from QWidget instead of QObject
payno Jul 11, 2023
30d9e93
WaiterOverlay: use plot when posible
payno Jul 11, 2023
77f8afc
WaiterOverlay: redefine `setParent`
payno Jul 11, 2023
810d485
WaiterOverlay: fix parenting since is a QWidget
payno Jul 20, 2023
2d8cb40
waiteroverlay: fix - QPoint expects ints
payno Jul 20, 2023
50e9a94
Update src/silx/gui/plot/ImageStack.py
payno Jul 21, 2023
911a63a
Update src/silx/gui/plot/ImageStack.py
payno Jul 21, 2023
407575f
Update src/silx/gui/utils/waiteroverlay.py
payno Jul 21, 2023
58ddb8e
Update src/silx/gui/utils/waiteroverlay.py
payno Jul 21, 2023
0ef0c81
Update src/silx/gui/utils/waiteroverlay.py
payno Jul 21, 2023
94fccca
ImageStack: fix `isAutoResetZoom` since _PlotWithWaitingLabel has bee…
payno Jul 21, 2023
4c9cd06
WaiterOverlay: add an example with a QFrame
payno Jul 21, 2023
ea37ebc
Update src/silx/gui/utils/waiteroverlay.py
payno Jul 21, 2023
c5afb73
WaiterOverlay: move it to from silx.gui.utils to silx.gui.widgets
payno Jul 21, 2023
0b95d5f
Refactor parent handling, add support of backend update, use visibili…
t20100 Jul 20, 2023
415db4d
Rename module to WaiterOverlay
t20100 Jul 21, 2023
7248525
move WaiterOverlay test to silx.gui.widgets.test
t20100 Jul 21, 2023
5b6fac4
remove useless change of visibility
t20100 Jul 21, 2023
8c8f0e9
update WaiterOverlay test
t20100 Jul 21, 2023
06c00d1
rename WaiterOverlay -> WaitingOverlay
t20100 Jul 21, 2023
9a3640c
Add text getter and a test
t20100 Jul 21, 2023
88bfea5
fix ImageStack: WaiterOverlay has been renamed WaitingOverlay
payno Aug 28, 2023
94134fc
WaitingWidget: fix some usage of setWaiting when now 'setvisible' mus…
payno Aug 28, 2023
94627da
WatingOverlay: by default: make the waiting button wait on constructi…
payno Aug 28, 2023
ee6ac1a
WaitingOverlay: expose `setIconSize`
payno Oct 24, 2023
d0cab26
examples: add an example of usage for the `WaitingOverlay` widget
payno Oct 24, 2023
2a5007a
silx.gui.plot.ImageStack: increase waiting overlay icon size
payno Oct 24, 2023
9c1ea99
Update examples/waiterOverlay.py
payno Oct 25, 2023
f7642d1
Update examples/waiterOverlay.py
payno Oct 25, 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
56 changes: 56 additions & 0 deletions examples/waiterOverlay.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import numpy.random
from silx.gui import qt
from silx.gui.widgets.WaitingOverlay import WaitingOverlay
from silx.gui.plot import Plot2D


class MyMainWindow(qt.QMainWindow):
"""
Dummy demonstration window that create an image in a thread and update the plot
"""
payno marked this conversation as resolved.
Show resolved Hide resolved

WAITING_TIME = 2000 # ms

def __init__(self, parent=None):
super().__init__(parent)

# central plot
self._plot = Plot2D()
self._waitingOverlay = WaitingOverlay(self._plot)
self.setCentralWidget(self._plot)

# button to trigger image generation
self._rightPanel = qt.QWidget(self)
self._rightPanel.setLayout(qt.QVBoxLayout())
self._button = qt.QPushButton("generate image", self)
self._rightPanel.layout().addWidget(self._button)

self._dockWidget = qt.QDockWidget()
self._dockWidget.setWidget(self._rightPanel)
self.addDockWidget(qt.Qt.RightDockWidgetArea, self._dockWidget)

# set up
self._waitingOverlay.hide()
self._waitingOverlay.setIconSize(qt.QSize(60, 60))
# connect signal / slot
self._button.released.connect(self._triggerImageCalculation)
payno marked this conversation as resolved.
Show resolved Hide resolved

def _generateRandomData(self):
self.setData(numpy.random.random(1000 * 500).reshape((1000, 500)))
self._button.setEnabled(True)

def setData(self, data):
self._plot.addImage(data)
self._waitingOverlay.hide()

def _triggerImageCalculation(self):
self._plot.clear()
self._button.setEnabled(False)
self._waitingOverlay.show()
qt.QTimer.singleShot(self.WAITING_TIME, self._generateRandomData)


qapp = qt.QApplication([])
window = MyMainWindow()
window.show()
qapp.exec_()
119 changes: 18 additions & 101 deletions src/silx/gui/plot/ImageStack.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,106 +28,18 @@
__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 silx.gui.widgets.FrameBrowser import HorizontalSliderWithBrowser
import time
import threading
import typing
import logging
from silx.gui.widgets.WaitingOverlay import WaitingOverlay

_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)
Expand Down Expand Up @@ -283,10 +195,13 @@ def __init__(self, parent=None) -> None:
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.setWindowTitle("Image stack")
self.setCentralWidget(self._plot)

Expand All @@ -311,6 +226,7 @@ def __init__(self, parent=None) -> None:

def close(self) -> bool:
self._freeLoadingThreads()
self._waitingOverlay.close()
self._plot.close()
super(ImageStack, self).close()

Expand Down Expand Up @@ -345,7 +261,7 @@ def getPlotWidget(self) -> Plot2D:
: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"""
Expand Down Expand Up @@ -395,7 +311,8 @@ def _urlLoaded(self) -> None:
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)
Expand Down Expand Up @@ -581,10 +498,12 @@ def setCurrentUrl(self, url: typing.Union[DataUrl, str]) -> None:
self._plot.clear()
else:
if self._current_url.path() in self._urlData:
self._plot.setData(self._urlData[url.path()])
self._waitingOverlay.setVisible(False)
self._plot.addImage(self._urlData[url.path()], resetzoom=self._autoResetZoom)
else:
self._plot.clear()
payno marked this conversation as resolved.
Show resolved Hide resolved
self._load(url)
self._notifyLoading()
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)
Expand Down Expand Up @@ -617,22 +536,20 @@ def _urlsToIndex(urls):
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:
payno marked this conversation as resolved.
Show resolved Hide resolved
"""

:return: True if a reset is done when the image change
:rtype: bool
"""
return self._plot.isAutoResetZoom()
return self._autoResetZoom
109 changes: 109 additions & 0 deletions src/silx/gui/widgets/WaitingOverlay.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import weakref
from typing import Optional
from silx.gui.widgets.WaitingPushButton import WaitingPushButton
from silx.gui import qt
from silx.gui.qt import inspect as qt_inspect
from silx.gui.plot import PlotWidget


class WaitingOverlay(qt.QWidget):
"""Widget overlaying another widget with a processing wheel icon.

:param parent: widget on top of which to display the "processing/waiting wheel"
"""

def __init__(self, parent: qt.QWidget) -> None:
super().__init__(parent)
self.setContentsMargins(0, 0, 0, 0)

self._waitingButton = WaitingPushButton(self)
self._waitingButton.setDown(True)
self._waitingButton.setWaiting(True)
self._waitingButton.setStyleSheet("QPushButton { background-color: rgba(150, 150, 150, 40); border: 0px; border-radius: 10px; }")
self._registerParent(parent)

def text(self) -> str:
"""Returns displayed text"""
return self._waitingButton.text()

def setText(self, text: str):
"""Set displayed text"""
self._waitingButton.setText(text)
self._resize()

def _listenedWidget(self, parent: qt.QWidget) -> qt.QWidget:
"""Returns widget to register event filter to according to parent"""
if isinstance(parent, PlotWidget):
return parent.getWidgetHandle()
return parent

def _backendChanged(self):
self._listenedWidget(self.parent()).installEventFilter(self)
self._resizeLater()

def _registerParent(self, parent: Optional[qt.QWidget]):
if parent is None:
return
self._listenedWidget(parent).installEventFilter(self)
if isinstance(parent, PlotWidget):
parent.sigBackendChanged.connect(self._backendChanged)
self._resize()

def _unregisterParent(self, parent: Optional[qt.QWidget]):
if parent is None:
return
if isinstance(parent, PlotWidget):
parent.sigBackendChanged.disconnect(self._backendChanged)
self._listenedWidget(parent).removeEventFilter(self)

def setParent(self, parent: qt.QWidget):
self._unregisterParent(self.parent())
super().setParent(parent)
self._registerParent(parent)

def showEvent(self, event: qt.QShowEvent):
super().showEvent(event)
self._waitingButton.setVisible(True)

def hideEvent(self, event: qt.QHideEvent):
super().hideEvent(event)
self._waitingButton.setVisible(False)

def _resize(self):
if not qt_inspect.isValid(self):
return # For _resizeLater in case the widget has been deleted

parent = self.parent()
if parent is None:
return

size = self._waitingButton.sizeHint()
if isinstance(parent, PlotWidget):
offset = parent.getWidgetHandle().mapTo(parent, qt.QPoint(0, 0))
left, top, width, height = parent.getPlotBoundsInPixels()
rect = qt.QRect(
qt.QPoint(
int(offset.x() + left + width / 2 - size.width() / 2),
int(offset.y() + top + height / 2 - size.height() / 2),
),
size,
)
else:
position = parent.size()
position = (position - size) / 2
rect = qt.QRect(qt.QPoint(position.width(), position.height()), size)
self.setGeometry(rect)
self.raise_()

def _resizeLater(self):
qt.QTimer.singleShot(0, self._resize)

def eventFilter(self, watched: qt.QWidget, event: qt.QEvent):
if event.type() == qt.QEvent.Resize:
self._resize()
self._resizeLater() # Defer resize for the receiver to have handled it
payno marked this conversation as resolved.
Show resolved Hide resolved
return super().eventFilter(watched, event)

# expose Waiting push button API
def setIconSize(self, size):
self._waitingButton.setIconSize(size)
31 changes: 31 additions & 0 deletions src/silx/gui/widgets/test/test_waitingoverlay.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import pytest
from silx.gui import qt
from silx.gui.widgets.WaitingOverlay import WaitingOverlay
from silx.gui.plot import Plot2D
from silx.gui.plot.PlotWidget import PlotWidget


@pytest.mark.parametrize("widget_parent", (Plot2D, qt.QFrame))
def test_show(qapp, qapp_utils, widget_parent):
"""Simple test of the WaitingOverlay component"""
widget = widget_parent()
widget.setAttribute(qt.Qt.WA_DeleteOnClose)

waitingOverlay = WaitingOverlay(widget)
waitingOverlay.setAttribute(qt.Qt.WA_DeleteOnClose)

widget.show()
qapp_utils.qWaitForWindowExposed(widget)
assert waitingOverlay._waitingButton.isWaiting()

waitingOverlay.setText("test")
qapp.processEvents()
assert waitingOverlay.text() == "test"
qapp_utils.qWait(1000)

waitingOverlay.hide()
qapp.processEvents()

widget.close()
waitingOverlay.close()
qapp.processEvents()