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 all 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
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()
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)
Loading