2019-02-05 04:59:03 +01:00
# Copyright: Ankitects Pty Ltd and contributors
2012-12-21 08:51:59 +01:00
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
2020-02-10 08:58:54 +01:00
from __future__ import annotations
2012-12-21 08:51:59 +01:00
import time
2019-12-20 10:19:03 +01:00
2017-12-28 09:31:05 +01:00
import aqt . forms
2022-02-18 10:00:12 +01:00
from anki . _legacy import print_deprecation_warning
2019-12-20 10:19:03 +01:00
from aqt . qt import *
2021-03-26 04:48:26 +01:00
from aqt . utils import disable_help_button , tr
2012-12-21 08:51:59 +01:00
# Progress info
##########################################################################
2019-12-23 01:34:10 +01:00
class ProgressManager :
2021-02-01 13:08:56 +01:00
def __init__ ( self , mw : aqt . AnkiQt ) - > None :
2012-12-21 08:51:59 +01:00
self . mw = mw
2021-03-17 05:51:59 +01:00
self . app = mw . app
2012-12-21 08:51:59 +01:00
self . inDB = False
2013-01-28 22:45:29 +01:00
self . blockUpdates = False
2021-10-03 10:59:42 +02:00
self . _show_timer : QTimer | None = None
self . _busy_cursor_timer : QTimer | None = None
self . _win : ProgressDialog | None = None
2012-12-21 08:51:59 +01:00
self . _levels = 0
2017-12-28 09:31:05 +01:00
# Safer timers
2012-12-21 08:51:59 +01:00
##########################################################################
2022-02-24 12:15:56 +01:00
# Custom timers which avoid firing while a progress dialog is active
2020-03-06 04:21:24 +01:00
# (likely due to some long-running DB operation)
2012-12-21 08:51:59 +01:00
2021-02-01 13:08:56 +01:00
def timer (
2022-02-18 10:00:12 +01:00
self ,
ms : int ,
func : Callable ,
repeat : bool ,
requiresCollection : bool = True ,
* ,
parent : QObject = None ,
2021-02-01 13:08:56 +01:00
) - > QTimer :
2022-02-24 12:15:56 +01:00
""" Create and start a standard Anki timer. For an alternative see `single_shot()`.
2020-09-30 02:01:06 +02:00
If the timer fires while a progress window is shown :
- if it is a repeating timer , it will wait the same delay again
- if it is non - repeating , it will try again in 100 ms
If requiresCollection is True , the timer will not fire if the
collection has been unloaded . Setting it to False will allow the
timer to fire even when there is no collection , but will still
2022-02-24 12:15:56 +01:00
only fire when there is no current progress dialog .
Issues and alternative
- - -
The created timer will only be destroyed when ` parent ` is destroyed .
This can cause memory leaks , because anything captured by ` func ` isn ' t freed either.
If there is no QObject that will get destroyed reasonably soon , and you have to
pass ` mw ` , you should call ` deleteLater ( ) ` on the returned QTimer as soon as
it ' s served its purpose, or use `single_shot()`.
Also note that you may not be able to pass an adequate parent , if you want to
make a callback after a widget closes . If you passed that widget , the timer
would get destroyed before it could fire .
"""
2020-09-30 02:01:06 +02:00
2022-02-18 10:00:12 +01:00
if parent is None :
print_deprecation_warning (
" to avoid memory leaks, pass an appropriate parent to progress.timer() "
2022-02-24 12:15:56 +01:00
" or use progress.single_shot() "
2022-02-18 10:00:12 +01:00
)
parent = self . mw
2022-02-24 12:15:56 +01:00
qtimer = QTimer ( parent )
if not repeat :
qtimer . setSingleShot ( True )
qconnect ( qtimer . timeout , self . _get_handler ( func , repeat , requiresCollection ) )
qtimer . start ( ms )
return qtimer
def single_shot (
self ,
ms : int ,
func : Callable [ [ ] , None ] ,
requires_collection : bool = True ,
) - > None :
""" Create and start a one-off Anki timer. For an alternative and more
documentation , see ` timer ( ) ` .
Issues and alternative
- - -
` single_shot ( ) ` cleans itself up , so a passed closure won ' t leak any memory.
However , if ` func ` references a QObject other than ` mw ` , which gets deleted before the
timer fires , an Exception is raised . To avoid this , either use ` timer ( ) ` passing
that object as the parent , or check in ` func ` with ` sip . isdeleted ( object ) ` if
it still exists .
On the other hand , if a widget is supposed to make an external callback after it closes ,
you likely want to use ` single_shot ( ) ` , which will fire even if the calling
widget is already destroyed .
"""
QTimer . singleShot ( ms , self . _get_handler ( func , False , requires_collection ) )
def _get_handler (
self , func : Callable [ [ ] , None ] , repeat : bool , requires_collection : bool
) - > Callable [ [ ] , None ] :
2021-02-01 14:28:21 +01:00
def handler ( ) - > None :
2022-02-24 12:15:56 +01:00
if requires_collection and not self . mw . col :
2020-09-30 02:01:06 +02:00
# no current collection; timer is no longer valid
2021-02-11 01:09:06 +01:00
print ( f " Ignored progress func as collection unloaded: { repr ( func ) } " )
2020-09-30 02:01:06 +02:00
return
if not self . _levels :
# no current progress; safe to fire
2012-12-21 08:51:59 +01:00
func ( )
2020-09-30 02:01:06 +02:00
else :
if repeat :
# skip this time; we'll fire again
pass
else :
# retry in 100ms
2022-02-24 12:15:56 +01:00
self . single_shot ( 100 , func , requires_collection )
2019-12-23 01:34:10 +01:00
2022-02-24 12:15:56 +01:00
return handler
2012-12-21 08:51:59 +01:00
# Creating progress dialogs
##########################################################################
2020-02-10 08:58:54 +01:00
def start (
2021-02-02 15:00:29 +01:00
self ,
max : int = 0 ,
min : int = 0 ,
2021-10-03 10:59:42 +02:00
label : str | None = None ,
parent : QWidget | None = None ,
2021-02-02 15:00:29 +01:00
immediate : bool = False ,
2021-10-03 10:59:42 +02:00
) - > ProgressDialog | None :
2012-12-21 08:51:59 +01:00
self . _levels + = 1
if self . _levels > 1 :
2020-02-10 08:58:54 +01:00
return None
2012-12-21 08:51:59 +01:00
# setup window
2017-08-16 05:20:29 +02:00
parent = parent or self . app . activeWindow ( )
if not parent and self . mw . isVisible ( ) :
parent = self . mw
2021-03-26 04:48:26 +01:00
label = label or tr . qt_misc_processing ( )
2020-02-10 08:58:54 +01:00
self . _win = ProgressDialog ( parent )
2017-12-28 09:31:05 +01:00
self . _win . form . progressBar . setMinimum ( min )
self . _win . form . progressBar . setMaximum ( max )
2018-01-14 09:05:43 +01:00
self . _win . form . progressBar . setTextVisible ( False )
2017-12-28 09:31:05 +01:00
self . _win . form . label . setText ( label )
2012-12-21 08:51:59 +01:00
self . _win . setWindowTitle ( " Anki " )
2021-10-05 05:53:01 +02:00
self . _win . setWindowModality ( Qt . WindowModality . ApplicationModal )
2017-08-16 05:20:29 +02:00
self . _win . setMinimumWidth ( 300 )
2021-03-16 10:33:26 +01:00
self . _busy_cursor_timer = QTimer ( self . mw )
self . _busy_cursor_timer . setSingleShot ( True )
self . _busy_cursor_timer . start ( 300 )
qconnect ( self . _busy_cursor_timer . timeout , self . _set_busy_cursor )
2020-08-01 04:50:45 +02:00
self . _shown : float = 0
2012-12-21 08:51:59 +01:00
self . _counter = min
self . _min = min
self . _max = max
self . _firstTime = time . time ( )
2020-03-06 04:14:37 +01:00
self . _show_timer = QTimer ( self . mw )
self . _show_timer . setSingleShot ( True )
2020-05-31 03:24:33 +02:00
self . _show_timer . start ( immediate and 100 or 600 )
2020-05-04 05:23:08 +02:00
qconnect ( self . _show_timer . timeout , self . _on_show_timer )
2017-01-17 08:15:50 +01:00
return self . _win
2012-12-21 08:51:59 +01:00
2020-05-30 04:28:22 +02:00
def update (
self ,
2021-10-03 10:59:42 +02:00
label : str | None = None ,
value : int | None = None ,
2021-02-02 15:00:29 +01:00
process : bool = True ,
maybeShow : bool = True ,
2021-10-03 10:59:42 +02:00
max : int | None = None ,
2020-05-30 04:28:22 +02:00
) - > None :
2019-12-23 01:34:10 +01:00
# print self._min, self._counter, self._max, label, time.time() - self._lastTime
2020-03-06 04:21:24 +01:00
if not self . mw . inMainThread ( ) :
print ( " progress.update() called on wrong thread " )
return
2012-12-21 08:51:59 +01:00
if maybeShow :
self . _maybeShow ( )
2018-01-14 03:16:47 +01:00
if not self . _shown :
return
2012-12-21 08:51:59 +01:00
if label :
2017-12-28 09:31:05 +01:00
self . _win . form . label . setText ( label )
2020-06-08 12:28:11 +02:00
self . _max = max or 0
self . _win . form . progressBar . setMaximum ( self . _max )
2018-01-14 03:16:47 +01:00
if self . _max :
2019-12-23 01:34:10 +01:00
self . _counter = value or ( self . _counter + 1 )
2017-12-28 09:31:05 +01:00
self . _win . form . progressBar . setValue ( self . _counter )
2020-06-08 12:28:11 +02:00
2021-02-01 13:08:56 +01:00
def finish ( self ) - > None :
2012-12-21 08:51:59 +01:00
self . _levels - = 1
self . _levels = max ( 0 , self . _levels )
2020-01-30 23:47:05 +01:00
if self . _levels == 0 :
if self . _win :
self . _closeWin ( )
2021-03-16 10:33:26 +01:00
if self . _busy_cursor_timer :
self . _busy_cursor_timer . stop ( )
self . _busy_cursor_timer = None
self . _restore_cursor ( )
2020-03-06 04:14:37 +01:00
if self . _show_timer :
self . _show_timer . stop ( )
self . _show_timer = None
2012-12-21 08:51:59 +01:00
2021-02-01 13:08:56 +01:00
def clear ( self ) - > None :
2012-12-21 08:51:59 +01:00
" Restore the interface after an error. "
if self . _levels :
self . _levels = 1
self . finish ( )
2021-02-01 13:08:56 +01:00
def _maybeShow ( self ) - > None :
2012-12-21 08:51:59 +01:00
if not self . _levels :
return
if self . _shown :
return
delta = time . time ( ) - self . _firstTime
if delta > 0.5 :
2017-01-25 07:50:57 +01:00
self . _showWin ( )
2020-08-01 04:50:45 +02:00
def _showWin ( self ) - > None :
2017-01-25 07:50:57 +01:00
self . _shown = time . time ( )
self . _win . show ( )
2020-05-04 05:23:08 +02:00
def _closeWin ( self ) - > None :
2017-01-25 07:50:57 +01:00
if self . _shown :
while True :
# give the window system a second to present
# window before we close it again - fixes
# progress window getting stuck, especially
# on ubuntu 16.10+
elap = time . time ( ) - self . _shown
if elap > = 0.5 :
break
2021-10-05 05:53:01 +02:00
self . app . processEvents ( QEventLoop . ProcessEventsFlag . ExcludeUserInputEvents ) # type: ignore #possibly related to https://github.com/python/mypy/issues/6910
clear_unused_tags and browser redraw improvements
- clear_unused_tags() is now undoable, and returns the number of removed
notes
- add a new mw.query_op() helper for immutable queries
- decouple "freeze/unfreeze ui state" hooks from the "interface update
required" hook, so that the former is fired even on error, and can be
made re-entrant
- use a 'block_updates' flag in Python, instead of setUpdatesEnabled(),
as the latter has the side-effect of preventing child windows like
tooltips from appearing, and forces a full redrawn when updates are
enabled again. The new behaviour leads to the card list blanking out
when a long-running op is running, but in the future if we cache the
cell values we can just display them from the cache instead.
- we were indiscriminately saving the note with saveNow(), due to the
call to saveTags(). Changed so that it only saves when the tags field
is focused.
- drain the "on_done" queue on main before launching a new background
task, to lower the chances of something in on_done making a small query
to the DB and hanging until a long op finishes
- the duplicate check in the editor was executed after the webview loads,
leading to it hanging until the sidebar finishes loading. Run it at
set_note() time instead, so that the editor loads first.
- don't throw an error when a long-running op started with with_progress()
finishes after the window it was launched from has closed
- don't throw an error when the browser is closed before the sidebar
has finished loading
2021-03-17 12:27:42 +01:00
# if the parent window has been deleted, the progress dialog may have
# already been dropped; delete it if it hasn't been
if not sip . isdeleted ( self . _win ) :
self . _win . cancel ( )
2017-12-28 09:31:05 +01:00
self . _win = None
2020-05-04 05:23:08 +02:00
self . _shown = 0
2012-12-21 08:51:59 +01:00
2021-03-16 10:33:26 +01:00
def _set_busy_cursor ( self ) - > None :
2021-10-05 05:53:01 +02:00
self . mw . app . setOverrideCursor ( QCursor ( Qt . CursorShape . WaitCursor ) )
2012-12-21 08:51:59 +01:00
2021-03-16 10:33:26 +01:00
def _restore_cursor ( self ) - > None :
2012-12-21 08:51:59 +01:00
self . app . restoreOverrideCursor ( )
2021-02-01 13:08:56 +01:00
def busy ( self ) - > int :
2012-12-21 08:51:59 +01:00
" True if processing. "
return self . _levels
2020-02-10 08:58:54 +01:00
2021-02-01 13:08:56 +01:00
def _on_show_timer ( self ) - > None :
2020-03-06 04:14:37 +01:00
self . _show_timer = None
self . _showWin ( )
2020-05-30 04:28:22 +02:00
def want_cancel ( self ) - > bool :
win = self . _win
if win :
return win . wantCancel
else :
return False
2020-05-31 06:43:27 +02:00
def set_title ( self , title : str ) - > None :
win = self . _win
if win :
win . setWindowTitle ( title )
2020-02-10 08:58:54 +01:00
class ProgressDialog ( QDialog ) :
2021-02-01 13:08:56 +01:00
def __init__ ( self , parent : QWidget ) - > None :
2020-02-10 08:58:54 +01:00
QDialog . __init__ ( self , parent )
2021-01-07 05:24:49 +01:00
disable_help_button ( self )
2020-02-10 08:58:54 +01:00
self . form = aqt . forms . progress . Ui_Dialog ( )
self . form . setupUi ( self )
self . _closingDown = False
self . wantCancel = False
2020-05-30 06:19:21 +02:00
# required for smooth progress bars
self . form . progressBar . setStyleSheet ( " QProgressBar::chunk { width: 1px; } " )
2020-02-10 08:58:54 +01:00
2021-02-01 13:08:56 +01:00
def cancel ( self ) - > None :
2020-02-10 08:58:54 +01:00
self . _closingDown = True
self . hide ( )
2021-02-02 15:00:29 +01:00
def closeEvent ( self , evt : QCloseEvent ) - > None :
2020-02-10 08:58:54 +01:00
if self . _closingDown :
evt . accept ( )
else :
self . wantCancel = True
evt . ignore ( )
2021-02-02 15:00:29 +01:00
def keyPressEvent ( self , evt : QKeyEvent ) - > None :
2021-10-05 05:53:01 +02:00
if evt . key ( ) == Qt . Key . Key_Escape :
2020-02-10 08:58:54 +01:00
evt . ignore ( )
self . wantCancel = True