import os import sys import threading from qtpy import QtWidgets as QW from hydrus.core import HydrusData from hydrus.core import HydrusExceptions from hydrus.core import HydrusGlobals as HG from hydrus.client.gui import QtPorting as QP # this does one thing neatly class AsyncQtJob( object ): def __init__( self, win, work_callable, publish_callable, errback_callable = None, errback_ui_cleanup_callable = None ): # ultimate improvement here is to move to QObject/QThread and do the notifications through signals and slots (which will disconnect on object deletion) self._win = win self._work_callable = work_callable self._publish_callable = publish_callable self._errback_callable = errback_callable self._errback_ui_cleanup_callable = errback_ui_cleanup_callable def _DefaultErrback( self, etype, value, tb ): HydrusData.ShowExceptionTuple( etype, value, tb ) message = 'An error occured in a background task. If you had UI waiting on a fetch job, the dialog/panel may need to be closed and re-opened.' message += os.linesep * 2 message += 'The error info will show as a popup and also be printed to log. Hydev may want to know about this error, at least to improve error handling.' message += os.linesep * 2 message += 'Error summary: {}'.format( value ) QW.QMessageBox.warning( self._win, 'Error', message ) if self._errback_ui_cleanup_callable is not None: self._errback_ui_cleanup_callable() def _doWork( self ): def qt_deliver_result( result ): if not QP.isValid( self._win ): return self._publish_callable( result ) try: result = self._work_callable() except Exception as e: ( etype, value, tb ) = sys.exc_info() if self._errback_callable is None: c = self._DefaultErrback else: c = self._errback_callable try: HG.client_controller.CallBlockingToQt( self._win, c, etype, value, tb ) except ( HydrusExceptions.QtDeadWindowException, HydrusExceptions.ShutdownException ): return return try: HG.client_controller.CallBlockingToQt( self._win, qt_deliver_result, result ) except ( HydrusExceptions.QtDeadWindowException, HydrusExceptions.ShutdownException ): return except Exception as e: ( etype, value, tb ) = sys.exc_info() if self._errback_callable is None: c = self._DefaultErrback else: c = self._errback_callable try: HG.client_controller.CallBlockingToQt( self._win, c, etype, value, tb ) except ( HydrusExceptions.QtDeadWindowException, HydrusExceptions.ShutdownException ): return def start( self ): HG.client_controller.CallToThread( self._doWork ) # this can refresh dirty stuff n times and won't spam work class AsyncQtUpdater( object ): def __init__( self, win, loading_callable, work_callable, publish_callable ): # ultimate improvement here is to move to QObject/QThread and do the notifications through signals and slots (which will disconnect on object deletion) self._win = win self._loading_callable = loading_callable self._work_callable = work_callable self._publish_callable = publish_callable self._calllater_waiting = False self._work_needs_to_restart = False self._is_working = False self._lock = threading.Lock() def _doWork( self ): def qt_deliver_result( result ): if self._win is None or not QP.isValid( self._win ): self._win = None return self._publish_callable( result ) with self._lock: self._calllater_waiting = False self._work_needs_to_restart = False self._is_working = True try: result = self._work_callable() try: HG.client_controller.CallBlockingToQt( self._win, qt_deliver_result, result ) except ( HydrusExceptions.QtDeadWindowException, HydrusExceptions.ShutdownException ): self._win = None return finally: with self._lock: self._is_working = False if self._work_needs_to_restart and not self._calllater_waiting: QP.CallAfter( self.update ) def _startWork( self ): HG.client_controller.CallToThread( self._doWork ) def update( self ): if self._win is None or not QP.isValid( self._win ): self._win = None return with self._lock: if self._is_working: self._work_needs_to_restart = True elif not self._calllater_waiting: self._loading_callable() self._calllater_waiting = True self._startWork() class FastThreadToGUIUpdater( object ): def __init__( self, win, func ): self._win = win self._func = func self._lock = threading.Lock() self._args = None self._kwargs = None self._callafter_waiting = False self._work_needs_to_restart = False self._is_working = False def QtDoIt( self ): if self._win is None or not QP.isValid( self._win ): self._win = None return with self._lock: self._callafter_waiting = False self._work_needs_to_restart = False self._is_working = True args = self._args kwargs = self._kwargs try: self._func( *args, **kwargs ) except HydrusExceptions.ShutdownException: pass finally: with self._lock: self._is_working = False if self._work_needs_to_restart and not self._callafter_waiting: self._callafter_waiting = True QP.CallAfter( self.QtDoIt ) # the point here is that we can spam this a hundred times a second, updating the args and kwargs, and Qt will catch up to it when it can # if Qt feels like running fast, it'll update at 60fps # if not, we won't get bungled up with 10,000+ pubsub events in the event queue def Update( self, *args, **kwargs ): if self._win is None: return with self._lock: self._args = args self._kwargs = kwargs if self._is_working: self._work_needs_to_restart = True elif not ( self._callafter_waiting or HG.view_shutdown ): QP.CallAfter( self.QtDoIt )