Skip to content

Commit

Permalink
Merge pull request #3876 from silx-kit/fix_3807
Browse files Browse the repository at this point in the history
gui: add WaiterOverlay. To display processing wheel on top of another widget
  • Loading branch information
t20100 authored Nov 21, 2023
2 parents ea03f17 + f7642d1 commit e3b5eab
Show file tree
Hide file tree
Showing 4 changed files with 239 additions and 101 deletions.
81 changes: 81 additions & 0 deletions examples/waiterOverlay.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!/usr/bin/env python
# /*##########################################################################
#
# Copyright (c) 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
# 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.
#
# ###########################################################################*/
"""
Demonstration window that displays a wait icon until the plot is updated
"""

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):

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.clicked.connect(self._triggerImageCalculation)

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()
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:
"""
: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
return super().eventFilter(watched, event)

# expose Waiting push button API
def setIconSize(self, size):
self._waitingButton.setIconSize(size)
Loading

0 comments on commit e3b5eab

Please sign in to comment.