diff options
Diffstat (limited to 'silx/gui/widgets/ThreadPoolPushButton.py')
-rw-r--r-- | silx/gui/widgets/ThreadPoolPushButton.py | 233 |
1 files changed, 233 insertions, 0 deletions
diff --git a/silx/gui/widgets/ThreadPoolPushButton.py b/silx/gui/widgets/ThreadPoolPushButton.py new file mode 100644 index 0000000..29e831d --- /dev/null +++ b/silx/gui/widgets/ThreadPoolPushButton.py @@ -0,0 +1,233 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 2016 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. +# +# ###########################################################################*/ +"""ThreadPoolPushButton module +""" + +__authors__ = ["V. Valls"] +__license__ = "MIT" +__date__ = "13/10/2016" + +import logging +from .. import qt +from .WaitingPushButton import WaitingPushButton + + +_logger = logging.getLogger(__name__) + + +class _Wrapper(qt.QRunnable): + """Wrapper to allow to call a function into a `QThreadPool` and + sending signals during the life cycle of the object""" + + def __init__(self, signalHolder, function, args, kwargs): + """Constructor""" + super(_Wrapper, self).__init__() + self.__signalHolder = signalHolder + self.__callable = function + self.__args = args + self.__kwargs = kwargs + + def run(self): + holder = self.__signalHolder + holder.started.emit() + try: + result = self.__callable(*self.__args, **self.__kwargs) + holder.succeeded.emit(result) + except Exception as e: + module = self.__callable.__module__ + name = self.__callable.__name__ + _logger.error("Error while executing callable %s.%s.", module, name, exc_info=True) + holder.failed.emit(e) + finally: + holder.finished.emit() + holder._sigReleaseRunner.emit(self) + + def autoDelete(self): + """Returns true to ask the QThreadPool to manage the life cycle of + this QRunner.""" + return True + + +class ThreadPoolPushButton(WaitingPushButton): + """ + ThreadPoolPushButton provides a simple push button to execute + a threaded task with user feedback when the task is running. + + The task can be defined with the method `setCallable`. It takes a python + function and arguments as parameters. + + WARNING: This task is run in a separate thread. + + Everytime the button is pushed a new runner is created to execute the + function with defined arguments. An animated waiting icon is displayed + to show the activity. By default the button is disabled when an execution + is requested. This behaviour can be disabled by using + `setDisabledWhenWaiting`. + + When the button is clicked a `beforeExecuting` signal is sent from the + Qt main thread. Then the task is started in a thread pool and the following + signals are emitted from the thread pool. Right before calling the + registered callable, the widget emits a `started` signal. + When the task ends, its result is emitted by the `succeeded` signal, but + if it fails the signal `failed` is emitted with the resulting exception. + At the end, the `finished` signal is emitted. + + The task can be programatically executed by using `executeCallable`. + + >>> # Compute a value + >>> import math + >>> button = ThreadPoolPushButton(text="Compute 2^16") + >>> button.setCallable(math.pow, 2, 16) + >>> button.succeeded.connect(print) # python3 + + >>> # Compute a wrong value + >>> import math + >>> button = ThreadPoolPushButton(text="Compute sqrt(-1)") + >>> button.setCallable(math.sqrt, -1) + >>> button.failed.connect(print) # python3 + """ + + def __init__(self, parent=None, text=None, icon=None): + """Constructor + + :param str text: Text displayed on the button + :param qt.QIcon icon: Icon displayed on the button + :param qt.QWidget parent: Parent of the widget + """ + WaitingPushButton.__init__(self, parent=parent, text=text, icon=icon) + self.__callable = None + self.__args = None + self.__kwargs = None + self.__runnerCount = 0 + self.__runnerSet = set([]) + self.clicked.connect(self.executeCallable) + self.finished.connect(self.__runnerFinished) + self._sigReleaseRunner.connect(self.__releaseRunner) + + beforeExecuting = qt.Signal() + """Signal emitted just before execution of the callable by the main Qt + thread. In synchronous mode (direct mode), it can be used to define + dynamically `setCallable`, or to execute something in the Qt thread before + the execution, or both.""" + + started = qt.Signal() + """Signal emitted from the thread pool when the defined callable is + started. + + WARNING: This signal is emitted from the thread performing the task, and + might be received after the registered callable has been called. If you + want to perform some initialisation or set the callable to run, use the + `beforeExecuting` signal instead. + """ + + finished = qt.Signal() + """Signal emitted from the thread pool when the defined callable is + finished""" + + succeeded = qt.Signal(object) + """Signal emitted from the thread pool when the callable exit with a + success. + + The parameter of the signal is the result returned by the callable. + """ + + failed = qt.Signal(object) + """Signal emitted emitted from the thread pool when the callable raises an + exception. + + The parameter of the signal is the raised exception. + """ + + _sigReleaseRunner = qt.Signal(object) + """Callback to release runners""" + + def __runnerStarted(self): + """Called when a runner is started. + + Count the number of executed tasks to change the state of the widget. + """ + self.__runnerCount += 1 + if self.__runnerCount > 0: + self.wait() + + def __runnerFinished(self): + """Called when a runner is finished. + + Count the number of executed tasks to change the state of the widget. + """ + self.__runnerCount -= 1 + if self.__runnerCount <= 0: + self.stopWaiting() + + @qt.Slot() + def executeCallable(self): + """Execute the defined callable in QThreadPool. + + First emit a `beforeExecuting` signal. + If callable is not defined, nothing append. + If a callable is defined, it will be started + as a new thread using the `QThreadPool` system. At start of the thread + the `started` will be emitted. When the callable returns a result it + is emitted by the `succeeded` signal. If the callable fail, the signal + `failed` is emitted with the resulting exception. Then the `finished` + signal is emitted. + """ + self.beforeExecuting.emit() + if self.__callable is None: + return + self.__runnerStarted() + runner = self._createRunner(self.__callable, self.__args, self.__kwargs) + qt.QThreadPool.globalInstance().start(runner) + self.__runnerSet.add(runner) + + def __releaseRunner(self, runner): + self.__runnerSet.remove(runner) + + def _createRunner(self, function, args, kwargs): + """Create a QRunnable from a callable object. + + :param callable function: A callable Python object. + :param list args: List of arguments to call the function. + :param dict kwargs: Dictionary of arguments used to call the function. + :rtpye: qt.QRunnable + """ + runnable = _Wrapper(self, function, args, kwargs) + return runnable + + def setCallable(self, function, *args, **kwargs): + """Define a callable which will be executed on QThreadPool everytime + the button is clicked. + + To retrieve the results, connect to the `succeeded` signal. + + WARNING: The callable will be called in a separate thread. + + :param callable function: A callable Python object + :param list args: List of arguments to call the function. + :param dict kwargs: Dictionary of arguments used to call the function. + """ + self.__callable = function + self.__args = args + self.__kwargs = kwargs |