hydrus/hydrus/client/gui/ClientGUIAsync.py

310 lines
8.5 KiB
Python

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 )