mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
Merge branch 'master' into dyn-deckconf
This commit is contained in:
commit
c18af2a0a9
101 changed files with 1795 additions and 1416 deletions
|
@ -15,9 +15,9 @@ Pre-built Python packages are available on PyPI. They are useful if you wish to:
|
||||||
- Get code completion when developing add-ons
|
- Get code completion when developing add-ons
|
||||||
- Make command line scripts that modify .anki2 files via Anki's Python libraries
|
- Make command line scripts that modify .anki2 files via Anki's Python libraries
|
||||||
|
|
||||||
You will need Python 3.8 or 3.9 installed. If you do not have Python yet, please
|
You will need the 64 bit version of Python 3.8 or 3.9 installed. If you do not
|
||||||
see the platform-specific instructions in the "Building from source" section below
|
have Python yet, please see the platform-specific instructions in the "Building
|
||||||
for more info.
|
from source" section below for more info.
|
||||||
|
|
||||||
**Mac/Linux**:
|
**Mac/Linux**:
|
||||||
|
|
||||||
|
|
|
@ -19,8 +19,9 @@ $ brew install rsync bazelisk
|
||||||
|
|
||||||
**Install Python 3.8**:
|
**Install Python 3.8**:
|
||||||
|
|
||||||
Install Python 3.8 from <https://python.org>. You may be able to use
|
Install Python 3.8 from <https://python.org>. We have heard reports
|
||||||
the Homebrew version instead, but this is untested.
|
of issues with pyenv and homebrew, so the package from python.org is
|
||||||
|
the only recommended approach.
|
||||||
|
|
||||||
Python 3.9 is not currently recommended, as pylint does not support it yet.
|
Python 3.9 is not currently recommended, as pylint does not support it yet.
|
||||||
|
|
||||||
|
|
|
@ -17,8 +17,8 @@ components enabled on the right.
|
||||||
|
|
||||||
**Python 3.8**:
|
**Python 3.8**:
|
||||||
|
|
||||||
Download Python 3.8 from <https://python.org>. Run the installer, and
|
Download the 64 bit Python 3.8 from <https://python.org>. Run the installer,
|
||||||
customize the installation. Select "install for all users", and choose
|
and customize the installation. Select "install for all users", and choose
|
||||||
the install path as c:\python. Currently the build scripts require
|
the install path as c:\python. Currently the build scripts require
|
||||||
Python to be installed in that location.
|
Python to be installed in that location.
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@ decorator==4.4.2
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
distro==1.5.0
|
distro==1.5.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
flask-cors==3.0.9
|
flask-cors==3.0.10
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
flask==1.1.2
|
flask==1.1.2
|
||||||
# via
|
# via
|
||||||
|
@ -54,7 +54,7 @@ isort==5.7.0
|
||||||
# pylint
|
# pylint
|
||||||
itsdangerous==1.1.0
|
itsdangerous==1.1.0
|
||||||
# via flask
|
# via flask
|
||||||
jinja2==2.11.2
|
jinja2==2.11.3
|
||||||
# via flask
|
# via flask
|
||||||
jsonschema==3.2.0
|
jsonschema==3.2.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
|
@ -72,13 +72,13 @@ mypy-extensions==0.4.3
|
||||||
# via
|
# via
|
||||||
# black
|
# black
|
||||||
# mypy
|
# mypy
|
||||||
mypy-protobuf==1.23
|
mypy-protobuf==1.24
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
mypy==0.790
|
mypy==0.800
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
orjson==3.4.6
|
orjson==3.4.7
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
packaging==20.8
|
packaging==20.9
|
||||||
# via pytest
|
# via pytest
|
||||||
pathspec==0.8.1
|
pathspec==0.8.1
|
||||||
# via black
|
# via black
|
||||||
|
@ -102,7 +102,7 @@ pyrsistent==0.17.3
|
||||||
# via jsonschema
|
# via jsonschema
|
||||||
pysocks==1.7.1
|
pysocks==1.7.1
|
||||||
# via requests
|
# via requests
|
||||||
pytest==6.2.1
|
pytest==6.2.2
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
pytoml==0.1.21
|
pytoml==0.1.21
|
||||||
# via compare-locales
|
# via compare-locales
|
||||||
|
@ -142,7 +142,7 @@ typing-extensions==3.7.4.3
|
||||||
# via
|
# via
|
||||||
# black
|
# black
|
||||||
# mypy
|
# mypy
|
||||||
urllib3==1.26.2
|
urllib3==1.26.3
|
||||||
# via requests
|
# via requests
|
||||||
waitress==2.0.0b1
|
waitress==2.0.0b1
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
|
@ -154,9 +154,9 @@ wrapt==1.12.1
|
||||||
# via astroid
|
# via astroid
|
||||||
|
|
||||||
# The following packages are considered to be unsafe in a requirements file:
|
# The following packages are considered to be unsafe in a requirements file:
|
||||||
pip==20.3.3
|
pip==21.0.1
|
||||||
# via pip-tools
|
# via pip-tools
|
||||||
setuptools==51.1.1
|
setuptools==52.0.0
|
||||||
# via jsonschema
|
# via jsonschema
|
||||||
|
|
||||||
# manually added for now; ensure it and the earlier winrt are not removed on update
|
# manually added for now; ensure it and the earlier winrt are not removed on update
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
from typing import Any
|
def buildhash() -> str: ...
|
||||||
|
def open_backend(data: bytes) -> Backend: ...
|
||||||
def buildhash(*args, **kwargs) -> Any: ...
|
|
||||||
def open_backend(*args, **kwargs) -> Any: ...
|
|
||||||
|
|
||||||
class Backend:
|
class Backend:
|
||||||
@classmethod
|
@classmethod
|
||||||
def __init__(self, *args, **kwargs) -> None: ...
|
def command(self, method: int, data: bytes) -> bytes: ...
|
||||||
def command(self, *args, **kwargs) -> Any: ...
|
def db_command(self, data: bytes) -> bytes: ...
|
||||||
def db_command(self, *args, **kwargs) -> Any: ...
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import enum
|
||||||
import os
|
import os
|
||||||
import pprint
|
import pprint
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import weakref
|
import weakref
|
||||||
|
@ -107,7 +108,7 @@ class Collection:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def backend(self) -> RustBackend:
|
def backend(self) -> RustBackend:
|
||||||
traceback.print_stack()
|
traceback.print_stack(file=sys.stdout)
|
||||||
print()
|
print()
|
||||||
print(
|
print(
|
||||||
"Accessing the backend directly will break in the future. Please use the public methods on Collection instead."
|
"Accessing the backend directly will break in the future. Please use the public methods on Collection instead."
|
||||||
|
@ -280,7 +281,7 @@ class Collection:
|
||||||
self.db.rollback()
|
self.db.rollback()
|
||||||
self.db.begin()
|
self.db.begin()
|
||||||
|
|
||||||
def reopen(self, after_full_sync=False) -> None:
|
def reopen(self, after_full_sync: bool = False) -> None:
|
||||||
assert not self.db
|
assert not self.db
|
||||||
assert self.path.endswith(".anki2")
|
assert self.path.endswith(".anki2")
|
||||||
|
|
||||||
|
@ -409,7 +410,7 @@ class Collection:
|
||||||
def cardCount(self) -> Any:
|
def cardCount(self) -> Any:
|
||||||
return self.db.scalar("select count() from cards")
|
return self.db.scalar("select count() from cards")
|
||||||
|
|
||||||
def remove_cards_and_orphaned_notes(self, card_ids: Sequence[int]):
|
def remove_cards_and_orphaned_notes(self, card_ids: Sequence[int]) -> None:
|
||||||
"You probably want .remove_notes_by_card() instead."
|
"You probably want .remove_notes_by_card() instead."
|
||||||
self._backend.remove_cards(card_ids=card_ids)
|
self._backend.remove_cards(card_ids=card_ids)
|
||||||
|
|
||||||
|
@ -505,7 +506,7 @@ class Collection:
|
||||||
dupes = []
|
dupes = []
|
||||||
fields: Dict[int, int] = {}
|
fields: Dict[int, int] = {}
|
||||||
|
|
||||||
def ordForMid(mid):
|
def ordForMid(mid: int) -> int:
|
||||||
if mid not in fields:
|
if mid not in fields:
|
||||||
model = self.models.get(mid)
|
model = self.models.get(mid)
|
||||||
for c, f in enumerate(model["flds"]):
|
for c, f in enumerate(model["flds"]):
|
||||||
|
@ -539,7 +540,10 @@ class Collection:
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def build_search_string(
|
def build_search_string(
|
||||||
self, *terms: Union[str, SearchTerm], negate=False, match_any=False
|
self,
|
||||||
|
*terms: Union[str, SearchTerm],
|
||||||
|
negate: bool = False,
|
||||||
|
match_any: bool = False,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Helper function for the backend's search string operations.
|
"""Helper function for the backend's search string operations.
|
||||||
|
|
||||||
|
@ -576,11 +580,11 @@ class Collection:
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return default
|
return default
|
||||||
|
|
||||||
def set_config(self, key: str, val: Any):
|
def set_config(self, key: str, val: Any) -> None:
|
||||||
self.setMod()
|
self.setMod()
|
||||||
self.conf.set(key, val)
|
self.conf.set(key, val)
|
||||||
|
|
||||||
def remove_config(self, key):
|
def remove_config(self, key: str) -> None:
|
||||||
self.setMod()
|
self.setMod()
|
||||||
self.conf.remove(key)
|
self.conf.remove(key)
|
||||||
|
|
||||||
|
@ -779,11 +783,11 @@ table.review-log {{ {revlog_style} }}
|
||||||
# Logging
|
# Logging
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def log(self, *args, **kwargs) -> None:
|
def log(self, *args: Any, **kwargs: Any) -> None:
|
||||||
if not self._should_log:
|
if not self._should_log:
|
||||||
return
|
return
|
||||||
|
|
||||||
def customRepr(x):
|
def customRepr(x: Any) -> str:
|
||||||
if isinstance(x, str):
|
if isinstance(x, str):
|
||||||
return x
|
return x
|
||||||
return pprint.pformat(x)
|
return pprint.pformat(x)
|
||||||
|
@ -865,7 +869,7 @@ table.review-log {{ {revlog_style} }}
|
||||||
def get_preferences(self) -> Preferences:
|
def get_preferences(self) -> Preferences:
|
||||||
return self._backend.get_preferences()
|
return self._backend.get_preferences()
|
||||||
|
|
||||||
def set_preferences(self, prefs: Preferences):
|
def set_preferences(self, prefs: Preferences) -> None:
|
||||||
self._backend.set_preferences(prefs)
|
self._backend.set_preferences(prefs)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ from __future__ import annotations
|
||||||
import copy
|
import copy
|
||||||
import weakref
|
import weakref
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from weakref import ref
|
||||||
|
|
||||||
import anki
|
import anki
|
||||||
from anki.errors import NotFoundError
|
from anki.errors import NotFoundError
|
||||||
|
@ -46,7 +47,7 @@ class ConfigManager:
|
||||||
# Legacy dict interface
|
# Legacy dict interface
|
||||||
#########################
|
#########################
|
||||||
|
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key: str) -> Any:
|
||||||
val = self.get_immutable(key)
|
val = self.get_immutable(key)
|
||||||
if isinstance(val, list):
|
if isinstance(val, list):
|
||||||
print(
|
print(
|
||||||
|
@ -61,28 +62,28 @@ class ConfigManager:
|
||||||
else:
|
else:
|
||||||
return val
|
return val
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
def __setitem__(self, key: str, value: Any) -> None:
|
||||||
self.set(key, value)
|
self.set(key, value)
|
||||||
|
|
||||||
def get(self, key, default=None):
|
def get(self, key: str, default: Any = None) -> Any:
|
||||||
try:
|
try:
|
||||||
return self[key]
|
return self[key]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return default
|
return default
|
||||||
|
|
||||||
def setdefault(self, key, default):
|
def setdefault(self, key: str, default: Any) -> Any:
|
||||||
if key not in self:
|
if key not in self:
|
||||||
self[key] = default
|
self[key] = default
|
||||||
return self[key]
|
return self[key]
|
||||||
|
|
||||||
def __contains__(self, key):
|
def __contains__(self, key: str) -> bool:
|
||||||
try:
|
try:
|
||||||
self.get_immutable(key)
|
self.get_immutable(key)
|
||||||
return True
|
return True
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def __delitem__(self, key):
|
def __delitem__(self, key: str) -> None:
|
||||||
self.remove(key)
|
self.remove(key)
|
||||||
|
|
||||||
|
|
||||||
|
@ -95,13 +96,13 @@ class ConfigManager:
|
||||||
|
|
||||||
|
|
||||||
class WrappedList(list):
|
class WrappedList(list):
|
||||||
def __init__(self, conf, key, val):
|
def __init__(self, conf: ref[ConfigManager], key: str, val: Any) -> None:
|
||||||
self.key = key
|
self.key = key
|
||||||
self.conf = conf
|
self.conf = conf
|
||||||
self.orig = copy.deepcopy(val)
|
self.orig = copy.deepcopy(val)
|
||||||
super().__init__(val)
|
super().__init__(val)
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self) -> None:
|
||||||
cur = list(self)
|
cur = list(self)
|
||||||
conf = self.conf()
|
conf = self.conf()
|
||||||
if conf and self.orig != cur:
|
if conf and self.orig != cur:
|
||||||
|
@ -109,13 +110,13 @@ class WrappedList(list):
|
||||||
|
|
||||||
|
|
||||||
class WrappedDict(dict):
|
class WrappedDict(dict):
|
||||||
def __init__(self, conf, key, val):
|
def __init__(self, conf: ref[ConfigManager], key: str, val: Any) -> None:
|
||||||
self.key = key
|
self.key = key
|
||||||
self.conf = conf
|
self.conf = conf
|
||||||
self.orig = copy.deepcopy(val)
|
self.orig = copy.deepcopy(val)
|
||||||
super().__init__(val)
|
super().__init__(val)
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self) -> None:
|
||||||
cur = dict(self)
|
cur = dict(self)
|
||||||
conf = self.conf()
|
conf = self.conf()
|
||||||
if conf and self.orig != cur:
|
if conf and self.orig != cur:
|
||||||
|
|
|
@ -92,7 +92,7 @@ REVLOG_RESCHED = 4
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
|
|
||||||
def _tr(col: Optional[anki.collection.Collection]):
|
def _tr(col: Optional[anki.collection.Collection]) -> Any:
|
||||||
if col:
|
if col:
|
||||||
return col.tr
|
return col.tr
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -32,7 +32,7 @@ class DB:
|
||||||
del d["_db"]
|
del d["_db"]
|
||||||
return f"{super().__repr__()} {pprint.pformat(d, width=300)}"
|
return f"{super().__repr__()} {pprint.pformat(d, width=300)}"
|
||||||
|
|
||||||
def execute(self, sql: str, *a, **ka) -> Cursor:
|
def execute(self, sql: str, *a: Any, **ka: Any) -> Cursor:
|
||||||
s = sql.strip().lower()
|
s = sql.strip().lower()
|
||||||
# mark modified?
|
# mark modified?
|
||||||
for stmt in "insert", "update", "delete":
|
for stmt in "insert", "update", "delete":
|
||||||
|
@ -76,36 +76,36 @@ class DB:
|
||||||
def rollback(self) -> None:
|
def rollback(self) -> None:
|
||||||
self._db.rollback()
|
self._db.rollback()
|
||||||
|
|
||||||
def scalar(self, *a, **kw) -> Any:
|
def scalar(self, *a: Any, **kw: Any) -> Any:
|
||||||
res = self.execute(*a, **kw).fetchone()
|
res = self.execute(*a, **kw).fetchone()
|
||||||
if res:
|
if res:
|
||||||
return res[0]
|
return res[0]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def all(self, *a, **kw) -> List:
|
def all(self, *a: Any, **kw: Any) -> List:
|
||||||
return self.execute(*a, **kw).fetchall()
|
return self.execute(*a, **kw).fetchall()
|
||||||
|
|
||||||
def first(self, *a, **kw) -> Any:
|
def first(self, *a: Any, **kw: Any) -> Any:
|
||||||
c = self.execute(*a, **kw)
|
c = self.execute(*a, **kw)
|
||||||
res = c.fetchone()
|
res = c.fetchone()
|
||||||
c.close()
|
c.close()
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def list(self, *a, **kw) -> List:
|
def list(self, *a: Any, **kw: Any) -> List:
|
||||||
return [x[0] for x in self.execute(*a, **kw)]
|
return [x[0] for x in self.execute(*a, **kw)]
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
self._db.text_factory = None
|
self._db.text_factory = None
|
||||||
self._db.close()
|
self._db.close()
|
||||||
|
|
||||||
def set_progress_handler(self, *args) -> None:
|
def set_progress_handler(self, *args: Any) -> None:
|
||||||
self._db.set_progress_handler(*args)
|
self._db.set_progress_handler(*args)
|
||||||
|
|
||||||
def __enter__(self) -> "DB":
|
def __enter__(self) -> "DB":
|
||||||
self._db.execute("begin")
|
self._db.execute("begin")
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __exit__(self, exc_type, *args) -> None:
|
def __exit__(self, *args: Any) -> None:
|
||||||
self._db.close()
|
self._db.close()
|
||||||
|
|
||||||
def totalChanges(self) -> Any:
|
def totalChanges(self) -> Any:
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
from re import Match
|
||||||
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Union
|
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Union
|
||||||
|
|
||||||
import anki
|
import anki
|
||||||
|
@ -43,7 +44,11 @@ class DBProxy:
|
||||||
################
|
################
|
||||||
|
|
||||||
def _query(
|
def _query(
|
||||||
self, sql: str, *args: ValueForDB, first_row_only: bool = False, **kwargs
|
self,
|
||||||
|
sql: str,
|
||||||
|
*args: ValueForDB,
|
||||||
|
first_row_only: bool = False,
|
||||||
|
**kwargs: ValueForDB,
|
||||||
) -> List[Row]:
|
) -> List[Row]:
|
||||||
# mark modified?
|
# mark modified?
|
||||||
s = sql.strip().lower()
|
s = sql.strip().lower()
|
||||||
|
@ -57,20 +62,22 @@ class DBProxy:
|
||||||
# Query shortcuts
|
# Query shortcuts
|
||||||
###################
|
###################
|
||||||
|
|
||||||
def all(self, sql: str, *args: ValueForDB, **kwargs) -> List[Row]:
|
def all(self, sql: str, *args: ValueForDB, **kwargs: ValueForDB) -> List[Row]:
|
||||||
return self._query(sql, *args, **kwargs)
|
return self._query(sql, *args, first_row_only=False, **kwargs)
|
||||||
|
|
||||||
def list(self, sql: str, *args: ValueForDB, **kwargs) -> List[ValueFromDB]:
|
def list(
|
||||||
return [x[0] for x in self._query(sql, *args, **kwargs)]
|
self, sql: str, *args: ValueForDB, **kwargs: ValueForDB
|
||||||
|
) -> List[ValueFromDB]:
|
||||||
|
return [x[0] for x in self._query(sql, *args, first_row_only=False, **kwargs)]
|
||||||
|
|
||||||
def first(self, sql: str, *args: ValueForDB, **kwargs) -> Optional[Row]:
|
def first(self, sql: str, *args: ValueForDB, **kwargs: ValueForDB) -> Optional[Row]:
|
||||||
rows = self._query(sql, *args, first_row_only=True, **kwargs)
|
rows = self._query(sql, *args, first_row_only=True, **kwargs)
|
||||||
if rows:
|
if rows:
|
||||||
return rows[0]
|
return rows[0]
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def scalar(self, sql: str, *args: ValueForDB, **kwargs) -> ValueFromDB:
|
def scalar(self, sql: str, *args: ValueForDB, **kwargs: ValueForDB) -> ValueFromDB:
|
||||||
rows = self._query(sql, *args, first_row_only=True, **kwargs)
|
rows = self._query(sql, *args, first_row_only=True, **kwargs)
|
||||||
if rows:
|
if rows:
|
||||||
return rows[0][0]
|
return rows[0][0]
|
||||||
|
@ -109,7 +116,7 @@ def emulate_named_args(
|
||||||
n = len(args2)
|
n = len(args2)
|
||||||
arg_num[key] = n
|
arg_num[key] = n
|
||||||
# update refs
|
# update refs
|
||||||
def repl(m):
|
def repl(m: Match) -> str:
|
||||||
arg = m.group(1)
|
arg = m.group(1)
|
||||||
return f"?{arg_num[arg]}"
|
return f"?{arg_num[arg]}"
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,8 @@ from __future__ import annotations
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
import pprint
|
import pprint
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Union
|
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Union
|
||||||
|
|
||||||
import anki # pylint: disable=unused-import
|
import anki # pylint: disable=unused-import
|
||||||
|
@ -43,36 +45,37 @@ class DecksDictProxy:
|
||||||
def __init__(self, col: anki.collection.Collection):
|
def __init__(self, col: anki.collection.Collection):
|
||||||
self._col = col.weakref()
|
self._col = col.weakref()
|
||||||
|
|
||||||
def _warn(self):
|
def _warn(self) -> None:
|
||||||
|
traceback.print_stack(file=sys.stdout)
|
||||||
print("add-on should use methods on col.decks, not col.decks.decks dict")
|
print("add-on should use methods on col.decks, not col.decks.decks dict")
|
||||||
|
|
||||||
def __getitem__(self, item):
|
def __getitem__(self, item: Any) -> Any:
|
||||||
self._warn()
|
self._warn()
|
||||||
return self._col.decks.get(int(item))
|
return self._col.decks.get(int(item))
|
||||||
|
|
||||||
def __setitem__(self, key, val):
|
def __setitem__(self, key: Any, val: Any) -> None:
|
||||||
self._warn()
|
self._warn()
|
||||||
self._col.decks.save(val)
|
self._col.decks.save(val)
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self) -> int:
|
||||||
self._warn()
|
self._warn()
|
||||||
return len(self._col.decks.all_names_and_ids())
|
return len(self._col.decks.all_names_and_ids())
|
||||||
|
|
||||||
def keys(self):
|
def keys(self) -> Any:
|
||||||
self._warn()
|
self._warn()
|
||||||
return [str(nt.id) for nt in self._col.decks.all_names_and_ids()]
|
return [str(nt.id) for nt in self._col.decks.all_names_and_ids()]
|
||||||
|
|
||||||
def values(self):
|
def values(self) -> Any:
|
||||||
self._warn()
|
self._warn()
|
||||||
return self._col.decks.all()
|
return self._col.decks.all()
|
||||||
|
|
||||||
def items(self):
|
def items(self) -> Any:
|
||||||
self._warn()
|
self._warn()
|
||||||
return [(str(nt["id"]), nt) for nt in self._col.decks.all()]
|
return [(str(nt["id"]), nt) for nt in self._col.decks.all()]
|
||||||
|
|
||||||
def __contains__(self, item):
|
def __contains__(self, item: Any) -> bool:
|
||||||
self._warn()
|
self._warn()
|
||||||
self._col.decks.have(item)
|
return self._col.decks.have(item)
|
||||||
|
|
||||||
|
|
||||||
class DeckManager:
|
class DeckManager:
|
||||||
|
@ -97,7 +100,7 @@ class DeckManager:
|
||||||
self.update(g, preserve_usn=False)
|
self.update(g, preserve_usn=False)
|
||||||
|
|
||||||
# legacy
|
# legacy
|
||||||
def flush(self):
|
def flush(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
|
@ -135,7 +138,7 @@ class DeckManager:
|
||||||
self.col._backend.remove_deck(did)
|
self.col._backend.remove_deck(did)
|
||||||
|
|
||||||
def all_names_and_ids(
|
def all_names_and_ids(
|
||||||
self, skip_empty_default=False, include_filtered=True
|
self, skip_empty_default: bool = False, include_filtered: bool = True
|
||||||
) -> Sequence[DeckNameID]:
|
) -> Sequence[DeckNameID]:
|
||||||
"A sorted sequence of deck names and IDs."
|
"A sorted sequence of deck names and IDs."
|
||||||
return self.col._backend.get_deck_names(
|
return self.col._backend.get_deck_names(
|
||||||
|
@ -195,12 +198,12 @@ class DeckManager:
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
def collapse(self, did) -> None:
|
def collapse(self, did: int) -> None:
|
||||||
deck = self.get(did)
|
deck = self.get(did)
|
||||||
deck["collapsed"] = not deck["collapsed"]
|
deck["collapsed"] = not deck["collapsed"]
|
||||||
self.save(deck)
|
self.save(deck)
|
||||||
|
|
||||||
def collapseBrowser(self, did) -> None:
|
def collapseBrowser(self, did: int) -> None:
|
||||||
deck = self.get(did)
|
deck = self.get(did)
|
||||||
collapsed = deck.get("browserCollapsed", False)
|
collapsed = deck.get("browserCollapsed", False)
|
||||||
deck["browserCollapsed"] = not collapsed
|
deck["browserCollapsed"] = not collapsed
|
||||||
|
@ -241,7 +244,7 @@ class DeckManager:
|
||||||
return self.get_legacy(id)
|
return self.get_legacy(id)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def update(self, g: Deck, preserve_usn=True) -> None:
|
def update(self, g: Deck, preserve_usn: bool = True) -> None:
|
||||||
"Add or update an existing deck. Used for syncing and merging."
|
"Add or update an existing deck. Used for syncing and merging."
|
||||||
try:
|
try:
|
||||||
g["id"] = self.col._backend.add_or_update_deck_legacy(
|
g["id"] = self.col._backend.add_or_update_deck_legacy(
|
||||||
|
@ -303,7 +306,7 @@ class DeckManager:
|
||||||
except NotFoundError:
|
except NotFoundError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def update_config(self, conf: DeckConfig, preserve_usn=False) -> None:
|
def update_config(self, conf: DeckConfig, preserve_usn: bool = False) -> None:
|
||||||
conf["id"] = self.col._backend.add_or_update_deck_config_legacy(
|
conf["id"] = self.col._backend.add_or_update_deck_config_legacy(
|
||||||
config=to_json_bytes(conf), preserve_usn_and_mtime=preserve_usn
|
config=to_json_bytes(conf), preserve_usn_and_mtime=preserve_usn
|
||||||
)
|
)
|
||||||
|
@ -325,7 +328,7 @@ class DeckManager:
|
||||||
) -> int:
|
) -> int:
|
||||||
return self.add_config(name, clone_from)["id"]
|
return self.add_config(name, clone_from)["id"]
|
||||||
|
|
||||||
def remove_config(self, id) -> None:
|
def remove_config(self, id: int) -> None:
|
||||||
"Remove a configuration and update all decks using it."
|
"Remove a configuration and update all decks using it."
|
||||||
self.col.modSchema(check=True)
|
self.col.modSchema(check=True)
|
||||||
for g in self.all():
|
for g in self.all():
|
||||||
|
@ -341,14 +344,14 @@ class DeckManager:
|
||||||
grp["conf"] = id
|
grp["conf"] = id
|
||||||
self.save(grp)
|
self.save(grp)
|
||||||
|
|
||||||
def didsForConf(self, conf) -> List[int]:
|
def didsForConf(self, conf: DeckConfig) -> List[int]:
|
||||||
dids = []
|
dids = []
|
||||||
for deck in self.all():
|
for deck in self.all():
|
||||||
if "conf" in deck and deck["conf"] == conf["id"]:
|
if "conf" in deck and deck["conf"] == conf["id"]:
|
||||||
dids.append(deck["id"])
|
dids.append(deck["id"])
|
||||||
return dids
|
return dids
|
||||||
|
|
||||||
def restoreToDefault(self, conf) -> None:
|
def restoreToDefault(self, conf: DeckConfig) -> None:
|
||||||
oldOrder = conf["new"]["order"]
|
oldOrder = conf["new"]["order"]
|
||||||
new = from_json_bytes(self.col._backend.new_deck_config_legacy())
|
new = from_json_bytes(self.col._backend.new_deck_config_legacy())
|
||||||
new["id"] = conf["id"]
|
new["id"] = conf["id"]
|
||||||
|
@ -380,7 +383,7 @@ class DeckManager:
|
||||||
return deck["name"]
|
return deck["name"]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def setDeck(self, cids, did) -> None:
|
def setDeck(self, cids: List[int], did: int) -> None:
|
||||||
self.col.db.execute(
|
self.col.db.execute(
|
||||||
"update cards set did=?,usn=?,mod=? where id in " + ids2str(cids),
|
"update cards set did=?,usn=?,mod=? where id in " + ids2str(cids),
|
||||||
did,
|
did,
|
||||||
|
@ -424,7 +427,7 @@ class DeckManager:
|
||||||
self.col.conf["activeDecks"] = active
|
self.col.conf["activeDecks"] = active
|
||||||
|
|
||||||
# don't use this, it will likely go away
|
# don't use this, it will likely go away
|
||||||
def update_active(self):
|
def update_active(self) -> None:
|
||||||
self.select(self.current()["id"])
|
self.select(self.current()["id"])
|
||||||
|
|
||||||
# Parents/children
|
# Parents/children
|
||||||
|
@ -480,7 +483,7 @@ class DeckManager:
|
||||||
# Change to Dict[int, "DeckManager.childMapNode"] when MyPy allow recursive type
|
# Change to Dict[int, "DeckManager.childMapNode"] when MyPy allow recursive type
|
||||||
|
|
||||||
def childDids(self, did: int, childMap: DeckManager.childMapNode) -> List:
|
def childDids(self, did: int, childMap: DeckManager.childMapNode) -> List:
|
||||||
def gather(node: DeckManager.childMapNode, arr):
|
def gather(node: DeckManager.childMapNode, arr: List) -> None:
|
||||||
for did, child in node.items():
|
for did, child in node.items():
|
||||||
arr.append(did)
|
arr.append(did)
|
||||||
gather(child, arr)
|
gather(child, arr)
|
||||||
|
|
|
@ -3,8 +3,6 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import anki._backend.backend_pb2 as _pb
|
import anki._backend.backend_pb2 as _pb
|
||||||
|
|
||||||
# fixme: notfounderror etc need to be in rsbackend.py
|
# fixme: notfounderror etc need to be in rsbackend.py
|
||||||
|
@ -88,17 +86,15 @@ def backend_exception_to_pylib(err: _pb.BackendError) -> Exception:
|
||||||
return StringError(err.localized)
|
return StringError(err.localized)
|
||||||
|
|
||||||
|
|
||||||
|
# FIXME: this is only used with "abortSchemaMod", but currently some
|
||||||
|
# add-ons depend on it
|
||||||
class AnkiError(Exception):
|
class AnkiError(Exception):
|
||||||
def __init__(self, type, **data) -> None:
|
def __init__(self, type: str) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.type = type
|
self.type = type
|
||||||
self.data = data
|
|
||||||
|
|
||||||
def __str__(self) -> Any:
|
def __str__(self) -> str:
|
||||||
m = self.type
|
return self.type
|
||||||
if self.data:
|
|
||||||
m += ": %s" % repr(self.data)
|
|
||||||
return m
|
|
||||||
|
|
||||||
|
|
||||||
class DeckRenameError(Exception):
|
class DeckRenameError(Exception):
|
||||||
|
@ -106,5 +102,5 @@ class DeckRenameError(Exception):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.description = description
|
self.description = description
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return "Couldn't rename deck: " + self.description
|
return "Couldn't rename deck: " + self.description
|
||||||
|
|
|
@ -16,10 +16,10 @@ class Finder:
|
||||||
self.col = col.weakref()
|
self.col = col.weakref()
|
||||||
print("Finder() is deprecated, please use col.find_cards() or .find_notes()")
|
print("Finder() is deprecated, please use col.find_cards() or .find_notes()")
|
||||||
|
|
||||||
def findCards(self, query, order):
|
def findCards(self, query: Any, order: Any) -> Any:
|
||||||
return self.col.find_cards(query, order)
|
return self.col.find_cards(query, order)
|
||||||
|
|
||||||
def findNotes(self, query):
|
def findNotes(self, query: Any) -> Any:
|
||||||
return self.col.find_notes(query)
|
return self.col.find_notes(query)
|
||||||
|
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ def fieldNamesForNotes(col: Collection, nids: List[int]) -> List[str]:
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
|
|
||||||
def fieldNames(col, downcase=True) -> List:
|
def fieldNames(col: Collection, downcase: bool = True) -> List:
|
||||||
fields: Set[str] = set()
|
fields: Set[str] = set()
|
||||||
for m in col.models.all():
|
for m in col.models.all():
|
||||||
for f in m["flds"]:
|
for f in m["flds"]:
|
||||||
|
|
|
@ -25,7 +25,7 @@ from anki.hooks_gen import *
|
||||||
_hooks: Dict[str, List[Callable[..., Any]]] = {}
|
_hooks: Dict[str, List[Callable[..., Any]]] = {}
|
||||||
|
|
||||||
|
|
||||||
def runHook(hook: str, *args) -> None:
|
def runHook(hook: str, *args: Any) -> None:
|
||||||
"Run all functions on hook."
|
"Run all functions on hook."
|
||||||
hookFuncs = _hooks.get(hook, None)
|
hookFuncs = _hooks.get(hook, None)
|
||||||
if hookFuncs:
|
if hookFuncs:
|
||||||
|
@ -37,7 +37,7 @@ def runHook(hook: str, *args) -> None:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
def runFilter(hook: str, arg: Any, *args) -> Any:
|
def runFilter(hook: str, arg: Any, *args: Any) -> Any:
|
||||||
hookFuncs = _hooks.get(hook, None)
|
hookFuncs = _hooks.get(hook, None)
|
||||||
if hookFuncs:
|
if hookFuncs:
|
||||||
for func in hookFuncs:
|
for func in hookFuncs:
|
||||||
|
@ -57,7 +57,7 @@ def addHook(hook: str, func: Callable) -> None:
|
||||||
_hooks[hook].append(func)
|
_hooks[hook].append(func)
|
||||||
|
|
||||||
|
|
||||||
def remHook(hook, func) -> None:
|
def remHook(hook: Any, func: Any) -> None:
|
||||||
"Remove a function if is on hook."
|
"Remove a function if is on hook."
|
||||||
hook = _hooks.get(hook, [])
|
hook = _hooks.get(hook, [])
|
||||||
if func in hook:
|
if func in hook:
|
||||||
|
@ -72,10 +72,10 @@ def remHook(hook, func) -> None:
|
||||||
#
|
#
|
||||||
# If you call wrap() with pos='around', the original function will not be called
|
# If you call wrap() with pos='around', the original function will not be called
|
||||||
# automatically but can be called with _old().
|
# automatically but can be called with _old().
|
||||||
def wrap(old, new, pos="after") -> Callable:
|
def wrap(old: Any, new: Any, pos: str = "after") -> Callable:
|
||||||
"Override an existing function."
|
"Override an existing function."
|
||||||
|
|
||||||
def repl(*args, **kwargs):
|
def repl(*args: Any, **kwargs: Any) -> Any:
|
||||||
if pos == "after":
|
if pos == "after":
|
||||||
old(*args, **kwargs)
|
old(*args, **kwargs)
|
||||||
return new(*args, **kwargs)
|
return new(*args, **kwargs)
|
||||||
|
@ -85,7 +85,7 @@ def wrap(old, new, pos="after") -> Callable:
|
||||||
else:
|
else:
|
||||||
return new(_old=old, *args, **kwargs)
|
return new(_old=old, *args, **kwargs)
|
||||||
|
|
||||||
def decorator_wrapper(f, *args, **kwargs):
|
def decorator_wrapper(f: Any, *args: Any, **kwargs: Any) -> Any:
|
||||||
return repl(*args, **kwargs)
|
return repl(*args, **kwargs)
|
||||||
|
|
||||||
return decorator.decorator(decorator_wrapper)(old)
|
return decorator.decorator(decorator_wrapper)(old)
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
Wrapper for requests that adds a callback for tracking upload/download progress.
|
Wrapper for requests that adds a callback for tracking upload/download progress.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
from typing import Any, Callable, Dict, Optional
|
from typing import Any, Callable, Dict, Optional
|
||||||
|
@ -28,24 +30,23 @@ class HttpClient:
|
||||||
self.progress_hook = progress_hook
|
self.progress_hook = progress_hook
|
||||||
self.session = requests.Session()
|
self.session = requests.Session()
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self) -> HttpClient:
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __exit__(self, *args):
|
def __exit__(self, *args: Any) -> None:
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def close(self):
|
def close(self) -> None:
|
||||||
if self.session:
|
if self.session:
|
||||||
self.session.close()
|
self.session.close()
|
||||||
self.session = None
|
self.session = None
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self) -> None:
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def post(self, url: str, data: Any, headers: Optional[Dict[str, str]]) -> Response:
|
def post(
|
||||||
data = _MonitoringFile(
|
self, url: str, data: bytes, headers: Optional[Dict[str, str]]
|
||||||
data, hook=self.progress_hook
|
) -> Response:
|
||||||
) # pytype: disable=wrong-arg-types
|
|
||||||
headers["User-Agent"] = self._agentName()
|
headers["User-Agent"] = self._agentName()
|
||||||
return self.session.post(
|
return self.session.post(
|
||||||
url,
|
url,
|
||||||
|
@ -56,7 +57,7 @@ class HttpClient:
|
||||||
verify=self.verify,
|
verify=self.verify,
|
||||||
) # pytype: disable=wrong-arg-types
|
) # pytype: disable=wrong-arg-types
|
||||||
|
|
||||||
def get(self, url, headers=None) -> Response:
|
def get(self, url: str, headers: Dict[str, str] = None) -> Response:
|
||||||
if headers is None:
|
if headers is None:
|
||||||
headers = {}
|
headers = {}
|
||||||
headers["User-Agent"] = self._agentName()
|
headers["User-Agent"] = self._agentName()
|
||||||
|
@ -64,7 +65,7 @@ class HttpClient:
|
||||||
url, stream=True, headers=headers, timeout=self.timeout, verify=self.verify
|
url, stream=True, headers=headers, timeout=self.timeout, verify=self.verify
|
||||||
)
|
)
|
||||||
|
|
||||||
def streamContent(self, resp) -> bytes:
|
def streamContent(self, resp: Response) -> bytes:
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
|
|
||||||
buf = io.BytesIO()
|
buf = io.BytesIO()
|
||||||
|
@ -87,15 +88,3 @@ if os.environ.get("ANKI_NOVERIFYSSL"):
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
warnings.filterwarnings("ignore")
|
warnings.filterwarnings("ignore")
|
||||||
|
|
||||||
|
|
||||||
class _MonitoringFile(io.BufferedReader):
|
|
||||||
def __init__(self, raw: io.RawIOBase, hook: Optional[ProgressCallback]):
|
|
||||||
io.BufferedReader.__init__(self, raw)
|
|
||||||
self.hook = hook
|
|
||||||
|
|
||||||
def read(self, size=-1) -> bytes:
|
|
||||||
data = io.BufferedReader.read(self, HTTP_BUF_SIZE)
|
|
||||||
if self.hook:
|
|
||||||
self.hook(len(data), 0)
|
|
||||||
return data
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import locale
|
import locale
|
||||||
import re
|
import re
|
||||||
from typing import TYPE_CHECKING, Optional, Tuple
|
from typing import TYPE_CHECKING, Any, Optional, Tuple
|
||||||
|
|
||||||
import anki
|
import anki
|
||||||
import anki._backend.backend_pb2 as _pb
|
import anki._backend.backend_pb2 as _pb
|
||||||
|
@ -169,7 +169,7 @@ def ngettext(single: str, plural: str, n: int) -> str:
|
||||||
return plural
|
return plural
|
||||||
|
|
||||||
|
|
||||||
def tr_legacyglobal(*args, **kwargs) -> str:
|
def tr_legacyglobal(*args: Any, **kwargs: Any) -> str:
|
||||||
"Should use col.tr() instead."
|
"Should use col.tr() instead."
|
||||||
if current_i18n:
|
if current_i18n:
|
||||||
return current_i18n.translate(*args, **kwargs)
|
return current_i18n.translate(*args, **kwargs)
|
||||||
|
|
|
@ -11,7 +11,7 @@ import time
|
||||||
import urllib.error
|
import urllib.error
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from typing import Any, Callable, List, Optional, Tuple
|
from typing import Any, Callable, List, Match, Optional, Tuple
|
||||||
|
|
||||||
import anki
|
import anki
|
||||||
import anki._backend.backend_pb2 as _pb
|
import anki._backend.backend_pb2 as _pb
|
||||||
|
@ -197,7 +197,7 @@ class MediaManager:
|
||||||
else:
|
else:
|
||||||
fn = urllib.parse.quote
|
fn = urllib.parse.quote
|
||||||
|
|
||||||
def repl(match):
|
def repl(match: Match) -> str:
|
||||||
tag = match.group(0)
|
tag = match.group(0)
|
||||||
fname = match.group("fname")
|
fname = match.group("fname")
|
||||||
if re.match("(https?|ftp)://", fname):
|
if re.match("(https?|ftp)://", fname):
|
||||||
|
|
|
@ -5,7 +5,9 @@ from __future__ import annotations
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
import pprint
|
import pprint
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
|
import traceback
|
||||||
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
|
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
|
||||||
|
|
||||||
import anki # pylint: disable=unused-import
|
import anki # pylint: disable=unused-import
|
||||||
|
@ -39,36 +41,37 @@ class ModelsDictProxy:
|
||||||
def __init__(self, col: anki.collection.Collection):
|
def __init__(self, col: anki.collection.Collection):
|
||||||
self._col = col.weakref()
|
self._col = col.weakref()
|
||||||
|
|
||||||
def _warn(self):
|
def _warn(self) -> None:
|
||||||
|
traceback.print_stack(file=sys.stdout)
|
||||||
print("add-on should use methods on col.models, not col.models.models dict")
|
print("add-on should use methods on col.models, not col.models.models dict")
|
||||||
|
|
||||||
def __getitem__(self, item):
|
def __getitem__(self, item: Any) -> Any:
|
||||||
self._warn()
|
self._warn()
|
||||||
return self._col.models.get(int(item))
|
return self._col.models.get(int(item))
|
||||||
|
|
||||||
def __setitem__(self, key, val):
|
def __setitem__(self, key: str, val: Any) -> None:
|
||||||
self._warn()
|
self._warn()
|
||||||
self._col.models.save(val)
|
self._col.models.save(val)
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self) -> int:
|
||||||
self._warn()
|
self._warn()
|
||||||
return len(self._col.models.all_names_and_ids())
|
return len(self._col.models.all_names_and_ids())
|
||||||
|
|
||||||
def keys(self):
|
def keys(self) -> Any:
|
||||||
self._warn()
|
self._warn()
|
||||||
return [str(nt.id) for nt in self._col.models.all_names_and_ids()]
|
return [str(nt.id) for nt in self._col.models.all_names_and_ids()]
|
||||||
|
|
||||||
def values(self):
|
def values(self) -> Any:
|
||||||
self._warn()
|
self._warn()
|
||||||
return self._col.models.all()
|
return self._col.models.all()
|
||||||
|
|
||||||
def items(self):
|
def items(self) -> Any:
|
||||||
self._warn()
|
self._warn()
|
||||||
return [(str(nt["id"]), nt) for nt in self._col.models.all()]
|
return [(str(nt["id"]), nt) for nt in self._col.models.all()]
|
||||||
|
|
||||||
def __contains__(self, item):
|
def __contains__(self, item: Any) -> bool:
|
||||||
self._warn()
|
self._warn()
|
||||||
self._col.models.have(item)
|
return self._col.models.have(item)
|
||||||
|
|
||||||
|
|
||||||
class ModelManager:
|
class ModelManager:
|
||||||
|
@ -123,7 +126,7 @@ class ModelManager:
|
||||||
def _get_cached(self, ntid: int) -> Optional[NoteType]:
|
def _get_cached(self, ntid: int) -> Optional[NoteType]:
|
||||||
return self._cache.get(ntid)
|
return self._cache.get(ntid)
|
||||||
|
|
||||||
def _clear_cache(self):
|
def _clear_cache(self) -> None:
|
||||||
self._cache = {}
|
self._cache = {}
|
||||||
|
|
||||||
# Listing note types
|
# Listing note types
|
||||||
|
@ -218,7 +221,7 @@ class ModelManager:
|
||||||
"Delete model, and all its cards/notes."
|
"Delete model, and all its cards/notes."
|
||||||
self.remove(m["id"])
|
self.remove(m["id"])
|
||||||
|
|
||||||
def remove_all_notetypes(self):
|
def remove_all_notetypes(self) -> None:
|
||||||
for nt in self.all_names_and_ids():
|
for nt in self.all_names_and_ids():
|
||||||
self._remove_from_cache(nt.id)
|
self._remove_from_cache(nt.id)
|
||||||
self.col._backend.remove_notetype(nt.id)
|
self.col._backend.remove_notetype(nt.id)
|
||||||
|
@ -236,7 +239,7 @@ class ModelManager:
|
||||||
if existing_id is not None and existing_id != m["id"]:
|
if existing_id is not None and existing_id != m["id"]:
|
||||||
m["name"] += "-" + checksum(str(time.time()))[:5]
|
m["name"] += "-" + checksum(str(time.time()))[:5]
|
||||||
|
|
||||||
def update(self, m: NoteType, preserve_usn=True) -> None:
|
def update(self, m: NoteType, preserve_usn: bool = True) -> None:
|
||||||
"Add or update an existing model. Use .save() instead."
|
"Add or update an existing model. Use .save() instead."
|
||||||
self._remove_from_cache(m["id"])
|
self._remove_from_cache(m["id"])
|
||||||
self.ensureNameUnique(m)
|
self.ensureNameUnique(m)
|
||||||
|
|
|
@ -113,7 +113,7 @@ class Note:
|
||||||
def __setitem__(self, key: str, value: str) -> None:
|
def __setitem__(self, key: str, value: str) -> None:
|
||||||
self.fields[self._fieldOrd(key)] = value
|
self.fields[self._fieldOrd(key)] = value
|
||||||
|
|
||||||
def __contains__(self, key) -> bool:
|
def __contains__(self, key: str) -> bool:
|
||||||
return key in self._fmap
|
return key in self._fmap
|
||||||
|
|
||||||
# Tags
|
# Tags
|
||||||
|
|
|
@ -350,7 +350,7 @@ limit %d"""
|
||||||
lastIvl = -(self._delayForGrade(conf, lastLeft))
|
lastIvl = -(self._delayForGrade(conf, lastLeft))
|
||||||
ivl = card.ivl if leaving else -(self._delayForGrade(conf, card.left))
|
ivl = card.ivl if leaving else -(self._delayForGrade(conf, card.left))
|
||||||
|
|
||||||
def log():
|
def log() -> None:
|
||||||
self.col.db.execute(
|
self.col.db.execute(
|
||||||
"insert into revlog values (?,?,?,?,?,?,?,?,?)",
|
"insert into revlog values (?,?,?,?,?,?,?,?,?)",
|
||||||
int(time.time() * 1000),
|
int(time.time() * 1000),
|
||||||
|
@ -450,7 +450,7 @@ and due <= ? limit ?)""",
|
||||||
self._revQueue: List[Any] = []
|
self._revQueue: List[Any] = []
|
||||||
self._revDids = self.col.decks.active()[:]
|
self._revDids = self.col.decks.active()[:]
|
||||||
|
|
||||||
def _fillRev(self, recursing=False) -> bool:
|
def _fillRev(self, recursing: bool = False) -> bool:
|
||||||
"True if a review card can be fetched."
|
"True if a review card can be fetched."
|
||||||
if self._revQueue:
|
if self._revQueue:
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -166,7 +166,7 @@ class Scheduler:
|
||||||
self._restorePreviewCard(card)
|
self._restorePreviewCard(card)
|
||||||
self._removeFromFiltered(card)
|
self._removeFromFiltered(card)
|
||||||
|
|
||||||
def _reset_counts(self):
|
def _reset_counts(self) -> None:
|
||||||
tree = self.deck_due_tree(self.col.decks.selected())
|
tree = self.deck_due_tree(self.col.decks.selected())
|
||||||
node = self.col.decks.find_deck_in_tree(tree, int(self.col.conf["curDeck"]))
|
node = self.col.decks.find_deck_in_tree(tree, int(self.col.conf["curDeck"]))
|
||||||
if not node:
|
if not node:
|
||||||
|
@ -187,7 +187,7 @@ class Scheduler:
|
||||||
new, lrn, rev = counts
|
new, lrn, rev = counts
|
||||||
return (new, lrn, rev)
|
return (new, lrn, rev)
|
||||||
|
|
||||||
def _is_finished(self):
|
def _is_finished(self) -> bool:
|
||||||
"Don't use this, it is a stop-gap until this code is refactored."
|
"Don't use this, it is a stop-gap until this code is refactored."
|
||||||
return not any((self.newCount, self.revCount, self._immediate_learn_count))
|
return not any((self.newCount, self.revCount, self._immediate_learn_count))
|
||||||
|
|
||||||
|
@ -229,8 +229,12 @@ order by due"""
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def update_stats(
|
def update_stats(
|
||||||
self, deck_id: int, new_delta=0, review_delta=0, milliseconds_delta=0
|
self,
|
||||||
):
|
deck_id: int,
|
||||||
|
new_delta: int = 0,
|
||||||
|
review_delta: int = 0,
|
||||||
|
milliseconds_delta: int = 0,
|
||||||
|
) -> None:
|
||||||
self.col._backend.update_stats(
|
self.col._backend.update_stats(
|
||||||
deck_id=deck_id,
|
deck_id=deck_id,
|
||||||
new_delta=new_delta,
|
new_delta=new_delta,
|
||||||
|
@ -321,7 +325,7 @@ order by due"""
|
||||||
self._newQueue: List[int] = []
|
self._newQueue: List[int] = []
|
||||||
self._updateNewCardRatio()
|
self._updateNewCardRatio()
|
||||||
|
|
||||||
def _fillNew(self, recursing=False) -> bool:
|
def _fillNew(self, recursing: bool = False) -> bool:
|
||||||
if self._newQueue:
|
if self._newQueue:
|
||||||
return True
|
return True
|
||||||
if not self.newCount:
|
if not self.newCount:
|
||||||
|
@ -841,7 +845,7 @@ and due <= ? limit ?)"""
|
||||||
def _resetRev(self) -> None:
|
def _resetRev(self) -> None:
|
||||||
self._revQueue: List[int] = []
|
self._revQueue: List[int] = []
|
||||||
|
|
||||||
def _fillRev(self, recursing=False) -> bool:
|
def _fillRev(self, recursing: bool = False) -> bool:
|
||||||
"True if a review card can be fetched."
|
"True if a review card can be fetched."
|
||||||
if self._revQueue:
|
if self._revQueue:
|
||||||
return True
|
return True
|
||||||
|
@ -947,7 +951,7 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
|
||||||
self._removeFromFiltered(card)
|
self._removeFromFiltered(card)
|
||||||
|
|
||||||
def _logRev(self, card: Card, ease: int, delay: int, type: int) -> None:
|
def _logRev(self, card: Card, ease: int, delay: int, type: int) -> None:
|
||||||
def log():
|
def log() -> None:
|
||||||
self.col.db.execute(
|
self.col.db.execute(
|
||||||
"insert into revlog values (?,?,?,?,?,?,?,?,?)",
|
"insert into revlog values (?,?,?,?,?,?,?,?,?)",
|
||||||
int(time.time() * 1000),
|
int(time.time() * 1000),
|
||||||
|
@ -1344,7 +1348,7 @@ due = (case when odue>0 then odue else due end), odue = 0, odid = 0, usn = ? whe
|
||||||
mode = BuryOrSuspendMode.BURY_SCHED
|
mode = BuryOrSuspendMode.BURY_SCHED
|
||||||
self.col._backend.bury_or_suspend_cards(card_ids=ids, mode=mode)
|
self.col._backend.bury_or_suspend_cards(card_ids=ids, mode=mode)
|
||||||
|
|
||||||
def bury_note(self, note: Note):
|
def bury_note(self, note: Note) -> None:
|
||||||
self.bury_cards(note.card_ids())
|
self.bury_cards(note.card_ids())
|
||||||
|
|
||||||
# legacy
|
# legacy
|
||||||
|
@ -1472,7 +1476,7 @@ and (queue={QUEUE_TYPE_NEW} or (queue={QUEUE_TYPE_REV} and due<=?))""",
|
||||||
def orderCards(self, did: int) -> None:
|
def orderCards(self, did: int) -> None:
|
||||||
self.col._backend.sort_deck(deck_id=did, randomize=False)
|
self.col._backend.sort_deck(deck_id=did, randomize=False)
|
||||||
|
|
||||||
def resortConf(self, conf) -> None:
|
def resortConf(self, conf: DeckConfig) -> None:
|
||||||
for did in self.col.decks.didsForConf(conf):
|
for did in self.col.decks.didsForConf(conf):
|
||||||
if conf["new"]["order"] == 0:
|
if conf["new"]["order"] == 0:
|
||||||
self.randomizeCards(did)
|
self.randomizeCards(did)
|
||||||
|
|
|
@ -140,7 +140,7 @@ from revlog where id > ? """
|
||||||
relrn = relrn or 0
|
relrn = relrn or 0
|
||||||
filt = filt or 0
|
filt = filt or 0
|
||||||
# studied
|
# studied
|
||||||
def bold(s):
|
def bold(s: str) -> str:
|
||||||
return "<b>" + str(s) + "</b>"
|
return "<b>" + str(s) + "</b>"
|
||||||
|
|
||||||
if cards:
|
if cards:
|
||||||
|
@ -298,7 +298,7 @@ group by day order by day"""
|
||||||
# pylint: disable=invalid-unary-operand-type
|
# pylint: disable=invalid-unary-operand-type
|
||||||
conf["xaxis"]["min"] = -days + 0.5
|
conf["xaxis"]["min"] = -days + 0.5
|
||||||
|
|
||||||
def plot(id, data, ylabel, ylabel2):
|
def plot(id: str, data: Any, ylabel: str, ylabel2: str) -> str:
|
||||||
return self._graph(
|
return self._graph(
|
||||||
id, data=data, conf=conf, xunit=chunk, ylabel=ylabel, ylabel2=ylabel2
|
id, data=data, conf=conf, xunit=chunk, ylabel=ylabel, ylabel2=ylabel2
|
||||||
)
|
)
|
||||||
|
@ -333,7 +333,7 @@ group by day order by day"""
|
||||||
# pylint: disable=invalid-unary-operand-type
|
# pylint: disable=invalid-unary-operand-type
|
||||||
conf["xaxis"]["min"] = -days + 0.5
|
conf["xaxis"]["min"] = -days + 0.5
|
||||||
|
|
||||||
def plot(id, data, ylabel, ylabel2):
|
def plot(id: str, data: Any, ylabel: str, ylabel2: str) -> str:
|
||||||
return self._graph(
|
return self._graph(
|
||||||
id, data=data, conf=conf, xunit=chunk, ylabel=ylabel, ylabel2=ylabel2
|
id, data=data, conf=conf, xunit=chunk, ylabel=ylabel, ylabel2=ylabel2
|
||||||
)
|
)
|
||||||
|
|
|
@ -11,9 +11,10 @@ import os
|
||||||
import socket
|
import socket
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
from http import HTTPStatus
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from tempfile import NamedTemporaryFile
|
from tempfile import NamedTemporaryFile
|
||||||
from typing import Optional
|
from typing import Iterable, Optional
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import flask
|
import flask
|
||||||
|
@ -89,7 +90,7 @@ def handle_sync_request(method_str: str) -> Response:
|
||||||
elif method == Method.FULL_DOWNLOAD:
|
elif method == Method.FULL_DOWNLOAD:
|
||||||
path = outdata.decode("utf8")
|
path = outdata.decode("utf8")
|
||||||
|
|
||||||
def stream_reply():
|
def stream_reply() -> Iterable[bytes]:
|
||||||
with open(path, "rb") as f:
|
with open(path, "rb") as f:
|
||||||
while chunk := f.read(16 * 1024):
|
while chunk := f.read(16 * 1024):
|
||||||
yield chunk
|
yield chunk
|
||||||
|
@ -106,7 +107,7 @@ def handle_sync_request(method_str: str) -> Response:
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
def after_full_sync():
|
def after_full_sync() -> None:
|
||||||
# the server methods do not reopen the collection after a full sync,
|
# the server methods do not reopen the collection after a full sync,
|
||||||
# so we need to
|
# so we need to
|
||||||
col.reopen(after_full_sync=False)
|
col.reopen(after_full_sync=False)
|
||||||
|
@ -146,15 +147,17 @@ def get_method(
|
||||||
|
|
||||||
|
|
||||||
@app.route("/<path:pathin>", methods=["POST"])
|
@app.route("/<path:pathin>", methods=["POST"])
|
||||||
def handle_request(pathin: str):
|
def handle_request(pathin: str) -> Response:
|
||||||
path = pathin
|
path = pathin
|
||||||
print(int(time.time()), flask.request.remote_addr, path)
|
print(int(time.time()), flask.request.remote_addr, path)
|
||||||
|
|
||||||
if path.startswith("sync/"):
|
if path.startswith("sync/"):
|
||||||
return handle_sync_request(path.split("/", maxsplit=1)[1])
|
return handle_sync_request(path.split("/", maxsplit=1)[1])
|
||||||
|
else:
|
||||||
|
return flask.make_response("not found", HTTPStatus.NOT_FOUND)
|
||||||
|
|
||||||
|
|
||||||
def folder():
|
def folder() -> str:
|
||||||
folder = os.getenv("FOLDER", os.path.expanduser("~/.syncserver"))
|
folder = os.getenv("FOLDER", os.path.expanduser("~/.syncserver"))
|
||||||
if not os.path.exists(folder):
|
if not os.path.exists(folder):
|
||||||
print("creating", folder)
|
print("creating", folder)
|
||||||
|
@ -162,11 +165,11 @@ def folder():
|
||||||
return folder
|
return folder
|
||||||
|
|
||||||
|
|
||||||
def col_path():
|
def col_path() -> str:
|
||||||
return os.path.join(folder(), "collection.server.anki2")
|
return os.path.join(folder(), "collection.server.anki2")
|
||||||
|
|
||||||
|
|
||||||
def serve():
|
def serve() -> None:
|
||||||
global col
|
global col
|
||||||
|
|
||||||
col = Collection(col_path(), server=True)
|
col = Collection(col_path(), server=True)
|
||||||
|
|
|
@ -13,7 +13,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import pprint
|
import pprint
|
||||||
import re
|
import re
|
||||||
from typing import Collection, List, Optional, Sequence, Tuple
|
from typing import Collection, List, Match, Optional, Sequence, Tuple
|
||||||
|
|
||||||
import anki # pylint: disable=unused-import
|
import anki # pylint: disable=unused-import
|
||||||
import anki._backend.backend_pb2 as _pb
|
import anki._backend.backend_pb2 as _pb
|
||||||
|
@ -48,7 +48,7 @@ class TagManager:
|
||||||
#############################################################
|
#############################################################
|
||||||
|
|
||||||
def register(
|
def register(
|
||||||
self, tags: Collection[str], usn: Optional[int] = None, clear=False
|
self, tags: Collection[str], usn: Optional[int] = None, clear: bool = False
|
||||||
) -> None:
|
) -> None:
|
||||||
print("tags.register() is deprecated and no longer works")
|
print("tags.register() is deprecated and no longer works")
|
||||||
|
|
||||||
|
@ -56,10 +56,10 @@ class TagManager:
|
||||||
"Clear unused tags and add any missing tags from notes to the tag list."
|
"Clear unused tags and add any missing tags from notes to the tag list."
|
||||||
self.clear_unused_tags()
|
self.clear_unused_tags()
|
||||||
|
|
||||||
def clear_unused_tags(self):
|
def clear_unused_tags(self) -> None:
|
||||||
self.col._backend.clear_unused_tags()
|
self.col._backend.clear_unused_tags()
|
||||||
|
|
||||||
def byDeck(self, did, children=False) -> List[str]:
|
def byDeck(self, did: int, children: bool = False) -> List[str]:
|
||||||
basequery = "select n.tags from cards c, notes n WHERE c.nid = n.id"
|
basequery = "select n.tags from cards c, notes n WHERE c.nid = n.id"
|
||||||
if not children:
|
if not children:
|
||||||
query = basequery + " AND c.did=?"
|
query = basequery + " AND c.did=?"
|
||||||
|
@ -72,7 +72,7 @@ class TagManager:
|
||||||
res = self.col.db.list(query)
|
res = self.col.db.list(query)
|
||||||
return list(set(self.split(" ".join(res))))
|
return list(set(self.split(" ".join(res))))
|
||||||
|
|
||||||
def set_collapsed(self, tag: str, collapsed: bool):
|
def set_collapsed(self, tag: str, collapsed: bool) -> None:
|
||||||
"Set browser collapse state for tag, registering the tag if missing."
|
"Set browser collapse state for tag, registering the tag if missing."
|
||||||
self.col._backend.set_tag_collapsed(name=tag, collapsed=collapsed)
|
self.col._backend.set_tag_collapsed(name=tag, collapsed=collapsed)
|
||||||
|
|
||||||
|
@ -139,9 +139,9 @@ class TagManager:
|
||||||
def remFromStr(self, deltags: str, tags: str) -> str:
|
def remFromStr(self, deltags: str, tags: str) -> str:
|
||||||
"Delete tags if they exist."
|
"Delete tags if they exist."
|
||||||
|
|
||||||
def wildcard(pat, str):
|
def wildcard(pat: str, repl: str) -> Match:
|
||||||
pat = re.escape(pat).replace("\\*", ".*")
|
pat = re.escape(pat).replace("\\*", ".*")
|
||||||
return re.match("^" + pat + "$", str, re.IGNORECASE)
|
return re.match("^" + pat + "$", repl, re.IGNORECASE)
|
||||||
|
|
||||||
currentTags = self.split(tags)
|
currentTags = self.split(tags)
|
||||||
for tag in self.split(deltags):
|
for tag in self.split(deltags):
|
||||||
|
|
|
@ -3,9 +3,7 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
# some add-ons expect json to be in the utils module
|
import json as _json
|
||||||
import json # pylint: disable=unused-import
|
|
||||||
import locale
|
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import random
|
import random
|
||||||
|
@ -20,7 +18,7 @@ import traceback
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from hashlib import sha1
|
from hashlib import sha1
|
||||||
from html.entities import name2codepoint
|
from html.entities import name2codepoint
|
||||||
from typing import Iterable, Iterator, List, Optional, Union
|
from typing import Any, Iterable, Iterator, List, Match, Optional, Union
|
||||||
|
|
||||||
from anki.dbproxy import DBProxy
|
from anki.dbproxy import DBProxy
|
||||||
|
|
||||||
|
@ -34,8 +32,17 @@ try:
|
||||||
from_json_bytes = orjson.loads
|
from_json_bytes = orjson.loads
|
||||||
except:
|
except:
|
||||||
print("orjson is missing; DB operations will be slower")
|
print("orjson is missing; DB operations will be slower")
|
||||||
to_json_bytes = lambda obj: json.dumps(obj).encode("utf8") # type: ignore
|
to_json_bytes = lambda obj: _json.dumps(obj).encode("utf8") # type: ignore
|
||||||
from_json_bytes = json.loads
|
from_json_bytes = _json.loads
|
||||||
|
|
||||||
|
|
||||||
|
def __getattr__(name: str) -> Any:
|
||||||
|
if name == "json":
|
||||||
|
traceback.print_stack(file=sys.stdout)
|
||||||
|
print("add-on should import json directly, not from anki.utils")
|
||||||
|
return _json
|
||||||
|
raise AttributeError(f"module {__name__} has no attribute {name}")
|
||||||
|
|
||||||
|
|
||||||
# Time handling
|
# Time handling
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
@ -46,22 +53,6 @@ def intTime(scale: int = 1) -> int:
|
||||||
return int(time.time() * scale)
|
return int(time.time() * scale)
|
||||||
|
|
||||||
|
|
||||||
# Locale
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
|
|
||||||
def fmtPercentage(float_value, point=1) -> str:
|
|
||||||
"Return float with percentage sign"
|
|
||||||
fmt = "%" + "0.%(b)df" % {"b": point}
|
|
||||||
return locale.format_string(fmt, float_value) + "%"
|
|
||||||
|
|
||||||
|
|
||||||
def fmtFloat(float_value, point=1) -> str:
|
|
||||||
"Return a string with decimal separator according to current locale"
|
|
||||||
fmt = "%" + "0.%(b)df" % {"b": point}
|
|
||||||
return locale.format_string(fmt, float_value)
|
|
||||||
|
|
||||||
|
|
||||||
# HTML
|
# HTML
|
||||||
##############################################################################
|
##############################################################################
|
||||||
reComment = re.compile("(?s)<!--.*?-->")
|
reComment = re.compile("(?s)<!--.*?-->")
|
||||||
|
@ -114,7 +105,7 @@ def entsToTxt(html: str) -> str:
|
||||||
# replace it first
|
# replace it first
|
||||||
html = html.replace(" ", " ")
|
html = html.replace(" ", " ")
|
||||||
|
|
||||||
def fixup(m):
|
def fixup(m: Match) -> str:
|
||||||
text = m.group(0)
|
text = m.group(0)
|
||||||
if text[:2] == "&#":
|
if text[:2] == "&#":
|
||||||
# character reference
|
# character reference
|
||||||
|
@ -140,14 +131,6 @@ def entsToTxt(html: str) -> str:
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
|
||||||
def hexifyID(id) -> str:
|
|
||||||
return "%x" % int(id)
|
|
||||||
|
|
||||||
|
|
||||||
def dehexifyID(id) -> int:
|
|
||||||
return int(id, 16)
|
|
||||||
|
|
||||||
|
|
||||||
def ids2str(ids: Iterable[Union[int, str]]) -> str:
|
def ids2str(ids: Iterable[Union[int, str]]) -> str:
|
||||||
"""Given a list of integers, return a string '(int1,int2,...)'."""
|
"""Given a list of integers, return a string '(int1,int2,...)'."""
|
||||||
return "(%s)" % ",".join(str(i) for i in ids)
|
return "(%s)" % ",".join(str(i) for i in ids)
|
||||||
|
@ -195,23 +178,6 @@ def guid64() -> str:
|
||||||
return base91(random.randint(0, 2 ** 64 - 1))
|
return base91(random.randint(0, 2 ** 64 - 1))
|
||||||
|
|
||||||
|
|
||||||
# increment a guid by one, for note type conflicts
|
|
||||||
def incGuid(guid) -> str:
|
|
||||||
return _incGuid(guid[::-1])[::-1]
|
|
||||||
|
|
||||||
|
|
||||||
def _incGuid(guid) -> str:
|
|
||||||
s = string
|
|
||||||
table = s.ascii_letters + s.digits + _base91_extra_chars
|
|
||||||
idx = table.index(guid[0])
|
|
||||||
if idx + 1 == len(table):
|
|
||||||
# overflow
|
|
||||||
guid = table[0] + _incGuid(guid[1:])
|
|
||||||
else:
|
|
||||||
guid = table[idx + 1] + guid[1:]
|
|
||||||
return guid
|
|
||||||
|
|
||||||
|
|
||||||
# Fields
|
# Fields
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
@ -250,7 +216,7 @@ def tmpdir() -> str:
|
||||||
global _tmpdir
|
global _tmpdir
|
||||||
if not _tmpdir:
|
if not _tmpdir:
|
||||||
|
|
||||||
def cleanup():
|
def cleanup() -> None:
|
||||||
if os.path.exists(_tmpdir):
|
if os.path.exists(_tmpdir):
|
||||||
shutil.rmtree(_tmpdir)
|
shutil.rmtree(_tmpdir)
|
||||||
|
|
||||||
|
@ -294,7 +260,7 @@ def noBundledLibs() -> Iterator[None]:
|
||||||
os.environ["LD_LIBRARY_PATH"] = oldlpath
|
os.environ["LD_LIBRARY_PATH"] = oldlpath
|
||||||
|
|
||||||
|
|
||||||
def call(argv: List[str], wait: bool = True, **kwargs) -> int:
|
def call(argv: List[str], wait: bool = True, **kwargs: Any) -> int:
|
||||||
"Execute a command. If WAIT, return exit code."
|
"Execute a command. If WAIT, return exit code."
|
||||||
# ensure we don't open a separate window for forking process on windows
|
# ensure we don't open a separate window for forking process on windows
|
||||||
if isWin:
|
if isWin:
|
||||||
|
@ -338,7 +304,7 @@ devMode = os.getenv("ANKIDEV", "")
|
||||||
invalidFilenameChars = ':*?"<>|'
|
invalidFilenameChars = ':*?"<>|'
|
||||||
|
|
||||||
|
|
||||||
def invalidFilename(str, dirsep=True) -> Optional[str]:
|
def invalidFilename(str: str, dirsep: bool = True) -> Optional[str]:
|
||||||
for c in invalidFilenameChars:
|
for c in invalidFilenameChars:
|
||||||
if c in str:
|
if c in str:
|
||||||
return c
|
return c
|
||||||
|
@ -384,7 +350,7 @@ class TimedLog:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._last = time.time()
|
self._last = time.time()
|
||||||
|
|
||||||
def log(self, s) -> None:
|
def log(self, s: str) -> None:
|
||||||
path, num, fn, y = traceback.extract_stack(limit=2)[0]
|
path, num, fn, y = traceback.extract_stack(limit=2)[0]
|
||||||
sys.stderr.write(
|
sys.stderr.write(
|
||||||
"%5dms: %s(): %s\n" % ((time.time() - self._last) * 1000, fn, s)
|
"%5dms: %s(): %s\n" % ((time.time() - self._last) * 1000, fn, s)
|
||||||
|
|
|
@ -9,6 +9,14 @@ warn_redundant_casts = True
|
||||||
warn_unused_configs = True
|
warn_unused_configs = True
|
||||||
strict_equality = true
|
strict_equality = true
|
||||||
|
|
||||||
|
[mypy-anki.*]
|
||||||
|
disallow_untyped_defs = True
|
||||||
|
[mypy-anki.importing.*]
|
||||||
|
disallow_untyped_defs = False
|
||||||
|
[mypy-anki.exporting]
|
||||||
|
disallow_untyped_defs = False
|
||||||
|
|
||||||
|
|
||||||
[mypy-win32file]
|
[mypy-win32file]
|
||||||
ignore_missing_imports = True
|
ignore_missing_imports = True
|
||||||
[mypy-win32pipe]
|
[mypy-win32pipe]
|
||||||
|
|
|
@ -44,7 +44,7 @@ py_proto_library_typed = rule(
|
||||||
"mypy_protobuf": attr.label(
|
"mypy_protobuf": attr.label(
|
||||||
executable = True,
|
executable = True,
|
||||||
cfg = "exec",
|
cfg = "exec",
|
||||||
default = Label("//pylib/tools:mypy_protobuf"),
|
default = Label("//pylib/tools:protoc-gen-mypy"),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,8 +2,8 @@ load("@rules_python//python:defs.bzl", "py_binary", "py_library")
|
||||||
load("@py_deps//:requirements.bzl", "requirement")
|
load("@py_deps//:requirements.bzl", "requirement")
|
||||||
|
|
||||||
py_binary(
|
py_binary(
|
||||||
name = "mypy_protobuf",
|
name = "protoc-gen-mypy",
|
||||||
srcs = [requirement("mypy-protobuf").replace(":pkg", ":mypy_protobuf.py")],
|
srcs = ["protoc-gen-mypy.py"],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
],
|
],
|
||||||
|
@ -17,7 +17,7 @@ py_binary(
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
],
|
],
|
||||||
deps = [
|
deps = [
|
||||||
":mypy_protobuf",
|
":protoc-gen-mypy",
|
||||||
"@rules_python//python/runfiles",
|
"@rules_python//python/runfiles",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
@ -36,8 +36,7 @@ class Hook:
|
||||||
types = []
|
types = []
|
||||||
for arg in self.args or []:
|
for arg in self.args or []:
|
||||||
(name, type) = arg.split(":")
|
(name, type) = arg.split(":")
|
||||||
if "." in type:
|
type = '"' + type.strip() + '"'
|
||||||
type = '"' + type.strip() + '"'
|
|
||||||
types.append(type)
|
types.append(type)
|
||||||
types_str = ", ".join(types)
|
types_str = ", ".join(types)
|
||||||
return f"Callable[[{types_str}], {self.return_type or 'None'}]"
|
return f"Callable[[{types_str}], {self.return_type or 'None'}]"
|
||||||
|
|
9
pylib/tools/protoc-gen-mypy.py
Executable file
9
pylib/tools/protoc-gen-mypy.py
Executable file
|
@ -0,0 +1,9 @@
|
||||||
|
# copied from mypy_protobuf:bin - simple launch wrapper
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from mypy_protobuf import main
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0])
|
||||||
|
sys.exit(main())
|
|
@ -10,7 +10,7 @@ import os
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import traceback
|
import traceback
|
||||||
from typing import Any, Callable, Dict, Optional, Union
|
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
||||||
|
|
||||||
import anki.lang
|
import anki.lang
|
||||||
from anki import version as _version
|
from anki import version as _version
|
||||||
|
@ -102,10 +102,10 @@ class DialogManager:
|
||||||
self._dialogs[name][1] = instance
|
self._dialogs[name][1] = instance
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
def markClosed(self, name: str):
|
def markClosed(self, name: str) -> None:
|
||||||
self._dialogs[name] = [self._dialogs[name][0], None]
|
self._dialogs[name] = [self._dialogs[name][0], None]
|
||||||
|
|
||||||
def allClosed(self):
|
def allClosed(self) -> bool:
|
||||||
return not any(x[1] for x in self._dialogs.values())
|
return not any(x[1] for x in self._dialogs.values())
|
||||||
|
|
||||||
def closeAll(self, onsuccess: Callable[[], None]) -> Optional[bool]:
|
def closeAll(self, onsuccess: Callable[[], None]) -> Optional[bool]:
|
||||||
|
@ -119,7 +119,7 @@ class DialogManager:
|
||||||
if not instance:
|
if not instance:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
def callback():
|
def callback() -> None:
|
||||||
if self.allClosed():
|
if self.allClosed():
|
||||||
onsuccess()
|
onsuccess()
|
||||||
else:
|
else:
|
||||||
|
@ -189,12 +189,12 @@ def setupLangAndBackend(
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# add _ and ngettext globals used by legacy code
|
# add _ and ngettext globals used by legacy code
|
||||||
def fn__(arg):
|
def fn__(arg) -> None:
|
||||||
print("".join(traceback.format_stack()[-2]))
|
print("".join(traceback.format_stack()[-2]))
|
||||||
print("_ global will break in the future; please see anki/lang.py")
|
print("_ global will break in the future; please see anki/lang.py")
|
||||||
return arg
|
return arg
|
||||||
|
|
||||||
def fn_ngettext(a, b, c):
|
def fn_ngettext(a, b, c) -> None:
|
||||||
print("".join(traceback.format_stack()[-2]))
|
print("".join(traceback.format_stack()[-2]))
|
||||||
print("ngettext global will break in the future; please see anki/lang.py")
|
print("ngettext global will break in the future; please see anki/lang.py")
|
||||||
return b
|
return b
|
||||||
|
@ -244,11 +244,11 @@ class AnkiApp(QApplication):
|
||||||
KEY = "anki" + checksum(getpass.getuser())
|
KEY = "anki" + checksum(getpass.getuser())
|
||||||
TMOUT = 30000
|
TMOUT = 30000
|
||||||
|
|
||||||
def __init__(self, argv):
|
def __init__(self, argv) -> None:
|
||||||
QApplication.__init__(self, argv)
|
QApplication.__init__(self, argv)
|
||||||
self._argv = argv
|
self._argv = argv
|
||||||
|
|
||||||
def secondInstance(self):
|
def secondInstance(self) -> bool:
|
||||||
# we accept only one command line argument. if it's missing, send
|
# we accept only one command line argument. if it's missing, send
|
||||||
# a blank screen to just raise the existing window
|
# a blank screen to just raise the existing window
|
||||||
opts, args = parseArgs(self._argv)
|
opts, args = parseArgs(self._argv)
|
||||||
|
@ -267,7 +267,7 @@ class AnkiApp(QApplication):
|
||||||
self._srv.listen(self.KEY)
|
self._srv.listen(self.KEY)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def sendMsg(self, txt):
|
def sendMsg(self, txt) -> bool:
|
||||||
sock = QLocalSocket(self)
|
sock = QLocalSocket(self)
|
||||||
sock.connectToServer(self.KEY, QIODevice.WriteOnly)
|
sock.connectToServer(self.KEY, QIODevice.WriteOnly)
|
||||||
if not sock.waitForConnected(self.TMOUT):
|
if not sock.waitForConnected(self.TMOUT):
|
||||||
|
@ -286,7 +286,7 @@ class AnkiApp(QApplication):
|
||||||
sock.disconnectFromServer()
|
sock.disconnectFromServer()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def onRecv(self):
|
def onRecv(self) -> None:
|
||||||
sock = self._srv.nextPendingConnection()
|
sock = self._srv.nextPendingConnection()
|
||||||
if not sock.waitForReadyRead(self.TMOUT):
|
if not sock.waitForReadyRead(self.TMOUT):
|
||||||
sys.stderr.write(sock.errorString())
|
sys.stderr.write(sock.errorString())
|
||||||
|
@ -298,14 +298,14 @@ class AnkiApp(QApplication):
|
||||||
# OS X file/url handler
|
# OS X file/url handler
|
||||||
##################################################
|
##################################################
|
||||||
|
|
||||||
def event(self, evt):
|
def event(self, evt) -> bool:
|
||||||
if evt.type() == QEvent.FileOpen:
|
if evt.type() == QEvent.FileOpen:
|
||||||
self.appMsg.emit(evt.file() or "raise") # type: ignore
|
self.appMsg.emit(evt.file() or "raise") # type: ignore
|
||||||
return True
|
return True
|
||||||
return QApplication.event(self, evt)
|
return QApplication.event(self, evt)
|
||||||
|
|
||||||
|
|
||||||
def parseArgs(argv):
|
def parseArgs(argv) -> Tuple[argparse.Namespace, List[str]]:
|
||||||
"Returns (opts, args)."
|
"Returns (opts, args)."
|
||||||
# py2app fails to strip this in some instances, then anki dies
|
# py2app fails to strip this in some instances, then anki dies
|
||||||
# as there's no such profile
|
# as there's no such profile
|
||||||
|
@ -330,7 +330,7 @@ def parseArgs(argv):
|
||||||
return parser.parse_known_args(argv[1:])
|
return parser.parse_known_args(argv[1:])
|
||||||
|
|
||||||
|
|
||||||
def setupGL(pm):
|
def setupGL(pm) -> None:
|
||||||
if isMac:
|
if isMac:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -343,7 +343,7 @@ def setupGL(pm):
|
||||||
ctypes.CDLL("libGL.so.1", ctypes.RTLD_GLOBAL)
|
ctypes.CDLL("libGL.so.1", ctypes.RTLD_GLOBAL)
|
||||||
|
|
||||||
# catch opengl errors
|
# catch opengl errors
|
||||||
def msgHandler(category, ctx, msg):
|
def msgHandler(category, ctx, msg) -> None:
|
||||||
if category == QtDebugMsg:
|
if category == QtDebugMsg:
|
||||||
category = "debug"
|
category = "debug"
|
||||||
elif category == QtInfoMsg:
|
elif category == QtInfoMsg:
|
||||||
|
@ -400,7 +400,7 @@ def setupGL(pm):
|
||||||
PROFILE_CODE = os.environ.get("ANKI_PROFILE_CODE")
|
PROFILE_CODE = os.environ.get("ANKI_PROFILE_CODE")
|
||||||
|
|
||||||
|
|
||||||
def write_profile_results():
|
def write_profile_results() -> None:
|
||||||
|
|
||||||
profiler.disable()
|
profiler.disable()
|
||||||
profiler.dump_stats("anki.prof")
|
profiler.dump_stats("anki.prof")
|
||||||
|
@ -408,7 +408,7 @@ def write_profile_results():
|
||||||
print("use 'bazel run qt:profile' to explore")
|
print("use 'bazel run qt:profile' to explore")
|
||||||
|
|
||||||
|
|
||||||
def run():
|
def run() -> None:
|
||||||
try:
|
try:
|
||||||
_run()
|
_run()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -420,7 +420,7 @@ def run():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _run(argv=None, exec=True):
|
def _run(argv=None, exec=True) -> Optional[AnkiApp]:
|
||||||
"""Start AnkiQt application or reuse an existing instance if one exists.
|
"""Start AnkiQt application or reuse an existing instance if one exists.
|
||||||
|
|
||||||
If the function is invoked with exec=False, the AnkiQt will not enter
|
If the function is invoked with exec=False, the AnkiQt will not enter
|
||||||
|
@ -441,12 +441,12 @@ def _run(argv=None, exec=True):
|
||||||
|
|
||||||
if opts.version:
|
if opts.version:
|
||||||
print(f"Anki {appVersion}")
|
print(f"Anki {appVersion}")
|
||||||
return
|
return None
|
||||||
elif opts.syncserver:
|
elif opts.syncserver:
|
||||||
from anki.syncserver import serve
|
from anki.syncserver import serve
|
||||||
|
|
||||||
serve()
|
serve()
|
||||||
return
|
return None
|
||||||
|
|
||||||
if PROFILE_CODE:
|
if PROFILE_CODE:
|
||||||
|
|
||||||
|
@ -465,7 +465,7 @@ def _run(argv=None, exec=True):
|
||||||
except AnkiRestart as error:
|
except AnkiRestart as error:
|
||||||
if error.exitcode:
|
if error.exitcode:
|
||||||
sys.exit(error.exitcode)
|
sys.exit(error.exitcode)
|
||||||
return
|
return None
|
||||||
except:
|
except:
|
||||||
# will handle below
|
# will handle below
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
@ -500,7 +500,7 @@ def _run(argv=None, exec=True):
|
||||||
app = AnkiApp(argv)
|
app = AnkiApp(argv)
|
||||||
if app.secondInstance():
|
if app.secondInstance():
|
||||||
# we've signaled the primary instance, so we should close
|
# we've signaled the primary instance, so we should close
|
||||||
return
|
return None
|
||||||
|
|
||||||
if not pm:
|
if not pm:
|
||||||
QMessageBox.critical(
|
QMessageBox.critical(
|
||||||
|
@ -508,7 +508,7 @@ def _run(argv=None, exec=True):
|
||||||
tr(TR.QT_MISC_ERROR),
|
tr(TR.QT_MISC_ERROR),
|
||||||
tr(TR.PROFILES_COULD_NOT_CREATE_DATA_FOLDER),
|
tr(TR.PROFILES_COULD_NOT_CREATE_DATA_FOLDER),
|
||||||
)
|
)
|
||||||
return
|
return None
|
||||||
|
|
||||||
# disable icons on mac; this must be done before window created
|
# disable icons on mac; this must be done before window created
|
||||||
if isMac:
|
if isMac:
|
||||||
|
@ -548,7 +548,7 @@ def _run(argv=None, exec=True):
|
||||||
tr(TR.QT_MISC_ERROR),
|
tr(TR.QT_MISC_ERROR),
|
||||||
tr(TR.QT_MISC_NO_TEMP_FOLDER),
|
tr(TR.QT_MISC_NO_TEMP_FOLDER),
|
||||||
)
|
)
|
||||||
return
|
return None
|
||||||
|
|
||||||
if pmLoadResult.firstTime:
|
if pmLoadResult.firstTime:
|
||||||
pm.setDefaultLang(lang[0])
|
pm.setDefaultLang(lang[0])
|
||||||
|
@ -590,3 +590,5 @@ def _run(argv=None, exec=True):
|
||||||
|
|
||||||
if PROFILE_CODE:
|
if PROFILE_CODE:
|
||||||
write_profile_results()
|
write_profile_results()
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
|
@ -13,20 +13,20 @@ from aqt.utils import TR, disable_help_button, supportText, tooltip, tr
|
||||||
|
|
||||||
|
|
||||||
class ClosableQDialog(QDialog):
|
class ClosableQDialog(QDialog):
|
||||||
def reject(self):
|
def reject(self) -> None:
|
||||||
aqt.dialogs.markClosed("About")
|
aqt.dialogs.markClosed("About")
|
||||||
QDialog.reject(self)
|
QDialog.reject(self)
|
||||||
|
|
||||||
def accept(self):
|
def accept(self) -> None:
|
||||||
aqt.dialogs.markClosed("About")
|
aqt.dialogs.markClosed("About")
|
||||||
QDialog.accept(self)
|
QDialog.accept(self)
|
||||||
|
|
||||||
def closeWithCallback(self, callback):
|
def closeWithCallback(self, callback) -> None:
|
||||||
self.reject()
|
self.reject()
|
||||||
callback()
|
callback()
|
||||||
|
|
||||||
|
|
||||||
def show(mw):
|
def show(mw) -> QDialog:
|
||||||
dialog = ClosableQDialog(mw)
|
dialog = ClosableQDialog(mw)
|
||||||
disable_help_button(dialog)
|
disable_help_button(dialog)
|
||||||
mw.setupDialogGC(dialog)
|
mw.setupDialogGC(dialog)
|
||||||
|
@ -55,7 +55,7 @@ def show(mw):
|
||||||
modified = "mod"
|
modified = "mod"
|
||||||
return f"{name} ['{addon.dir_name}', {installed}, '{addon.human_version}', {modified}]"
|
return f"{name} ['{addon.dir_name}', {installed}, '{addon.human_version}', {modified}]"
|
||||||
|
|
||||||
def onCopy():
|
def onCopy() -> None:
|
||||||
addmgr = mw.addonManager
|
addmgr = mw.addonManager
|
||||||
active = []
|
active = []
|
||||||
activeids = []
|
activeids = []
|
||||||
|
|
|
@ -65,7 +65,7 @@ class AddCards(QDialog):
|
||||||
)
|
)
|
||||||
self.deckChooser = aqt.deckchooser.DeckChooser(self.mw, self.form.deckArea)
|
self.deckChooser = aqt.deckchooser.DeckChooser(self.mw, self.form.deckArea)
|
||||||
|
|
||||||
def helpRequested(self):
|
def helpRequested(self) -> None:
|
||||||
openHelp(HelpPage.ADDING_CARD_AND_NOTE)
|
openHelp(HelpPage.ADDING_CARD_AND_NOTE)
|
||||||
|
|
||||||
def setupButtons(self) -> None:
|
def setupButtons(self) -> None:
|
||||||
|
@ -137,7 +137,7 @@ class AddCards(QDialog):
|
||||||
def removeTempNote(self, note: Note) -> None:
|
def removeTempNote(self, note: Note) -> None:
|
||||||
print("removeTempNote() will go away")
|
print("removeTempNote() will go away")
|
||||||
|
|
||||||
def addHistory(self, note):
|
def addHistory(self, note: Note) -> None:
|
||||||
self.history.insert(0, note.id)
|
self.history.insert(0, note.id)
|
||||||
self.history = self.history[:15]
|
self.history = self.history[:15]
|
||||||
self.historyButton.setEnabled(True)
|
self.historyButton.setEnabled(True)
|
||||||
|
@ -161,7 +161,7 @@ class AddCards(QDialog):
|
||||||
gui_hooks.add_cards_will_show_history_menu(self, m)
|
gui_hooks.add_cards_will_show_history_menu(self, m)
|
||||||
m.exec_(self.historyButton.mapToGlobal(QPoint(0, 0)))
|
m.exec_(self.historyButton.mapToGlobal(QPoint(0, 0)))
|
||||||
|
|
||||||
def editHistory(self, nid):
|
def editHistory(self, nid) -> None:
|
||||||
aqt.dialogs.open("Browser", self.mw, search=(SearchTerm(nid=nid),))
|
aqt.dialogs.open("Browser", self.mw, search=(SearchTerm(nid=nid),))
|
||||||
|
|
||||||
def addNote(self, note) -> Optional[Note]:
|
def addNote(self, note) -> Optional[Note]:
|
||||||
|
@ -186,10 +186,10 @@ class AddCards(QDialog):
|
||||||
gui_hooks.add_cards_did_add_note(note)
|
gui_hooks.add_cards_did_add_note(note)
|
||||||
return note
|
return note
|
||||||
|
|
||||||
def addCards(self):
|
def addCards(self) -> None:
|
||||||
self.editor.saveNow(self._addCards)
|
self.editor.saveNow(self._addCards)
|
||||||
|
|
||||||
def _addCards(self):
|
def _addCards(self) -> None:
|
||||||
self.editor.saveAddModeVars()
|
self.editor.saveAddModeVars()
|
||||||
if not self.addNote(self.editor.note):
|
if not self.addNote(self.editor.note):
|
||||||
return
|
return
|
||||||
|
@ -202,7 +202,7 @@ class AddCards(QDialog):
|
||||||
self.onReset(keep=True)
|
self.onReset(keep=True)
|
||||||
self.mw.col.autosave()
|
self.mw.col.autosave()
|
||||||
|
|
||||||
def keyPressEvent(self, evt):
|
def keyPressEvent(self, evt: QKeyEvent) -> None:
|
||||||
"Show answer on RET or register answer."
|
"Show answer on RET or register answer."
|
||||||
if evt.key() in (Qt.Key_Enter, Qt.Key_Return) and self.editor.tags.hasFocus():
|
if evt.key() in (Qt.Key_Enter, Qt.Key_Return) and self.editor.tags.hasFocus():
|
||||||
evt.accept()
|
evt.accept()
|
||||||
|
@ -225,7 +225,7 @@ class AddCards(QDialog):
|
||||||
QDialog.reject(self)
|
QDialog.reject(self)
|
||||||
|
|
||||||
def ifCanClose(self, onOk: Callable) -> None:
|
def ifCanClose(self, onOk: Callable) -> None:
|
||||||
def afterSave():
|
def afterSave() -> None:
|
||||||
ok = self.editor.fieldsAreBlank(self.previousNote) or askUser(
|
ok = self.editor.fieldsAreBlank(self.previousNote) or askUser(
|
||||||
tr(TR.ADDING_CLOSE_AND_LOSE_CURRENT_INPUT), defaultno=True
|
tr(TR.ADDING_CLOSE_AND_LOSE_CURRENT_INPUT), defaultno=True
|
||||||
)
|
)
|
||||||
|
@ -234,8 +234,8 @@ class AddCards(QDialog):
|
||||||
|
|
||||||
self.editor.saveNow(afterSave)
|
self.editor.saveNow(afterSave)
|
||||||
|
|
||||||
def closeWithCallback(self, cb):
|
def closeWithCallback(self, cb) -> None:
|
||||||
def doClose():
|
def doClose() -> None:
|
||||||
self._reject()
|
self._reject()
|
||||||
cb()
|
cb()
|
||||||
|
|
||||||
|
|
|
@ -605,7 +605,7 @@ class AddonManager:
|
||||||
def _addon_schema_path(self, dir: str) -> str:
|
def _addon_schema_path(self, dir: str) -> str:
|
||||||
return os.path.join(self.addonsFolder(dir), "config.schema.json")
|
return os.path.join(self.addonsFolder(dir), "config.schema.json")
|
||||||
|
|
||||||
def _addon_schema(self, dir: str):
|
def _addon_schema(self, dir: str) -> Any:
|
||||||
path = self._addon_schema_path(dir)
|
path = self._addon_schema_path(dir)
|
||||||
try:
|
try:
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
|
@ -867,9 +867,10 @@ class AddonsDialog(QDialog):
|
||||||
def onInstallFiles(self, paths: Optional[List[str]] = None) -> Optional[bool]:
|
def onInstallFiles(self, paths: Optional[List[str]] = None) -> Optional[bool]:
|
||||||
if not paths:
|
if not paths:
|
||||||
key = tr(TR.ADDONS_PACKAGED_ANKI_ADDON) + " (*{})".format(self.mgr.ext)
|
key = tr(TR.ADDONS_PACKAGED_ANKI_ADDON) + " (*{})".format(self.mgr.ext)
|
||||||
paths = getFile(
|
paths_ = getFile(
|
||||||
self, tr(TR.ADDONS_INSTALL_ADDONS), None, key, key="addons", multi=True
|
self, tr(TR.ADDONS_INSTALL_ADDONS), None, key, key="addons", multi=True
|
||||||
)
|
)
|
||||||
|
paths = paths_ # type: ignore
|
||||||
if not paths:
|
if not paths:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -4,7 +4,7 @@
|
||||||
import copy
|
import copy
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Match, Optional
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki.cards import Card
|
from anki.cards import Card
|
||||||
|
@ -14,6 +14,7 @@ from anki.lang import without_unicode_isolation
|
||||||
from anki.notes import Note
|
from anki.notes import Note
|
||||||
from anki.template import TemplateRenderContext
|
from anki.template import TemplateRenderContext
|
||||||
from aqt import AnkiQt, gui_hooks
|
from aqt import AnkiQt, gui_hooks
|
||||||
|
from aqt.forms.browserdisp import Ui_Dialog
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.schema_change_tracker import ChangeTracker
|
from aqt.schema_change_tracker import ChangeTracker
|
||||||
from aqt.sound import av_player, play_clicked_audio
|
from aqt.sound import av_player, play_clicked_audio
|
||||||
|
@ -90,7 +91,7 @@ class CardLayout(QDialog):
|
||||||
# as users tend to accidentally type into the template
|
# as users tend to accidentally type into the template
|
||||||
self.setFocus()
|
self.setFocus()
|
||||||
|
|
||||||
def redraw_everything(self):
|
def redraw_everything(self) -> None:
|
||||||
self.ignore_change_signals = True
|
self.ignore_change_signals = True
|
||||||
self.updateTopArea()
|
self.updateTopArea()
|
||||||
self.ignore_change_signals = False
|
self.ignore_change_signals = False
|
||||||
|
@ -104,13 +105,13 @@ class CardLayout(QDialog):
|
||||||
self.fill_fields_from_template()
|
self.fill_fields_from_template()
|
||||||
self.renderPreview()
|
self.renderPreview()
|
||||||
|
|
||||||
def _isCloze(self):
|
def _isCloze(self) -> bool:
|
||||||
return self.model["type"] == MODEL_CLOZE
|
return self.model["type"] == MODEL_CLOZE
|
||||||
|
|
||||||
# Top area
|
# Top area
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def setupTopArea(self):
|
def setupTopArea(self) -> None:
|
||||||
self.topArea = QWidget()
|
self.topArea = QWidget()
|
||||||
self.topArea.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
|
self.topArea.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
|
||||||
self.topAreaForm = aqt.forms.clayout_top.Ui_Form()
|
self.topAreaForm = aqt.forms.clayout_top.Ui_Form()
|
||||||
|
@ -125,10 +126,10 @@ class CardLayout(QDialog):
|
||||||
)
|
)
|
||||||
self.topAreaForm.card_type_label.setText(tr(TR.CARD_TEMPLATES_CARD_TYPE))
|
self.topAreaForm.card_type_label.setText(tr(TR.CARD_TEMPLATES_CARD_TYPE))
|
||||||
|
|
||||||
def updateTopArea(self):
|
def updateTopArea(self) -> None:
|
||||||
self.updateCardNames()
|
self.updateCardNames()
|
||||||
|
|
||||||
def updateCardNames(self):
|
def updateCardNames(self) -> None:
|
||||||
self.ignore_change_signals = True
|
self.ignore_change_signals = True
|
||||||
combo = self.topAreaForm.templatesBox
|
combo = self.topAreaForm.templatesBox
|
||||||
combo.clear()
|
combo.clear()
|
||||||
|
@ -139,7 +140,7 @@ class CardLayout(QDialog):
|
||||||
combo.setEnabled(not self._isCloze())
|
combo.setEnabled(not self._isCloze())
|
||||||
self.ignore_change_signals = False
|
self.ignore_change_signals = False
|
||||||
|
|
||||||
def _summarizedName(self, idx: int, tmpl: Dict):
|
def _summarizedName(self, idx: int, tmpl: Dict) -> str:
|
||||||
return "{}: {}: {} -> {}".format(
|
return "{}: {}: {} -> {}".format(
|
||||||
idx + 1,
|
idx + 1,
|
||||||
tmpl["name"],
|
tmpl["name"],
|
||||||
|
@ -170,7 +171,7 @@ class CardLayout(QDialog):
|
||||||
s += "+..."
|
s += "+..."
|
||||||
return s
|
return s
|
||||||
|
|
||||||
def setupShortcuts(self):
|
def setupShortcuts(self) -> None:
|
||||||
self.tform.front_button.setToolTip(shortcut("Ctrl+1"))
|
self.tform.front_button.setToolTip(shortcut("Ctrl+1"))
|
||||||
self.tform.back_button.setToolTip(shortcut("Ctrl+2"))
|
self.tform.back_button.setToolTip(shortcut("Ctrl+2"))
|
||||||
self.tform.style_button.setToolTip(shortcut("Ctrl+3"))
|
self.tform.style_button.setToolTip(shortcut("Ctrl+3"))
|
||||||
|
@ -193,7 +194,7 @@ class CardLayout(QDialog):
|
||||||
# Main area setup
|
# Main area setup
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def setupMainArea(self):
|
def setupMainArea(self) -> None:
|
||||||
split = self.mainArea = QSplitter()
|
split = self.mainArea = QSplitter()
|
||||||
split.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
split.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||||
split.setOrientation(Qt.Horizontal)
|
split.setOrientation(Qt.Horizontal)
|
||||||
|
@ -216,7 +217,7 @@ class CardLayout(QDialog):
|
||||||
split.addWidget(right)
|
split.addWidget(right)
|
||||||
split.setCollapsible(1, False)
|
split.setCollapsible(1, False)
|
||||||
|
|
||||||
def setup_edit_area(self):
|
def setup_edit_area(self) -> None:
|
||||||
tform = self.tform
|
tform = self.tform
|
||||||
|
|
||||||
tform.front_button.setText(tr(TR.CARD_TEMPLATES_FRONT_TEMPLATE))
|
tform.front_button.setText(tr(TR.CARD_TEMPLATES_FRONT_TEMPLATE))
|
||||||
|
@ -248,7 +249,7 @@ class CardLayout(QDialog):
|
||||||
qconnect(widg.textChanged, self.on_search_changed)
|
qconnect(widg.textChanged, self.on_search_changed)
|
||||||
qconnect(widg.returnPressed, self.on_search_next)
|
qconnect(widg.returnPressed, self.on_search_next)
|
||||||
|
|
||||||
def setup_cloze_number_box(self):
|
def setup_cloze_number_box(self) -> None:
|
||||||
names = (tr(TR.CARD_TEMPLATES_CLOZE, val=n) for n in self.cloze_numbers)
|
names = (tr(TR.CARD_TEMPLATES_CLOZE, val=n) for n in self.cloze_numbers)
|
||||||
self.pform.cloze_number_combo.addItems(names)
|
self.pform.cloze_number_combo.addItems(names)
|
||||||
try:
|
try:
|
||||||
|
@ -266,7 +267,7 @@ class CardLayout(QDialog):
|
||||||
self.have_autoplayed = False
|
self.have_autoplayed = False
|
||||||
self._renderPreview()
|
self._renderPreview()
|
||||||
|
|
||||||
def on_editor_toggled(self):
|
def on_editor_toggled(self) -> None:
|
||||||
if self.tform.front_button.isChecked():
|
if self.tform.front_button.isChecked():
|
||||||
self.current_editor_index = 0
|
self.current_editor_index = 0
|
||||||
self.pform.preview_front.setChecked(True)
|
self.pform.preview_front.setChecked(True)
|
||||||
|
@ -283,7 +284,7 @@ class CardLayout(QDialog):
|
||||||
|
|
||||||
self.fill_fields_from_template()
|
self.fill_fields_from_template()
|
||||||
|
|
||||||
def on_search_changed(self, text: str):
|
def on_search_changed(self, text: str) -> None:
|
||||||
editor = self.tform.edit_area
|
editor = self.tform.edit_area
|
||||||
if not editor.find(text):
|
if not editor.find(text):
|
||||||
# try again from top
|
# try again from top
|
||||||
|
@ -293,11 +294,11 @@ class CardLayout(QDialog):
|
||||||
if not editor.find(text):
|
if not editor.find(text):
|
||||||
tooltip("No matches found.")
|
tooltip("No matches found.")
|
||||||
|
|
||||||
def on_search_next(self):
|
def on_search_next(self) -> None:
|
||||||
text = self.tform.search_edit.text()
|
text = self.tform.search_edit.text()
|
||||||
self.on_search_changed(text)
|
self.on_search_changed(text)
|
||||||
|
|
||||||
def setup_preview(self):
|
def setup_preview(self) -> None:
|
||||||
pform = self.pform
|
pform = self.pform
|
||||||
self.preview_web = AnkiWebView(title="card layout")
|
self.preview_web = AnkiWebView(title="card layout")
|
||||||
pform.verticalLayout.addWidget(self.preview_web)
|
pform.verticalLayout.addWidget(self.preview_web)
|
||||||
|
@ -336,19 +337,19 @@ class CardLayout(QDialog):
|
||||||
self.cloze_numbers = []
|
self.cloze_numbers = []
|
||||||
self.pform.cloze_number_combo.setHidden(True)
|
self.pform.cloze_number_combo.setHidden(True)
|
||||||
|
|
||||||
def on_fill_empty_action_toggled(self):
|
def on_fill_empty_action_toggled(self) -> None:
|
||||||
self.fill_empty_action_toggled = not self.fill_empty_action_toggled
|
self.fill_empty_action_toggled = not self.fill_empty_action_toggled
|
||||||
self.on_preview_toggled()
|
self.on_preview_toggled()
|
||||||
|
|
||||||
def on_night_mode_action_toggled(self):
|
def on_night_mode_action_toggled(self) -> None:
|
||||||
self.night_mode_is_enabled = not self.night_mode_is_enabled
|
self.night_mode_is_enabled = not self.night_mode_is_enabled
|
||||||
self.on_preview_toggled()
|
self.on_preview_toggled()
|
||||||
|
|
||||||
def on_mobile_class_action_toggled(self):
|
def on_mobile_class_action_toggled(self) -> None:
|
||||||
self.mobile_emulation_enabled = not self.mobile_emulation_enabled
|
self.mobile_emulation_enabled = not self.mobile_emulation_enabled
|
||||||
self.on_preview_toggled()
|
self.on_preview_toggled()
|
||||||
|
|
||||||
def on_preview_settings(self):
|
def on_preview_settings(self) -> None:
|
||||||
m = QMenu(self)
|
m = QMenu(self)
|
||||||
|
|
||||||
a = m.addAction(tr(TR.CARD_TEMPLATES_FILL_EMPTY))
|
a = m.addAction(tr(TR.CARD_TEMPLATES_FILL_EMPTY))
|
||||||
|
@ -370,7 +371,7 @@ class CardLayout(QDialog):
|
||||||
|
|
||||||
m.exec_(self.pform.preview_settings.mapToGlobal(QPoint(0, 0)))
|
m.exec_(self.pform.preview_settings.mapToGlobal(QPoint(0, 0)))
|
||||||
|
|
||||||
def on_preview_toggled(self):
|
def on_preview_toggled(self) -> None:
|
||||||
self.have_autoplayed = False
|
self.have_autoplayed = False
|
||||||
self._renderPreview()
|
self._renderPreview()
|
||||||
|
|
||||||
|
@ -388,7 +389,7 @@ class CardLayout(QDialog):
|
||||||
# Buttons
|
# Buttons
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def setupButtons(self):
|
def setupButtons(self) -> None:
|
||||||
l = self.buttons = QHBoxLayout()
|
l = self.buttons = QHBoxLayout()
|
||||||
help = QPushButton(tr(TR.ACTIONS_HELP))
|
help = QPushButton(tr(TR.ACTIONS_HELP))
|
||||||
help.setAutoDefault(False)
|
help.setAutoDefault(False)
|
||||||
|
@ -424,7 +425,7 @@ class CardLayout(QDialog):
|
||||||
return self.templates[0]
|
return self.templates[0]
|
||||||
return self.templates[self.ord]
|
return self.templates[self.ord]
|
||||||
|
|
||||||
def fill_fields_from_template(self):
|
def fill_fields_from_template(self) -> None:
|
||||||
t = self.current_template()
|
t = self.current_template()
|
||||||
self.ignore_change_signals = True
|
self.ignore_change_signals = True
|
||||||
|
|
||||||
|
@ -438,7 +439,7 @@ class CardLayout(QDialog):
|
||||||
self.tform.edit_area.setPlainText(text)
|
self.tform.edit_area.setPlainText(text)
|
||||||
self.ignore_change_signals = False
|
self.ignore_change_signals = False
|
||||||
|
|
||||||
def write_edits_to_template_and_redraw(self):
|
def write_edits_to_template_and_redraw(self) -> None:
|
||||||
if self.ignore_change_signals:
|
if self.ignore_change_signals:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -458,14 +459,14 @@ class CardLayout(QDialog):
|
||||||
# Preview
|
# Preview
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
_previewTimer = None
|
_previewTimer: Optional[QTimer] = None
|
||||||
|
|
||||||
def renderPreview(self):
|
def renderPreview(self) -> None:
|
||||||
# schedule a preview when timing stops
|
# schedule a preview when timing stops
|
||||||
self.cancelPreviewTimer()
|
self.cancelPreviewTimer()
|
||||||
self._previewTimer = self.mw.progress.timer(200, self._renderPreview, False)
|
self._previewTimer = self.mw.progress.timer(200, self._renderPreview, False)
|
||||||
|
|
||||||
def cancelPreviewTimer(self):
|
def cancelPreviewTimer(self) -> None:
|
||||||
if self._previewTimer:
|
if self._previewTimer:
|
||||||
self._previewTimer.stop()
|
self._previewTimer.stop()
|
||||||
self._previewTimer = None
|
self._previewTimer = None
|
||||||
|
@ -512,14 +513,14 @@ class CardLayout(QDialog):
|
||||||
|
|
||||||
self.updateCardNames()
|
self.updateCardNames()
|
||||||
|
|
||||||
def maybeTextInput(self, txt, type="q"):
|
def maybeTextInput(self, txt: str, type: str = "q") -> str:
|
||||||
if "[[type:" not in txt:
|
if "[[type:" not in txt:
|
||||||
return txt
|
return txt
|
||||||
origLen = len(txt)
|
origLen = len(txt)
|
||||||
txt = txt.replace("<hr id=answer>", "")
|
txt = txt.replace("<hr id=answer>", "")
|
||||||
hadHR = origLen != len(txt)
|
hadHR = origLen != len(txt)
|
||||||
|
|
||||||
def answerRepl(match):
|
def answerRepl(match: Match) -> str:
|
||||||
res = self.mw.reviewer.correct("exomple", "an example")
|
res = self.mw.reviewer.correct("exomple", "an example")
|
||||||
if hadHR:
|
if hadHR:
|
||||||
res = "<hr id=answer>" + res
|
res = "<hr id=answer>" + res
|
||||||
|
@ -555,14 +556,15 @@ class CardLayout(QDialog):
|
||||||
# Card operations
|
# Card operations
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def onRemove(self):
|
def onRemove(self) -> None:
|
||||||
if len(self.templates) < 2:
|
if len(self.templates) < 2:
|
||||||
return showInfo(tr(TR.CARD_TEMPLATES_AT_LEAST_ONE_CARD_TYPE_IS))
|
showInfo(tr(TR.CARD_TEMPLATES_AT_LEAST_ONE_CARD_TYPE_IS))
|
||||||
|
return
|
||||||
|
|
||||||
def get_count():
|
def get_count() -> int:
|
||||||
return self.mm.template_use_count(self.model["id"], self.ord)
|
return self.mm.template_use_count(self.model["id"], self.ord)
|
||||||
|
|
||||||
def on_done(fut):
|
def on_done(fut) -> None:
|
||||||
card_cnt = fut.result()
|
card_cnt = fut.result()
|
||||||
|
|
||||||
template = self.current_template()
|
template = self.current_template()
|
||||||
|
@ -592,7 +594,7 @@ class CardLayout(QDialog):
|
||||||
|
|
||||||
self.redraw_everything()
|
self.redraw_everything()
|
||||||
|
|
||||||
def onRename(self):
|
def onRename(self) -> None:
|
||||||
template = self.current_template()
|
template = self.current_template()
|
||||||
name = getOnlyText(tr(TR.ACTIONS_NEW_NAME), default=template["name"]).replace(
|
name = getOnlyText(tr(TR.ACTIONS_NEW_NAME), default=template["name"]).replace(
|
||||||
'"', ""
|
'"', ""
|
||||||
|
@ -603,18 +605,18 @@ class CardLayout(QDialog):
|
||||||
template["name"] = name
|
template["name"] = name
|
||||||
self.redraw_everything()
|
self.redraw_everything()
|
||||||
|
|
||||||
def onReorder(self):
|
def onReorder(self) -> None:
|
||||||
n = len(self.templates)
|
n = len(self.templates)
|
||||||
template = self.current_template()
|
template = self.current_template()
|
||||||
current_pos = self.templates.index(template) + 1
|
current_pos = self.templates.index(template) + 1
|
||||||
pos = getOnlyText(
|
pos_txt = getOnlyText(
|
||||||
tr(TR.CARD_TEMPLATES_ENTER_NEW_CARD_POSITION_1, val=n),
|
tr(TR.CARD_TEMPLATES_ENTER_NEW_CARD_POSITION_1, val=n),
|
||||||
default=str(current_pos),
|
default=str(current_pos),
|
||||||
)
|
)
|
||||||
if not pos:
|
if not pos_txt:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
pos = int(pos)
|
pos = int(pos_txt)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return
|
return
|
||||||
if pos < 1 or pos > n:
|
if pos < 1 or pos > n:
|
||||||
|
@ -628,7 +630,7 @@ class CardLayout(QDialog):
|
||||||
self.ord = new_idx
|
self.ord = new_idx
|
||||||
self.redraw_everything()
|
self.redraw_everything()
|
||||||
|
|
||||||
def _newCardName(self):
|
def _newCardName(self) -> str:
|
||||||
n = len(self.templates) + 1
|
n = len(self.templates) + 1
|
||||||
while 1:
|
while 1:
|
||||||
name = without_unicode_isolation(tr(TR.CARD_TEMPLATES_CARD, val=n))
|
name = without_unicode_isolation(tr(TR.CARD_TEMPLATES_CARD, val=n))
|
||||||
|
@ -637,7 +639,7 @@ class CardLayout(QDialog):
|
||||||
n += 1
|
n += 1
|
||||||
return name
|
return name
|
||||||
|
|
||||||
def onAddCard(self):
|
def onAddCard(self) -> None:
|
||||||
cnt = self.mw.col.models.useCount(self.model)
|
cnt = self.mw.col.models.useCount(self.model)
|
||||||
txt = tr(TR.CARD_TEMPLATES_THIS_WILL_CREATE_CARD_PROCEED, count=cnt)
|
txt = tr(TR.CARD_TEMPLATES_THIS_WILL_CREATE_CARD_PROCEED, count=cnt)
|
||||||
if not askUser(txt):
|
if not askUser(txt):
|
||||||
|
@ -653,12 +655,12 @@ class CardLayout(QDialog):
|
||||||
self.ord = len(self.templates) - 1
|
self.ord = len(self.templates) - 1
|
||||||
self.redraw_everything()
|
self.redraw_everything()
|
||||||
|
|
||||||
def onFlip(self):
|
def onFlip(self) -> None:
|
||||||
old = self.current_template()
|
old = self.current_template()
|
||||||
self._flipQA(old, old)
|
self._flipQA(old, old)
|
||||||
self.redraw_everything()
|
self.redraw_everything()
|
||||||
|
|
||||||
def _flipQA(self, src, dst):
|
def _flipQA(self, src, dst) -> None:
|
||||||
m = re.match("(?s)(.+)<hr id=answer>(.+)", src["afmt"])
|
m = re.match("(?s)(.+)<hr id=answer>(.+)", src["afmt"])
|
||||||
if not m:
|
if not m:
|
||||||
showInfo(tr(TR.CARD_TEMPLATES_ANKI_COULDNT_FIND_THE_LINE_BETWEEN))
|
showInfo(tr(TR.CARD_TEMPLATES_ANKI_COULDNT_FIND_THE_LINE_BETWEEN))
|
||||||
|
@ -666,9 +668,8 @@ class CardLayout(QDialog):
|
||||||
self.change_tracker.mark_basic()
|
self.change_tracker.mark_basic()
|
||||||
dst["afmt"] = "{{FrontSide}}\n\n<hr id=answer>\n\n%s" % src["qfmt"]
|
dst["afmt"] = "{{FrontSide}}\n\n<hr id=answer>\n\n%s" % src["qfmt"]
|
||||||
dst["qfmt"] = m.group(2).strip()
|
dst["qfmt"] = m.group(2).strip()
|
||||||
return True
|
|
||||||
|
|
||||||
def onMore(self):
|
def onMore(self) -> None:
|
||||||
m = QMenu(self)
|
m = QMenu(self)
|
||||||
|
|
||||||
if not self._isCloze():
|
if not self._isCloze():
|
||||||
|
@ -699,7 +700,7 @@ class CardLayout(QDialog):
|
||||||
|
|
||||||
m.exec_(self.topAreaForm.templateOptions.mapToGlobal(QPoint(0, 0)))
|
m.exec_(self.topAreaForm.templateOptions.mapToGlobal(QPoint(0, 0)))
|
||||||
|
|
||||||
def onBrowserDisplay(self):
|
def onBrowserDisplay(self) -> None:
|
||||||
d = QDialog()
|
d = QDialog()
|
||||||
disable_help_button(d)
|
disable_help_button(d)
|
||||||
f = aqt.forms.browserdisp.Ui_Dialog()
|
f = aqt.forms.browserdisp.Ui_Dialog()
|
||||||
|
@ -714,7 +715,7 @@ class CardLayout(QDialog):
|
||||||
qconnect(f.buttonBox.accepted, lambda: self.onBrowserDisplayOk(f))
|
qconnect(f.buttonBox.accepted, lambda: self.onBrowserDisplayOk(f))
|
||||||
d.exec_()
|
d.exec_()
|
||||||
|
|
||||||
def onBrowserDisplayOk(self, f):
|
def onBrowserDisplayOk(self, f: Ui_Dialog) -> None:
|
||||||
t = self.current_template()
|
t = self.current_template()
|
||||||
self.change_tracker.mark_basic()
|
self.change_tracker.mark_basic()
|
||||||
t["bqfmt"] = f.qfmt.text().strip()
|
t["bqfmt"] = f.qfmt.text().strip()
|
||||||
|
@ -727,7 +728,7 @@ class CardLayout(QDialog):
|
||||||
if key in t:
|
if key in t:
|
||||||
del t[key]
|
del t[key]
|
||||||
|
|
||||||
def onTargetDeck(self):
|
def onTargetDeck(self) -> None:
|
||||||
from aqt.tagedit import TagEdit
|
from aqt.tagedit import TagEdit
|
||||||
|
|
||||||
t = self.current_template()
|
t = self.current_template()
|
||||||
|
@ -759,7 +760,7 @@ class CardLayout(QDialog):
|
||||||
else:
|
else:
|
||||||
t["did"] = self.col.decks.id(te.text())
|
t["did"] = self.col.decks.id(te.text())
|
||||||
|
|
||||||
def onAddField(self):
|
def onAddField(self) -> None:
|
||||||
diag = QDialog(self)
|
diag = QDialog(self)
|
||||||
form = aqt.forms.addfield.Ui_Dialog()
|
form = aqt.forms.addfield.Ui_Dialog()
|
||||||
form.setupUi(diag)
|
form.setupUi(diag)
|
||||||
|
@ -779,7 +780,7 @@ class CardLayout(QDialog):
|
||||||
form.size.value(),
|
form.size.value(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _addField(self, field, font, size):
|
def _addField(self, field, font, size) -> None:
|
||||||
text = self.tform.edit_area.toPlainText()
|
text = self.tform.edit_area.toPlainText()
|
||||||
text += "\n<div style='font-family: %s; font-size: %spx;'>{{%s}}</div>\n" % (
|
text += "\n<div style='font-family: %s; font-size: %spx;'>{{%s}}</div>\n" % (
|
||||||
font,
|
font,
|
||||||
|
@ -794,10 +795,10 @@ class CardLayout(QDialog):
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def accept(self) -> None:
|
def accept(self) -> None:
|
||||||
def save():
|
def save() -> None:
|
||||||
self.mm.save(self.model)
|
self.mm.save(self.model)
|
||||||
|
|
||||||
def on_done(fut):
|
def on_done(fut) -> None:
|
||||||
try:
|
try:
|
||||||
fut.result()
|
fut.result()
|
||||||
except TemplateError as e:
|
except TemplateError as e:
|
||||||
|
@ -828,5 +829,5 @@ class CardLayout(QDialog):
|
||||||
self.rendered_card = None
|
self.rendered_card = None
|
||||||
self.mw = None
|
self.mw = None
|
||||||
|
|
||||||
def onHelp(self):
|
def onHelp(self) -> None:
|
||||||
openHelp(HelpPage.TEMPLATES)
|
openHelp(HelpPage.TEMPLATES)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# Copyright: Ankitects Pty Ltd and contributors
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki.collection import SearchTerm
|
from anki.collection import SearchTerm
|
||||||
from anki.consts import *
|
from anki.consts import *
|
||||||
|
@ -35,7 +36,7 @@ class CustomStudy(QDialog):
|
||||||
f.radioNew.click()
|
f.radioNew.click()
|
||||||
self.exec_()
|
self.exec_()
|
||||||
|
|
||||||
def setupSignals(self):
|
def setupSignals(self) -> None:
|
||||||
f = self.form
|
f = self.form
|
||||||
qconnect(f.radioNew.clicked, lambda: self.onRadioChange(RADIO_NEW))
|
qconnect(f.radioNew.clicked, lambda: self.onRadioChange(RADIO_NEW))
|
||||||
qconnect(f.radioRev.clicked, lambda: self.onRadioChange(RADIO_REV))
|
qconnect(f.radioRev.clicked, lambda: self.onRadioChange(RADIO_REV))
|
||||||
|
@ -44,7 +45,7 @@ class CustomStudy(QDialog):
|
||||||
qconnect(f.radioPreview.clicked, lambda: self.onRadioChange(RADIO_PREVIEW))
|
qconnect(f.radioPreview.clicked, lambda: self.onRadioChange(RADIO_PREVIEW))
|
||||||
qconnect(f.radioCram.clicked, lambda: self.onRadioChange(RADIO_CRAM))
|
qconnect(f.radioCram.clicked, lambda: self.onRadioChange(RADIO_CRAM))
|
||||||
|
|
||||||
def onRadioChange(self, idx):
|
def onRadioChange(self, idx: int) -> None:
|
||||||
f = self.form
|
f = self.form
|
||||||
sp = f.spin
|
sp = f.spin
|
||||||
smin = 1
|
smin = 1
|
||||||
|
@ -56,7 +57,7 @@ class CustomStudy(QDialog):
|
||||||
typeShow = False
|
typeShow = False
|
||||||
ok = tr(TR.CUSTOM_STUDY_OK)
|
ok = tr(TR.CUSTOM_STUDY_OK)
|
||||||
|
|
||||||
def plus(num):
|
def plus(num) -> str:
|
||||||
if num == 1000:
|
if num == 1000:
|
||||||
num = "1000+"
|
num = "1000+"
|
||||||
return "<b>" + str(num) + "</b>"
|
return "<b>" + str(num) + "</b>"
|
||||||
|
@ -123,7 +124,7 @@ class CustomStudy(QDialog):
|
||||||
f.buttonBox.button(QDialogButtonBox.Ok).setText(ok)
|
f.buttonBox.button(QDialogButtonBox.Ok).setText(ok)
|
||||||
self.radioIdx = idx
|
self.radioIdx = idx
|
||||||
|
|
||||||
def accept(self):
|
def accept(self) -> None:
|
||||||
f = self.form
|
f = self.form
|
||||||
i = self.radioIdx
|
i = self.radioIdx
|
||||||
spin = f.spin.value()
|
spin = f.spin.value()
|
||||||
|
@ -132,13 +133,15 @@ class CustomStudy(QDialog):
|
||||||
self.mw.col.decks.save(self.deck)
|
self.mw.col.decks.save(self.deck)
|
||||||
self.mw.col.sched.extendLimits(spin, 0)
|
self.mw.col.sched.extendLimits(spin, 0)
|
||||||
self.mw.reset()
|
self.mw.reset()
|
||||||
return QDialog.accept(self)
|
QDialog.accept(self)
|
||||||
|
return
|
||||||
elif i == RADIO_REV:
|
elif i == RADIO_REV:
|
||||||
self.deck["extendRev"] = spin
|
self.deck["extendRev"] = spin
|
||||||
self.mw.col.decks.save(self.deck)
|
self.mw.col.decks.save(self.deck)
|
||||||
self.mw.col.sched.extendLimits(0, spin)
|
self.mw.col.sched.extendLimits(0, spin)
|
||||||
self.mw.reset()
|
self.mw.reset()
|
||||||
return QDialog.accept(self)
|
QDialog.accept(self)
|
||||||
|
return
|
||||||
elif i == RADIO_CRAM:
|
elif i == RADIO_CRAM:
|
||||||
tags = self._getTags()
|
tags = self._getTags()
|
||||||
# the rest create a filtered deck
|
# the rest create a filtered deck
|
||||||
|
@ -146,7 +149,8 @@ class CustomStudy(QDialog):
|
||||||
if cur:
|
if cur:
|
||||||
if not cur["dyn"]:
|
if not cur["dyn"]:
|
||||||
showInfo(tr(TR.CUSTOM_STUDY_MUST_RENAME_DECK))
|
showInfo(tr(TR.CUSTOM_STUDY_MUST_RENAME_DECK))
|
||||||
return QDialog.accept(self)
|
QDialog.accept(self)
|
||||||
|
return
|
||||||
else:
|
else:
|
||||||
# safe to empty
|
# safe to empty
|
||||||
self.mw.col.sched.empty_filtered_deck(cur["id"])
|
self.mw.col.sched.empty_filtered_deck(cur["id"])
|
||||||
|
@ -211,7 +215,8 @@ class CustomStudy(QDialog):
|
||||||
# generate cards
|
# generate cards
|
||||||
self.created_custom_study = True
|
self.created_custom_study = True
|
||||||
if not self.mw.col.sched.rebuild_filtered_deck(dyn["id"]):
|
if not self.mw.col.sched.rebuild_filtered_deck(dyn["id"]):
|
||||||
return showWarning(tr(TR.CUSTOM_STUDY_NO_CARDS_MATCHED_THE_CRITERIA_YOU))
|
showWarning(tr(TR.CUSTOM_STUDY_NO_CARDS_MATCHED_THE_CRITERIA_YOU))
|
||||||
|
return
|
||||||
self.mw.moveToState("overview")
|
self.mw.moveToState("overview")
|
||||||
QDialog.accept(self)
|
QDialog.accept(self)
|
||||||
|
|
||||||
|
@ -222,7 +227,7 @@ class CustomStudy(QDialog):
|
||||||
# fixme: clean up the empty custom study deck
|
# fixme: clean up the empty custom study deck
|
||||||
QDialog.reject(self)
|
QDialog.reject(self)
|
||||||
|
|
||||||
def _getTags(self):
|
def _getTags(self) -> str:
|
||||||
from aqt.taglimit import TagLimit
|
from aqt.taglimit import TagLimit
|
||||||
|
|
||||||
return TagLimit(self.mw, self).tags
|
return TagLimit(self.mw, self).tags
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
|
load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
|
||||||
|
load("//qt/aqt/data/web/pages:defs.bzl", "copy_page")
|
||||||
load("compile_sass.bzl", "compile_sass")
|
load("compile_sass.bzl", "compile_sass")
|
||||||
|
|
||||||
compile_sass(
|
compile_sass(
|
||||||
|
@ -16,11 +17,21 @@ copy_file(
|
||||||
out = "core.css",
|
out = "core.css",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
copy_page(
|
||||||
|
name = "editor",
|
||||||
|
srcs = [
|
||||||
|
"editor.css",
|
||||||
|
"editable.css",
|
||||||
|
],
|
||||||
|
package = "//ts/editor",
|
||||||
|
)
|
||||||
|
|
||||||
filegroup(
|
filegroup(
|
||||||
name = "css",
|
name = "css",
|
||||||
srcs = [
|
srcs = [
|
||||||
"core.css",
|
"core.css",
|
||||||
"css_local",
|
"css_local",
|
||||||
|
"editor",
|
||||||
],
|
],
|
||||||
visibility = ["//qt:__subpackages__"],
|
visibility = ["//qt:__subpackages__"],
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
load("@npm//@bazel/typescript:index.bzl", "ts_library")
|
load("@npm//@bazel/typescript:index.bzl", "ts_library")
|
||||||
|
load("@npm//@bazel/rollup:index.bzl", "rollup_bundle")
|
||||||
|
load("//qt/aqt/data/web/pages:defs.bzl", "copy_page")
|
||||||
load("//ts:prettier.bzl", "prettier_test")
|
load("//ts:prettier.bzl", "prettier_test")
|
||||||
|
load("//ts:eslint.bzl", "eslint_test")
|
||||||
|
|
||||||
ts_library(
|
ts_library(
|
||||||
name = "pycmd",
|
name = "pycmd",
|
||||||
srcs = ["pycmd.d.ts"],
|
srcs = ["pycmd.d.ts"],
|
||||||
|
visibility = ["//qt/aqt/data/web/js:__subpackages__"],
|
||||||
)
|
)
|
||||||
|
|
||||||
ts_library(
|
ts_library(
|
||||||
|
@ -26,10 +30,19 @@ filegroup(
|
||||||
output_group = "es5_sources",
|
output_group = "es5_sources",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
copy_page(
|
||||||
|
name = "editor",
|
||||||
|
srcs = [
|
||||||
|
"editor.js",
|
||||||
|
],
|
||||||
|
package = "//ts/editor",
|
||||||
|
)
|
||||||
|
|
||||||
filegroup(
|
filegroup(
|
||||||
name = "js",
|
name = "js",
|
||||||
srcs = [
|
srcs = [
|
||||||
"aqt_es5",
|
"aqt_es5",
|
||||||
|
"editor",
|
||||||
"mathjax.js",
|
"mathjax.js",
|
||||||
"//qt/aqt/data/web/js/vendor",
|
"//qt/aqt/data/web/js/vendor",
|
||||||
],
|
],
|
||||||
|
@ -47,4 +60,7 @@ prettier_test(
|
||||||
# srcs = glob(["*.ts"]),
|
# srcs = glob(["*.ts"]),
|
||||||
# )
|
# )
|
||||||
|
|
||||||
exports_files(["mathjax.js"])
|
exports_files([
|
||||||
|
"mathjax.js",
|
||||||
|
"tsconfig.json",
|
||||||
|
])
|
||||||
|
|
|
@ -9,7 +9,7 @@ from aqt.qt import *
|
||||||
from aqt.utils import showText, tooltip
|
from aqt.utils import showText, tooltip
|
||||||
|
|
||||||
|
|
||||||
def on_progress(mw: aqt.main.AnkiQt):
|
def on_progress(mw: aqt.main.AnkiQt) -> None:
|
||||||
progress = mw.col.latest_progress()
|
progress = mw.col.latest_progress()
|
||||||
if progress.kind != ProgressKind.DatabaseCheck:
|
if progress.kind != ProgressKind.DatabaseCheck:
|
||||||
return
|
return
|
||||||
|
@ -24,14 +24,14 @@ def on_progress(mw: aqt.main.AnkiQt):
|
||||||
|
|
||||||
|
|
||||||
def check_db(mw: aqt.AnkiQt) -> None:
|
def check_db(mw: aqt.AnkiQt) -> None:
|
||||||
def on_timer():
|
def on_timer() -> None:
|
||||||
on_progress(mw)
|
on_progress(mw)
|
||||||
|
|
||||||
timer = QTimer(mw)
|
timer = QTimer(mw)
|
||||||
qconnect(timer.timeout, on_timer)
|
qconnect(timer.timeout, on_timer)
|
||||||
timer.start(100)
|
timer.start(100)
|
||||||
|
|
||||||
def on_future_done(fut):
|
def on_future_done(fut) -> None:
|
||||||
timer.stop()
|
timer.stop()
|
||||||
ret, ok = fut.result()
|
ret, ok = fut.result()
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ from aqt.utils import TR, askUser, getOnlyText, openLink, shortcut, showWarning,
|
||||||
|
|
||||||
|
|
||||||
class DeckBrowserBottomBar:
|
class DeckBrowserBottomBar:
|
||||||
def __init__(self, deck_browser: DeckBrowser):
|
def __init__(self, deck_browser: DeckBrowser) -> None:
|
||||||
self.deck_browser = deck_browser
|
self.deck_browser = deck_browser
|
||||||
|
|
||||||
|
|
||||||
|
@ -51,14 +51,14 @@ class DeckBrowser:
|
||||||
self.bottom = BottomBar(mw, mw.bottomWeb)
|
self.bottom = BottomBar(mw, mw.bottomWeb)
|
||||||
self.scrollPos = QPoint(0, 0)
|
self.scrollPos = QPoint(0, 0)
|
||||||
|
|
||||||
def show(self):
|
def show(self) -> None:
|
||||||
av_player.stop_and_clear_queue()
|
av_player.stop_and_clear_queue()
|
||||||
self.web.set_bridge_command(self._linkHandler, self)
|
self.web.set_bridge_command(self._linkHandler, self)
|
||||||
self._renderPage()
|
self._renderPage()
|
||||||
# redraw top bar for theme change
|
# redraw top bar for theme change
|
||||||
self.mw.toolbar.redraw()
|
self.mw.toolbar.redraw()
|
||||||
|
|
||||||
def refresh(self):
|
def refresh(self) -> None:
|
||||||
self._renderPage()
|
self._renderPage()
|
||||||
|
|
||||||
# Event handlers
|
# Event handlers
|
||||||
|
@ -90,7 +90,7 @@ class DeckBrowser:
|
||||||
self._collapse(int(arg))
|
self._collapse(int(arg))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _selDeck(self, did):
|
def _selDeck(self, did) -> None:
|
||||||
self.mw.col.decks.select(did)
|
self.mw.col.decks.select(did)
|
||||||
self.mw.onOverview()
|
self.mw.onOverview()
|
||||||
|
|
||||||
|
@ -108,14 +108,14 @@ class DeckBrowser:
|
||||||
</center>
|
</center>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _renderPage(self, reuse=False):
|
def _renderPage(self, reuse=False) -> None:
|
||||||
if not reuse:
|
if not reuse:
|
||||||
self._dueTree = self.mw.col.sched.deck_due_tree()
|
self._dueTree = self.mw.col.sched.deck_due_tree()
|
||||||
self.__renderPage(None)
|
self.__renderPage(None)
|
||||||
return
|
return
|
||||||
self.web.evalWithCallback("window.pageYOffset", self.__renderPage)
|
self.web.evalWithCallback("window.pageYOffset", self.__renderPage)
|
||||||
|
|
||||||
def __renderPage(self, offset):
|
def __renderPage(self, offset) -> None:
|
||||||
content = DeckBrowserContent(
|
content = DeckBrowserContent(
|
||||||
tree=self._renderDeckTree(self._dueTree),
|
tree=self._renderDeckTree(self._dueTree),
|
||||||
stats=self._renderStats(),
|
stats=self._renderStats(),
|
||||||
|
@ -137,10 +137,10 @@ class DeckBrowser:
|
||||||
self._scrollToOffset(offset)
|
self._scrollToOffset(offset)
|
||||||
gui_hooks.deck_browser_did_render(self)
|
gui_hooks.deck_browser_did_render(self)
|
||||||
|
|
||||||
def _scrollToOffset(self, offset):
|
def _scrollToOffset(self, offset) -> None:
|
||||||
self.web.eval("$(function() { window.scrollTo(0, %d, 'instant'); });" % offset)
|
self.web.eval("$(function() { window.scrollTo(0, %d, 'instant'); });" % offset)
|
||||||
|
|
||||||
def _renderStats(self):
|
def _renderStats(self) -> str:
|
||||||
return '<div id="studiedToday"><span>{}</span></div>'.format(
|
return '<div id="studiedToday"><span>{}</span></div>'.format(
|
||||||
self.mw.col.studied_today(),
|
self.mw.col.studied_today(),
|
||||||
)
|
)
|
||||||
|
@ -170,7 +170,7 @@ class DeckBrowser:
|
||||||
|
|
||||||
due = node.review_count + node.learn_count
|
due = node.review_count + node.learn_count
|
||||||
|
|
||||||
def indent():
|
def indent() -> str:
|
||||||
return " " * 6 * (node.level - 1)
|
return " " * 6 * (node.level - 1)
|
||||||
|
|
||||||
if node.deck_id == ctx.current_deck_id:
|
if node.deck_id == ctx.current_deck_id:
|
||||||
|
@ -202,7 +202,7 @@ class DeckBrowser:
|
||||||
node.name,
|
node.name,
|
||||||
)
|
)
|
||||||
# due counts
|
# due counts
|
||||||
def nonzeroColour(cnt, klass):
|
def nonzeroColour(cnt: int, klass: str) -> str:
|
||||||
if not cnt:
|
if not cnt:
|
||||||
klass = "zero-count"
|
klass = "zero-count"
|
||||||
return f'<span class="{klass}">{cnt}</span>'
|
return f'<span class="{klass}">{cnt}</span>'
|
||||||
|
@ -222,7 +222,7 @@ class DeckBrowser:
|
||||||
buf += self._render_deck_node(child, ctx)
|
buf += self._render_deck_node(child, ctx)
|
||||||
return buf
|
return buf
|
||||||
|
|
||||||
def _topLevelDragRow(self):
|
def _topLevelDragRow(self) -> str:
|
||||||
return "<tr class='top-level-drag-row'><td colspan='6'> </td></tr>"
|
return "<tr class='top-level-drag-row'><td colspan='6'> </td></tr>"
|
||||||
|
|
||||||
# Options
|
# Options
|
||||||
|
@ -235,13 +235,13 @@ class DeckBrowser:
|
||||||
a = m.addAction(tr(TR.ACTIONS_OPTIONS))
|
a = m.addAction(tr(TR.ACTIONS_OPTIONS))
|
||||||
qconnect(a.triggered, lambda b, did=did: self._options(did))
|
qconnect(a.triggered, lambda b, did=did: self._options(did))
|
||||||
a = m.addAction(tr(TR.ACTIONS_EXPORT))
|
a = m.addAction(tr(TR.ACTIONS_EXPORT))
|
||||||
qconnect(a.triggered, lambda b, did=did: self._export(did))
|
qconnect(a.triggered, lambda b, did=did: self._export(int(did)))
|
||||||
a = m.addAction(tr(TR.ACTIONS_DELETE))
|
a = m.addAction(tr(TR.ACTIONS_DELETE))
|
||||||
qconnect(a.triggered, lambda b, did=did: self._delete(int(did)))
|
qconnect(a.triggered, lambda b, did=did: self._delete(int(did)))
|
||||||
gui_hooks.deck_browser_will_show_options_menu(m, int(did))
|
gui_hooks.deck_browser_will_show_options_menu(m, int(did))
|
||||||
m.exec_(QCursor.pos())
|
m.exec_(QCursor.pos())
|
||||||
|
|
||||||
def _export(self, did):
|
def _export(self, did: int) -> None:
|
||||||
self.mw.onExport(did=did)
|
self.mw.onExport(did=did)
|
||||||
|
|
||||||
def _rename(self, did: int) -> None:
|
def _rename(self, did: int) -> None:
|
||||||
|
@ -256,10 +256,11 @@ class DeckBrowser:
|
||||||
self.mw.col.decks.rename(deck, newName)
|
self.mw.col.decks.rename(deck, newName)
|
||||||
gui_hooks.sidebar_should_refresh_decks()
|
gui_hooks.sidebar_should_refresh_decks()
|
||||||
except DeckRenameError as e:
|
except DeckRenameError as e:
|
||||||
return showWarning(e.description)
|
showWarning(e.description)
|
||||||
|
return
|
||||||
self.show()
|
self.show()
|
||||||
|
|
||||||
def _options(self, did):
|
def _options(self, did) -> None:
|
||||||
# select the deck first, because the dyn deck conf assumes the deck
|
# select the deck first, because the dyn deck conf assumes the deck
|
||||||
# we're editing is the current one
|
# we're editing is the current one
|
||||||
self.mw.col.decks.select(did)
|
self.mw.col.decks.select(did)
|
||||||
|
@ -296,10 +297,10 @@ class DeckBrowser:
|
||||||
def _delete(self, did: int) -> None:
|
def _delete(self, did: int) -> None:
|
||||||
if self.ask_delete_deck(did):
|
if self.ask_delete_deck(did):
|
||||||
|
|
||||||
def do_delete():
|
def do_delete() -> None:
|
||||||
return self.mw.col.decks.rem(did, True)
|
return self.mw.col.decks.rem(did, True)
|
||||||
|
|
||||||
def on_done(fut: Future):
|
def on_done(fut: Future) -> None:
|
||||||
self.show()
|
self.show()
|
||||||
res = fut.result() # Required to check for errors
|
res = fut.result() # Required to check for errors
|
||||||
|
|
||||||
|
@ -315,7 +316,7 @@ class DeckBrowser:
|
||||||
["Ctrl+Shift+I", "import", tr(TR.DECKS_IMPORT_FILE)],
|
["Ctrl+Shift+I", "import", tr(TR.DECKS_IMPORT_FILE)],
|
||||||
]
|
]
|
||||||
|
|
||||||
def _drawButtons(self):
|
def _drawButtons(self) -> None:
|
||||||
buf = ""
|
buf = ""
|
||||||
drawLinks = deepcopy(self.drawLinks)
|
drawLinks = deepcopy(self.drawLinks)
|
||||||
for b in drawLinks:
|
for b in drawLinks:
|
||||||
|
@ -331,5 +332,5 @@ class DeckBrowser:
|
||||||
web_context=DeckBrowserBottomBar(self),
|
web_context=DeckBrowserBottomBar(self),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _onShared(self):
|
def _onShared(self) -> None:
|
||||||
openLink(aqt.appShared + "decks/")
|
openLink(aqt.appShared + "decks/")
|
||||||
|
|
|
@ -2,12 +2,13 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from PyQt5.QtWidgets import QLineEdit
|
from PyQt5.QtWidgets import QLineEdit
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki.consts import NEW_CARDS_RANDOM
|
from anki.consts import NEW_CARDS_RANDOM
|
||||||
|
from anki.decks import DeckConfig
|
||||||
from anki.lang import without_unicode_isolation
|
from anki.lang import without_unicode_isolation
|
||||||
from aqt import gui_hooks
|
from aqt import gui_hooks
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
|
@ -28,7 +29,7 @@ from aqt.utils import (
|
||||||
|
|
||||||
|
|
||||||
class DeckConf(QDialog):
|
class DeckConf(QDialog):
|
||||||
def __init__(self, mw: aqt.AnkiQt, deck: Dict):
|
def __init__(self, mw: aqt.AnkiQt, deck: Dict) -> None:
|
||||||
QDialog.__init__(self, mw)
|
QDialog.__init__(self, mw)
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
self.deck = deck
|
self.deck = deck
|
||||||
|
@ -60,7 +61,7 @@ class DeckConf(QDialog):
|
||||||
self.exec_()
|
self.exec_()
|
||||||
saveGeom(self, "deckconf")
|
saveGeom(self, "deckconf")
|
||||||
|
|
||||||
def setupCombos(self):
|
def setupCombos(self) -> None:
|
||||||
import anki.consts as cs
|
import anki.consts as cs
|
||||||
|
|
||||||
f = self.form
|
f = self.form
|
||||||
|
@ -70,12 +71,12 @@ class DeckConf(QDialog):
|
||||||
# Conf list
|
# Conf list
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def setupConfs(self):
|
def setupConfs(self) -> None:
|
||||||
qconnect(self.form.dconf.currentIndexChanged, self.onConfChange)
|
qconnect(self.form.dconf.currentIndexChanged, self.onConfChange)
|
||||||
self.conf = None
|
self.conf: Optional[DeckConfig] = None
|
||||||
self.loadConfs()
|
self.loadConfs()
|
||||||
|
|
||||||
def loadConfs(self):
|
def loadConfs(self) -> None:
|
||||||
current = self.deck["conf"]
|
current = self.deck["conf"]
|
||||||
self.confList = self.mw.col.decks.allConf()
|
self.confList = self.mw.col.decks.allConf()
|
||||||
self.confList.sort(key=itemgetter("name"))
|
self.confList.sort(key=itemgetter("name"))
|
||||||
|
@ -92,7 +93,7 @@ class DeckConf(QDialog):
|
||||||
self._origNewOrder = self.confList[startOn]["new"]["order"]
|
self._origNewOrder = self.confList[startOn]["new"]["order"]
|
||||||
self.onConfChange(startOn)
|
self.onConfChange(startOn)
|
||||||
|
|
||||||
def confOpts(self):
|
def confOpts(self) -> None:
|
||||||
m = QMenu(self.mw)
|
m = QMenu(self.mw)
|
||||||
a = m.addAction(tr(TR.ACTIONS_ADD))
|
a = m.addAction(tr(TR.ACTIONS_ADD))
|
||||||
qconnect(a.triggered, self.addGroup)
|
qconnect(a.triggered, self.addGroup)
|
||||||
|
@ -106,7 +107,7 @@ class DeckConf(QDialog):
|
||||||
a.setEnabled(False)
|
a.setEnabled(False)
|
||||||
m.exec_(QCursor.pos())
|
m.exec_(QCursor.pos())
|
||||||
|
|
||||||
def onConfChange(self, idx):
|
def onConfChange(self, idx) -> None:
|
||||||
if self.ignoreConfChange:
|
if self.ignoreConfChange:
|
||||||
return
|
return
|
||||||
if self.conf:
|
if self.conf:
|
||||||
|
@ -159,7 +160,7 @@ class DeckConf(QDialog):
|
||||||
self.saveConf()
|
self.saveConf()
|
||||||
self.loadConfs()
|
self.loadConfs()
|
||||||
|
|
||||||
def setChildren(self):
|
def setChildren(self) -> None:
|
||||||
if not askUser(tr(TR.SCHEDULING_SET_ALL_DECKS_BELOW_TO, val=self.deck["name"])):
|
if not askUser(tr(TR.SCHEDULING_SET_ALL_DECKS_BELOW_TO, val=self.deck["name"])):
|
||||||
return
|
return
|
||||||
for did in self.childDids:
|
for did in self.childDids:
|
||||||
|
@ -173,8 +174,8 @@ class DeckConf(QDialog):
|
||||||
# Loading
|
# Loading
|
||||||
##################################################
|
##################################################
|
||||||
|
|
||||||
def listToUser(self, l):
|
def listToUser(self, l) -> str:
|
||||||
def num_to_user(n: Union[int, float]):
|
def num_to_user(n: Union[int, float]) -> str:
|
||||||
if n == round(n):
|
if n == round(n):
|
||||||
return str(int(n))
|
return str(int(n))
|
||||||
else:
|
else:
|
||||||
|
@ -182,7 +183,7 @@ class DeckConf(QDialog):
|
||||||
|
|
||||||
return " ".join(map(num_to_user, l))
|
return " ".join(map(num_to_user, l))
|
||||||
|
|
||||||
def parentLimText(self, type="new"):
|
def parentLimText(self, type="new") -> str:
|
||||||
# top level?
|
# top level?
|
||||||
if "::" not in self.deck["name"]:
|
if "::" not in self.deck["name"]:
|
||||||
return ""
|
return ""
|
||||||
|
@ -196,7 +197,7 @@ class DeckConf(QDialog):
|
||||||
lim = min(x, lim)
|
lim = min(x, lim)
|
||||||
return tr(TR.SCHEDULING_PARENT_LIMIT, val=lim)
|
return tr(TR.SCHEDULING_PARENT_LIMIT, val=lim)
|
||||||
|
|
||||||
def loadConf(self):
|
def loadConf(self) -> None:
|
||||||
self.conf = self.mw.col.decks.confForDid(self.deck["id"])
|
self.conf = self.mw.col.decks.confForDid(self.deck["id"])
|
||||||
# new
|
# new
|
||||||
c = self.conf["new"]
|
c = self.conf["new"]
|
||||||
|
@ -238,7 +239,7 @@ class DeckConf(QDialog):
|
||||||
f.desc.setPlainText(self.deck["desc"])
|
f.desc.setPlainText(self.deck["desc"])
|
||||||
gui_hooks.deck_conf_did_load_config(self, self.deck, self.conf)
|
gui_hooks.deck_conf_did_load_config(self, self.deck, self.conf)
|
||||||
|
|
||||||
def onRestore(self):
|
def onRestore(self) -> None:
|
||||||
self.mw.progress.start()
|
self.mw.progress.start()
|
||||||
self.mw.col.decks.restoreToDefault(self.conf)
|
self.mw.col.decks.restoreToDefault(self.conf)
|
||||||
self.mw.progress.finish()
|
self.mw.progress.finish()
|
||||||
|
@ -247,7 +248,7 @@ class DeckConf(QDialog):
|
||||||
# New order
|
# New order
|
||||||
##################################################
|
##################################################
|
||||||
|
|
||||||
def onNewOrderChanged(self, new):
|
def onNewOrderChanged(self, new) -> None:
|
||||||
old = self.conf["new"]["order"]
|
old = self.conf["new"]["order"]
|
||||||
if old == new:
|
if old == new:
|
||||||
return
|
return
|
||||||
|
@ -280,7 +281,7 @@ class DeckConf(QDialog):
|
||||||
return
|
return
|
||||||
conf[key] = ret
|
conf[key] = ret
|
||||||
|
|
||||||
def saveConf(self):
|
def saveConf(self) -> None:
|
||||||
# new
|
# new
|
||||||
c = self.conf["new"]
|
c = self.conf["new"]
|
||||||
f = self.form
|
f = self.form
|
||||||
|
@ -324,10 +325,10 @@ class DeckConf(QDialog):
|
||||||
self.mw.col.decks.save(self.deck)
|
self.mw.col.decks.save(self.deck)
|
||||||
self.mw.col.decks.save(self.conf)
|
self.mw.col.decks.save(self.conf)
|
||||||
|
|
||||||
def reject(self):
|
def reject(self) -> None:
|
||||||
self.accept()
|
self.accept()
|
||||||
|
|
||||||
def accept(self):
|
def accept(self) -> None:
|
||||||
self.saveConf()
|
self.saveConf()
|
||||||
self.mw.reset()
|
self.mw.reset()
|
||||||
QDialog.accept(self)
|
QDialog.accept(self)
|
||||||
|
|
|
@ -34,7 +34,7 @@ class DeckConf(QDialog):
|
||||||
search: Optional[str] = None,
|
search: Optional[str] = None,
|
||||||
search_2: Optional[str] = None,
|
search_2: Optional[str] = None,
|
||||||
deck: Optional[Deck] = None,
|
deck: Optional[Deck] = None,
|
||||||
):
|
) -> None:
|
||||||
"""If 'deck' is an existing filtered deck, load and modify its settings.
|
"""If 'deck' is an existing filtered deck, load and modify its settings.
|
||||||
Otherwise, build a new one and derive settings from the current deck.
|
Otherwise, build a new one and derive settings from the current deck.
|
||||||
"""
|
"""
|
||||||
|
@ -141,7 +141,7 @@ class DeckConf(QDialog):
|
||||||
self.form.search_2.setFocus()
|
self.form.search_2.setFocus()
|
||||||
self.form.search_2.selectAll()
|
self.form.search_2.selectAll()
|
||||||
|
|
||||||
def initialSetup(self):
|
def initialSetup(self) -> None:
|
||||||
import anki.consts as cs
|
import anki.consts as cs
|
||||||
|
|
||||||
self.form.order.addItems(list(cs.dynOrderLabels(self.mw.col).values()))
|
self.form.order.addItems(list(cs.dynOrderLabels(self.mw.col).values()))
|
||||||
|
@ -165,12 +165,12 @@ class DeckConf(QDialog):
|
||||||
else:
|
else:
|
||||||
aqt.dialogs.open("Browser", self.mw, search=(search,))
|
aqt.dialogs.open("Browser", self.mw, search=(search,))
|
||||||
|
|
||||||
def _onReschedToggled(self, _state: int):
|
def _onReschedToggled(self, _state: int) -> None:
|
||||||
self.form.previewDelayWidget.setVisible(
|
self.form.previewDelayWidget.setVisible(
|
||||||
not self.form.resched.isChecked() and self.mw.col.schedVer() > 1
|
not self.form.resched.isChecked() and self.mw.col.schedVer() > 1
|
||||||
)
|
)
|
||||||
|
|
||||||
def loadConf(self, deck: Optional[Deck] = None):
|
def loadConf(self, deck: Optional[Deck] = None) -> None:
|
||||||
f = self.form
|
f = self.form
|
||||||
d = deck or self.deck
|
d = deck or self.deck
|
||||||
|
|
||||||
|
@ -205,7 +205,7 @@ class DeckConf(QDialog):
|
||||||
f.secondFilter.setChecked(False)
|
f.secondFilter.setChecked(False)
|
||||||
f.filter2group.setVisible(False)
|
f.filter2group.setVisible(False)
|
||||||
|
|
||||||
def saveConf(self):
|
def saveConf(self) -> None:
|
||||||
f = self.form
|
f = self.form
|
||||||
d = self.deck
|
d = self.deck
|
||||||
|
|
||||||
|
@ -235,7 +235,7 @@ class DeckConf(QDialog):
|
||||||
|
|
||||||
self.mw.col.decks.save(d)
|
self.mw.col.decks.save(d)
|
||||||
|
|
||||||
def reject(self):
|
def reject(self) -> None:
|
||||||
if self.did:
|
if self.did:
|
||||||
self.mw.col.decks.rem(self.did)
|
self.mw.col.decks.rem(self.did)
|
||||||
self.mw.col.decks.select(self.old_deck["id"])
|
self.mw.col.decks.select(self.old_deck["id"])
|
||||||
|
@ -243,7 +243,7 @@ class DeckConf(QDialog):
|
||||||
QDialog.reject(self)
|
QDialog.reject(self)
|
||||||
aqt.dialogs.markClosed("DynDeckConfDialog")
|
aqt.dialogs.markClosed("DynDeckConfDialog")
|
||||||
|
|
||||||
def accept(self):
|
def accept(self) -> None:
|
||||||
try:
|
try:
|
||||||
self.saveConf()
|
self.saveConf()
|
||||||
except InvalidInput as err:
|
except InvalidInput as err:
|
||||||
|
|
|
@ -48,14 +48,14 @@ class EditCurrent(QDialog):
|
||||||
return
|
return
|
||||||
self.editor.setNote(n)
|
self.editor.setNote(n)
|
||||||
|
|
||||||
def reopen(self, mw):
|
def reopen(self, mw) -> None:
|
||||||
tooltip("Please finish editing the existing card first.")
|
tooltip("Please finish editing the existing card first.")
|
||||||
self.onReset()
|
self.onReset()
|
||||||
|
|
||||||
def reject(self):
|
def reject(self) -> None:
|
||||||
self.saveAndClose()
|
self.saveAndClose()
|
||||||
|
|
||||||
def saveAndClose(self):
|
def saveAndClose(self) -> None:
|
||||||
self.editor.saveNow(self._saveAndClose)
|
self.editor.saveNow(self._saveAndClose)
|
||||||
|
|
||||||
def _saveAndClose(self) -> None:
|
def _saveAndClose(self) -> None:
|
||||||
|
@ -74,8 +74,8 @@ class EditCurrent(QDialog):
|
||||||
aqt.dialogs.markClosed("EditCurrent")
|
aqt.dialogs.markClosed("EditCurrent")
|
||||||
QDialog.reject(self)
|
QDialog.reject(self)
|
||||||
|
|
||||||
def closeWithCallback(self, onsuccess):
|
def closeWithCallback(self, onsuccess) -> None:
|
||||||
def callback():
|
def callback() -> None:
|
||||||
self._saveAndClose()
|
self._saveAndClose()
|
||||||
onsuccess()
|
onsuccess()
|
||||||
|
|
||||||
|
|
168
qt/aqt/editor.py
168
qt/aqt/editor.py
|
@ -12,7 +12,7 @@ import urllib.parse
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import warnings
|
import warnings
|
||||||
from random import randrange
|
from random import randrange
|
||||||
from typing import Callable, List, Optional, Tuple
|
from typing import Any, Callable, Dict, List, Match, Optional, Tuple, cast
|
||||||
|
|
||||||
import bs4
|
import bs4
|
||||||
import requests
|
import requests
|
||||||
|
@ -92,7 +92,9 @@ _html = """
|
||||||
|
|
||||||
# caller is responsible for resetting note on reset
|
# caller is responsible for resetting note on reset
|
||||||
class Editor:
|
class Editor:
|
||||||
def __init__(self, mw: AnkiQt, widget, parentWindow, addMode=False) -> None:
|
def __init__(
|
||||||
|
self, mw: AnkiQt, widget: QWidget, parentWindow: QWidget, addMode: bool = False
|
||||||
|
) -> None:
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
self.widget = widget
|
self.widget = widget
|
||||||
self.parentWindow = parentWindow
|
self.parentWindow = parentWindow
|
||||||
|
@ -110,7 +112,7 @@ class Editor:
|
||||||
# Initial setup
|
# Initial setup
|
||||||
############################################################
|
############################################################
|
||||||
|
|
||||||
def setupOuter(self):
|
def setupOuter(self) -> None:
|
||||||
l = QVBoxLayout()
|
l = QVBoxLayout()
|
||||||
l.setContentsMargins(0, 0, 0, 0)
|
l.setContentsMargins(0, 0, 0, 0)
|
||||||
l.setSpacing(0)
|
l.setSpacing(0)
|
||||||
|
@ -229,7 +231,7 @@ class Editor:
|
||||||
# Top buttons
|
# Top buttons
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def resourceToData(self, path):
|
def resourceToData(self, path: str) -> str:
|
||||||
"""Convert a file (specified by a path) into a data URI."""
|
"""Convert a file (specified by a path) into a data URI."""
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
raise FileNotFoundError
|
raise FileNotFoundError
|
||||||
|
@ -251,21 +253,21 @@ class Editor:
|
||||||
keys: str = None,
|
keys: str = None,
|
||||||
disables: bool = True,
|
disables: bool = True,
|
||||||
rightside: bool = True,
|
rightside: bool = True,
|
||||||
):
|
) -> str:
|
||||||
"""Assign func to bridge cmd, register shortcut, return button"""
|
"""Assign func to bridge cmd, register shortcut, return button"""
|
||||||
if func:
|
if func:
|
||||||
self._links[cmd] = func
|
self._links[cmd] = func
|
||||||
|
|
||||||
if keys:
|
if keys:
|
||||||
|
|
||||||
def on_activated():
|
def on_activated() -> None:
|
||||||
func(self)
|
func(self)
|
||||||
|
|
||||||
if toggleable:
|
if toggleable:
|
||||||
# generate a random id for triggering toggle
|
# generate a random id for triggering toggle
|
||||||
id = id or str(randrange(1_000_000))
|
id = id or str(randrange(1_000_000))
|
||||||
|
|
||||||
def on_hotkey():
|
def on_hotkey() -> None:
|
||||||
on_activated()
|
on_activated()
|
||||||
self.web.eval(f'toggleEditorButton("#{id}");')
|
self.web.eval(f'toggleEditorButton("#{id}");')
|
||||||
|
|
||||||
|
@ -383,26 +385,26 @@ class Editor:
|
||||||
keys, fn, _ = row
|
keys, fn, _ = row
|
||||||
QShortcut(QKeySequence(keys), self.widget, activated=fn) # type: ignore
|
QShortcut(QKeySequence(keys), self.widget, activated=fn) # type: ignore
|
||||||
|
|
||||||
def _addFocusCheck(self, fn):
|
def _addFocusCheck(self, fn: Callable) -> Callable:
|
||||||
def checkFocus():
|
def checkFocus() -> None:
|
||||||
if self.currentField is None:
|
if self.currentField is None:
|
||||||
return
|
return
|
||||||
fn()
|
fn()
|
||||||
|
|
||||||
return checkFocus
|
return checkFocus
|
||||||
|
|
||||||
def onFields(self):
|
def onFields(self) -> None:
|
||||||
self.saveNow(self._onFields)
|
self.saveNow(self._onFields)
|
||||||
|
|
||||||
def _onFields(self):
|
def _onFields(self) -> None:
|
||||||
from aqt.fields import FieldDialog
|
from aqt.fields import FieldDialog
|
||||||
|
|
||||||
FieldDialog(self.mw, self.note.model(), parent=self.parentWindow)
|
FieldDialog(self.mw, self.note.model(), parent=self.parentWindow)
|
||||||
|
|
||||||
def onCardLayout(self):
|
def onCardLayout(self) -> None:
|
||||||
self.saveNow(self._onCardLayout)
|
self.saveNow(self._onCardLayout)
|
||||||
|
|
||||||
def _onCardLayout(self):
|
def _onCardLayout(self) -> None:
|
||||||
from aqt.clayout import CardLayout
|
from aqt.clayout import CardLayout
|
||||||
|
|
||||||
if self.card:
|
if self.card:
|
||||||
|
@ -422,16 +424,16 @@ class Editor:
|
||||||
# JS->Python bridge
|
# JS->Python bridge
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def onBridgeCmd(self, cmd) -> None:
|
def onBridgeCmd(self, cmd: str) -> None:
|
||||||
if not self.note:
|
if not self.note:
|
||||||
# shutdown
|
# shutdown
|
||||||
return
|
return
|
||||||
# focus lost or key/button pressed?
|
# focus lost or key/button pressed?
|
||||||
if cmd.startswith("blur") or cmd.startswith("key"):
|
if cmd.startswith("blur") or cmd.startswith("key"):
|
||||||
(type, ord, nid, txt) = cmd.split(":", 3)
|
(type, ord_str, nid_str, txt) = cmd.split(":", 3)
|
||||||
ord = int(ord)
|
ord = int(ord_str)
|
||||||
try:
|
try:
|
||||||
nid = int(nid)
|
nid = int(nid_str)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
nid = 0
|
nid = 0
|
||||||
if nid != self.note.id:
|
if nid != self.note.id:
|
||||||
|
@ -465,13 +467,15 @@ class Editor:
|
||||||
else:
|
else:
|
||||||
print("uncaught cmd", cmd)
|
print("uncaught cmd", cmd)
|
||||||
|
|
||||||
def mungeHTML(self, txt):
|
def mungeHTML(self, txt: str) -> str:
|
||||||
return gui_hooks.editor_will_munge_html(txt, self)
|
return gui_hooks.editor_will_munge_html(txt, self)
|
||||||
|
|
||||||
# Setting/unsetting the current note
|
# Setting/unsetting the current note
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def setNote(self, note, hide=True, focusTo=None):
|
def setNote(
|
||||||
|
self, note: Optional[Note], hide: bool = True, focusTo: Optional[int] = None
|
||||||
|
) -> None:
|
||||||
"Make NOTE the current note."
|
"Make NOTE the current note."
|
||||||
self.note = note
|
self.note = note
|
||||||
self.currentField = None
|
self.currentField = None
|
||||||
|
@ -482,10 +486,10 @@ class Editor:
|
||||||
if hide:
|
if hide:
|
||||||
self.widget.hide()
|
self.widget.hide()
|
||||||
|
|
||||||
def loadNoteKeepingFocus(self):
|
def loadNoteKeepingFocus(self) -> None:
|
||||||
self.loadNote(self.currentField)
|
self.loadNote(self.currentField)
|
||||||
|
|
||||||
def loadNote(self, focusTo=None) -> None:
|
def loadNote(self, focusTo: Optional[int] = None) -> None:
|
||||||
if not self.note:
|
if not self.note:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -496,7 +500,7 @@ class Editor:
|
||||||
self.widget.show()
|
self.widget.show()
|
||||||
self.updateTags()
|
self.updateTags()
|
||||||
|
|
||||||
def oncallback(arg):
|
def oncallback(arg: Any) -> None:
|
||||||
if not self.note:
|
if not self.note:
|
||||||
return
|
return
|
||||||
self.setupForegroundButton()
|
self.setupForegroundButton()
|
||||||
|
@ -520,7 +524,7 @@ class Editor:
|
||||||
for f in self.note.model()["flds"]
|
for f in self.note.model()["flds"]
|
||||||
]
|
]
|
||||||
|
|
||||||
def saveNow(self, callback, keepFocus=False):
|
def saveNow(self, callback: Callable, keepFocus: bool = False) -> None:
|
||||||
"Save unsaved edits then call callback()."
|
"Save unsaved edits then call callback()."
|
||||||
if not self.note:
|
if not self.note:
|
||||||
# calling code may not expect the callback to fire immediately
|
# calling code may not expect the callback to fire immediately
|
||||||
|
@ -529,7 +533,7 @@ class Editor:
|
||||||
self.saveTags()
|
self.saveTags()
|
||||||
self.web.evalWithCallback("saveNow(%d)" % keepFocus, lambda res: callback())
|
self.web.evalWithCallback("saveNow(%d)" % keepFocus, lambda res: callback())
|
||||||
|
|
||||||
def checkValid(self):
|
def checkValid(self) -> None:
|
||||||
cols = [""] * len(self.note.fields)
|
cols = [""] * len(self.note.fields)
|
||||||
err = self.note.dupeOrEmpty()
|
err = self.note.dupeOrEmpty()
|
||||||
if err == 2:
|
if err == 2:
|
||||||
|
@ -537,7 +541,7 @@ class Editor:
|
||||||
|
|
||||||
self.web.eval("setBackgrounds(%s);" % json.dumps(cols))
|
self.web.eval("setBackgrounds(%s);" % json.dumps(cols))
|
||||||
|
|
||||||
def showDupes(self):
|
def showDupes(self) -> None:
|
||||||
aqt.dialogs.open(
|
aqt.dialogs.open(
|
||||||
"Browser",
|
"Browser",
|
||||||
self.mw,
|
self.mw,
|
||||||
|
@ -551,7 +555,7 @@ class Editor:
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def fieldsAreBlank(self, previousNote=None):
|
def fieldsAreBlank(self, previousNote: Optional[Note] = None) -> bool:
|
||||||
if not self.note:
|
if not self.note:
|
||||||
return True
|
return True
|
||||||
m = self.note.model()
|
m = self.note.model()
|
||||||
|
@ -564,7 +568,7 @@ class Editor:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self) -> None:
|
||||||
self.setNote(None)
|
self.setNote(None)
|
||||||
# prevent any remaining evalWithCallback() events from firing after C++ object deleted
|
# prevent any remaining evalWithCallback() events from firing after C++ object deleted
|
||||||
self.web = None
|
self.web = None
|
||||||
|
@ -572,11 +576,11 @@ class Editor:
|
||||||
# HTML editing
|
# HTML editing
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def onHtmlEdit(self):
|
def onHtmlEdit(self) -> None:
|
||||||
field = self.currentField
|
field = self.currentField
|
||||||
self.saveNow(lambda: self._onHtmlEdit(field))
|
self.saveNow(lambda: self._onHtmlEdit(field))
|
||||||
|
|
||||||
def _onHtmlEdit(self, field):
|
def _onHtmlEdit(self, field: int) -> None:
|
||||||
d = QDialog(self.widget, Qt.Window)
|
d = QDialog(self.widget, Qt.Window)
|
||||||
form = aqt.forms.edithtml.Ui_Dialog()
|
form = aqt.forms.edithtml.Ui_Dialog()
|
||||||
form.setupUi(d)
|
form.setupUi(d)
|
||||||
|
@ -609,7 +613,7 @@ class Editor:
|
||||||
# Tag handling
|
# Tag handling
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def setupTags(self):
|
def setupTags(self) -> None:
|
||||||
import aqt.tagedit
|
import aqt.tagedit
|
||||||
|
|
||||||
g = QGroupBox(self.widget)
|
g = QGroupBox(self.widget)
|
||||||
|
@ -631,7 +635,7 @@ class Editor:
|
||||||
g.setLayout(tb)
|
g.setLayout(tb)
|
||||||
self.outerLayout.addWidget(g)
|
self.outerLayout.addWidget(g)
|
||||||
|
|
||||||
def updateTags(self):
|
def updateTags(self) -> None:
|
||||||
if self.tags.col != self.mw.col:
|
if self.tags.col != self.mw.col:
|
||||||
self.tags.setCol(self.mw.col)
|
self.tags.setCol(self.mw.col)
|
||||||
if not self.tags.text() or not self.addMode:
|
if not self.tags.text() or not self.addMode:
|
||||||
|
@ -645,44 +649,44 @@ class Editor:
|
||||||
self.note.flush()
|
self.note.flush()
|
||||||
gui_hooks.editor_did_update_tags(self.note)
|
gui_hooks.editor_did_update_tags(self.note)
|
||||||
|
|
||||||
def saveAddModeVars(self):
|
def saveAddModeVars(self) -> None:
|
||||||
if self.addMode:
|
if self.addMode:
|
||||||
# save tags to model
|
# save tags to model
|
||||||
m = self.note.model()
|
m = self.note.model()
|
||||||
m["tags"] = self.note.tags
|
m["tags"] = self.note.tags
|
||||||
self.mw.col.models.save(m, updateReqs=False)
|
self.mw.col.models.save(m, updateReqs=False)
|
||||||
|
|
||||||
def hideCompleters(self):
|
def hideCompleters(self) -> None:
|
||||||
self.tags.hideCompleter()
|
self.tags.hideCompleter()
|
||||||
|
|
||||||
def onFocusTags(self):
|
def onFocusTags(self) -> None:
|
||||||
self.tags.setFocus()
|
self.tags.setFocus()
|
||||||
|
|
||||||
# Format buttons
|
# Format buttons
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def toggleBold(self):
|
def toggleBold(self) -> None:
|
||||||
self.web.eval("setFormat('bold');")
|
self.web.eval("setFormat('bold');")
|
||||||
|
|
||||||
def toggleItalic(self):
|
def toggleItalic(self) -> None:
|
||||||
self.web.eval("setFormat('italic');")
|
self.web.eval("setFormat('italic');")
|
||||||
|
|
||||||
def toggleUnderline(self):
|
def toggleUnderline(self) -> None:
|
||||||
self.web.eval("setFormat('underline');")
|
self.web.eval("setFormat('underline');")
|
||||||
|
|
||||||
def toggleSuper(self):
|
def toggleSuper(self) -> None:
|
||||||
self.web.eval("setFormat('superscript');")
|
self.web.eval("setFormat('superscript');")
|
||||||
|
|
||||||
def toggleSub(self):
|
def toggleSub(self) -> None:
|
||||||
self.web.eval("setFormat('subscript');")
|
self.web.eval("setFormat('subscript');")
|
||||||
|
|
||||||
def removeFormat(self):
|
def removeFormat(self) -> None:
|
||||||
self.web.eval("setFormat('removeFormat');")
|
self.web.eval("setFormat('removeFormat');")
|
||||||
|
|
||||||
def onCloze(self):
|
def onCloze(self) -> None:
|
||||||
self.saveNow(self._onCloze, keepFocus=True)
|
self.saveNow(self._onCloze, keepFocus=True)
|
||||||
|
|
||||||
def _onCloze(self):
|
def _onCloze(self) -> None:
|
||||||
# check that the model is set up for cloze deletion
|
# check that the model is set up for cloze deletion
|
||||||
if self.note.model()["type"] != MODEL_CLOZE:
|
if self.note.model()["type"] != MODEL_CLOZE:
|
||||||
if self.addMode:
|
if self.addMode:
|
||||||
|
@ -706,16 +710,16 @@ class Editor:
|
||||||
# Foreground colour
|
# Foreground colour
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def setupForegroundButton(self):
|
def setupForegroundButton(self) -> None:
|
||||||
self.fcolour = self.mw.pm.profile.get("lastColour", "#00f")
|
self.fcolour = self.mw.pm.profile.get("lastColour", "#00f")
|
||||||
self.onColourChanged()
|
self.onColourChanged()
|
||||||
|
|
||||||
# use last colour
|
# use last colour
|
||||||
def onForeground(self):
|
def onForeground(self) -> None:
|
||||||
self._wrapWithColour(self.fcolour)
|
self._wrapWithColour(self.fcolour)
|
||||||
|
|
||||||
# choose new colour
|
# choose new colour
|
||||||
def onChangeCol(self):
|
def onChangeCol(self) -> None:
|
||||||
if isLin:
|
if isLin:
|
||||||
new = QColorDialog.getColor(
|
new = QColorDialog.getColor(
|
||||||
QColor(self.fcolour), None, None, QColorDialog.DontUseNativeDialog
|
QColor(self.fcolour), None, None, QColorDialog.DontUseNativeDialog
|
||||||
|
@ -729,32 +733,38 @@ class Editor:
|
||||||
self.onColourChanged()
|
self.onColourChanged()
|
||||||
self._wrapWithColour(self.fcolour)
|
self._wrapWithColour(self.fcolour)
|
||||||
|
|
||||||
def _updateForegroundButton(self):
|
def _updateForegroundButton(self) -> None:
|
||||||
self.web.eval("setFGButton('%s')" % self.fcolour)
|
self.web.eval("setFGButton('%s')" % self.fcolour)
|
||||||
|
|
||||||
def onColourChanged(self):
|
def onColourChanged(self) -> None:
|
||||||
self._updateForegroundButton()
|
self._updateForegroundButton()
|
||||||
self.mw.pm.profile["lastColour"] = self.fcolour
|
self.mw.pm.profile["lastColour"] = self.fcolour
|
||||||
|
|
||||||
def _wrapWithColour(self, colour):
|
def _wrapWithColour(self, colour: str) -> None:
|
||||||
self.web.eval("setFormat('forecolor', '%s')" % colour)
|
self.web.eval("setFormat('forecolor', '%s')" % colour)
|
||||||
|
|
||||||
# Audio/video/images
|
# Audio/video/images
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def onAddMedia(self):
|
def onAddMedia(self) -> None:
|
||||||
extension_filter = " ".join(
|
extension_filter = " ".join(
|
||||||
"*." + extension for extension in sorted(itertools.chain(pics, audio))
|
"*." + extension for extension in sorted(itertools.chain(pics, audio))
|
||||||
)
|
)
|
||||||
key = tr(TR.EDITING_MEDIA) + " (" + extension_filter + ")"
|
key = tr(TR.EDITING_MEDIA) + " (" + extension_filter + ")"
|
||||||
|
|
||||||
def accept(file):
|
def accept(file: str) -> None:
|
||||||
self.addMedia(file, canDelete=True)
|
self.addMedia(file, canDelete=True)
|
||||||
|
|
||||||
file = getFile(self.widget, tr(TR.EDITING_ADD_MEDIA), accept, key, key="media")
|
file = getFile(
|
||||||
|
self.widget,
|
||||||
|
tr(TR.EDITING_ADD_MEDIA),
|
||||||
|
cast(Callable[[Any], None], accept),
|
||||||
|
key,
|
||||||
|
key="media",
|
||||||
|
)
|
||||||
self.parentWindow.activateWindow()
|
self.parentWindow.activateWindow()
|
||||||
|
|
||||||
def addMedia(self, path, canDelete=False):
|
def addMedia(self, path: str, canDelete: bool = False) -> None:
|
||||||
try:
|
try:
|
||||||
html = self._addMedia(path, canDelete)
|
html = self._addMedia(path, canDelete)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -762,7 +772,7 @@ class Editor:
|
||||||
return
|
return
|
||||||
self.web.eval("setFormat('inserthtml', %s);" % json.dumps(html))
|
self.web.eval("setFormat('inserthtml', %s);" % json.dumps(html))
|
||||||
|
|
||||||
def _addMedia(self, path, canDelete=False):
|
def _addMedia(self, path: str, canDelete: bool = False) -> str:
|
||||||
"Add to media folder and return local img or sound tag."
|
"Add to media folder and return local img or sound tag."
|
||||||
# copy to media folder
|
# copy to media folder
|
||||||
fname = self.mw.col.media.addFile(path)
|
fname = self.mw.col.media.addFile(path)
|
||||||
|
@ -779,7 +789,7 @@ class Editor:
|
||||||
def _addMediaFromData(self, fname: str, data: bytes) -> str:
|
def _addMediaFromData(self, fname: str, data: bytes) -> str:
|
||||||
return self.mw.col.media.writeData(fname, data)
|
return self.mw.col.media.writeData(fname, data)
|
||||||
|
|
||||||
def onRecSound(self):
|
def onRecSound(self) -> None:
|
||||||
aqt.sound.record_audio(
|
aqt.sound.record_audio(
|
||||||
self.parentWindow,
|
self.parentWindow,
|
||||||
self.mw,
|
self.mw,
|
||||||
|
@ -813,7 +823,7 @@ class Editor:
|
||||||
# not a supported type
|
# not a supported type
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def isURL(self, s):
|
def isURL(self, s: str) -> bool:
|
||||||
s = s.lower()
|
s = s.lower()
|
||||||
return (
|
return (
|
||||||
s.startswith("http://")
|
s.startswith("http://")
|
||||||
|
@ -962,23 +972,23 @@ class Editor:
|
||||||
)
|
)
|
||||||
|
|
||||||
def doDrop(self, html: str, internal: bool, extended: bool = False) -> None:
|
def doDrop(self, html: str, internal: bool, extended: bool = False) -> None:
|
||||||
def pasteIfField(ret):
|
def pasteIfField(ret: bool) -> None:
|
||||||
if ret:
|
if ret:
|
||||||
self.doPaste(html, internal, extended)
|
self.doPaste(html, internal, extended)
|
||||||
|
|
||||||
p = self.web.mapFromGlobal(QCursor.pos())
|
p = self.web.mapFromGlobal(QCursor.pos())
|
||||||
self.web.evalWithCallback(f"focusIfField({p.x()}, {p.y()});", pasteIfField)
|
self.web.evalWithCallback(f"focusIfField({p.x()}, {p.y()});", pasteIfField)
|
||||||
|
|
||||||
def onPaste(self):
|
def onPaste(self) -> None:
|
||||||
self.web.onPaste()
|
self.web.onPaste()
|
||||||
|
|
||||||
def onCutOrCopy(self):
|
def onCutOrCopy(self) -> None:
|
||||||
self.web.flagAnkiText()
|
self.web.flagAnkiText()
|
||||||
|
|
||||||
# Advanced menu
|
# Advanced menu
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def onAdvanced(self):
|
def onAdvanced(self) -> None:
|
||||||
m = QMenu(self.mw)
|
m = QMenu(self.mw)
|
||||||
|
|
||||||
for text, handler, shortcut in (
|
for text, handler, shortcut in (
|
||||||
|
@ -1005,28 +1015,28 @@ class Editor:
|
||||||
# LaTeX
|
# LaTeX
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def insertLatex(self):
|
def insertLatex(self) -> None:
|
||||||
self.web.eval("wrap('[latex]', '[/latex]');")
|
self.web.eval("wrap('[latex]', '[/latex]');")
|
||||||
|
|
||||||
def insertLatexEqn(self):
|
def insertLatexEqn(self) -> None:
|
||||||
self.web.eval("wrap('[$]', '[/$]');")
|
self.web.eval("wrap('[$]', '[/$]');")
|
||||||
|
|
||||||
def insertLatexMathEnv(self):
|
def insertLatexMathEnv(self) -> None:
|
||||||
self.web.eval("wrap('[$$]', '[/$$]');")
|
self.web.eval("wrap('[$$]', '[/$$]');")
|
||||||
|
|
||||||
def insertMathjaxInline(self):
|
def insertMathjaxInline(self) -> None:
|
||||||
self.web.eval("wrap('\\\\(', '\\\\)');")
|
self.web.eval("wrap('\\\\(', '\\\\)');")
|
||||||
|
|
||||||
def insertMathjaxBlock(self):
|
def insertMathjaxBlock(self) -> None:
|
||||||
self.web.eval("wrap('\\\\[', '\\\\]');")
|
self.web.eval("wrap('\\\\[', '\\\\]');")
|
||||||
|
|
||||||
def insertMathjaxChemistry(self):
|
def insertMathjaxChemistry(self) -> None:
|
||||||
self.web.eval("wrap('\\\\(\\\\ce{', '}\\\\)');")
|
self.web.eval("wrap('\\\\(\\\\ce{', '}\\\\)');")
|
||||||
|
|
||||||
# Links from HTML
|
# Links from HTML
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
_links = dict(
|
_links: Dict[str, Callable] = dict(
|
||||||
fields=onFields,
|
fields=onFields,
|
||||||
cards=onCardLayout,
|
cards=onCardLayout,
|
||||||
bold=toggleBold,
|
bold=toggleBold,
|
||||||
|
@ -1052,7 +1062,7 @@ class Editor:
|
||||||
|
|
||||||
|
|
||||||
class EditorWebView(AnkiWebView):
|
class EditorWebView(AnkiWebView):
|
||||||
def __init__(self, parent, editor):
|
def __init__(self, parent: QWidget, editor: Editor) -> None:
|
||||||
AnkiWebView.__init__(self, title="editor")
|
AnkiWebView.__init__(self, title="editor")
|
||||||
self.editor = editor
|
self.editor = editor
|
||||||
self.strip = self.editor.mw.pm.profile["stripHTML"]
|
self.strip = self.editor.mw.pm.profile["stripHTML"]
|
||||||
|
@ -1062,15 +1072,15 @@ class EditorWebView(AnkiWebView):
|
||||||
qconnect(clip.dataChanged, self._onClipboardChange)
|
qconnect(clip.dataChanged, self._onClipboardChange)
|
||||||
gui_hooks.editor_web_view_did_init(self)
|
gui_hooks.editor_web_view_did_init(self)
|
||||||
|
|
||||||
def _onClipboardChange(self):
|
def _onClipboardChange(self) -> None:
|
||||||
if self._markInternal:
|
if self._markInternal:
|
||||||
self._markInternal = False
|
self._markInternal = False
|
||||||
self._flagAnkiText()
|
self._flagAnkiText()
|
||||||
|
|
||||||
def onCut(self):
|
def onCut(self) -> None:
|
||||||
self.triggerPageAction(QWebEnginePage.Cut)
|
self.triggerPageAction(QWebEnginePage.Cut)
|
||||||
|
|
||||||
def onCopy(self):
|
def onCopy(self) -> None:
|
||||||
self.triggerPageAction(QWebEnginePage.Copy)
|
self.triggerPageAction(QWebEnginePage.Copy)
|
||||||
|
|
||||||
def _wantsExtendedPaste(self) -> bool:
|
def _wantsExtendedPaste(self) -> bool:
|
||||||
|
@ -1093,10 +1103,10 @@ class EditorWebView(AnkiWebView):
|
||||||
def onMiddleClickPaste(self) -> None:
|
def onMiddleClickPaste(self) -> None:
|
||||||
self._onPaste(QClipboard.Selection)
|
self._onPaste(QClipboard.Selection)
|
||||||
|
|
||||||
def dragEnterEvent(self, evt):
|
def dragEnterEvent(self, evt: QDragEnterEvent) -> None:
|
||||||
evt.accept()
|
evt.accept()
|
||||||
|
|
||||||
def dropEvent(self, evt):
|
def dropEvent(self, evt: QDropEvent) -> None:
|
||||||
extended = self._wantsExtendedPaste()
|
extended = self._wantsExtendedPaste()
|
||||||
mime = evt.mimeData()
|
mime = evt.mimeData()
|
||||||
|
|
||||||
|
@ -1177,7 +1187,7 @@ class EditorWebView(AnkiWebView):
|
||||||
token = html.escape(token).replace("\t", " " * 4)
|
token = html.escape(token).replace("\t", " " * 4)
|
||||||
# if there's more than one consecutive space,
|
# if there's more than one consecutive space,
|
||||||
# use non-breaking spaces for the second one on
|
# use non-breaking spaces for the second one on
|
||||||
def repl(match):
|
def repl(match: Match) -> None:
|
||||||
return match.group(1).replace(" ", " ") + " "
|
return match.group(1).replace(" ", " ") + " "
|
||||||
|
|
||||||
token = re.sub(" ( +)", repl, token)
|
token = re.sub(" ( +)", repl, token)
|
||||||
|
@ -1223,11 +1233,11 @@ class EditorWebView(AnkiWebView):
|
||||||
return self.editor.fnameToLink(fname)
|
return self.editor.fnameToLink(fname)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def flagAnkiText(self):
|
def flagAnkiText(self) -> None:
|
||||||
# be ready to adjust when clipboard event fires
|
# be ready to adjust when clipboard event fires
|
||||||
self._markInternal = True
|
self._markInternal = True
|
||||||
|
|
||||||
def _flagAnkiText(self):
|
def _flagAnkiText(self) -> None:
|
||||||
# add a comment in the clipboard html so we can tell text is copied
|
# add a comment in the clipboard html so we can tell text is copied
|
||||||
# from us and doesn't need to be stripped
|
# from us and doesn't need to be stripped
|
||||||
clip = self.editor.mw.app.clipboard()
|
clip = self.editor.mw.app.clipboard()
|
||||||
|
@ -1255,20 +1265,20 @@ class EditorWebView(AnkiWebView):
|
||||||
# QFont returns "Kozuka Gothic Pro L" but WebEngine expects "Kozuka Gothic Pro Light"
|
# QFont returns "Kozuka Gothic Pro L" but WebEngine expects "Kozuka Gothic Pro Light"
|
||||||
# - there may be other cases like a trailing 'Bold' that need fixing, but will
|
# - there may be other cases like a trailing 'Bold' that need fixing, but will
|
||||||
# wait for further reports first.
|
# wait for further reports first.
|
||||||
def fontMungeHack(font):
|
def fontMungeHack(font: str) -> str:
|
||||||
return re.sub(" L$", " Light", font)
|
return re.sub(" L$", " Light", font)
|
||||||
|
|
||||||
|
|
||||||
def munge_html(txt, editor):
|
def munge_html(txt: str, editor: Editor) -> str:
|
||||||
return "" if txt in ("<br>", "<div><br></div>") else txt
|
return "" if txt in ("<br>", "<div><br></div>") else txt
|
||||||
|
|
||||||
|
|
||||||
def remove_null_bytes(txt, editor):
|
def remove_null_bytes(txt: str, editor: Editor) -> str:
|
||||||
# misbehaving apps may include a null byte in the text
|
# misbehaving apps may include a null byte in the text
|
||||||
return txt.replace("\x00", "")
|
return txt.replace("\x00", "")
|
||||||
|
|
||||||
|
|
||||||
def reverse_url_quoting(txt, editor):
|
def reverse_url_quoting(txt: str, editor: Editor) -> str:
|
||||||
# reverse the url quoting we added to get images to display
|
# reverse the url quoting we added to get images to display
|
||||||
return editor.mw.col.media.escape_media_filenames(txt, unescape=True)
|
return editor.mw.col.media.escape_media_filenames(txt, unescape=True)
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ from aqt.utils import TR, disable_help_button, restoreGeom, saveGeom, tooltip, t
|
||||||
def show_empty_cards(mw: aqt.main.AnkiQt) -> None:
|
def show_empty_cards(mw: aqt.main.AnkiQt) -> None:
|
||||||
mw.progress.start()
|
mw.progress.start()
|
||||||
|
|
||||||
def on_done(fut):
|
def on_done(fut) -> None:
|
||||||
mw.progress.finish()
|
mw.progress.finish()
|
||||||
report: EmptyCardsReport = fut.result()
|
report: EmptyCardsReport = fut.result()
|
||||||
if not report.notes:
|
if not report.notes:
|
||||||
|
@ -54,7 +54,7 @@ class EmptyCardsDialog(QDialog):
|
||||||
style = "<style>.allempty { color: red; }</style>"
|
style = "<style>.allempty { color: red; }</style>"
|
||||||
self.form.webview.stdHtml(style + html, context=self)
|
self.form.webview.stdHtml(style + html, context=self)
|
||||||
|
|
||||||
def on_finished(code):
|
def on_finished(code) -> None:
|
||||||
saveGeom(self, "emptycards")
|
saveGeom(self, "emptycards")
|
||||||
|
|
||||||
qconnect(self.finished, on_finished)
|
qconnect(self.finished, on_finished)
|
||||||
|
@ -65,16 +65,16 @@ class EmptyCardsDialog(QDialog):
|
||||||
self._delete_button.setAutoDefault(False)
|
self._delete_button.setAutoDefault(False)
|
||||||
self._delete_button.clicked.connect(self._on_delete)
|
self._delete_button.clicked.connect(self._on_delete)
|
||||||
|
|
||||||
def _on_note_link_clicked(self, link):
|
def _on_note_link_clicked(self, link) -> None:
|
||||||
aqt.dialogs.open("Browser", self.mw, search=(link,))
|
aqt.dialogs.open("Browser", self.mw, search=(link,))
|
||||||
|
|
||||||
def _on_delete(self):
|
def _on_delete(self) -> None:
|
||||||
self.mw.progress.start()
|
self.mw.progress.start()
|
||||||
|
|
||||||
def delete():
|
def delete() -> int:
|
||||||
return self._delete_cards(self.form.keep_notes.isChecked())
|
return self._delete_cards(self.form.keep_notes.isChecked())
|
||||||
|
|
||||||
def on_done(fut):
|
def on_done(fut) -> None:
|
||||||
self.mw.progress.finish()
|
self.mw.progress.finish()
|
||||||
try:
|
try:
|
||||||
count = fut.result()
|
count = fut.result()
|
||||||
|
|
|
@ -5,16 +5,18 @@ import html
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from markdown import markdown
|
from markdown import markdown
|
||||||
|
|
||||||
from aqt import mw
|
from aqt import mw
|
||||||
|
from aqt.main import AnkiQt
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.utils import TR, showText, showWarning, supportText, tr
|
from aqt.utils import TR, showText, showWarning, supportText, tr
|
||||||
|
|
||||||
if not os.environ.get("DEBUG"):
|
if not os.environ.get("DEBUG"):
|
||||||
|
|
||||||
def excepthook(etype, val, tb):
|
def excepthook(etype, val, tb) -> None:
|
||||||
sys.stderr.write(
|
sys.stderr.write(
|
||||||
"Caught exception:\n%s\n"
|
"Caught exception:\n%s\n"
|
||||||
% ("".join(traceback.format_exception(etype, val, tb)))
|
% ("".join(traceback.format_exception(etype, val, tb)))
|
||||||
|
@ -29,20 +31,20 @@ class ErrorHandler(QObject):
|
||||||
|
|
||||||
errorTimer = pyqtSignal()
|
errorTimer = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, mw):
|
def __init__(self, mw: AnkiQt) -> None:
|
||||||
QObject.__init__(self, mw)
|
QObject.__init__(self, mw)
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
self.timer = None
|
self.timer: Optional[QTimer] = None
|
||||||
qconnect(self.errorTimer, self._setTimer)
|
qconnect(self.errorTimer, self._setTimer)
|
||||||
self.pool = ""
|
self.pool = ""
|
||||||
self._oldstderr = sys.stderr
|
self._oldstderr = sys.stderr
|
||||||
sys.stderr = self
|
sys.stderr = self
|
||||||
|
|
||||||
def unload(self):
|
def unload(self) -> None:
|
||||||
sys.stderr = self._oldstderr
|
sys.stderr = self._oldstderr
|
||||||
sys.excepthook = None
|
sys.excepthook = None
|
||||||
|
|
||||||
def write(self, data):
|
def write(self, data: str) -> None:
|
||||||
# dump to stdout
|
# dump to stdout
|
||||||
sys.stdout.write(data)
|
sys.stdout.write(data)
|
||||||
# save in buffer
|
# save in buffer
|
||||||
|
@ -50,12 +52,12 @@ class ErrorHandler(QObject):
|
||||||
# and update timer
|
# and update timer
|
||||||
self.setTimer()
|
self.setTimer()
|
||||||
|
|
||||||
def setTimer(self):
|
def setTimer(self) -> None:
|
||||||
# we can't create a timer from a different thread, so we post a
|
# we can't create a timer from a different thread, so we post a
|
||||||
# message to the object on the main thread
|
# message to the object on the main thread
|
||||||
self.errorTimer.emit() # type: ignore
|
self.errorTimer.emit() # type: ignore
|
||||||
|
|
||||||
def _setTimer(self):
|
def _setTimer(self) -> None:
|
||||||
if not self.timer:
|
if not self.timer:
|
||||||
self.timer = QTimer(self.mw)
|
self.timer = QTimer(self.mw)
|
||||||
qconnect(self.timer.timeout, self.onTimeout)
|
qconnect(self.timer.timeout, self.onTimeout)
|
||||||
|
@ -63,10 +65,10 @@ class ErrorHandler(QObject):
|
||||||
self.timer.setSingleShot(True)
|
self.timer.setSingleShot(True)
|
||||||
self.timer.start()
|
self.timer.start()
|
||||||
|
|
||||||
def tempFolderMsg(self):
|
def tempFolderMsg(self) -> str:
|
||||||
return tr(TR.QT_MISC_UNABLE_TO_ACCESS_ANKI_MEDIA_FOLDER)
|
return tr(TR.QT_MISC_UNABLE_TO_ACCESS_ANKI_MEDIA_FOLDER)
|
||||||
|
|
||||||
def onTimeout(self):
|
def onTimeout(self) -> None:
|
||||||
error = html.escape(self.pool)
|
error = html.escape(self.pool)
|
||||||
self.pool = ""
|
self.pool = ""
|
||||||
self.mw.progress.clear()
|
self.mw.progress.clear()
|
||||||
|
@ -75,15 +77,19 @@ class ErrorHandler(QObject):
|
||||||
if "DeprecationWarning" in error:
|
if "DeprecationWarning" in error:
|
||||||
return
|
return
|
||||||
if "10013" in error:
|
if "10013" in error:
|
||||||
return showWarning(tr(TR.QT_MISC_YOUR_FIREWALL_OR_ANTIVIRUS_PROGRAM_IS))
|
showWarning(tr(TR.QT_MISC_YOUR_FIREWALL_OR_ANTIVIRUS_PROGRAM_IS))
|
||||||
|
return
|
||||||
if "no default input" in error.lower():
|
if "no default input" in error.lower():
|
||||||
return showWarning(tr(TR.QT_MISC_PLEASE_CONNECT_A_MICROPHONE_AND_ENSURE))
|
showWarning(tr(TR.QT_MISC_PLEASE_CONNECT_A_MICROPHONE_AND_ENSURE))
|
||||||
|
return
|
||||||
if "invalidTempFolder" in error:
|
if "invalidTempFolder" in error:
|
||||||
return showWarning(self.tempFolderMsg())
|
showWarning(self.tempFolderMsg())
|
||||||
|
return
|
||||||
if "Beautiful Soup is not an HTTP client" in error:
|
if "Beautiful Soup is not an HTTP client" in error:
|
||||||
return
|
return
|
||||||
if "database or disk is full" in error or "Errno 28" in error:
|
if "database or disk is full" in error or "Errno 28" in error:
|
||||||
return showWarning(tr(TR.QT_MISC_YOUR_COMPUTERS_STORAGE_MAY_BE_FULL))
|
showWarning(tr(TR.QT_MISC_YOUR_COMPUTERS_STORAGE_MAY_BE_FULL))
|
||||||
|
return
|
||||||
if "disk I/O error" in error:
|
if "disk I/O error" in error:
|
||||||
showWarning(markdown(tr(TR.ERRORS_ACCESSING_DB)))
|
showWarning(markdown(tr(TR.ERRORS_ACCESSING_DB)))
|
||||||
return
|
return
|
||||||
|
@ -99,7 +105,7 @@ class ErrorHandler(QObject):
|
||||||
txt = txt + "<div style='white-space: pre-wrap'>" + error + "</div>"
|
txt = txt + "<div style='white-space: pre-wrap'>" + error + "</div>"
|
||||||
showText(txt, type="html", copyBtn=True)
|
showText(txt, type="html", copyBtn=True)
|
||||||
|
|
||||||
def _addonText(self, error):
|
def _addonText(self, error: str) -> str:
|
||||||
matches = re.findall(r"addons21/(.*?)/", error)
|
matches = re.findall(r"addons21/(.*?)/", error)
|
||||||
if not matches:
|
if not matches:
|
||||||
return ""
|
return ""
|
||||||
|
|
|
@ -42,7 +42,7 @@ class ExportDialog(QDialog):
|
||||||
self.setup(did)
|
self.setup(did)
|
||||||
self.exec_()
|
self.exec_()
|
||||||
|
|
||||||
def setup(self, did: Optional[int]):
|
def setup(self, did: Optional[int]) -> None:
|
||||||
self.exporters = exporters(self.col)
|
self.exporters = exporters(self.col)
|
||||||
# if a deck specified, start with .apkg type selected
|
# if a deck specified, start with .apkg type selected
|
||||||
idx = 0
|
idx = 0
|
||||||
|
@ -71,7 +71,7 @@ class ExportDialog(QDialog):
|
||||||
index = self.frm.deck.findText(name)
|
index = self.frm.deck.findText(name)
|
||||||
self.frm.deck.setCurrentIndex(index)
|
self.frm.deck.setCurrentIndex(index)
|
||||||
|
|
||||||
def exporterChanged(self, idx):
|
def exporterChanged(self, idx: int) -> None:
|
||||||
self.exporter = self.exporters[idx][1](self.col)
|
self.exporter = self.exporters[idx][1](self.col)
|
||||||
self.isApkg = self.exporter.ext == ".apkg"
|
self.isApkg = self.exporter.ext == ".apkg"
|
||||||
self.isVerbatim = getattr(self.exporter, "verbatim", False)
|
self.isVerbatim = getattr(self.exporter, "verbatim", False)
|
||||||
|
@ -94,7 +94,7 @@ class ExportDialog(QDialog):
|
||||||
# show deck list?
|
# show deck list?
|
||||||
self.frm.deck.setVisible(not self.isVerbatim)
|
self.frm.deck.setVisible(not self.isVerbatim)
|
||||||
|
|
||||||
def accept(self):
|
def accept(self) -> None:
|
||||||
self.exporter.includeSched = self.frm.includeSched.isChecked()
|
self.exporter.includeSched = self.frm.includeSched.isChecked()
|
||||||
self.exporter.includeMedia = self.frm.includeMedia.isChecked()
|
self.exporter.includeMedia = self.frm.includeMedia.isChecked()
|
||||||
self.exporter.includeTags = self.frm.includeTags.isChecked()
|
self.exporter.includeTags = self.frm.includeTags.isChecked()
|
||||||
|
@ -155,17 +155,17 @@ class ExportDialog(QDialog):
|
||||||
os.unlink(file)
|
os.unlink(file)
|
||||||
|
|
||||||
# progress handler
|
# progress handler
|
||||||
def exported_media(cnt):
|
def exported_media(cnt) -> None:
|
||||||
self.mw.taskman.run_on_main(
|
self.mw.taskman.run_on_main(
|
||||||
lambda: self.mw.progress.update(
|
lambda: self.mw.progress.update(
|
||||||
label=tr(TR.EXPORTING_EXPORTED_MEDIA_FILE, count=cnt)
|
label=tr(TR.EXPORTING_EXPORTED_MEDIA_FILE, count=cnt)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def do_export():
|
def do_export() -> None:
|
||||||
self.exporter.exportInto(file)
|
self.exporter.exportInto(file)
|
||||||
|
|
||||||
def on_done(future: Future):
|
def on_done(future: Future) -> None:
|
||||||
self.mw.progress.finish()
|
self.mw.progress.finish()
|
||||||
hooks.media_files_did_export.remove(exported_media)
|
hooks.media_files_did_export.remove(exported_media)
|
||||||
# raises if exporter failed
|
# raises if exporter failed
|
||||||
|
@ -177,7 +177,7 @@ class ExportDialog(QDialog):
|
||||||
|
|
||||||
self.mw.taskman.run_in_background(do_export, on_done)
|
self.mw.taskman.run_in_background(do_export, on_done)
|
||||||
|
|
||||||
def on_export_finished(self):
|
def on_export_finished(self) -> None:
|
||||||
if self.isVerbatim:
|
if self.isVerbatim:
|
||||||
msg = tr(TR.EXPORTING_COLLECTION_EXPORTED)
|
msg = tr(TR.EXPORTING_COLLECTION_EXPORTED)
|
||||||
self.mw.reopen()
|
self.mw.reopen()
|
||||||
|
|
|
@ -23,7 +23,7 @@ from aqt.utils import (
|
||||||
|
|
||||||
|
|
||||||
class FieldDialog(QDialog):
|
class FieldDialog(QDialog):
|
||||||
def __init__(self, mw: AnkiQt, nt: NoteType, parent=None):
|
def __init__(self, mw: AnkiQt, nt: NoteType, parent=None) -> None:
|
||||||
QDialog.__init__(self, parent or mw)
|
QDialog.__init__(self, parent or mw)
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
self.col = self.mw.col
|
self.col = self.mw.col
|
||||||
|
@ -41,7 +41,7 @@ class FieldDialog(QDialog):
|
||||||
self.form.buttonBox.button(QDialogButtonBox.Help).setAutoDefault(False)
|
self.form.buttonBox.button(QDialogButtonBox.Help).setAutoDefault(False)
|
||||||
self.form.buttonBox.button(QDialogButtonBox.Cancel).setAutoDefault(False)
|
self.form.buttonBox.button(QDialogButtonBox.Cancel).setAutoDefault(False)
|
||||||
self.form.buttonBox.button(QDialogButtonBox.Save).setAutoDefault(False)
|
self.form.buttonBox.button(QDialogButtonBox.Save).setAutoDefault(False)
|
||||||
self.currentIdx = None
|
self.currentIdx: Optional[int] = None
|
||||||
self.oldSortField = self.model["sortf"]
|
self.oldSortField = self.model["sortf"]
|
||||||
self.fillFields()
|
self.fillFields()
|
||||||
self.setupSignals()
|
self.setupSignals()
|
||||||
|
@ -52,13 +52,13 @@ class FieldDialog(QDialog):
|
||||||
|
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def fillFields(self):
|
def fillFields(self) -> None:
|
||||||
self.currentIdx = None
|
self.currentIdx = None
|
||||||
self.form.fieldList.clear()
|
self.form.fieldList.clear()
|
||||||
for c, f in enumerate(self.model["flds"]):
|
for c, f in enumerate(self.model["flds"]):
|
||||||
self.form.fieldList.addItem("{}: {}".format(c + 1, f["name"]))
|
self.form.fieldList.addItem("{}: {}".format(c + 1, f["name"]))
|
||||||
|
|
||||||
def setupSignals(self):
|
def setupSignals(self) -> None:
|
||||||
f = self.form
|
f = self.form
|
||||||
qconnect(f.fieldList.currentRowChanged, self.onRowChange)
|
qconnect(f.fieldList.currentRowChanged, self.onRowChange)
|
||||||
qconnect(f.fieldAdd.clicked, self.onAdd)
|
qconnect(f.fieldAdd.clicked, self.onAdd)
|
||||||
|
@ -68,7 +68,7 @@ class FieldDialog(QDialog):
|
||||||
qconnect(f.sortField.clicked, self.onSortField)
|
qconnect(f.sortField.clicked, self.onSortField)
|
||||||
qconnect(f.buttonBox.helpRequested, self.onHelp)
|
qconnect(f.buttonBox.helpRequested, self.onHelp)
|
||||||
|
|
||||||
def onDrop(self, ev):
|
def onDrop(self, ev) -> None:
|
||||||
fieldList = self.form.fieldList
|
fieldList = self.form.fieldList
|
||||||
indicatorPos = fieldList.dropIndicatorPosition()
|
indicatorPos = fieldList.dropIndicatorPosition()
|
||||||
dropPos = fieldList.indexAt(ev.pos()).row()
|
dropPos = fieldList.indexAt(ev.pos()).row()
|
||||||
|
@ -86,32 +86,34 @@ class FieldDialog(QDialog):
|
||||||
movePos -= 1
|
movePos -= 1
|
||||||
self.moveField(movePos + 1) # convert to 1 based.
|
self.moveField(movePos + 1) # convert to 1 based.
|
||||||
|
|
||||||
def onRowChange(self, idx):
|
def onRowChange(self, idx: int) -> None:
|
||||||
if idx == -1:
|
if idx == -1:
|
||||||
return
|
return
|
||||||
self.saveField()
|
self.saveField()
|
||||||
self.loadField(idx)
|
self.loadField(idx)
|
||||||
|
|
||||||
def _uniqueName(self, prompt, ignoreOrd=None, old=""):
|
def _uniqueName(
|
||||||
|
self, prompt: str, ignoreOrd: Optional[int] = None, old: str = ""
|
||||||
|
) -> Optional[str]:
|
||||||
txt = getOnlyText(prompt, default=old).replace('"', "").strip()
|
txt = getOnlyText(prompt, default=old).replace('"', "").strip()
|
||||||
if not txt:
|
if not txt:
|
||||||
return
|
return None
|
||||||
if txt[0] in "#^/":
|
if txt[0] in "#^/":
|
||||||
showWarning(tr(TR.FIELDS_NAME_FIRST_LETTER_NOT_VALID))
|
showWarning(tr(TR.FIELDS_NAME_FIRST_LETTER_NOT_VALID))
|
||||||
return
|
return None
|
||||||
for letter in """:{"}""":
|
for letter in """:{"}""":
|
||||||
if letter in txt:
|
if letter in txt:
|
||||||
showWarning(tr(TR.FIELDS_NAME_INVALID_LETTER))
|
showWarning(tr(TR.FIELDS_NAME_INVALID_LETTER))
|
||||||
return
|
return None
|
||||||
for f in self.model["flds"]:
|
for f in self.model["flds"]:
|
||||||
if ignoreOrd is not None and f["ord"] == ignoreOrd:
|
if ignoreOrd is not None and f["ord"] == ignoreOrd:
|
||||||
continue
|
continue
|
||||||
if f["name"] == txt:
|
if f["name"] == txt:
|
||||||
showWarning(tr(TR.FIELDS_THAT_FIELD_NAME_IS_ALREADY_USED))
|
showWarning(tr(TR.FIELDS_THAT_FIELD_NAME_IS_ALREADY_USED))
|
||||||
return
|
return None
|
||||||
return txt
|
return txt
|
||||||
|
|
||||||
def onRename(self):
|
def onRename(self) -> None:
|
||||||
idx = self.currentIdx
|
idx = self.currentIdx
|
||||||
f = self.model["flds"][idx]
|
f = self.model["flds"][idx]
|
||||||
name = self._uniqueName(tr(TR.ACTIONS_NEW_NAME), self.currentIdx, f["name"])
|
name = self._uniqueName(tr(TR.ACTIONS_NEW_NAME), self.currentIdx, f["name"])
|
||||||
|
@ -127,7 +129,7 @@ class FieldDialog(QDialog):
|
||||||
self.fillFields()
|
self.fillFields()
|
||||||
self.form.fieldList.setCurrentRow(idx)
|
self.form.fieldList.setCurrentRow(idx)
|
||||||
|
|
||||||
def onAdd(self):
|
def onAdd(self) -> None:
|
||||||
name = self._uniqueName(tr(TR.FIELDS_FIELD_NAME))
|
name = self._uniqueName(tr(TR.FIELDS_FIELD_NAME))
|
||||||
if not name:
|
if not name:
|
||||||
return
|
return
|
||||||
|
@ -139,9 +141,10 @@ class FieldDialog(QDialog):
|
||||||
self.fillFields()
|
self.fillFields()
|
||||||
self.form.fieldList.setCurrentRow(len(self.model["flds"]) - 1)
|
self.form.fieldList.setCurrentRow(len(self.model["flds"]) - 1)
|
||||||
|
|
||||||
def onDelete(self):
|
def onDelete(self) -> None:
|
||||||
if len(self.model["flds"]) < 2:
|
if len(self.model["flds"]) < 2:
|
||||||
return showWarning(tr(TR.FIELDS_NOTES_REQUIRE_AT_LEAST_ONE_FIELD))
|
showWarning(tr(TR.FIELDS_NOTES_REQUIRE_AT_LEAST_ONE_FIELD))
|
||||||
|
return
|
||||||
count = self.mm.useCount(self.model)
|
count = self.mm.useCount(self.model)
|
||||||
c = tr(TR.BROWSING_NOTE_COUNT, count=count)
|
c = tr(TR.BROWSING_NOTE_COUNT, count=count)
|
||||||
if not askUser(tr(TR.FIELDS_DELETE_FIELD_FROM, val=c)):
|
if not askUser(tr(TR.FIELDS_DELETE_FIELD_FROM, val=c)):
|
||||||
|
@ -155,7 +158,7 @@ class FieldDialog(QDialog):
|
||||||
self.fillFields()
|
self.fillFields()
|
||||||
self.form.fieldList.setCurrentRow(0)
|
self.form.fieldList.setCurrentRow(0)
|
||||||
|
|
||||||
def onPosition(self, delta=-1):
|
def onPosition(self, delta=-1) -> None:
|
||||||
idx = self.currentIdx
|
idx = self.currentIdx
|
||||||
l = len(self.model["flds"])
|
l = len(self.model["flds"])
|
||||||
txt = getOnlyText(tr(TR.FIELDS_NEW_POSITION_1, val=l), default=str(idx + 1))
|
txt = getOnlyText(tr(TR.FIELDS_NEW_POSITION_1, val=l), default=str(idx + 1))
|
||||||
|
@ -169,23 +172,23 @@ class FieldDialog(QDialog):
|
||||||
return
|
return
|
||||||
self.moveField(pos)
|
self.moveField(pos)
|
||||||
|
|
||||||
def onSortField(self):
|
def onSortField(self) -> None:
|
||||||
if not self.change_tracker.mark_schema():
|
if not self.change_tracker.mark_schema():
|
||||||
return False
|
return
|
||||||
# don't allow user to disable; it makes no sense
|
# don't allow user to disable; it makes no sense
|
||||||
self.form.sortField.setChecked(True)
|
self.form.sortField.setChecked(True)
|
||||||
self.mm.set_sort_index(self.model, self.form.fieldList.currentRow())
|
self.mm.set_sort_index(self.model, self.form.fieldList.currentRow())
|
||||||
|
|
||||||
def moveField(self, pos):
|
def moveField(self, pos) -> None:
|
||||||
if not self.change_tracker.mark_schema():
|
if not self.change_tracker.mark_schema():
|
||||||
return False
|
return
|
||||||
self.saveField()
|
self.saveField()
|
||||||
f = self.model["flds"][self.currentIdx]
|
f = self.model["flds"][self.currentIdx]
|
||||||
self.mm.reposition_field(self.model, f, pos - 1)
|
self.mm.reposition_field(self.model, f, pos - 1)
|
||||||
self.fillFields()
|
self.fillFields()
|
||||||
self.form.fieldList.setCurrentRow(pos - 1)
|
self.form.fieldList.setCurrentRow(pos - 1)
|
||||||
|
|
||||||
def loadField(self, idx):
|
def loadField(self, idx: int) -> None:
|
||||||
self.currentIdx = idx
|
self.currentIdx = idx
|
||||||
fld = self.model["flds"][idx]
|
fld = self.model["flds"][idx]
|
||||||
f = self.form
|
f = self.form
|
||||||
|
@ -195,7 +198,7 @@ class FieldDialog(QDialog):
|
||||||
f.sortField.setChecked(self.model["sortf"] == fld["ord"])
|
f.sortField.setChecked(self.model["sortf"] == fld["ord"])
|
||||||
f.rtl.setChecked(fld["rtl"])
|
f.rtl.setChecked(fld["rtl"])
|
||||||
|
|
||||||
def saveField(self):
|
def saveField(self) -> None:
|
||||||
# not initialized yet?
|
# not initialized yet?
|
||||||
if self.currentIdx is None:
|
if self.currentIdx is None:
|
||||||
return
|
return
|
||||||
|
@ -219,20 +222,20 @@ class FieldDialog(QDialog):
|
||||||
fld["rtl"] = rtl
|
fld["rtl"] = rtl
|
||||||
self.change_tracker.mark_basic()
|
self.change_tracker.mark_basic()
|
||||||
|
|
||||||
def reject(self):
|
def reject(self) -> None:
|
||||||
if self.change_tracker.changed():
|
if self.change_tracker.changed():
|
||||||
if not askUser("Discard changes?"):
|
if not askUser("Discard changes?"):
|
||||||
return
|
return
|
||||||
|
|
||||||
QDialog.reject(self)
|
QDialog.reject(self)
|
||||||
|
|
||||||
def accept(self):
|
def accept(self) -> None:
|
||||||
self.saveField()
|
self.saveField()
|
||||||
|
|
||||||
def save():
|
def save() -> None:
|
||||||
self.mm.save(self.model)
|
self.mm.save(self.model)
|
||||||
|
|
||||||
def on_done(fut):
|
def on_done(fut) -> None:
|
||||||
try:
|
try:
|
||||||
fut.result()
|
fut.result()
|
||||||
except TemplateError as e:
|
except TemplateError as e:
|
||||||
|
@ -245,5 +248,5 @@ class FieldDialog(QDialog):
|
||||||
|
|
||||||
self.mw.taskman.with_progress(save, on_done, self)
|
self.mw.taskman.with_progress(save, on_done, self)
|
||||||
|
|
||||||
def onHelp(self):
|
def onHelp(self) -> None:
|
||||||
openHelp(HelpPage.CUSTOMIZING_FIELDS)
|
openHelp(HelpPage.CUSTOMIZING_FIELDS)
|
||||||
|
|
|
@ -9,12 +9,13 @@ import traceback
|
||||||
import unicodedata
|
import unicodedata
|
||||||
import zipfile
|
import zipfile
|
||||||
from concurrent.futures import Future
|
from concurrent.futures import Future
|
||||||
from typing import Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
import anki.importing as importing
|
import anki.importing as importing
|
||||||
import aqt.deckchooser
|
import aqt.deckchooser
|
||||||
import aqt.forms
|
import aqt.forms
|
||||||
import aqt.modelchooser
|
import aqt.modelchooser
|
||||||
|
from anki.importing.apkg import AnkiPackageImporter
|
||||||
from aqt import AnkiQt, gui_hooks
|
from aqt import AnkiQt, gui_hooks
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.utils import (
|
from aqt.utils import (
|
||||||
|
@ -34,7 +35,7 @@ from aqt.utils import (
|
||||||
|
|
||||||
|
|
||||||
class ChangeMap(QDialog):
|
class ChangeMap(QDialog):
|
||||||
def __init__(self, mw: AnkiQt, model, current):
|
def __init__(self, mw: AnkiQt, model, current) -> None:
|
||||||
QDialog.__init__(self, mw, Qt.Window)
|
QDialog.__init__(self, mw, Qt.Window)
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
self.model = model
|
self.model = model
|
||||||
|
@ -59,11 +60,11 @@ class ChangeMap(QDialog):
|
||||||
self.frm.fields.setCurrentRow(n + 1)
|
self.frm.fields.setCurrentRow(n + 1)
|
||||||
self.field: Optional[str] = None
|
self.field: Optional[str] = None
|
||||||
|
|
||||||
def getField(self):
|
def getField(self) -> str:
|
||||||
self.exec_()
|
self.exec_()
|
||||||
return self.field
|
return self.field
|
||||||
|
|
||||||
def accept(self):
|
def accept(self) -> None:
|
||||||
row = self.frm.fields.currentRow()
|
row = self.frm.fields.currentRow()
|
||||||
if row < len(self.model["flds"]):
|
if row < len(self.model["flds"]):
|
||||||
self.field = self.model["flds"][row]["name"]
|
self.field = self.model["flds"][row]["name"]
|
||||||
|
@ -73,7 +74,7 @@ class ChangeMap(QDialog):
|
||||||
self.field = None
|
self.field = None
|
||||||
QDialog.accept(self)
|
QDialog.accept(self)
|
||||||
|
|
||||||
def reject(self):
|
def reject(self) -> None:
|
||||||
self.accept()
|
self.accept()
|
||||||
|
|
||||||
|
|
||||||
|
@ -106,19 +107,19 @@ class ImportDialog(QDialog):
|
||||||
self.frm.buttonBox.addButton(b, QDialogButtonBox.AcceptRole)
|
self.frm.buttonBox.addButton(b, QDialogButtonBox.AcceptRole)
|
||||||
self.exec_()
|
self.exec_()
|
||||||
|
|
||||||
def setupOptions(self):
|
def setupOptions(self) -> None:
|
||||||
self.model = self.mw.col.models.current()
|
self.model = self.mw.col.models.current()
|
||||||
self.modelChooser = aqt.modelchooser.ModelChooser(
|
self.modelChooser = aqt.modelchooser.ModelChooser(
|
||||||
self.mw, self.frm.modelArea, label=False
|
self.mw, self.frm.modelArea, label=False
|
||||||
)
|
)
|
||||||
self.deck = aqt.deckchooser.DeckChooser(self.mw, self.frm.deckArea, label=False)
|
self.deck = aqt.deckchooser.DeckChooser(self.mw, self.frm.deckArea, label=False)
|
||||||
|
|
||||||
def modelChanged(self, unused=None):
|
def modelChanged(self, unused: Any = None) -> None:
|
||||||
self.importer.model = self.mw.col.models.current()
|
self.importer.model = self.mw.col.models.current()
|
||||||
self.importer.initMapping()
|
self.importer.initMapping()
|
||||||
self.showMapping()
|
self.showMapping()
|
||||||
|
|
||||||
def onDelimiter(self):
|
def onDelimiter(self) -> None:
|
||||||
str = (
|
str = (
|
||||||
getOnlyText(
|
getOnlyText(
|
||||||
tr(TR.IMPORTING_BY_DEFAULT_ANKI_WILL_DETECT_THE),
|
tr(TR.IMPORTING_BY_DEFAULT_ANKI_WILL_DETECT_THE),
|
||||||
|
@ -135,14 +136,14 @@ class ImportDialog(QDialog):
|
||||||
return
|
return
|
||||||
self.hideMapping()
|
self.hideMapping()
|
||||||
|
|
||||||
def updateDelim():
|
def updateDelim() -> None:
|
||||||
self.importer.delimiter = str
|
self.importer.delimiter = str
|
||||||
self.importer.updateDelimiter()
|
self.importer.updateDelimiter()
|
||||||
|
|
||||||
self.showMapping(hook=updateDelim)
|
self.showMapping(hook=updateDelim)
|
||||||
self.updateDelimiterButtonText()
|
self.updateDelimiterButtonText()
|
||||||
|
|
||||||
def updateDelimiterButtonText(self):
|
def updateDelimiterButtonText(self) -> None:
|
||||||
if not self.importer.needDelimiter:
|
if not self.importer.needDelimiter:
|
||||||
return
|
return
|
||||||
if self.importer.delimiter:
|
if self.importer.delimiter:
|
||||||
|
@ -164,7 +165,7 @@ class ImportDialog(QDialog):
|
||||||
txt = tr(TR.IMPORTING_FIELDS_SEPARATED_BY, val=d)
|
txt = tr(TR.IMPORTING_FIELDS_SEPARATED_BY, val=d)
|
||||||
self.frm.autoDetect.setText(txt)
|
self.frm.autoDetect.setText(txt)
|
||||||
|
|
||||||
def accept(self):
|
def accept(self) -> None:
|
||||||
self.importer.mapping = self.mapping
|
self.importer.mapping = self.mapping
|
||||||
if not self.importer.mappingOk():
|
if not self.importer.mappingOk():
|
||||||
showWarning(tr(TR.IMPORTING_THE_FIRST_FIELD_OF_THE_NOTE))
|
showWarning(tr(TR.IMPORTING_THE_FIRST_FIELD_OF_THE_NOTE))
|
||||||
|
@ -182,7 +183,7 @@ class ImportDialog(QDialog):
|
||||||
self.mw.progress.start()
|
self.mw.progress.start()
|
||||||
self.mw.checkpoint(tr(TR.ACTIONS_IMPORT))
|
self.mw.checkpoint(tr(TR.ACTIONS_IMPORT))
|
||||||
|
|
||||||
def on_done(future: Future):
|
def on_done(future: Future) -> None:
|
||||||
self.mw.progress.finish()
|
self.mw.progress.finish()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -211,19 +212,21 @@ class ImportDialog(QDialog):
|
||||||
|
|
||||||
self.mw.taskman.run_in_background(self.importer.run, on_done)
|
self.mw.taskman.run_in_background(self.importer.run, on_done)
|
||||||
|
|
||||||
def setupMappingFrame(self):
|
def setupMappingFrame(self) -> None:
|
||||||
# qt seems to have a bug with adding/removing from a grid, so we add
|
# qt seems to have a bug with adding/removing from a grid, so we add
|
||||||
# to a separate object and add/remove that instead
|
# to a separate object and add/remove that instead
|
||||||
self.frame = QFrame(self.frm.mappingArea)
|
self.frame = QFrame(self.frm.mappingArea)
|
||||||
self.frm.mappingArea.setWidget(self.frame)
|
self.frm.mappingArea.setWidget(self.frame)
|
||||||
self.mapbox = QVBoxLayout(self.frame)
|
self.mapbox = QVBoxLayout(self.frame)
|
||||||
self.mapbox.setContentsMargins(0, 0, 0, 0)
|
self.mapbox.setContentsMargins(0, 0, 0, 0)
|
||||||
self.mapwidget = None
|
self.mapwidget: Optional[QWidget] = None
|
||||||
|
|
||||||
def hideMapping(self):
|
def hideMapping(self) -> None:
|
||||||
self.frm.mappingGroup.hide()
|
self.frm.mappingGroup.hide()
|
||||||
|
|
||||||
def showMapping(self, keepMapping=False, hook=None):
|
def showMapping(
|
||||||
|
self, keepMapping: bool = False, hook: Optional[Callable] = None
|
||||||
|
) -> None:
|
||||||
if hook:
|
if hook:
|
||||||
hook()
|
hook()
|
||||||
if not keepMapping:
|
if not keepMapping:
|
||||||
|
@ -255,7 +258,7 @@ class ImportDialog(QDialog):
|
||||||
self.grid.addWidget(button, num, 2)
|
self.grid.addWidget(button, num, 2)
|
||||||
qconnect(button.clicked, lambda _, s=self, n=num: s.changeMappingNum(n))
|
qconnect(button.clicked, lambda _, s=self, n=num: s.changeMappingNum(n))
|
||||||
|
|
||||||
def changeMappingNum(self, n):
|
def changeMappingNum(self, n) -> None:
|
||||||
f = ChangeMap(self.mw, self.importer.model, self.mapping[n]).getField()
|
f = ChangeMap(self.mw, self.importer.model, self.mapping[n]).getField()
|
||||||
try:
|
try:
|
||||||
# make sure we don't have it twice
|
# make sure we don't have it twice
|
||||||
|
@ -267,7 +270,7 @@ class ImportDialog(QDialog):
|
||||||
if getattr(self.importer, "delimiter", False):
|
if getattr(self.importer, "delimiter", False):
|
||||||
self.savedDelimiter = self.importer.delimiter
|
self.savedDelimiter = self.importer.delimiter
|
||||||
|
|
||||||
def updateDelim():
|
def updateDelim() -> None:
|
||||||
self.importer.delimiter = self.savedDelimiter
|
self.importer.delimiter = self.savedDelimiter
|
||||||
|
|
||||||
self.showMapping(hook=updateDelim, keepMapping=True)
|
self.showMapping(hook=updateDelim, keepMapping=True)
|
||||||
|
@ -280,22 +283,22 @@ class ImportDialog(QDialog):
|
||||||
gui_hooks.current_note_type_did_change.remove(self.modelChanged)
|
gui_hooks.current_note_type_did_change.remove(self.modelChanged)
|
||||||
QDialog.reject(self)
|
QDialog.reject(self)
|
||||||
|
|
||||||
def helpRequested(self):
|
def helpRequested(self) -> None:
|
||||||
openHelp(HelpPage.IMPORTING)
|
openHelp(HelpPage.IMPORTING)
|
||||||
|
|
||||||
def importModeChanged(self, newImportMode):
|
def importModeChanged(self, newImportMode) -> None:
|
||||||
if newImportMode == 0:
|
if newImportMode == 0:
|
||||||
self.frm.tagModified.setEnabled(True)
|
self.frm.tagModified.setEnabled(True)
|
||||||
else:
|
else:
|
||||||
self.frm.tagModified.setEnabled(False)
|
self.frm.tagModified.setEnabled(False)
|
||||||
|
|
||||||
|
|
||||||
def showUnicodeWarning():
|
def showUnicodeWarning() -> None:
|
||||||
"""Shorthand to show a standard warning."""
|
"""Shorthand to show a standard warning."""
|
||||||
showWarning(tr(TR.IMPORTING_SELECTED_FILE_WAS_NOT_IN_UTF8))
|
showWarning(tr(TR.IMPORTING_SELECTED_FILE_WAS_NOT_IN_UTF8))
|
||||||
|
|
||||||
|
|
||||||
def onImport(mw):
|
def onImport(mw: AnkiQt) -> None:
|
||||||
filt = ";;".join([x[0] for x in importing.Importers])
|
filt = ";;".join([x[0] for x in importing.Importers])
|
||||||
file = getFile(mw, tr(TR.ACTIONS_IMPORT), None, key="import", filter=filt)
|
file = getFile(mw, tr(TR.ACTIONS_IMPORT), None, key="import", filter=filt)
|
||||||
if not file:
|
if not file:
|
||||||
|
@ -314,7 +317,7 @@ def onImport(mw):
|
||||||
importFile(mw, file)
|
importFile(mw, file)
|
||||||
|
|
||||||
|
|
||||||
def importFile(mw, file):
|
def importFile(mw: AnkiQt, file: str) -> None:
|
||||||
importerClass = None
|
importerClass = None
|
||||||
done = False
|
done = False
|
||||||
for i in importing.Importers:
|
for i in importing.Importers:
|
||||||
|
@ -371,7 +374,7 @@ def importFile(mw, file):
|
||||||
# importing non-colpkg files
|
# importing non-colpkg files
|
||||||
mw.progress.start(immediate=True)
|
mw.progress.start(immediate=True)
|
||||||
|
|
||||||
def on_done(future: Future):
|
def on_done(future: Future) -> None:
|
||||||
mw.progress.finish()
|
mw.progress.finish()
|
||||||
try:
|
try:
|
||||||
future.result()
|
future.result()
|
||||||
|
@ -402,11 +405,11 @@ def importFile(mw, file):
|
||||||
mw.taskman.run_in_background(importer.run, on_done)
|
mw.taskman.run_in_background(importer.run, on_done)
|
||||||
|
|
||||||
|
|
||||||
def invalidZipMsg():
|
def invalidZipMsg() -> str:
|
||||||
return tr(TR.IMPORTING_THIS_FILE_DOES_NOT_APPEAR_TO)
|
return tr(TR.IMPORTING_THIS_FILE_DOES_NOT_APPEAR_TO)
|
||||||
|
|
||||||
|
|
||||||
def setupApkgImport(mw, importer):
|
def setupApkgImport(mw: AnkiQt, importer: AnkiPackageImporter) -> bool:
|
||||||
base = os.path.basename(importer.file).lower()
|
base = os.path.basename(importer.file).lower()
|
||||||
full = (
|
full = (
|
||||||
(base == "collection.apkg")
|
(base == "collection.apkg")
|
||||||
|
@ -424,16 +427,17 @@ def setupApkgImport(mw, importer):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
replaceWithApkg(mw, importer.file, mw.restoringBackup)
|
replaceWithApkg(mw, importer.file, mw.restoringBackup)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def replaceWithApkg(mw, file, backup):
|
def replaceWithApkg(mw, file, backup) -> None:
|
||||||
mw.unloadCollection(lambda: _replaceWithApkg(mw, file, backup))
|
mw.unloadCollection(lambda: _replaceWithApkg(mw, file, backup))
|
||||||
|
|
||||||
|
|
||||||
def _replaceWithApkg(mw, filename, backup):
|
def _replaceWithApkg(mw, filename, backup) -> None:
|
||||||
mw.progress.start(immediate=True)
|
mw.progress.start(immediate=True)
|
||||||
|
|
||||||
def do_import():
|
def do_import() -> None:
|
||||||
z = zipfile.ZipFile(filename)
|
z = zipfile.ZipFile(filename)
|
||||||
|
|
||||||
# v2 scheduler?
|
# v2 scheduler?
|
||||||
|
@ -468,7 +472,7 @@ def _replaceWithApkg(mw, filename, backup):
|
||||||
|
|
||||||
z.close()
|
z.close()
|
||||||
|
|
||||||
def on_done(future: Future):
|
def on_done(future: Future) -> None:
|
||||||
mw.progress.finish()
|
mw.progress.finish()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
Legacy support
|
Legacy support
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import List
|
from typing import Any, List
|
||||||
|
|
||||||
import anki
|
import anki
|
||||||
import aqt
|
import aqt
|
||||||
|
@ -31,7 +31,14 @@ def stripSounds(text) -> str:
|
||||||
return aqt.mw.col.media.strip_av_tags(text)
|
return aqt.mw.col.media.strip_av_tags(text)
|
||||||
|
|
||||||
|
|
||||||
def fmtTimeSpan(time, pad=0, point=0, short=False, inTime=False, unit=99):
|
def fmtTimeSpan(
|
||||||
|
time: Any,
|
||||||
|
pad: Any = 0,
|
||||||
|
point: Any = 0,
|
||||||
|
short: Any = False,
|
||||||
|
inTime: Any = False,
|
||||||
|
unit: Any = 99,
|
||||||
|
) -> Any:
|
||||||
print("fmtTimeSpan() has become col.format_timespan()")
|
print("fmtTimeSpan() has become col.format_timespan()")
|
||||||
return aqt.mw.col.format_timespan(time)
|
return aqt.mw.col.format_timespan(time)
|
||||||
|
|
||||||
|
|
160
qt/aqt/main.py
160
qt/aqt/main.py
|
@ -14,7 +14,7 @@ import weakref
|
||||||
import zipfile
|
import zipfile
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from typing import Any, Callable, List, Optional, Sequence, TextIO, Tuple, cast
|
from typing import Any, Callable, List, Optional, Sequence, TextIO, Tuple, Union, cast
|
||||||
|
|
||||||
import anki
|
import anki
|
||||||
import aqt
|
import aqt
|
||||||
|
@ -70,6 +70,7 @@ install_pylib_legacy()
|
||||||
|
|
||||||
|
|
||||||
class ResetReason(enum.Enum):
|
class ResetReason(enum.Enum):
|
||||||
|
Unknown = "unknown"
|
||||||
AddCardsAddNote = "addCardsAddNote"
|
AddCardsAddNote = "addCardsAddNote"
|
||||||
EditCurrentInit = "editCurrentInit"
|
EditCurrentInit = "editCurrentInit"
|
||||||
EditorBridgeCmd = "editorBridgeCmd"
|
EditorBridgeCmd = "editorBridgeCmd"
|
||||||
|
@ -85,7 +86,7 @@ class ResetReason(enum.Enum):
|
||||||
|
|
||||||
|
|
||||||
class ResetRequired:
|
class ResetRequired:
|
||||||
def __init__(self, mw: AnkiQt):
|
def __init__(self, mw: AnkiQt) -> None:
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
|
|
||||||
|
|
||||||
|
@ -137,7 +138,7 @@ class AnkiQt(QMainWindow):
|
||||||
else:
|
else:
|
||||||
fn = self.setupProfile
|
fn = self.setupProfile
|
||||||
|
|
||||||
def on_window_init():
|
def on_window_init() -> None:
|
||||||
fn()
|
fn()
|
||||||
gui_hooks.main_window_did_init()
|
gui_hooks.main_window_did_init()
|
||||||
|
|
||||||
|
@ -173,7 +174,7 @@ class AnkiQt(QMainWindow):
|
||||||
"Actions that are deferred until after add-on loading."
|
"Actions that are deferred until after add-on loading."
|
||||||
self.toolbar.draw()
|
self.toolbar.draw()
|
||||||
|
|
||||||
def setupProfileAfterWebviewsLoaded(self):
|
def setupProfileAfterWebviewsLoaded(self) -> None:
|
||||||
for w in (self.web, self.bottomWeb):
|
for w in (self.web, self.bottomWeb):
|
||||||
if not w._domDone:
|
if not w._domDone:
|
||||||
self.progress.timer(
|
self.progress.timer(
|
||||||
|
@ -204,7 +205,7 @@ class AnkiQt(QMainWindow):
|
||||||
self.onClose.emit() # type: ignore
|
self.onClose.emit() # type: ignore
|
||||||
evt.accept()
|
evt.accept()
|
||||||
|
|
||||||
def closeWithoutQuitting(self):
|
def closeWithoutQuitting(self) -> None:
|
||||||
self.closeFires = False
|
self.closeFires = False
|
||||||
self.close()
|
self.close()
|
||||||
self.closeFires = True
|
self.closeFires = True
|
||||||
|
@ -273,9 +274,10 @@ class AnkiQt(QMainWindow):
|
||||||
name = self.pm.profiles()[n]
|
name = self.pm.profiles()[n]
|
||||||
self.pm.load(name)
|
self.pm.load(name)
|
||||||
|
|
||||||
def openProfile(self):
|
def openProfile(self) -> None:
|
||||||
name = self.pm.profiles()[self.profileForm.profiles.currentRow()]
|
name = self.pm.profiles()[self.profileForm.profiles.currentRow()]
|
||||||
return self.pm.load(name)
|
self.pm.load(name)
|
||||||
|
return
|
||||||
|
|
||||||
def onOpenProfile(self) -> None:
|
def onOpenProfile(self) -> None:
|
||||||
self.profileDiag.hide()
|
self.profileDiag.hide()
|
||||||
|
@ -286,34 +288,37 @@ class AnkiQt(QMainWindow):
|
||||||
def profileNameOk(self, name: str) -> bool:
|
def profileNameOk(self, name: str) -> bool:
|
||||||
return not checkInvalidFilename(name) and name != "addons21"
|
return not checkInvalidFilename(name) and name != "addons21"
|
||||||
|
|
||||||
def onAddProfile(self):
|
def onAddProfile(self) -> None:
|
||||||
name = getOnlyText(tr(TR.ACTIONS_NAME)).strip()
|
name = getOnlyText(tr(TR.ACTIONS_NAME)).strip()
|
||||||
if name:
|
if name:
|
||||||
if name in self.pm.profiles():
|
if name in self.pm.profiles():
|
||||||
return showWarning(tr(TR.QT_MISC_NAME_EXISTS))
|
showWarning(tr(TR.QT_MISC_NAME_EXISTS))
|
||||||
|
return
|
||||||
if not self.profileNameOk(name):
|
if not self.profileNameOk(name):
|
||||||
return
|
return
|
||||||
self.pm.create(name)
|
self.pm.create(name)
|
||||||
self.pm.name = name
|
self.pm.name = name
|
||||||
self.refreshProfilesList()
|
self.refreshProfilesList()
|
||||||
|
|
||||||
def onRenameProfile(self):
|
def onRenameProfile(self) -> None:
|
||||||
name = getOnlyText(tr(TR.ACTIONS_NEW_NAME), default=self.pm.name).strip()
|
name = getOnlyText(tr(TR.ACTIONS_NEW_NAME), default=self.pm.name).strip()
|
||||||
if not name:
|
if not name:
|
||||||
return
|
return
|
||||||
if name == self.pm.name:
|
if name == self.pm.name:
|
||||||
return
|
return
|
||||||
if name in self.pm.profiles():
|
if name in self.pm.profiles():
|
||||||
return showWarning(tr(TR.QT_MISC_NAME_EXISTS))
|
showWarning(tr(TR.QT_MISC_NAME_EXISTS))
|
||||||
|
return
|
||||||
if not self.profileNameOk(name):
|
if not self.profileNameOk(name):
|
||||||
return
|
return
|
||||||
self.pm.rename(name)
|
self.pm.rename(name)
|
||||||
self.refreshProfilesList()
|
self.refreshProfilesList()
|
||||||
|
|
||||||
def onRemProfile(self):
|
def onRemProfile(self) -> None:
|
||||||
profs = self.pm.profiles()
|
profs = self.pm.profiles()
|
||||||
if len(profs) < 2:
|
if len(profs) < 2:
|
||||||
return showWarning(tr(TR.QT_MISC_THERE_MUST_BE_AT_LEAST_ONE))
|
showWarning(tr(TR.QT_MISC_THERE_MUST_BE_AT_LEAST_ONE))
|
||||||
|
return
|
||||||
# sure?
|
# sure?
|
||||||
if not askUser(
|
if not askUser(
|
||||||
tr(TR.QT_MISC_ALL_CARDS_NOTES_AND_MEDIA_FOR),
|
tr(TR.QT_MISC_ALL_CARDS_NOTES_AND_MEDIA_FOR),
|
||||||
|
@ -324,7 +329,7 @@ class AnkiQt(QMainWindow):
|
||||||
self.pm.remove(self.pm.name)
|
self.pm.remove(self.pm.name)
|
||||||
self.refreshProfilesList()
|
self.refreshProfilesList()
|
||||||
|
|
||||||
def onOpenBackup(self):
|
def onOpenBackup(self) -> None:
|
||||||
if not askUser(
|
if not askUser(
|
||||||
tr(TR.QT_MISC_REPLACE_YOUR_COLLECTION_WITH_AN_EARLIER),
|
tr(TR.QT_MISC_REPLACE_YOUR_COLLECTION_WITH_AN_EARLIER),
|
||||||
msgfunc=QMessageBox.warning,
|
msgfunc=QMessageBox.warning,
|
||||||
|
@ -332,7 +337,7 @@ class AnkiQt(QMainWindow):
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
def doOpen(path):
|
def doOpen(path) -> None:
|
||||||
self._openBackup(path)
|
self._openBackup(path)
|
||||||
|
|
||||||
getFile(
|
getFile(
|
||||||
|
@ -343,7 +348,7 @@ class AnkiQt(QMainWindow):
|
||||||
dir=self.pm.backupFolder(),
|
dir=self.pm.backupFolder(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _openBackup(self, path):
|
def _openBackup(self, path) -> None:
|
||||||
try:
|
try:
|
||||||
# move the existing collection to the trash, as it may not open
|
# move the existing collection to the trash, as it may not open
|
||||||
self.pm.trashCollection()
|
self.pm.trashCollection()
|
||||||
|
@ -358,14 +363,14 @@ class AnkiQt(QMainWindow):
|
||||||
|
|
||||||
self.onOpenProfile()
|
self.onOpenProfile()
|
||||||
|
|
||||||
def _on_downgrade(self):
|
def _on_downgrade(self) -> None:
|
||||||
self.progress.start()
|
self.progress.start()
|
||||||
profiles = self.pm.profiles()
|
profiles = self.pm.profiles()
|
||||||
|
|
||||||
def downgrade():
|
def downgrade() -> List[str]:
|
||||||
return self.pm.downgrade(profiles)
|
return self.pm.downgrade(profiles)
|
||||||
|
|
||||||
def on_done(future):
|
def on_done(future) -> None:
|
||||||
self.progress.finish()
|
self.progress.finish()
|
||||||
problems = future.result()
|
problems = future.result()
|
||||||
if not problems:
|
if not problems:
|
||||||
|
@ -407,7 +412,7 @@ class AnkiQt(QMainWindow):
|
||||||
self.pendingImport = None
|
self.pendingImport = None
|
||||||
gui_hooks.profile_did_open()
|
gui_hooks.profile_did_open()
|
||||||
|
|
||||||
def _onsuccess():
|
def _onsuccess() -> None:
|
||||||
self._refresh_after_sync()
|
self._refresh_after_sync()
|
||||||
if onsuccess:
|
if onsuccess:
|
||||||
onsuccess()
|
onsuccess()
|
||||||
|
@ -415,7 +420,7 @@ class AnkiQt(QMainWindow):
|
||||||
self.maybe_auto_sync_on_open_close(_onsuccess)
|
self.maybe_auto_sync_on_open_close(_onsuccess)
|
||||||
|
|
||||||
def unloadProfile(self, onsuccess: Callable) -> None:
|
def unloadProfile(self, onsuccess: Callable) -> None:
|
||||||
def callback():
|
def callback() -> None:
|
||||||
self._unloadProfile()
|
self._unloadProfile()
|
||||||
onsuccess()
|
onsuccess()
|
||||||
|
|
||||||
|
@ -445,7 +450,7 @@ class AnkiQt(QMainWindow):
|
||||||
def unloadProfileAndExit(self) -> None:
|
def unloadProfileAndExit(self) -> None:
|
||||||
self.unloadProfile(self.cleanupAndExit)
|
self.unloadProfile(self.cleanupAndExit)
|
||||||
|
|
||||||
def unloadProfileAndShowProfileManager(self):
|
def unloadProfileAndShowProfileManager(self) -> None:
|
||||||
self.unloadProfile(self.showProfileManager)
|
self.unloadProfile(self.showProfileManager)
|
||||||
|
|
||||||
def cleanupAndExit(self) -> None:
|
def cleanupAndExit(self) -> None:
|
||||||
|
@ -509,23 +514,23 @@ class AnkiQt(QMainWindow):
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _loadCollection(self):
|
def _loadCollection(self) -> None:
|
||||||
cpath = self.pm.collectionPath()
|
cpath = self.pm.collectionPath()
|
||||||
self.col = Collection(cpath, backend=self.backend, log=True)
|
self.col = Collection(cpath, backend=self.backend, log=True)
|
||||||
self.setEnabled(True)
|
self.setEnabled(True)
|
||||||
|
|
||||||
def reopen(self):
|
def reopen(self) -> None:
|
||||||
self.col.reopen()
|
self.col.reopen()
|
||||||
|
|
||||||
def unloadCollection(self, onsuccess: Callable) -> None:
|
def unloadCollection(self, onsuccess: Callable) -> None:
|
||||||
def after_media_sync():
|
def after_media_sync() -> None:
|
||||||
self._unloadCollection()
|
self._unloadCollection()
|
||||||
onsuccess()
|
onsuccess()
|
||||||
|
|
||||||
def after_sync():
|
def after_sync() -> None:
|
||||||
self.media_syncer.show_diag_until_finished(after_media_sync)
|
self.media_syncer.show_diag_until_finished(after_media_sync)
|
||||||
|
|
||||||
def before_sync():
|
def before_sync() -> None:
|
||||||
self.setEnabled(False)
|
self.setEnabled(False)
|
||||||
self.maybe_auto_sync_on_open_close(after_sync)
|
self.maybe_auto_sync_on_open_close(after_sync)
|
||||||
|
|
||||||
|
@ -563,7 +568,7 @@ class AnkiQt(QMainWindow):
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
class BackupThread(Thread):
|
class BackupThread(Thread):
|
||||||
def __init__(self, path, data):
|
def __init__(self, path, data) -> None:
|
||||||
Thread.__init__(self)
|
Thread.__init__(self)
|
||||||
self.path = path
|
self.path = path
|
||||||
self.data = data
|
self.data = data
|
||||||
|
@ -572,7 +577,7 @@ class AnkiQt(QMainWindow):
|
||||||
with open(self.path, "wb") as file:
|
with open(self.path, "wb") as file:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def run(self):
|
def run(self) -> None:
|
||||||
z = zipfile.ZipFile(self.path, "w", zipfile.ZIP_DEFLATED)
|
z = zipfile.ZipFile(self.path, "w", zipfile.ZIP_DEFLATED)
|
||||||
z.writestr("collection.anki2", self.data)
|
z.writestr("collection.anki2", self.data)
|
||||||
z.writestr("media", "{}")
|
z.writestr("media", "{}")
|
||||||
|
@ -655,10 +660,10 @@ class AnkiQt(QMainWindow):
|
||||||
return self.moveToState("deckBrowser")
|
return self.moveToState("deckBrowser")
|
||||||
self.overview.show()
|
self.overview.show()
|
||||||
|
|
||||||
def _reviewState(self, oldState):
|
def _reviewState(self, oldState: str) -> None:
|
||||||
self.reviewer.show()
|
self.reviewer.show()
|
||||||
|
|
||||||
def _reviewCleanup(self, newState):
|
def _reviewCleanup(self, newState: str) -> None:
|
||||||
if newState != "resetRequired" and newState != "review":
|
if newState != "resetRequired" and newState != "review":
|
||||||
self.reviewer.cleanup()
|
self.reviewer.cleanup()
|
||||||
|
|
||||||
|
@ -674,7 +679,12 @@ class AnkiQt(QMainWindow):
|
||||||
self.maybeEnableUndo()
|
self.maybeEnableUndo()
|
||||||
self.moveToState(self.state)
|
self.moveToState(self.state)
|
||||||
|
|
||||||
def requireReset(self, modal=False, reason="unknown", context=None):
|
def requireReset(
|
||||||
|
self,
|
||||||
|
modal: bool = False,
|
||||||
|
reason: ResetReason = ResetReason.Unknown,
|
||||||
|
context: Any = None,
|
||||||
|
) -> None:
|
||||||
"Signal queue needs to be rebuilt when edits are finished or by user."
|
"Signal queue needs to be rebuilt when edits are finished or by user."
|
||||||
self.autosave()
|
self.autosave()
|
||||||
self.resetModal = modal
|
self.resetModal = modal
|
||||||
|
@ -683,7 +693,7 @@ class AnkiQt(QMainWindow):
|
||||||
):
|
):
|
||||||
self.moveToState("resetRequired")
|
self.moveToState("resetRequired")
|
||||||
|
|
||||||
def interactiveState(self):
|
def interactiveState(self) -> bool:
|
||||||
"True if not in profile manager, syncing, etc."
|
"True if not in profile manager, syncing, etc."
|
||||||
return self.state in ("overview", "review", "deckBrowser")
|
return self.state in ("overview", "review", "deckBrowser")
|
||||||
|
|
||||||
|
@ -693,7 +703,7 @@ class AnkiQt(QMainWindow):
|
||||||
self.state = self.returnState
|
self.state = self.returnState
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
def delayedMaybeReset(self):
|
def delayedMaybeReset(self) -> None:
|
||||||
# if we redraw the page in a button click event it will often crash on
|
# if we redraw the page in a button click event it will often crash on
|
||||||
# windows
|
# windows
|
||||||
self.progress.timer(100, self.maybeReset, False)
|
self.progress.timer(100, self.maybeReset, False)
|
||||||
|
@ -794,9 +804,9 @@ title="%s" %s>%s</button>""" % (
|
||||||
signal.signal(signal.SIGINT, self.onUnixSignal)
|
signal.signal(signal.SIGINT, self.onUnixSignal)
|
||||||
signal.signal(signal.SIGTERM, self.onUnixSignal)
|
signal.signal(signal.SIGTERM, self.onUnixSignal)
|
||||||
|
|
||||||
def onUnixSignal(self, signum, frame):
|
def onUnixSignal(self, signum, frame) -> None:
|
||||||
# schedule a rollback & quit
|
# schedule a rollback & quit
|
||||||
def quit():
|
def quit() -> None:
|
||||||
self.col.db.rollback()
|
self.col.db.rollback()
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
|
@ -822,7 +832,7 @@ title="%s" %s>%s</button>""" % (
|
||||||
self.addonManager.loadAddons()
|
self.addonManager.loadAddons()
|
||||||
self.maybe_check_for_addon_updates()
|
self.maybe_check_for_addon_updates()
|
||||||
|
|
||||||
def maybe_check_for_addon_updates(self):
|
def maybe_check_for_addon_updates(self) -> None:
|
||||||
last_check = self.pm.last_addon_update_check()
|
last_check = self.pm.last_addon_update_check()
|
||||||
elap = intTime() - last_check
|
elap = intTime() - last_check
|
||||||
|
|
||||||
|
@ -865,7 +875,7 @@ title="%s" %s>%s</button>""" % (
|
||||||
# Syncing
|
# Syncing
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def on_sync_button_clicked(self):
|
def on_sync_button_clicked(self) -> None:
|
||||||
if self.media_syncer.is_syncing():
|
if self.media_syncer.is_syncing():
|
||||||
self.media_syncer.show_sync_log()
|
self.media_syncer.show_sync_log()
|
||||||
else:
|
else:
|
||||||
|
@ -878,16 +888,16 @@ title="%s" %s>%s</button>""" % (
|
||||||
else:
|
else:
|
||||||
self._sync_collection_and_media(self._refresh_after_sync)
|
self._sync_collection_and_media(self._refresh_after_sync)
|
||||||
|
|
||||||
def _refresh_after_sync(self):
|
def _refresh_after_sync(self) -> None:
|
||||||
self.toolbar.redraw()
|
self.toolbar.redraw()
|
||||||
|
|
||||||
def _sync_collection_and_media(self, after_sync: Callable[[], None]):
|
def _sync_collection_and_media(self, after_sync: Callable[[], None]) -> None:
|
||||||
"Caller should ensure auth available."
|
"Caller should ensure auth available."
|
||||||
# start media sync if not already running
|
# start media sync if not already running
|
||||||
if not self.media_syncer.is_syncing():
|
if not self.media_syncer.is_syncing():
|
||||||
self.media_syncer.start()
|
self.media_syncer.start()
|
||||||
|
|
||||||
def on_collection_sync_finished():
|
def on_collection_sync_finished() -> None:
|
||||||
self.col.clearUndo()
|
self.col.clearUndo()
|
||||||
self.col.models._clear_cache()
|
self.col.models._clear_cache()
|
||||||
gui_hooks.sync_did_finish()
|
gui_hooks.sync_did_finish()
|
||||||
|
@ -920,7 +930,7 @@ title="%s" %s>%s</button>""" % (
|
||||||
)
|
)
|
||||||
|
|
||||||
# legacy
|
# legacy
|
||||||
def _sync(self):
|
def _sync(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
onSync = on_sync_button_clicked
|
onSync = on_sync_button_clicked
|
||||||
|
@ -1027,7 +1037,7 @@ title="%s" %s>%s</button>""" % (
|
||||||
self.form.actionUndo.setEnabled(False)
|
self.form.actionUndo.setEnabled(False)
|
||||||
gui_hooks.undo_state_did_change(False)
|
gui_hooks.undo_state_did_change(False)
|
||||||
|
|
||||||
def checkpoint(self, name):
|
def checkpoint(self, name: str) -> None:
|
||||||
self.col.save(name)
|
self.col.save(name)
|
||||||
self.maybeEnableUndo()
|
self.maybeEnableUndo()
|
||||||
|
|
||||||
|
@ -1046,10 +1056,10 @@ title="%s" %s>%s</button>""" % (
|
||||||
def onBrowse(self) -> None:
|
def onBrowse(self) -> None:
|
||||||
aqt.dialogs.open("Browser", self, card=self.reviewer.card)
|
aqt.dialogs.open("Browser", self, card=self.reviewer.card)
|
||||||
|
|
||||||
def onEditCurrent(self):
|
def onEditCurrent(self) -> None:
|
||||||
aqt.dialogs.open("EditCurrent", self)
|
aqt.dialogs.open("EditCurrent", self)
|
||||||
|
|
||||||
def onDeckConf(self, deck=None):
|
def onDeckConf(self, deck: Optional[Deck] = None) -> None:
|
||||||
import aqt.deckconf
|
import aqt.deckconf
|
||||||
|
|
||||||
if not deck:
|
if not deck:
|
||||||
|
@ -1059,11 +1069,11 @@ title="%s" %s>%s</button>""" % (
|
||||||
else:
|
else:
|
||||||
aqt.deckconf.DeckConf(self, deck)
|
aqt.deckconf.DeckConf(self, deck)
|
||||||
|
|
||||||
def onOverview(self):
|
def onOverview(self) -> None:
|
||||||
self.col.reset()
|
self.col.reset()
|
||||||
self.moveToState("overview")
|
self.moveToState("overview")
|
||||||
|
|
||||||
def onStats(self):
|
def onStats(self) -> None:
|
||||||
deck = self._selectedDeck()
|
deck = self._selectedDeck()
|
||||||
if not deck:
|
if not deck:
|
||||||
return
|
return
|
||||||
|
@ -1073,21 +1083,21 @@ title="%s" %s>%s</button>""" % (
|
||||||
else:
|
else:
|
||||||
aqt.dialogs.open("NewDeckStats", self)
|
aqt.dialogs.open("NewDeckStats", self)
|
||||||
|
|
||||||
def onPrefs(self):
|
def onPrefs(self) -> None:
|
||||||
aqt.dialogs.open("Preferences", self)
|
aqt.dialogs.open("Preferences", self)
|
||||||
|
|
||||||
def onNoteTypes(self):
|
def onNoteTypes(self) -> None:
|
||||||
import aqt.models
|
import aqt.models
|
||||||
|
|
||||||
aqt.models.Models(self, self, fromMain=True)
|
aqt.models.Models(self, self, fromMain=True)
|
||||||
|
|
||||||
def onAbout(self):
|
def onAbout(self) -> None:
|
||||||
aqt.dialogs.open("About", self)
|
aqt.dialogs.open("About", self)
|
||||||
|
|
||||||
def onDonate(self):
|
def onDonate(self) -> None:
|
||||||
openLink(aqt.appDonate)
|
openLink(aqt.appDonate)
|
||||||
|
|
||||||
def onDocumentation(self):
|
def onDocumentation(self) -> None:
|
||||||
openHelp(HelpPage.INDEX)
|
openHelp(HelpPage.INDEX)
|
||||||
|
|
||||||
# Importing & exporting
|
# Importing & exporting
|
||||||
|
@ -1103,12 +1113,12 @@ title="%s" %s>%s</button>""" % (
|
||||||
aqt.importing.importFile(self, path)
|
aqt.importing.importFile(self, path)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def onImport(self):
|
def onImport(self) -> None:
|
||||||
import aqt.importing
|
import aqt.importing
|
||||||
|
|
||||||
aqt.importing.onImport(self)
|
aqt.importing.onImport(self)
|
||||||
|
|
||||||
def onExport(self, did=None):
|
def onExport(self, did: Optional[int] = None) -> None:
|
||||||
import aqt.exporting
|
import aqt.exporting
|
||||||
|
|
||||||
aqt.exporting.ExportDialog(self, did=did)
|
aqt.exporting.ExportDialog(self, did=did)
|
||||||
|
@ -1116,7 +1126,7 @@ title="%s" %s>%s</button>""" % (
|
||||||
# Installing add-ons from CLI / mimetype handler
|
# Installing add-ons from CLI / mimetype handler
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def installAddon(self, path: str, startup: bool = False):
|
def installAddon(self, path: str, startup: bool = False) -> None:
|
||||||
from aqt.addons import installAddonPackages
|
from aqt.addons import installAddonPackages
|
||||||
|
|
||||||
installAddonPackages(
|
installAddonPackages(
|
||||||
|
@ -1131,7 +1141,7 @@ title="%s" %s>%s</button>""" % (
|
||||||
# Cramming
|
# Cramming
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def onCram(self):
|
def onCram(self) -> None:
|
||||||
aqt.dialogs.open("DynDeckConfDialog", self)
|
aqt.dialogs.open("DynDeckConfDialog", self)
|
||||||
|
|
||||||
# Menu, title bar & status
|
# Menu, title bar & status
|
||||||
|
@ -1174,14 +1184,14 @@ title="%s" %s>%s</button>""" % (
|
||||||
qconnect(self.autoUpdate.clockIsOff, self.clockIsOff)
|
qconnect(self.autoUpdate.clockIsOff, self.clockIsOff)
|
||||||
self.autoUpdate.start()
|
self.autoUpdate.start()
|
||||||
|
|
||||||
def newVerAvail(self, ver):
|
def newVerAvail(self, ver) -> None:
|
||||||
if self.pm.meta.get("suppressUpdate", None) != ver:
|
if self.pm.meta.get("suppressUpdate", None) != ver:
|
||||||
aqt.update.askAndUpdate(self, ver)
|
aqt.update.askAndUpdate(self, ver)
|
||||||
|
|
||||||
def newMsg(self, data):
|
def newMsg(self, data) -> None:
|
||||||
aqt.update.showMessages(self, data)
|
aqt.update.showMessages(self, data)
|
||||||
|
|
||||||
def clockIsOff(self, diff):
|
def clockIsOff(self, diff) -> None:
|
||||||
if devMode:
|
if devMode:
|
||||||
print("clock is off; ignoring")
|
print("clock is off; ignoring")
|
||||||
return
|
return
|
||||||
|
@ -1202,13 +1212,13 @@ title="%s" %s>%s</button>""" % (
|
||||||
# SIGINT/SIGTERM is processed without a long delay
|
# SIGINT/SIGTERM is processed without a long delay
|
||||||
self.progress.timer(1000, lambda: None, True, False)
|
self.progress.timer(1000, lambda: None, True, False)
|
||||||
|
|
||||||
def onRefreshTimer(self):
|
def onRefreshTimer(self) -> None:
|
||||||
if self.state == "deckBrowser":
|
if self.state == "deckBrowser":
|
||||||
self.deckBrowser.refresh()
|
self.deckBrowser.refresh()
|
||||||
elif self.state == "overview":
|
elif self.state == "overview":
|
||||||
self.overview.refresh()
|
self.overview.refresh()
|
||||||
|
|
||||||
def on_autosync_timer(self):
|
def on_autosync_timer(self) -> None:
|
||||||
elap = self.media_syncer.seconds_since_last_sync()
|
elap = self.media_syncer.seconds_since_last_sync()
|
||||||
minutes = self.pm.auto_sync_media_minutes()
|
minutes = self.pm.auto_sync_media_minutes()
|
||||||
if not minutes:
|
if not minutes:
|
||||||
|
@ -1229,7 +1239,7 @@ title="%s" %s>%s</button>""" % (
|
||||||
|
|
||||||
self._activeWindowOnPlay: Optional[QWidget] = None
|
self._activeWindowOnPlay: Optional[QWidget] = None
|
||||||
|
|
||||||
def onOdueInvalid(self):
|
def onOdueInvalid(self) -> None:
|
||||||
showWarning(tr(TR.QT_MISC_INVALID_PROPERTY_FOUND_ON_CARD_PLEASE))
|
showWarning(tr(TR.QT_MISC_INVALID_PROPERTY_FOUND_ON_CARD_PLEASE))
|
||||||
|
|
||||||
def _isVideo(self, tag: AVTag) -> bool:
|
def _isVideo(self, tag: AVTag) -> bool:
|
||||||
|
@ -1274,7 +1284,7 @@ title="%s" %s>%s</button>""" % (
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
# this will gradually be phased out
|
# this will gradually be phased out
|
||||||
def onSchemaMod(self, arg):
|
def onSchemaMod(self, arg: bool) -> bool:
|
||||||
assert self.inMainThread()
|
assert self.inMainThread()
|
||||||
progress_shown = self.progress.busy()
|
progress_shown = self.progress.busy()
|
||||||
if progress_shown:
|
if progress_shown:
|
||||||
|
@ -1295,14 +1305,14 @@ title="%s" %s>%s</button>""" % (
|
||||||
# Advanced features
|
# Advanced features
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def onCheckDB(self):
|
def onCheckDB(self) -> None:
|
||||||
check_db(self)
|
check_db(self)
|
||||||
|
|
||||||
def on_check_media_db(self) -> None:
|
def on_check_media_db(self) -> None:
|
||||||
gui_hooks.media_check_will_start()
|
gui_hooks.media_check_will_start()
|
||||||
check_media_db(self)
|
check_media_db(self)
|
||||||
|
|
||||||
def onStudyDeck(self):
|
def onStudyDeck(self) -> None:
|
||||||
from aqt.studydeck import StudyDeck
|
from aqt.studydeck import StudyDeck
|
||||||
|
|
||||||
ret = StudyDeck(self, dyn=True, current=self.col.decks.current()["name"])
|
ret = StudyDeck(self, dyn=True, current=self.col.decks.current()["name"])
|
||||||
|
@ -1316,11 +1326,11 @@ title="%s" %s>%s</button>""" % (
|
||||||
# Debugging
|
# Debugging
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def onDebug(self):
|
def onDebug(self) -> None:
|
||||||
frm = self.debug_diag_form = aqt.forms.debug.Ui_Dialog()
|
frm = self.debug_diag_form = aqt.forms.debug.Ui_Dialog()
|
||||||
|
|
||||||
class DebugDialog(QDialog):
|
class DebugDialog(QDialog):
|
||||||
def reject(self):
|
def reject(self) -> None:
|
||||||
super().reject()
|
super().reject()
|
||||||
saveSplitter(frm.splitter, "DebugConsoleWindow")
|
saveSplitter(frm.splitter, "DebugConsoleWindow")
|
||||||
saveGeom(self, "DebugConsoleWindow")
|
saveGeom(self, "DebugConsoleWindow")
|
||||||
|
@ -1356,7 +1366,7 @@ title="%s" %s>%s</button>""" % (
|
||||||
a = menu.addAction("Clear Code")
|
a = menu.addAction("Clear Code")
|
||||||
a.setShortcuts(QKeySequence("ctrl+shift+l"))
|
a.setShortcuts(QKeySequence("ctrl+shift+l"))
|
||||||
qconnect(a.triggered, frm.text.clear)
|
qconnect(a.triggered, frm.text.clear)
|
||||||
menu.exec(QCursor.pos())
|
menu.exec_(QCursor.pos())
|
||||||
|
|
||||||
frm.log.contextMenuEvent = lambda ev: addContextMenu(ev, "log")
|
frm.log.contextMenuEvent = lambda ev: addContextMenu(ev, "log")
|
||||||
frm.text.contextMenuEvent = lambda ev: addContextMenu(ev, "text")
|
frm.text.contextMenuEvent = lambda ev: addContextMenu(ev, "text")
|
||||||
|
@ -1367,7 +1377,7 @@ title="%s" %s>%s</button>""" % (
|
||||||
mw = self
|
mw = self
|
||||||
|
|
||||||
class Stream:
|
class Stream:
|
||||||
def write(self, data):
|
def write(self, data) -> None:
|
||||||
mw._output += data
|
mw._output += data
|
||||||
|
|
||||||
if on:
|
if on:
|
||||||
|
@ -1418,7 +1428,7 @@ title="%s" %s>%s</button>""" % (
|
||||||
self._card_repr(card)
|
self._card_repr(card)
|
||||||
return card
|
return card
|
||||||
|
|
||||||
def onDebugPrint(self, frm):
|
def onDebugPrint(self, frm) -> None:
|
||||||
cursor = frm.text.textCursor()
|
cursor = frm.text.textCursor()
|
||||||
position = cursor.position()
|
position = cursor.position()
|
||||||
cursor.select(QTextCursor.LineUnderCursor)
|
cursor.select(QTextCursor.LineUnderCursor)
|
||||||
|
@ -1431,7 +1441,7 @@ title="%s" %s>%s</button>""" % (
|
||||||
frm.text.setTextCursor(cursor)
|
frm.text.setTextCursor(cursor)
|
||||||
self.onDebugRet(frm)
|
self.onDebugRet(frm)
|
||||||
|
|
||||||
def onDebugRet(self, frm):
|
def onDebugRet(self, frm) -> None:
|
||||||
import pprint
|
import pprint
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
|
@ -1509,14 +1519,15 @@ title="%s" %s>%s</button>""" % (
|
||||||
def setupAppMsg(self) -> None:
|
def setupAppMsg(self) -> None:
|
||||||
qconnect(self.app.appMsg, self.onAppMsg)
|
qconnect(self.app.appMsg, self.onAppMsg)
|
||||||
|
|
||||||
def onAppMsg(self, buf: str) -> Optional[QTimer]:
|
def onAppMsg(self, buf: str) -> None:
|
||||||
is_addon = self._isAddon(buf)
|
is_addon = self._isAddon(buf)
|
||||||
|
|
||||||
if self.state == "startup":
|
if self.state == "startup":
|
||||||
# try again in a second
|
# try again in a second
|
||||||
return self.progress.timer(
|
self.progress.timer(
|
||||||
1000, lambda: self.onAppMsg(buf), False, requiresCollection=False
|
1000, lambda: self.onAppMsg(buf), False, requiresCollection=False
|
||||||
)
|
)
|
||||||
|
return
|
||||||
elif self.state == "profileManager":
|
elif self.state == "profileManager":
|
||||||
# can't raise window while in profile manager
|
# can't raise window while in profile manager
|
||||||
if buf == "raise":
|
if buf == "raise":
|
||||||
|
@ -1526,7 +1537,8 @@ title="%s" %s>%s</button>""" % (
|
||||||
msg = tr(TR.QT_MISC_ADDON_WILL_BE_INSTALLED_WHEN_A)
|
msg = tr(TR.QT_MISC_ADDON_WILL_BE_INSTALLED_WHEN_A)
|
||||||
else:
|
else:
|
||||||
msg = tr(TR.QT_MISC_DECK_WILL_BE_IMPORTED_WHEN_A)
|
msg = tr(TR.QT_MISC_DECK_WILL_BE_IMPORTED_WHEN_A)
|
||||||
return tooltip(msg)
|
tooltip(msg)
|
||||||
|
return
|
||||||
if not self.interactiveState() or self.progress.busy():
|
if not self.interactiveState() or self.progress.busy():
|
||||||
# we can't raise the main window while in profile dialog, syncing, etc
|
# we can't raise the main window while in profile dialog, syncing, etc
|
||||||
if buf != "raise":
|
if buf != "raise":
|
||||||
|
|
|
@ -136,7 +136,7 @@ class MediaChecker:
|
||||||
diag.exec_()
|
diag.exec_()
|
||||||
saveGeom(diag, "checkmediadb")
|
saveGeom(diag, "checkmediadb")
|
||||||
|
|
||||||
def _on_render_latex(self):
|
def _on_render_latex(self) -> None:
|
||||||
self.progress_dialog = self.mw.progress.start()
|
self.progress_dialog = self.mw.progress.start()
|
||||||
try:
|
try:
|
||||||
out = self.mw.col.media.render_all_latex(self._on_render_latex_progress)
|
out = self.mw.col.media.render_all_latex(self._on_render_latex_progress)
|
||||||
|
@ -160,7 +160,7 @@ class MediaChecker:
|
||||||
self.mw.progress.update(tr(TR.MEDIA_CHECK_CHECKED, count=count))
|
self.mw.progress.update(tr(TR.MEDIA_CHECK_CHECKED, count=count))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _on_trash_files(self, fnames: Sequence[str]):
|
def _on_trash_files(self, fnames: Sequence[str]) -> None:
|
||||||
if not askUser(tr(TR.MEDIA_CHECK_DELETE_UNUSED_CONFIRM)):
|
if not askUser(tr(TR.MEDIA_CHECK_DELETE_UNUSED_CONFIRM)):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -183,14 +183,14 @@ class MediaChecker:
|
||||||
|
|
||||||
tooltip(tr(TR.MEDIA_CHECK_DELETE_UNUSED_COMPLETE, count=total))
|
tooltip(tr(TR.MEDIA_CHECK_DELETE_UNUSED_COMPLETE, count=total))
|
||||||
|
|
||||||
def _on_empty_trash(self):
|
def _on_empty_trash(self) -> None:
|
||||||
self.progress_dialog = self.mw.progress.start()
|
self.progress_dialog = self.mw.progress.start()
|
||||||
self._set_progress_enabled(True)
|
self._set_progress_enabled(True)
|
||||||
|
|
||||||
def empty_trash():
|
def empty_trash() -> None:
|
||||||
self.mw.col.media.empty_trash()
|
self.mw.col.media.empty_trash()
|
||||||
|
|
||||||
def on_done(fut: Future):
|
def on_done(fut: Future) -> None:
|
||||||
self.mw.progress.finish()
|
self.mw.progress.finish()
|
||||||
self._set_progress_enabled(False)
|
self._set_progress_enabled(False)
|
||||||
# check for errors
|
# check for errors
|
||||||
|
@ -200,14 +200,14 @@ class MediaChecker:
|
||||||
|
|
||||||
self.mw.taskman.run_in_background(empty_trash, on_done)
|
self.mw.taskman.run_in_background(empty_trash, on_done)
|
||||||
|
|
||||||
def _on_restore_trash(self):
|
def _on_restore_trash(self) -> None:
|
||||||
self.progress_dialog = self.mw.progress.start()
|
self.progress_dialog = self.mw.progress.start()
|
||||||
self._set_progress_enabled(True)
|
self._set_progress_enabled(True)
|
||||||
|
|
||||||
def restore_trash():
|
def restore_trash() -> None:
|
||||||
self.mw.col.media.restore_trash()
|
self.mw.col.media.restore_trash()
|
||||||
|
|
||||||
def on_done(fut: Future):
|
def on_done(fut: Future) -> None:
|
||||||
self.mw.progress.finish()
|
self.mw.progress.finish()
|
||||||
self._set_progress_enabled(False)
|
self._set_progress_enabled(False)
|
||||||
# check for errors
|
# check for errors
|
||||||
|
|
|
@ -12,6 +12,7 @@ import threading
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
import flask_cors # type: ignore
|
import flask_cors # type: ignore
|
||||||
|
@ -26,7 +27,7 @@ from aqt.qt import *
|
||||||
from aqt.utils import aqt_data_folder
|
from aqt.utils import aqt_data_folder
|
||||||
|
|
||||||
|
|
||||||
def _getExportFolder():
|
def _getExportFolder() -> str:
|
||||||
data_folder = aqt_data_folder()
|
data_folder = aqt_data_folder()
|
||||||
webInSrcFolder = os.path.abspath(os.path.join(data_folder, "web"))
|
webInSrcFolder = os.path.abspath(os.path.join(data_folder, "web"))
|
||||||
if os.path.exists(webInSrcFolder):
|
if os.path.exists(webInSrcFolder):
|
||||||
|
@ -52,11 +53,11 @@ class MediaServer(threading.Thread):
|
||||||
_ready = threading.Event()
|
_ready = threading.Event()
|
||||||
daemon = True
|
daemon = True
|
||||||
|
|
||||||
def __init__(self, mw: aqt.main.AnkiQt, *args, **kwargs):
|
def __init__(self, mw: aqt.main.AnkiQt, *args, **kwargs) -> None:
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.is_shutdown = False
|
self.is_shutdown = False
|
||||||
|
|
||||||
def run(self):
|
def run(self) -> None:
|
||||||
try:
|
try:
|
||||||
if devMode:
|
if devMode:
|
||||||
# idempotent if logging has already been set up
|
# idempotent if logging has already been set up
|
||||||
|
@ -83,7 +84,7 @@ class MediaServer(threading.Thread):
|
||||||
if not self.is_shutdown:
|
if not self.is_shutdown:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self) -> None:
|
||||||
self.is_shutdown = True
|
self.is_shutdown = True
|
||||||
sockets = list(self.server._map.values()) # type: ignore
|
sockets = list(self.server._map.values()) # type: ignore
|
||||||
for socket in sockets:
|
for socket in sockets:
|
||||||
|
@ -91,13 +92,13 @@ class MediaServer(threading.Thread):
|
||||||
# https://github.com/Pylons/webtest/blob/4b8a3ebf984185ff4fefb31b4d0cf82682e1fcf7/webtest/http.py#L93-L104
|
# https://github.com/Pylons/webtest/blob/4b8a3ebf984185ff4fefb31b4d0cf82682e1fcf7/webtest/http.py#L93-L104
|
||||||
self.server.task_dispatcher.shutdown()
|
self.server.task_dispatcher.shutdown()
|
||||||
|
|
||||||
def getPort(self):
|
def getPort(self) -> int:
|
||||||
self._ready.wait()
|
self._ready.wait()
|
||||||
return int(self.server.effective_port) # type: ignore
|
return int(self.server.effective_port) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
@app.route("/<path:pathin>", methods=["GET", "POST"])
|
@app.route("/<path:pathin>", methods=["GET", "POST"])
|
||||||
def allroutes(pathin):
|
def allroutes(pathin) -> Response:
|
||||||
try:
|
try:
|
||||||
directory, path = _redirectWebExports(pathin)
|
directory, path = _redirectWebExports(pathin)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
|
@ -171,7 +172,7 @@ def allroutes(pathin):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _redirectWebExports(path):
|
def _redirectWebExports(path) -> Tuple[str, str]:
|
||||||
# catch /_anki references and rewrite them to web export folder
|
# catch /_anki references and rewrite them to web export folder
|
||||||
targetPath = "_anki/"
|
targetPath = "_anki/"
|
||||||
if path.startswith(targetPath):
|
if path.startswith(targetPath):
|
||||||
|
|
|
@ -28,14 +28,14 @@ class LogEntryWithTime:
|
||||||
|
|
||||||
|
|
||||||
class MediaSyncer:
|
class MediaSyncer:
|
||||||
def __init__(self, mw: aqt.main.AnkiQt):
|
def __init__(self, mw: aqt.main.AnkiQt) -> None:
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
self._syncing: bool = False
|
self._syncing: bool = False
|
||||||
self._log: List[LogEntryWithTime] = []
|
self._log: List[LogEntryWithTime] = []
|
||||||
self._progress_timer: Optional[QTimer] = None
|
self._progress_timer: Optional[QTimer] = None
|
||||||
gui_hooks.media_sync_did_start_or_stop.append(self._on_start_stop)
|
gui_hooks.media_sync_did_start_or_stop.append(self._on_start_stop)
|
||||||
|
|
||||||
def _on_progress(self):
|
def _on_progress(self) -> None:
|
||||||
progress = self.mw.col.latest_progress()
|
progress = self.mw.col.latest_progress()
|
||||||
if progress.kind != ProgressKind.MediaSync:
|
if progress.kind != ProgressKind.MediaSync:
|
||||||
return
|
return
|
||||||
|
@ -88,7 +88,7 @@ class MediaSyncer:
|
||||||
else:
|
else:
|
||||||
self._log_and_notify(tr(TR.SYNC_MEDIA_COMPLETE))
|
self._log_and_notify(tr(TR.SYNC_MEDIA_COMPLETE))
|
||||||
|
|
||||||
def _handle_sync_error(self, exc: BaseException):
|
def _handle_sync_error(self, exc: BaseException) -> None:
|
||||||
if isinstance(exc, Interrupted):
|
if isinstance(exc, Interrupted):
|
||||||
self._log_and_notify(tr(TR.SYNC_MEDIA_ABORTED))
|
self._log_and_notify(tr(TR.SYNC_MEDIA_ABORTED))
|
||||||
return
|
return
|
||||||
|
@ -116,10 +116,10 @@ class MediaSyncer:
|
||||||
def _on_start_stop(self, running: bool) -> None:
|
def _on_start_stop(self, running: bool) -> None:
|
||||||
self.mw.toolbar.set_sync_active(running)
|
self.mw.toolbar.set_sync_active(running)
|
||||||
|
|
||||||
def show_sync_log(self):
|
def show_sync_log(self) -> None:
|
||||||
aqt.dialogs.open("sync_log", self.mw, self)
|
aqt.dialogs.open("sync_log", self.mw, self)
|
||||||
|
|
||||||
def show_diag_until_finished(self, on_finished: Callable[[], None]):
|
def show_diag_until_finished(self, on_finished: Callable[[], None]) -> None:
|
||||||
# nothing to do if not syncing
|
# nothing to do if not syncing
|
||||||
if not self.is_syncing():
|
if not self.is_syncing():
|
||||||
return on_finished()
|
return on_finished()
|
||||||
|
@ -129,7 +129,7 @@ class MediaSyncer:
|
||||||
|
|
||||||
timer: Optional[QTimer] = None
|
timer: Optional[QTimer] = None
|
||||||
|
|
||||||
def check_finished():
|
def check_finished() -> None:
|
||||||
if not self.is_syncing():
|
if not self.is_syncing():
|
||||||
timer.stop()
|
timer.stop()
|
||||||
on_finished()
|
on_finished()
|
||||||
|
@ -197,7 +197,7 @@ class MediaSyncDialog(QDialog):
|
||||||
asctime = time.asctime(time.localtime(stamp))
|
asctime = time.asctime(time.localtime(stamp))
|
||||||
return f"{asctime}: {text}"
|
return f"{asctime}: {text}"
|
||||||
|
|
||||||
def _entry_to_text(self, entry: LogEntryWithTime):
|
def _entry_to_text(self, entry: LogEntryWithTime) -> str:
|
||||||
if isinstance(entry.entry, str):
|
if isinstance(entry.entry, str):
|
||||||
txt = entry.entry
|
txt = entry.entry
|
||||||
elif isinstance(entry.entry, MediaSyncProgress):
|
elif isinstance(entry.entry, MediaSyncProgress):
|
||||||
|
@ -209,7 +209,7 @@ class MediaSyncDialog(QDialog):
|
||||||
def _logentry_to_text(self, e: MediaSyncProgress) -> str:
|
def _logentry_to_text(self, e: MediaSyncProgress) -> str:
|
||||||
return f"{e.added}, {e.removed}, {e.checked}"
|
return f"{e.added}, {e.removed}, {e.checked}"
|
||||||
|
|
||||||
def _on_log_entry(self, entry: LogEntryWithTime):
|
def _on_log_entry(self, entry: LogEntryWithTime) -> None:
|
||||||
self.form.plainTextEdit.appendPlainText(self._entry_to_text(entry))
|
self.form.plainTextEdit.appendPlainText(self._entry_to_text(entry))
|
||||||
if not self._syncer.is_syncing():
|
if not self._syncer.is_syncing():
|
||||||
self.abort_button.setHidden(True)
|
self.abort_button.setHidden(True)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright: Ankitects Pty Ltd and contributors
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
from typing import Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from aqt import AnkiQt, gui_hooks
|
from aqt import AnkiQt, gui_hooks
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
|
@ -74,7 +74,7 @@ class ModelChooser(QHBoxLayout):
|
||||||
# edit button
|
# edit button
|
||||||
edit = QPushButton(tr(TR.QT_MISC_MANAGE), clicked=self.onEdit) # type: ignore
|
edit = QPushButton(tr(TR.QT_MISC_MANAGE), clicked=self.onEdit) # type: ignore
|
||||||
|
|
||||||
def nameFunc():
|
def nameFunc() -> List[str]:
|
||||||
return sorted(self.deck.models.allNames())
|
return sorted(self.deck.models.allNames())
|
||||||
|
|
||||||
ret = StudyDeck(
|
ret = StudyDeck(
|
||||||
|
|
|
@ -57,7 +57,7 @@ class Models(QDialog):
|
||||||
# Models
|
# Models
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def maybe_select_provided_notetype(self):
|
def maybe_select_provided_notetype(self) -> None:
|
||||||
if not self.selected_notetype_id:
|
if not self.selected_notetype_id:
|
||||||
self.form.modelsList.setCurrentRow(0)
|
self.form.modelsList.setCurrentRow(0)
|
||||||
return
|
return
|
||||||
|
@ -219,7 +219,7 @@ class Models(QDialog):
|
||||||
class AddModel(QDialog):
|
class AddModel(QDialog):
|
||||||
model: Optional[NoteType]
|
model: Optional[NoteType]
|
||||||
|
|
||||||
def __init__(self, mw: AnkiQt, parent: Optional[QWidget] = None):
|
def __init__(self, mw: AnkiQt, parent: Optional[QWidget] = None) -> None:
|
||||||
self.parent_ = parent or mw
|
self.parent_ = parent or mw
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
self.col = mw.col
|
self.col = mw.col
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Optional
|
from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from aqt import gui_hooks
|
from aqt import gui_hooks
|
||||||
|
@ -14,7 +14,7 @@ from aqt.utils import TR, askUserDialog, openLink, shortcut, tooltip, tr
|
||||||
|
|
||||||
|
|
||||||
class OverviewBottomBar:
|
class OverviewBottomBar:
|
||||||
def __init__(self, overview: Overview):
|
def __init__(self, overview: Overview) -> None:
|
||||||
self.overview = overview
|
self.overview = overview
|
||||||
|
|
||||||
|
|
||||||
|
@ -44,13 +44,13 @@ class Overview:
|
||||||
self.web = mw.web
|
self.web = mw.web
|
||||||
self.bottom = BottomBar(mw, mw.bottomWeb)
|
self.bottom = BottomBar(mw, mw.bottomWeb)
|
||||||
|
|
||||||
def show(self):
|
def show(self) -> None:
|
||||||
av_player.stop_and_clear_queue()
|
av_player.stop_and_clear_queue()
|
||||||
self.web.set_bridge_command(self._linkHandler, self)
|
self.web.set_bridge_command(self._linkHandler, self)
|
||||||
self.mw.setStateShortcuts(self._shortcutKeys())
|
self.mw.setStateShortcuts(self._shortcutKeys())
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
def refresh(self):
|
def refresh(self) -> None:
|
||||||
self.mw.col.reset()
|
self.mw.col.reset()
|
||||||
self._renderPage()
|
self._renderPage()
|
||||||
self._renderBottom()
|
self._renderBottom()
|
||||||
|
@ -60,7 +60,7 @@ class Overview:
|
||||||
# Handlers
|
# Handlers
|
||||||
############################################################
|
############################################################
|
||||||
|
|
||||||
def _linkHandler(self, url):
|
def _linkHandler(self, url: str) -> bool:
|
||||||
if url == "study":
|
if url == "study":
|
||||||
self.mw.col.startTimebox()
|
self.mw.col.startTimebox()
|
||||||
self.mw.moveToState("review")
|
self.mw.moveToState("review")
|
||||||
|
@ -90,7 +90,7 @@ class Overview:
|
||||||
openLink(url)
|
openLink(url)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _shortcutKeys(self):
|
def _shortcutKeys(self) -> List[Tuple[str, Callable]]:
|
||||||
return [
|
return [
|
||||||
("o", self.mw.onDeckConf),
|
("o", self.mw.onDeckConf),
|
||||||
("r", self.onRebuildKey),
|
("r", self.onRebuildKey),
|
||||||
|
@ -99,24 +99,24 @@ class Overview:
|
||||||
("u", self.onUnbury),
|
("u", self.onUnbury),
|
||||||
]
|
]
|
||||||
|
|
||||||
def _filteredDeck(self):
|
def _filteredDeck(self) -> int:
|
||||||
return self.mw.col.decks.current()["dyn"]
|
return self.mw.col.decks.current()["dyn"]
|
||||||
|
|
||||||
def onRebuildKey(self):
|
def onRebuildKey(self) -> None:
|
||||||
if self._filteredDeck():
|
if self._filteredDeck():
|
||||||
self.mw.col.sched.rebuild_filtered_deck(self.mw.col.decks.selected())
|
self.mw.col.sched.rebuild_filtered_deck(self.mw.col.decks.selected())
|
||||||
self.mw.reset()
|
self.mw.reset()
|
||||||
|
|
||||||
def onEmptyKey(self):
|
def onEmptyKey(self) -> None:
|
||||||
if self._filteredDeck():
|
if self._filteredDeck():
|
||||||
self.mw.col.sched.empty_filtered_deck(self.mw.col.decks.selected())
|
self.mw.col.sched.empty_filtered_deck(self.mw.col.decks.selected())
|
||||||
self.mw.reset()
|
self.mw.reset()
|
||||||
|
|
||||||
def onCustomStudyKey(self):
|
def onCustomStudyKey(self) -> None:
|
||||||
if not self._filteredDeck():
|
if not self._filteredDeck():
|
||||||
self.onStudyMore()
|
self.onStudyMore()
|
||||||
|
|
||||||
def onUnbury(self):
|
def onUnbury(self) -> None:
|
||||||
if self.mw.col.schedVer() == 1:
|
if self.mw.col.schedVer() == 1:
|
||||||
self.mw.col.sched.unburyCardsForDeck()
|
self.mw.col.sched.unburyCardsForDeck()
|
||||||
self.mw.reset()
|
self.mw.reset()
|
||||||
|
@ -148,7 +148,7 @@ class Overview:
|
||||||
# HTML
|
# HTML
|
||||||
############################################################
|
############################################################
|
||||||
|
|
||||||
def _renderPage(self):
|
def _renderPage(self) -> None:
|
||||||
but = self.mw.button
|
but = self.mw.button
|
||||||
deck = self.mw.col.decks.current()
|
deck = self.mw.col.decks.current()
|
||||||
self.sid = deck.get("sharedFrom")
|
self.sid = deck.get("sharedFrom")
|
||||||
|
@ -175,10 +175,10 @@ class Overview:
|
||||||
context=self,
|
context=self,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _show_finished_screen(self):
|
def _show_finished_screen(self) -> None:
|
||||||
self.web.load_ts_page("congrats")
|
self.web.load_ts_page("congrats")
|
||||||
|
|
||||||
def _desc(self, deck):
|
def _desc(self, deck: Dict[str, Any]) -> str:
|
||||||
if deck["dyn"]:
|
if deck["dyn"]:
|
||||||
desc = tr(TR.STUDYING_THIS_IS_A_SPECIAL_DECK_FOR)
|
desc = tr(TR.STUDYING_THIS_IS_A_SPECIAL_DECK_FOR)
|
||||||
desc += " " + tr(TR.STUDYING_CARDS_WILL_BE_AUTOMATICALLY_RETURNED_TO)
|
desc += " " + tr(TR.STUDYING_CARDS_WILL_BE_AUTOMATICALLY_RETURNED_TO)
|
||||||
|
@ -227,7 +227,7 @@ class Overview:
|
||||||
# Bottom area
|
# Bottom area
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def _renderBottom(self):
|
def _renderBottom(self) -> None:
|
||||||
links = [
|
links = [
|
||||||
["O", "opts", tr(TR.ACTIONS_OPTIONS)],
|
["O", "opts", tr(TR.ACTIONS_OPTIONS)],
|
||||||
]
|
]
|
||||||
|
@ -254,7 +254,7 @@ class Overview:
|
||||||
# Studying more
|
# Studying more
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def onStudyMore(self):
|
def onStudyMore(self) -> None:
|
||||||
import aqt.customstudy
|
import aqt.customstudy
|
||||||
|
|
||||||
aqt.customstudy.CustomStudy(self.mw)
|
aqt.customstudy.CustomStudy(self.mw)
|
||||||
|
|
|
@ -34,7 +34,7 @@ def video_driver_name_for_platform(driver: VideoDriver) -> str:
|
||||||
|
|
||||||
|
|
||||||
class Preferences(QDialog):
|
class Preferences(QDialog):
|
||||||
def __init__(self, mw: AnkiQt):
|
def __init__(self, mw: AnkiQt) -> None:
|
||||||
QDialog.__init__(self, mw, Qt.Window)
|
QDialog.__init__(self, mw, Qt.Window)
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
self.prof = self.mw.pm.profile
|
self.prof = self.mw.pm.profile
|
||||||
|
@ -55,7 +55,7 @@ class Preferences(QDialog):
|
||||||
self.setupOptions()
|
self.setupOptions()
|
||||||
self.show()
|
self.show()
|
||||||
|
|
||||||
def accept(self):
|
def accept(self) -> None:
|
||||||
# avoid exception if main window is already closed
|
# avoid exception if main window is already closed
|
||||||
if not self.mw.col:
|
if not self.mw.col:
|
||||||
return
|
return
|
||||||
|
@ -68,19 +68,19 @@ class Preferences(QDialog):
|
||||||
self.done(0)
|
self.done(0)
|
||||||
aqt.dialogs.markClosed("Preferences")
|
aqt.dialogs.markClosed("Preferences")
|
||||||
|
|
||||||
def reject(self):
|
def reject(self) -> None:
|
||||||
self.accept()
|
self.accept()
|
||||||
|
|
||||||
# Language
|
# Language
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def setupLang(self):
|
def setupLang(self) -> None:
|
||||||
f = self.form
|
f = self.form
|
||||||
f.lang.addItems([x[0] for x in anki.lang.langs])
|
f.lang.addItems([x[0] for x in anki.lang.langs])
|
||||||
f.lang.setCurrentIndex(self.langIdx())
|
f.lang.setCurrentIndex(self.langIdx())
|
||||||
qconnect(f.lang.currentIndexChanged, self.onLangIdxChanged)
|
qconnect(f.lang.currentIndexChanged, self.onLangIdxChanged)
|
||||||
|
|
||||||
def langIdx(self):
|
def langIdx(self) -> int:
|
||||||
codes = [x[1] for x in anki.lang.langs]
|
codes = [x[1] for x in anki.lang.langs]
|
||||||
lang = anki.lang.currentLang
|
lang = anki.lang.currentLang
|
||||||
if lang in anki.lang.compatMap:
|
if lang in anki.lang.compatMap:
|
||||||
|
@ -92,7 +92,7 @@ class Preferences(QDialog):
|
||||||
except:
|
except:
|
||||||
return codes.index("en_US")
|
return codes.index("en_US")
|
||||||
|
|
||||||
def onLangIdxChanged(self, idx):
|
def onLangIdxChanged(self, idx) -> None:
|
||||||
code = anki.lang.langs[idx][1]
|
code = anki.lang.langs[idx][1]
|
||||||
self.mw.pm.setLang(code)
|
self.mw.pm.setLang(code)
|
||||||
showInfo(
|
showInfo(
|
||||||
|
@ -102,7 +102,7 @@ class Preferences(QDialog):
|
||||||
# Collection options
|
# Collection options
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def setupCollection(self):
|
def setupCollection(self) -> None:
|
||||||
import anki.consts as c
|
import anki.consts as c
|
||||||
|
|
||||||
f = self.form
|
f = self.form
|
||||||
|
@ -130,7 +130,7 @@ class Preferences(QDialog):
|
||||||
f.newSched.setChecked(True)
|
f.newSched.setChecked(True)
|
||||||
f.new_timezone.setChecked(s.new_timezone)
|
f.new_timezone.setChecked(s.new_timezone)
|
||||||
|
|
||||||
def setup_video_driver(self):
|
def setup_video_driver(self) -> None:
|
||||||
self.video_drivers = VideoDriver.all_for_platform()
|
self.video_drivers = VideoDriver.all_for_platform()
|
||||||
names = [
|
names = [
|
||||||
tr(TR.PREFERENCES_VIDEO_DRIVER, driver=video_driver_name_for_platform(d))
|
tr(TR.PREFERENCES_VIDEO_DRIVER, driver=video_driver_name_for_platform(d))
|
||||||
|
@ -141,13 +141,13 @@ class Preferences(QDialog):
|
||||||
self.video_drivers.index(self.mw.pm.video_driver())
|
self.video_drivers.index(self.mw.pm.video_driver())
|
||||||
)
|
)
|
||||||
|
|
||||||
def update_video_driver(self):
|
def update_video_driver(self) -> None:
|
||||||
new_driver = self.video_drivers[self.form.video_driver.currentIndex()]
|
new_driver = self.video_drivers[self.form.video_driver.currentIndex()]
|
||||||
if new_driver != self.mw.pm.video_driver():
|
if new_driver != self.mw.pm.video_driver():
|
||||||
self.mw.pm.set_video_driver(new_driver)
|
self.mw.pm.set_video_driver(new_driver)
|
||||||
showInfo(tr(TR.PREFERENCES_CHANGES_WILL_TAKE_EFFECT_WHEN_YOU))
|
showInfo(tr(TR.PREFERENCES_CHANGES_WILL_TAKE_EFFECT_WHEN_YOU))
|
||||||
|
|
||||||
def updateCollection(self):
|
def updateCollection(self) -> None:
|
||||||
f = self.form
|
f = self.form
|
||||||
d = self.mw.col
|
d = self.mw.col
|
||||||
|
|
||||||
|
@ -176,7 +176,7 @@ class Preferences(QDialog):
|
||||||
# Scheduler version
|
# Scheduler version
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def _updateSchedVer(self, wantNew):
|
def _updateSchedVer(self, wantNew) -> None:
|
||||||
haveNew = self.mw.col.schedVer() == 2
|
haveNew = self.mw.col.schedVer() == 2
|
||||||
|
|
||||||
# nothing to do?
|
# nothing to do?
|
||||||
|
@ -194,7 +194,7 @@ class Preferences(QDialog):
|
||||||
# Network
|
# Network
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def setupNetwork(self):
|
def setupNetwork(self) -> None:
|
||||||
self.form.media_log.setText(tr(TR.SYNC_MEDIA_LOG_BUTTON))
|
self.form.media_log.setText(tr(TR.SYNC_MEDIA_LOG_BUTTON))
|
||||||
qconnect(self.form.media_log.clicked, self.on_media_log)
|
qconnect(self.form.media_log.clicked, self.on_media_log)
|
||||||
self.form.syncOnProgramOpen.setChecked(self.prof["autoSync"])
|
self.form.syncOnProgramOpen.setChecked(self.prof["autoSync"])
|
||||||
|
@ -207,10 +207,10 @@ class Preferences(QDialog):
|
||||||
qconnect(self.form.syncDeauth.clicked, self.onSyncDeauth)
|
qconnect(self.form.syncDeauth.clicked, self.onSyncDeauth)
|
||||||
self.form.syncDeauth.setText(tr(TR.SYNC_LOG_OUT_BUTTON))
|
self.form.syncDeauth.setText(tr(TR.SYNC_LOG_OUT_BUTTON))
|
||||||
|
|
||||||
def on_media_log(self):
|
def on_media_log(self) -> None:
|
||||||
self.mw.media_syncer.show_sync_log()
|
self.mw.media_syncer.show_sync_log()
|
||||||
|
|
||||||
def _hideAuth(self):
|
def _hideAuth(self) -> None:
|
||||||
self.form.syncDeauth.setVisible(False)
|
self.form.syncDeauth.setVisible(False)
|
||||||
self.form.syncUser.setText("")
|
self.form.syncUser.setText("")
|
||||||
self.form.syncLabel.setText(
|
self.form.syncLabel.setText(
|
||||||
|
@ -225,7 +225,7 @@ class Preferences(QDialog):
|
||||||
self.mw.col.media.force_resync()
|
self.mw.col.media.force_resync()
|
||||||
self._hideAuth()
|
self._hideAuth()
|
||||||
|
|
||||||
def updateNetwork(self):
|
def updateNetwork(self) -> None:
|
||||||
self.prof["autoSync"] = self.form.syncOnProgramOpen.isChecked()
|
self.prof["autoSync"] = self.form.syncOnProgramOpen.isChecked()
|
||||||
self.prof["syncMedia"] = self.form.syncMedia.isChecked()
|
self.prof["syncMedia"] = self.form.syncMedia.isChecked()
|
||||||
self.mw.pm.set_auto_sync_media_minutes(
|
self.mw.pm.set_auto_sync_media_minutes(
|
||||||
|
@ -238,16 +238,16 @@ class Preferences(QDialog):
|
||||||
# Backup
|
# Backup
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def setupBackup(self):
|
def setupBackup(self) -> None:
|
||||||
self.form.numBackups.setValue(self.prof["numBackups"])
|
self.form.numBackups.setValue(self.prof["numBackups"])
|
||||||
|
|
||||||
def updateBackup(self):
|
def updateBackup(self) -> None:
|
||||||
self.prof["numBackups"] = self.form.numBackups.value()
|
self.prof["numBackups"] = self.form.numBackups.value()
|
||||||
|
|
||||||
# Basic & Advanced Options
|
# Basic & Advanced Options
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
def setupOptions(self):
|
def setupOptions(self) -> None:
|
||||||
self.form.pastePNG.setChecked(self.prof.get("pastePNG", False))
|
self.form.pastePNG.setChecked(self.prof.get("pastePNG", False))
|
||||||
self.form.uiScale.setValue(int(self.mw.pm.uiScale() * 100))
|
self.form.uiScale.setValue(int(self.mw.pm.uiScale() * 100))
|
||||||
self.form.pasteInvert.setChecked(self.prof.get("pasteInvert", False))
|
self.form.pasteInvert.setChecked(self.prof.get("pasteInvert", False))
|
||||||
|
@ -270,7 +270,7 @@ class Preferences(QDialog):
|
||||||
self._recording_drivers.index(self.mw.pm.recording_driver())
|
self._recording_drivers.index(self.mw.pm.recording_driver())
|
||||||
)
|
)
|
||||||
|
|
||||||
def updateOptions(self):
|
def updateOptions(self) -> None:
|
||||||
restart_required = False
|
restart_required = False
|
||||||
|
|
||||||
self.prof["pastePNG"] = self.form.pastePNG.isChecked()
|
self.prof["pastePNG"] = self.form.pastePNG.isChecked()
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
from typing import Any, Callable, Optional, Union
|
from typing import Any, Callable, Optional, Tuple, Union
|
||||||
|
|
||||||
from anki.cards import Card
|
from anki.cards import Card
|
||||||
from anki.collection import ConfigBoolKey
|
from anki.collection import ConfigBoolKey
|
||||||
|
@ -19,6 +19,7 @@ from aqt.qt import (
|
||||||
QPixmap,
|
QPixmap,
|
||||||
QShortcut,
|
QShortcut,
|
||||||
Qt,
|
Qt,
|
||||||
|
QTimer,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
QWidget,
|
QWidget,
|
||||||
qconnect,
|
qconnect,
|
||||||
|
@ -29,15 +30,19 @@ from aqt.theme import theme_manager
|
||||||
from aqt.utils import TR, disable_help_button, restoreGeom, saveGeom, tr
|
from aqt.utils import TR, disable_help_button, restoreGeom, saveGeom, tr
|
||||||
from aqt.webview import AnkiWebView
|
from aqt.webview import AnkiWebView
|
||||||
|
|
||||||
|
LastStateAndMod = Tuple[str, int, int]
|
||||||
|
|
||||||
|
|
||||||
class Previewer(QDialog):
|
class Previewer(QDialog):
|
||||||
_last_state = None
|
_last_state: Optional[LastStateAndMod] = None
|
||||||
_card_changed = False
|
_card_changed = False
|
||||||
_last_render: Union[int, float] = 0
|
_last_render: Union[int, float] = 0
|
||||||
_timer = None
|
_timer: Optional[QTimer] = None
|
||||||
_show_both_sides = False
|
_show_both_sides = False
|
||||||
|
|
||||||
def __init__(self, parent: QWidget, mw: AnkiQt, on_close: Callable[[], None]):
|
def __init__(
|
||||||
|
self, parent: QWidget, mw: AnkiQt, on_close: Callable[[], None]
|
||||||
|
) -> None:
|
||||||
super().__init__(None, Qt.Window)
|
super().__init__(None, Qt.Window)
|
||||||
self._open = True
|
self._open = True
|
||||||
self._parent = parent
|
self._parent = parent
|
||||||
|
@ -54,7 +59,7 @@ class Previewer(QDialog):
|
||||||
def card_changed(self) -> bool:
|
def card_changed(self) -> bool:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def open(self):
|
def open(self) -> None:
|
||||||
self._state = "question"
|
self._state = "question"
|
||||||
self._last_state = None
|
self._last_state = None
|
||||||
self._create_gui()
|
self._create_gui()
|
||||||
|
@ -62,7 +67,7 @@ class Previewer(QDialog):
|
||||||
self.render_card()
|
self.render_card()
|
||||||
self.show()
|
self.show()
|
||||||
|
|
||||||
def _create_gui(self):
|
def _create_gui(self) -> None:
|
||||||
self.setWindowTitle(tr(TR.ACTIONS_PREVIEW))
|
self.setWindowTitle(tr(TR.ACTIONS_PREVIEW))
|
||||||
|
|
||||||
self.close_shortcut = QShortcut(QKeySequence("Ctrl+Shift+P"), self)
|
self.close_shortcut = QShortcut(QKeySequence("Ctrl+Shift+P"), self)
|
||||||
|
@ -98,25 +103,25 @@ class Previewer(QDialog):
|
||||||
self.setLayout(self.vbox)
|
self.setLayout(self.vbox)
|
||||||
restoreGeom(self, "preview")
|
restoreGeom(self, "preview")
|
||||||
|
|
||||||
def _on_finished(self, ok):
|
def _on_finished(self, ok: int) -> None:
|
||||||
saveGeom(self, "preview")
|
saveGeom(self, "preview")
|
||||||
self.mw.progress.timer(100, self._on_close, False)
|
self.mw.progress.timer(100, self._on_close, False)
|
||||||
|
|
||||||
def _on_replay_audio(self):
|
def _on_replay_audio(self) -> None:
|
||||||
if self._state == "question":
|
if self._state == "question":
|
||||||
replay_audio(self.card(), True)
|
replay_audio(self.card(), True)
|
||||||
elif self._state == "answer":
|
elif self._state == "answer":
|
||||||
replay_audio(self.card(), False)
|
replay_audio(self.card(), False)
|
||||||
|
|
||||||
def close(self):
|
def close(self) -> None:
|
||||||
self._on_close()
|
self._on_close()
|
||||||
super().close()
|
super().close()
|
||||||
|
|
||||||
def _on_close(self):
|
def _on_close(self) -> None:
|
||||||
self._open = False
|
self._open = False
|
||||||
self._close_callback()
|
self._close_callback()
|
||||||
|
|
||||||
def _setup_web_view(self):
|
def _setup_web_view(self) -> None:
|
||||||
jsinc = [
|
jsinc = [
|
||||||
"js/vendor/jquery.min.js",
|
"js/vendor/jquery.min.js",
|
||||||
"js/vendor/css_browser_selector.min.js",
|
"js/vendor/css_browser_selector.min.js",
|
||||||
|
@ -136,7 +141,7 @@ class Previewer(QDialog):
|
||||||
if cmd.startswith("play:"):
|
if cmd.startswith("play:"):
|
||||||
play_clicked_audio(cmd, self.card())
|
play_clicked_audio(cmd, self.card())
|
||||||
|
|
||||||
def render_card(self):
|
def render_card(self) -> None:
|
||||||
self.cancel_timer()
|
self.cancel_timer()
|
||||||
# Keep track of whether render() has ever been called
|
# Keep track of whether render() has ever been called
|
||||||
# with cardChanged=True since the last successful render
|
# with cardChanged=True since the last successful render
|
||||||
|
@ -151,7 +156,7 @@ class Previewer(QDialog):
|
||||||
else:
|
else:
|
||||||
self._render_scheduled()
|
self._render_scheduled()
|
||||||
|
|
||||||
def cancel_timer(self):
|
def cancel_timer(self) -> None:
|
||||||
if self._timer:
|
if self._timer:
|
||||||
self._timer.stop()
|
self._timer.stop()
|
||||||
self._timer = None
|
self._timer = None
|
||||||
|
@ -214,7 +219,7 @@ class Previewer(QDialog):
|
||||||
self._web.eval("{}({},'{}');".format(func, json.dumps(txt), bodyclass))
|
self._web.eval("{}({},'{}');".format(func, json.dumps(txt), bodyclass))
|
||||||
self._card_changed = False
|
self._card_changed = False
|
||||||
|
|
||||||
def _on_show_both_sides(self, toggle):
|
def _on_show_both_sides(self, toggle: bool) -> None:
|
||||||
self._show_both_sides = toggle
|
self._show_both_sides = toggle
|
||||||
self.mw.col.set_config_bool(ConfigBoolKey.PREVIEW_BOTH_SIDES, toggle)
|
self.mw.col.set_config_bool(ConfigBoolKey.PREVIEW_BOTH_SIDES, toggle)
|
||||||
self.mw.col.setMod()
|
self.mw.col.setMod()
|
||||||
|
@ -222,7 +227,7 @@ class Previewer(QDialog):
|
||||||
self._state = "question"
|
self._state = "question"
|
||||||
self.render_card()
|
self.render_card()
|
||||||
|
|
||||||
def _state_and_mod(self):
|
def _state_and_mod(self) -> Tuple[str, int, int]:
|
||||||
c = self.card()
|
c = self.card()
|
||||||
n = c.note()
|
n = c.note()
|
||||||
n.load()
|
n.load()
|
||||||
|
@ -241,7 +246,7 @@ class MultiCardPreviewer(Previewer):
|
||||||
# need to state explicitly it's not implement to avoid W0223
|
# need to state explicitly it's not implement to avoid W0223
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def _create_gui(self):
|
def _create_gui(self) -> None:
|
||||||
super()._create_gui()
|
super()._create_gui()
|
||||||
self._prev = self.bbox.addButton("<", QDialogButtonBox.ActionRole)
|
self._prev = self.bbox.addButton("<", QDialogButtonBox.ActionRole)
|
||||||
self._prev.setAutoDefault(False)
|
self._prev.setAutoDefault(False)
|
||||||
|
@ -256,39 +261,39 @@ class MultiCardPreviewer(Previewer):
|
||||||
qconnect(self._prev.clicked, self._on_prev)
|
qconnect(self._prev.clicked, self._on_prev)
|
||||||
qconnect(self._next.clicked, self._on_next)
|
qconnect(self._next.clicked, self._on_next)
|
||||||
|
|
||||||
def _on_prev(self):
|
def _on_prev(self) -> None:
|
||||||
if self._state == "answer" and not self._show_both_sides:
|
if self._state == "answer" and not self._show_both_sides:
|
||||||
self._state = "question"
|
self._state = "question"
|
||||||
self.render_card()
|
self.render_card()
|
||||||
else:
|
else:
|
||||||
self._on_prev_card()
|
self._on_prev_card()
|
||||||
|
|
||||||
def _on_prev_card(self):
|
def _on_prev_card(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _on_next(self):
|
def _on_next(self) -> None:
|
||||||
if self._state == "question":
|
if self._state == "question":
|
||||||
self._state = "answer"
|
self._state = "answer"
|
||||||
self.render_card()
|
self.render_card()
|
||||||
else:
|
else:
|
||||||
self._on_next_card()
|
self._on_next_card()
|
||||||
|
|
||||||
def _on_next_card(self):
|
def _on_next_card(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _updateButtons(self):
|
def _updateButtons(self) -> None:
|
||||||
if not self._open:
|
if not self._open:
|
||||||
return
|
return
|
||||||
self._prev.setEnabled(self._should_enable_prev())
|
self._prev.setEnabled(self._should_enable_prev())
|
||||||
self._next.setEnabled(self._should_enable_next())
|
self._next.setEnabled(self._should_enable_next())
|
||||||
|
|
||||||
def _should_enable_prev(self):
|
def _should_enable_prev(self) -> bool:
|
||||||
return self._state == "answer" and not self._show_both_sides
|
return self._state == "answer" and not self._show_both_sides
|
||||||
|
|
||||||
def _should_enable_next(self):
|
def _should_enable_next(self) -> bool:
|
||||||
return self._state == "question"
|
return self._state == "question"
|
||||||
|
|
||||||
def _on_close(self):
|
def _on_close(self) -> None:
|
||||||
super()._on_close()
|
super()._on_close()
|
||||||
self._prev = None
|
self._prev = None
|
||||||
self._next = None
|
self._next = None
|
||||||
|
@ -312,20 +317,20 @@ class BrowserPreviewer(MultiCardPreviewer):
|
||||||
self._last_card_id = c.id
|
self._last_card_id = c.id
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
def _on_prev_card(self):
|
def _on_prev_card(self) -> None:
|
||||||
self._parent.editor.saveNow(
|
self._parent.editor.saveNow(
|
||||||
lambda: self._parent._moveCur(QAbstractItemView.MoveUp)
|
lambda: self._parent._moveCur(QAbstractItemView.MoveUp)
|
||||||
)
|
)
|
||||||
|
|
||||||
def _on_next_card(self):
|
def _on_next_card(self) -> None:
|
||||||
self._parent.editor.saveNow(
|
self._parent.editor.saveNow(
|
||||||
lambda: self._parent._moveCur(QAbstractItemView.MoveDown)
|
lambda: self._parent._moveCur(QAbstractItemView.MoveDown)
|
||||||
)
|
)
|
||||||
|
|
||||||
def _should_enable_prev(self):
|
def _should_enable_prev(self) -> bool:
|
||||||
return super()._should_enable_prev() or self._parent.currentRow() > 0
|
return super()._should_enable_prev() or self._parent.currentRow() > 0
|
||||||
|
|
||||||
def _should_enable_next(self):
|
def _should_enable_next(self) -> bool:
|
||||||
return (
|
return (
|
||||||
super()._should_enable_next()
|
super()._should_enable_next()
|
||||||
or self._parent.currentRow() < self._parent.model.rowCount(None) - 1
|
or self._parent.currentRow() < self._parent.model.rowCount(None) - 1
|
||||||
|
|
|
@ -113,17 +113,17 @@ class LoadMetaResult:
|
||||||
|
|
||||||
|
|
||||||
class AnkiRestart(SystemExit):
|
class AnkiRestart(SystemExit):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
self.exitcode = kwargs.pop("exitcode", 0)
|
self.exitcode = kwargs.pop("exitcode", 0)
|
||||||
super().__init__(*args, **kwargs) # type: ignore
|
super().__init__(*args, **kwargs) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
class ProfileManager:
|
class ProfileManager:
|
||||||
def __init__(self, base=None):
|
def __init__(self, base: Optional[str] = None) -> None: #
|
||||||
## Settings which should be forgotten each Anki restart
|
## Settings which should be forgotten each Anki restart
|
||||||
self.session = {}
|
self.session: Dict[str, Any] = {}
|
||||||
self.name = None
|
self.name: Optional[str] = None
|
||||||
self.db = None
|
self.db: Optional[DB] = None
|
||||||
self.profile: Optional[Dict] = None
|
self.profile: Optional[Dict] = None
|
||||||
# instantiate base folder
|
# instantiate base folder
|
||||||
self.base: str
|
self.base: str
|
||||||
|
@ -170,7 +170,7 @@ class ProfileManager:
|
||||||
return p
|
return p
|
||||||
return os.path.expanduser("~/Documents/Anki")
|
return os.path.expanduser("~/Documents/Anki")
|
||||||
|
|
||||||
def maybeMigrateFolder(self):
|
def maybeMigrateFolder(self) -> None:
|
||||||
newBase = self.base
|
newBase = self.base
|
||||||
oldBase = self._oldFolderLocation()
|
oldBase = self._oldFolderLocation()
|
||||||
|
|
||||||
|
@ -185,7 +185,7 @@ class ProfileManager:
|
||||||
self.base = newBase
|
self.base = newBase
|
||||||
shutil.move(oldBase, self.base)
|
shutil.move(oldBase, self.base)
|
||||||
|
|
||||||
def _tryToMigrateFolder(self, oldBase):
|
def _tryToMigrateFolder(self, oldBase) -> None:
|
||||||
from PyQt5 import QtGui, QtWidgets
|
from PyQt5 import QtGui, QtWidgets
|
||||||
|
|
||||||
app = QtWidgets.QApplication([])
|
app = QtWidgets.QApplication([])
|
||||||
|
@ -206,7 +206,7 @@ class ProfileManager:
|
||||||
confirmation.setText(
|
confirmation.setText(
|
||||||
"Anki needs to move its data folder from Documents/Anki to a new location. Proceed?"
|
"Anki needs to move its data folder from Documents/Anki to a new location. Proceed?"
|
||||||
)
|
)
|
||||||
retval = confirmation.exec()
|
retval = confirmation.exec_()
|
||||||
|
|
||||||
if retval == QMessageBox.Ok:
|
if retval == QMessageBox.Ok:
|
||||||
progress = QMessageBox()
|
progress = QMessageBox()
|
||||||
|
@ -228,7 +228,7 @@ class ProfileManager:
|
||||||
completion.setWindowTitle(window_title)
|
completion.setWindowTitle(window_title)
|
||||||
completion.setText("Migration complete. Please start Anki again.")
|
completion.setText("Migration complete. Please start Anki again.")
|
||||||
completion.show()
|
completion.show()
|
||||||
completion.exec()
|
completion.exec_()
|
||||||
else:
|
else:
|
||||||
diag = QMessageBox()
|
diag = QMessageBox()
|
||||||
diag.setIcon(QMessageBox.Warning)
|
diag.setIcon(QMessageBox.Warning)
|
||||||
|
@ -239,7 +239,7 @@ class ProfileManager:
|
||||||
"Migration aborted. If you would like to keep the old folder location, please "
|
"Migration aborted. If you would like to keep the old folder location, please "
|
||||||
"see the Startup Options section of the manual. Anki will now quit."
|
"see the Startup Options section of the manual. Anki will now quit."
|
||||||
)
|
)
|
||||||
diag.exec()
|
diag.exec_()
|
||||||
|
|
||||||
raise AnkiRestart(exitcode=0)
|
raise AnkiRestart(exitcode=0)
|
||||||
|
|
||||||
|
@ -269,7 +269,7 @@ class ProfileManager:
|
||||||
fn = super().find_class(module, name)
|
fn = super().find_class(module, name)
|
||||||
if module == "sip" and name == "_unpickle_type":
|
if module == "sip" and name == "_unpickle_type":
|
||||||
|
|
||||||
def wrapper(mod, obj, args):
|
def wrapper(mod, obj, args) -> Any:
|
||||||
if mod.startswith("PyQt4") and obj == "QByteArray":
|
if mod.startswith("PyQt4") and obj == "QByteArray":
|
||||||
# can't trust str objects from python 2
|
# can't trust str objects from python 2
|
||||||
return QByteArray()
|
return QByteArray()
|
||||||
|
@ -424,7 +424,7 @@ class ProfileManager:
|
||||||
os.makedirs(path)
|
os.makedirs(path)
|
||||||
return path
|
return path
|
||||||
|
|
||||||
def _setBaseFolder(self, cmdlineBase: None) -> None:
|
def _setBaseFolder(self, cmdlineBase: Optional[str]) -> None:
|
||||||
if cmdlineBase:
|
if cmdlineBase:
|
||||||
self.base = os.path.abspath(cmdlineBase)
|
self.base = os.path.abspath(cmdlineBase)
|
||||||
elif os.environ.get("ANKI_BASE"):
|
elif os.environ.get("ANKI_BASE"):
|
||||||
|
@ -534,7 +534,7 @@ create table if not exists profiles
|
||||||
def setDefaultLang(self, idx: int) -> None:
|
def setDefaultLang(self, idx: int) -> None:
|
||||||
# create dialog
|
# create dialog
|
||||||
class NoCloseDiag(QDialog):
|
class NoCloseDiag(QDialog):
|
||||||
def reject(self):
|
def reject(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
d = self.langDiag = NoCloseDiag()
|
d = self.langDiag = NoCloseDiag()
|
||||||
|
@ -665,7 +665,7 @@ create table if not exists profiles
|
||||||
pass
|
pass
|
||||||
return RecordingDriver.QtAudioInput
|
return RecordingDriver.QtAudioInput
|
||||||
|
|
||||||
def set_recording_driver(self, driver: RecordingDriver):
|
def set_recording_driver(self, driver: RecordingDriver) -> None:
|
||||||
self.profile["recordingDriver"] = driver.value
|
self.profile["recordingDriver"] = driver.value
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import time
|
import time
|
||||||
from typing import Optional
|
from typing import Callable, Optional
|
||||||
|
|
||||||
import aqt.forms
|
import aqt.forms
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
|
@ -15,13 +15,13 @@ from aqt.utils import TR, disable_help_button, tr
|
||||||
|
|
||||||
|
|
||||||
class ProgressManager:
|
class ProgressManager:
|
||||||
def __init__(self, mw):
|
def __init__(self, mw: aqt.AnkiQt) -> None:
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
self.app = QApplication.instance()
|
self.app = QApplication.instance()
|
||||||
self.inDB = False
|
self.inDB = False
|
||||||
self.blockUpdates = False
|
self.blockUpdates = False
|
||||||
self._show_timer: Optional[QTimer] = None
|
self._show_timer: Optional[QTimer] = None
|
||||||
self._win = None
|
self._win: Optional[ProgressDialog] = None
|
||||||
self._levels = 0
|
self._levels = 0
|
||||||
|
|
||||||
# Safer timers
|
# Safer timers
|
||||||
|
@ -29,7 +29,9 @@ class ProgressManager:
|
||||||
# A custom timer which avoids firing while a progress dialog is active
|
# A custom timer which avoids firing while a progress dialog is active
|
||||||
# (likely due to some long-running DB operation)
|
# (likely due to some long-running DB operation)
|
||||||
|
|
||||||
def timer(self, ms, func, repeat, requiresCollection=True):
|
def timer(
|
||||||
|
self, ms: int, func: Callable, repeat: bool, requiresCollection: bool = True
|
||||||
|
) -> QTimer:
|
||||||
"""Create and start a standard Anki timer.
|
"""Create and start a standard Anki timer.
|
||||||
|
|
||||||
If the timer fires while a progress window is shown:
|
If the timer fires while a progress window is shown:
|
||||||
|
@ -41,7 +43,7 @@ class ProgressManager:
|
||||||
timer to fire even when there is no collection, but will still
|
timer to fire even when there is no collection, but will still
|
||||||
only fire when there is no current progress dialog."""
|
only fire when there is no current progress dialog."""
|
||||||
|
|
||||||
def handler():
|
def handler() -> None:
|
||||||
if requiresCollection and not self.mw.col:
|
if requiresCollection and not self.mw.col:
|
||||||
# no current collection; timer is no longer valid
|
# no current collection; timer is no longer valid
|
||||||
print("Ignored progress func as collection unloaded: %s" % repr(func))
|
print("Ignored progress func as collection unloaded: %s" % repr(func))
|
||||||
|
@ -136,7 +138,7 @@ class ProgressManager:
|
||||||
self._updating = False
|
self._updating = False
|
||||||
self._lastUpdate = time.time()
|
self._lastUpdate = time.time()
|
||||||
|
|
||||||
def finish(self):
|
def finish(self) -> None:
|
||||||
self._levels -= 1
|
self._levels -= 1
|
||||||
self._levels = max(0, self._levels)
|
self._levels = max(0, self._levels)
|
||||||
if self._levels == 0:
|
if self._levels == 0:
|
||||||
|
@ -147,13 +149,13 @@ class ProgressManager:
|
||||||
self._show_timer.stop()
|
self._show_timer.stop()
|
||||||
self._show_timer = None
|
self._show_timer = None
|
||||||
|
|
||||||
def clear(self):
|
def clear(self) -> None:
|
||||||
"Restore the interface after an error."
|
"Restore the interface after an error."
|
||||||
if self._levels:
|
if self._levels:
|
||||||
self._levels = 1
|
self._levels = 1
|
||||||
self.finish()
|
self.finish()
|
||||||
|
|
||||||
def _maybeShow(self):
|
def _maybeShow(self) -> None:
|
||||||
if not self._levels:
|
if not self._levels:
|
||||||
return
|
return
|
||||||
if self._shown:
|
if self._shown:
|
||||||
|
@ -181,17 +183,17 @@ class ProgressManager:
|
||||||
self._win = None
|
self._win = None
|
||||||
self._shown = 0
|
self._shown = 0
|
||||||
|
|
||||||
def _setBusy(self):
|
def _setBusy(self) -> None:
|
||||||
self.mw.app.setOverrideCursor(QCursor(Qt.WaitCursor))
|
self.mw.app.setOverrideCursor(QCursor(Qt.WaitCursor))
|
||||||
|
|
||||||
def _unsetBusy(self):
|
def _unsetBusy(self) -> None:
|
||||||
self.app.restoreOverrideCursor()
|
self.app.restoreOverrideCursor()
|
||||||
|
|
||||||
def busy(self):
|
def busy(self) -> int:
|
||||||
"True if processing."
|
"True if processing."
|
||||||
return self._levels
|
return self._levels
|
||||||
|
|
||||||
def _on_show_timer(self):
|
def _on_show_timer(self) -> None:
|
||||||
self._show_timer = None
|
self._show_timer = None
|
||||||
self._showWin()
|
self._showWin()
|
||||||
|
|
||||||
|
@ -209,7 +211,7 @@ class ProgressManager:
|
||||||
|
|
||||||
|
|
||||||
class ProgressDialog(QDialog):
|
class ProgressDialog(QDialog):
|
||||||
def __init__(self, parent):
|
def __init__(self, parent: QWidget) -> None:
|
||||||
QDialog.__init__(self, parent)
|
QDialog.__init__(self, parent)
|
||||||
disable_help_button(self)
|
disable_help_button(self)
|
||||||
self.form = aqt.forms.progress.Ui_Dialog()
|
self.form = aqt.forms.progress.Ui_Dialog()
|
||||||
|
@ -219,18 +221,18 @@ class ProgressDialog(QDialog):
|
||||||
# required for smooth progress bars
|
# required for smooth progress bars
|
||||||
self.form.progressBar.setStyleSheet("QProgressBar::chunk { width: 1px; }")
|
self.form.progressBar.setStyleSheet("QProgressBar::chunk { width: 1px; }")
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self) -> None:
|
||||||
self._closingDown = True
|
self._closingDown = True
|
||||||
self.hide()
|
self.hide()
|
||||||
|
|
||||||
def closeEvent(self, evt):
|
def closeEvent(self, evt) -> None:
|
||||||
if self._closingDown:
|
if self._closingDown:
|
||||||
evt.accept()
|
evt.accept()
|
||||||
else:
|
else:
|
||||||
self.wantCancel = True
|
self.wantCancel = True
|
||||||
evt.ignore()
|
evt.ignore()
|
||||||
|
|
||||||
def keyPressEvent(self, evt):
|
def keyPressEvent(self, evt) -> None:
|
||||||
if evt.key() == Qt.Key_Escape:
|
if evt.key() == Qt.Key_Escape:
|
||||||
evt.ignore()
|
evt.ignore()
|
||||||
self.wantCancel = True
|
self.wantCancel = True
|
||||||
|
|
|
@ -30,7 +30,7 @@ except ImportError:
|
||||||
import sip # type: ignore
|
import sip # type: ignore
|
||||||
|
|
||||||
|
|
||||||
def debug():
|
def debug() -> None:
|
||||||
from pdb import set_trace
|
from pdb import set_trace
|
||||||
|
|
||||||
pyqtRemoveInputHook()
|
pyqtRemoveInputHook()
|
||||||
|
@ -39,7 +39,7 @@ def debug():
|
||||||
|
|
||||||
if os.environ.get("DEBUG"):
|
if os.environ.get("DEBUG"):
|
||||||
|
|
||||||
def info(type, value, tb):
|
def info(type, value, tb) -> None:
|
||||||
for line in traceback.format_exception(type, value, tb):
|
for line in traceback.format_exception(type, value, tb):
|
||||||
sys.stdout.write(line)
|
sys.stdout.write(line)
|
||||||
pyqtRemoveInputHook()
|
pyqtRemoveInputHook()
|
||||||
|
|
|
@ -8,7 +8,7 @@ import html
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import unicodedata as ucd
|
import unicodedata as ucd
|
||||||
from typing import Callable, List, Optional, Tuple, Union
|
from typing import Any, Callable, List, Match, Optional, Tuple, Union
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
|
|
||||||
|
@ -426,7 +426,7 @@ class Reviewer:
|
||||||
# compare with typed answer
|
# compare with typed answer
|
||||||
res = self.correct(given, cor, showBad=False)
|
res = self.correct(given, cor, showBad=False)
|
||||||
# and update the type answer area
|
# and update the type answer area
|
||||||
def repl(match):
|
def repl(match: Match) -> str:
|
||||||
# can't pass a string in directly, and can't use re.escape as it
|
# can't pass a string in directly, and can't use re.escape as it
|
||||||
# escapes too much
|
# escapes too much
|
||||||
s = """
|
s = """
|
||||||
|
@ -448,7 +448,7 @@ class Reviewer:
|
||||||
if not matches:
|
if not matches:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def noHint(txt):
|
def noHint(txt) -> str:
|
||||||
if "::" in txt:
|
if "::" in txt:
|
||||||
return txt.split("::")[0]
|
return txt.split("::")[0]
|
||||||
return txt
|
return txt
|
||||||
|
@ -652,7 +652,7 @@ time = %(time)d;
|
||||||
def _answerButtons(self) -> str:
|
def _answerButtons(self) -> str:
|
||||||
default = self._defaultEase()
|
default = self._defaultEase()
|
||||||
|
|
||||||
def but(i, label):
|
def but(i, label) -> str:
|
||||||
if i == default:
|
if i == default:
|
||||||
extra = """id="defease" class="focus" """
|
extra = """id="defease" class="focus" """
|
||||||
else:
|
else:
|
||||||
|
@ -697,7 +697,7 @@ time = %(time)d;
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
# note the shortcuts listed here also need to be defined above
|
# note the shortcuts listed here also need to be defined above
|
||||||
def _contextMenu(self):
|
def _contextMenu(self) -> List[Any]:
|
||||||
currentFlag = self.card and self.card.userFlag()
|
currentFlag = self.card and self.card.userFlag()
|
||||||
opts = [
|
opts = [
|
||||||
[
|
[
|
||||||
|
@ -834,7 +834,7 @@ time = %(time)d;
|
||||||
tooltip(tr(TR.STUDYING_NOTE_BURIED))
|
tooltip(tr(TR.STUDYING_NOTE_BURIED))
|
||||||
|
|
||||||
def onRecordVoice(self) -> None:
|
def onRecordVoice(self) -> None:
|
||||||
def after_record(path: str):
|
def after_record(path: str) -> None:
|
||||||
self._recordedAudio = path
|
self._recordedAudio = path
|
||||||
self.onReplayRecorded()
|
self.onReplayRecorded()
|
||||||
|
|
||||||
|
|
|
@ -17,10 +17,10 @@ class Change(enum.Enum):
|
||||||
class ChangeTracker:
|
class ChangeTracker:
|
||||||
_changed = Change.NO_CHANGE
|
_changed = Change.NO_CHANGE
|
||||||
|
|
||||||
def __init__(self, mw: AnkiQt):
|
def __init__(self, mw: AnkiQt) -> None:
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
|
|
||||||
def mark_basic(self):
|
def mark_basic(self) -> None:
|
||||||
if self._changed == Change.NO_CHANGE:
|
if self._changed == Change.NO_CHANGE:
|
||||||
self._changed = Change.BASIC_CHANGE
|
self._changed = Change.BASIC_CHANGE
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,17 @@ from __future__ import annotations
|
||||||
|
|
||||||
from concurrent.futures import Future
|
from concurrent.futures import Future
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Sequence, Tuple, cast
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Any,
|
||||||
|
Dict,
|
||||||
|
Iterable,
|
||||||
|
List,
|
||||||
|
Optional,
|
||||||
|
Sequence,
|
||||||
|
Tuple,
|
||||||
|
cast,
|
||||||
|
)
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki.collection import ConfigBoolKey, SearchTerm
|
from anki.collection import ConfigBoolKey, SearchTerm
|
||||||
|
@ -101,7 +111,7 @@ class SidebarModel(QAbstractItemModel):
|
||||||
self.root = root
|
self.root = root
|
||||||
self._cache_rows(root)
|
self._cache_rows(root)
|
||||||
|
|
||||||
def _cache_rows(self, node: SidebarItem):
|
def _cache_rows(self, node: SidebarItem) -> None:
|
||||||
"Cache index of children in parent."
|
"Cache index of children in parent."
|
||||||
for row, item in enumerate(node.children):
|
for row, item in enumerate(node.children):
|
||||||
item.row_in_parent = row
|
item.row_in_parent = row
|
||||||
|
@ -168,12 +178,12 @@ class SidebarModel(QAbstractItemModel):
|
||||||
else:
|
else:
|
||||||
return QVariant(theme_manager.icon_from_resources(item.icon))
|
return QVariant(theme_manager.icon_from_resources(item.icon))
|
||||||
|
|
||||||
def supportedDropActions(self):
|
def supportedDropActions(self) -> Qt.DropActions:
|
||||||
return Qt.MoveAction
|
return cast(Qt.DropActions, Qt.MoveAction)
|
||||||
|
|
||||||
def flags(self, index: QModelIndex):
|
def flags(self, index: QModelIndex) -> Qt.ItemFlags:
|
||||||
if not index.isValid():
|
if not index.isValid():
|
||||||
return Qt.ItemIsEnabled
|
return cast(Qt.ItemFlags, Qt.ItemIsEnabled)
|
||||||
flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
|
flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
|
||||||
|
|
||||||
item: SidebarItem = index.internalPointer()
|
item: SidebarItem = index.internalPointer()
|
||||||
|
@ -183,7 +193,7 @@ class SidebarModel(QAbstractItemModel):
|
||||||
):
|
):
|
||||||
flags |= Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled
|
flags |= Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled
|
||||||
|
|
||||||
return flags
|
return cast(Qt.ItemFlags, flags)
|
||||||
|
|
||||||
# Helpers
|
# Helpers
|
||||||
######################################################################
|
######################################################################
|
||||||
|
@ -193,7 +203,9 @@ class SidebarModel(QAbstractItemModel):
|
||||||
return theme_manager.icon_from_resources(iconRef)
|
return theme_manager.icon_from_resources(iconRef)
|
||||||
|
|
||||||
|
|
||||||
def expand_where_necessary(model: SidebarModel, tree: QTreeView, parent=None) -> None:
|
def expand_where_necessary(
|
||||||
|
model: SidebarModel, tree: QTreeView, parent: Optional[QModelIndex] = None
|
||||||
|
) -> None:
|
||||||
parent = parent or QModelIndex()
|
parent = parent or QModelIndex()
|
||||||
for row in range(model.rowCount(parent)):
|
for row in range(model.rowCount(parent)):
|
||||||
idx = model.index(row, 0, parent)
|
idx = model.index(row, 0, parent)
|
||||||
|
@ -213,7 +225,7 @@ class FilterModel(QSortFilterProxyModel):
|
||||||
|
|
||||||
|
|
||||||
class SidebarSearchBar(QLineEdit):
|
class SidebarSearchBar(QLineEdit):
|
||||||
def __init__(self, sidebar: SidebarTreeView):
|
def __init__(self, sidebar: SidebarTreeView) -> None:
|
||||||
QLineEdit.__init__(self, sidebar)
|
QLineEdit.__init__(self, sidebar)
|
||||||
self.setPlaceholderText(sidebar.col.tr(TR.BROWSING_SIDEBAR_FILTER))
|
self.setPlaceholderText(sidebar.col.tr(TR.BROWSING_SIDEBAR_FILTER))
|
||||||
self.sidebar = sidebar
|
self.sidebar = sidebar
|
||||||
|
@ -223,14 +235,14 @@ class SidebarSearchBar(QLineEdit):
|
||||||
qconnect(self.timer.timeout, self.onSearch)
|
qconnect(self.timer.timeout, self.onSearch)
|
||||||
qconnect(self.textChanged, self.onTextChanged)
|
qconnect(self.textChanged, self.onTextChanged)
|
||||||
|
|
||||||
def onTextChanged(self, text: str):
|
def onTextChanged(self, text: str) -> None:
|
||||||
if not self.timer.isActive():
|
if not self.timer.isActive():
|
||||||
self.timer.start()
|
self.timer.start()
|
||||||
|
|
||||||
def onSearch(self):
|
def onSearch(self) -> None:
|
||||||
self.sidebar.search_for(self.text())
|
self.sidebar.search_for(self.text())
|
||||||
|
|
||||||
def keyPressEvent(self, evt):
|
def keyPressEvent(self, evt: QKeyEvent) -> None:
|
||||||
if evt.key() in (Qt.Key_Up, Qt.Key_Down):
|
if evt.key() in (Qt.Key_Up, Qt.Key_Down):
|
||||||
self.sidebar.setFocus()
|
self.sidebar.setFocus()
|
||||||
elif evt.key() in (Qt.Key_Enter, Qt.Key_Return):
|
elif evt.key() in (Qt.Key_Enter, Qt.Key_Return):
|
||||||
|
@ -293,7 +305,7 @@ class SidebarTreeView(QTreeView):
|
||||||
if not self.isVisible():
|
if not self.isVisible():
|
||||||
return
|
return
|
||||||
|
|
||||||
def on_done(fut: Future):
|
def on_done(fut: Future) -> None:
|
||||||
root = fut.result()
|
root = fut.result()
|
||||||
model = SidebarModel(root)
|
model = SidebarModel(root)
|
||||||
|
|
||||||
|
@ -308,7 +320,7 @@ class SidebarTreeView(QTreeView):
|
||||||
|
|
||||||
self.mw.taskman.run_in_background(self._root_tree, on_done)
|
self.mw.taskman.run_in_background(self._root_tree, on_done)
|
||||||
|
|
||||||
def search_for(self, text: str):
|
def search_for(self, text: str) -> None:
|
||||||
if not text.strip():
|
if not text.strip():
|
||||||
self.current_search = None
|
self.current_search = None
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
@ -331,7 +343,7 @@ class SidebarTreeView(QTreeView):
|
||||||
|
|
||||||
def drawRow(
|
def drawRow(
|
||||||
self, painter: QPainter, options: QStyleOptionViewItem, idx: QModelIndex
|
self, painter: QPainter, options: QStyleOptionViewItem, idx: QModelIndex
|
||||||
):
|
) -> None:
|
||||||
if self.current_search is None:
|
if self.current_search is None:
|
||||||
return super().drawRow(painter, options, idx)
|
return super().drawRow(painter, options, idx)
|
||||||
if not (item := self.model().item_for_index(idx)):
|
if not (item := self.model().item_for_index(idx)):
|
||||||
|
@ -364,7 +376,7 @@ class SidebarTreeView(QTreeView):
|
||||||
if not source_ids:
|
if not source_ids:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def on_done(fut):
|
def on_done(fut: Future) -> None:
|
||||||
fut.result()
|
fut.result()
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
|
@ -444,7 +456,7 @@ class SidebarTreeView(QTreeView):
|
||||||
collapse_key: ConfigBoolKeyValue,
|
collapse_key: ConfigBoolKeyValue,
|
||||||
type: Optional[SidebarItemType] = None,
|
type: Optional[SidebarItemType] = None,
|
||||||
) -> SidebarItem:
|
) -> SidebarItem:
|
||||||
def update(expanded: bool):
|
def update(expanded: bool) -> None:
|
||||||
self.col.set_config_bool(collapse_key, not expanded)
|
self.col.set_config_bool(collapse_key, not expanded)
|
||||||
|
|
||||||
top = SidebarItem(
|
top = SidebarItem(
|
||||||
|
@ -486,7 +498,7 @@ class SidebarTreeView(QTreeView):
|
||||||
type=SidebarItemType.SAVED_SEARCH_ROOT,
|
type=SidebarItemType.SAVED_SEARCH_ROOT,
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_click():
|
def on_click() -> None:
|
||||||
self.show_context_menu(root, None)
|
self.show_context_menu(root, None)
|
||||||
|
|
||||||
root.onClick = on_click
|
root.onClick = on_click
|
||||||
|
@ -503,10 +515,12 @@ class SidebarTreeView(QTreeView):
|
||||||
def _tag_tree(self, root: SidebarItem) -> None:
|
def _tag_tree(self, root: SidebarItem) -> None:
|
||||||
icon = ":/icons/tag.svg"
|
icon = ":/icons/tag.svg"
|
||||||
|
|
||||||
def render(root: SidebarItem, nodes: Iterable[TagTreeNode], head="") -> None:
|
def render(
|
||||||
|
root: SidebarItem, nodes: Iterable[TagTreeNode], head: str = ""
|
||||||
|
) -> None:
|
||||||
for node in nodes:
|
for node in nodes:
|
||||||
|
|
||||||
def toggle_expand():
|
def toggle_expand() -> Callable[[bool], None]:
|
||||||
full_name = head + node.name # pylint: disable=cell-var-from-loop
|
full_name = head + node.name # pylint: disable=cell-var-from-loop
|
||||||
return lambda expanded: self.mw.col.tags.set_collapsed(
|
return lambda expanded: self.mw.col.tags.set_collapsed(
|
||||||
full_name, not expanded
|
full_name, not expanded
|
||||||
|
@ -537,10 +551,12 @@ class SidebarTreeView(QTreeView):
|
||||||
def _deck_tree(self, root: SidebarItem) -> None:
|
def _deck_tree(self, root: SidebarItem) -> None:
|
||||||
icon = ":/icons/deck.svg"
|
icon = ":/icons/deck.svg"
|
||||||
|
|
||||||
def render(root, nodes: Iterable[DeckTreeNode], head="") -> None:
|
def render(
|
||||||
|
root: SidebarItem, nodes: Iterable[DeckTreeNode], head: str = ""
|
||||||
|
) -> None:
|
||||||
for node in nodes:
|
for node in nodes:
|
||||||
|
|
||||||
def toggle_expand():
|
def toggle_expand() -> Callable[[bool], None]:
|
||||||
did = node.deck_id # pylint: disable=cell-var-from-loop
|
did = node.deck_id # pylint: disable=cell-var-from-loop
|
||||||
return lambda _: self.mw.col.decks.collapseBrowser(did)
|
return lambda _: self.mw.col.decks.collapseBrowser(did)
|
||||||
|
|
||||||
|
@ -613,7 +629,7 @@ class SidebarTreeView(QTreeView):
|
||||||
return
|
return
|
||||||
self.show_context_menu(item, idx)
|
self.show_context_menu(item, idx)
|
||||||
|
|
||||||
def show_context_menu(self, item: SidebarItem, idx: Optional[QModelIndex]):
|
def show_context_menu(self, item: SidebarItem, idx: Optional[QModelIndex]) -> None:
|
||||||
m = QMenu()
|
m = QMenu()
|
||||||
|
|
||||||
if item.item_type in self.context_menus:
|
if item.item_type in self.context_menus:
|
||||||
|
@ -680,7 +696,8 @@ class SidebarTreeView(QTreeView):
|
||||||
try:
|
try:
|
||||||
self.mw.col.decks.rename(deck, new_name)
|
self.mw.col.decks.rename(deck, new_name)
|
||||||
except DeckRenameError as e:
|
except DeckRenameError as e:
|
||||||
return showWarning(e.description)
|
showWarning(e.description)
|
||||||
|
return
|
||||||
self.refresh()
|
self.refresh()
|
||||||
self.mw.deckBrowser.refresh()
|
self.mw.deckBrowser.refresh()
|
||||||
|
|
||||||
|
@ -690,11 +707,11 @@ class SidebarTreeView(QTreeView):
|
||||||
def _remove_tag(self, item: "aqt.browser.SidebarItem") -> None:
|
def _remove_tag(self, item: "aqt.browser.SidebarItem") -> None:
|
||||||
old_name = item.full_name
|
old_name = item.full_name
|
||||||
|
|
||||||
def do_remove():
|
def do_remove() -> None:
|
||||||
self.mw.col.tags.remove(old_name)
|
self.mw.col.tags.remove(old_name)
|
||||||
self.col.tags.rename(old_name, "")
|
self.col.tags.rename(old_name, "")
|
||||||
|
|
||||||
def on_done(fut: Future):
|
def on_done(fut: Future) -> None:
|
||||||
self.mw.requireReset(reason=ResetReason.BrowserRemoveTags, context=self)
|
self.mw.requireReset(reason=ResetReason.BrowserRemoveTags, context=self)
|
||||||
self.browser.model.endReset()
|
self.browser.model.endReset()
|
||||||
fut.result()
|
fut.result()
|
||||||
|
@ -713,11 +730,11 @@ class SidebarTreeView(QTreeView):
|
||||||
if new_name == old_name or not new_name:
|
if new_name == old_name or not new_name:
|
||||||
return
|
return
|
||||||
|
|
||||||
def do_rename():
|
def do_rename() -> int:
|
||||||
self.mw.col.tags.remove(old_name)
|
self.mw.col.tags.remove(old_name)
|
||||||
return self.col.tags.rename(old_name, new_name)
|
return self.col.tags.rename(old_name, new_name)
|
||||||
|
|
||||||
def on_done(fut: Future):
|
def on_done(fut: Future) -> None:
|
||||||
self.mw.requireReset(reason=ResetReason.BrowserAddTags, context=self)
|
self.mw.requireReset(reason=ResetReason.BrowserAddTags, context=self)
|
||||||
self.browser.model.endReset()
|
self.browser.model.endReset()
|
||||||
|
|
||||||
|
@ -739,10 +756,10 @@ class SidebarTreeView(QTreeView):
|
||||||
did = item.id
|
did = item.id
|
||||||
if self.mw.deckBrowser.ask_delete_deck(did):
|
if self.mw.deckBrowser.ask_delete_deck(did):
|
||||||
|
|
||||||
def do_delete():
|
def do_delete() -> None:
|
||||||
return self.mw.col.decks.rem(did, True)
|
return self.mw.col.decks.rem(did, True)
|
||||||
|
|
||||||
def on_done(fut: Future):
|
def on_done(fut: Future) -> None:
|
||||||
self.mw.requireReset(reason=ResetReason.BrowserDeleteDeck, context=self)
|
self.mw.requireReset(reason=ResetReason.BrowserDeleteDeck, context=self)
|
||||||
self.browser.search()
|
self.browser.search()
|
||||||
self.browser.model.endReset()
|
self.browser.model.endReset()
|
||||||
|
@ -777,7 +794,7 @@ class SidebarTreeView(QTreeView):
|
||||||
self.col.set_config("savedFilters", conf)
|
self.col.set_config("savedFilters", conf)
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
def save_current_search(self, _item=None) -> None:
|
def save_current_search(self, _item: Any = None) -> None:
|
||||||
try:
|
try:
|
||||||
filt = self.col.build_search_string(
|
filt = self.col.build_search_string(
|
||||||
self.browser.form.searchEdit.lineEdit().text()
|
self.browser.form.searchEdit.lineEdit().text()
|
||||||
|
|
|
@ -438,7 +438,7 @@ class MpvManager(MPV, SoundOrVideoPlayer):
|
||||||
|
|
||||||
|
|
||||||
class SimpleMplayerSlaveModePlayer(SimpleMplayerPlayer):
|
class SimpleMplayerSlaveModePlayer(SimpleMplayerPlayer):
|
||||||
def __init__(self, taskman: TaskManager):
|
def __init__(self, taskman: TaskManager) -> None:
|
||||||
super().__init__(taskman)
|
super().__init__(taskman)
|
||||||
self.args.append("-slave")
|
self.args.append("-slave")
|
||||||
|
|
||||||
|
@ -494,7 +494,7 @@ def encode_mp3(mw: aqt.AnkiQt, src_wav: str, on_done: Callable[[str], None]) ->
|
||||||
"Encode the provided wav file to .mp3, and call on_done() with the path."
|
"Encode the provided wav file to .mp3, and call on_done() with the path."
|
||||||
dst_mp3 = src_wav.replace(".wav", "%d.mp3" % time.time())
|
dst_mp3 = src_wav.replace(".wav", "%d.mp3" % time.time())
|
||||||
|
|
||||||
def _on_done(fut: Future):
|
def _on_done(fut: Future) -> None:
|
||||||
fut.result()
|
fut.result()
|
||||||
on_done(dst_mp3)
|
on_done(dst_mp3)
|
||||||
|
|
||||||
|
@ -509,7 +509,7 @@ class Recorder(ABC):
|
||||||
# seconds to wait before recording
|
# seconds to wait before recording
|
||||||
STARTUP_DELAY = 0.3
|
STARTUP_DELAY = 0.3
|
||||||
|
|
||||||
def __init__(self, output_path: str):
|
def __init__(self, output_path: str) -> None:
|
||||||
self.output_path = output_path
|
self.output_path = output_path
|
||||||
|
|
||||||
def start(self, on_done: Callable[[], None]) -> None:
|
def start(self, on_done: Callable[[], None]) -> None:
|
||||||
|
@ -517,7 +517,7 @@ class Recorder(ABC):
|
||||||
self._started_at = time.time()
|
self._started_at = time.time()
|
||||||
on_done()
|
on_done()
|
||||||
|
|
||||||
def stop(self, on_done: Callable[[str], None]):
|
def stop(self, on_done: Callable[[str], None]) -> None:
|
||||||
"Stop recording, then call on_done() when finished."
|
"Stop recording, then call on_done() when finished."
|
||||||
on_done(self.output_path)
|
on_done(self.output_path)
|
||||||
|
|
||||||
|
@ -525,7 +525,7 @@ class Recorder(ABC):
|
||||||
"Seconds since recording started."
|
"Seconds since recording started."
|
||||||
return time.time() - self._started_at
|
return time.time() - self._started_at
|
||||||
|
|
||||||
def on_timer(self):
|
def on_timer(self) -> None:
|
||||||
"Will be called periodically."
|
"Will be called periodically."
|
||||||
|
|
||||||
|
|
||||||
|
@ -534,7 +534,7 @@ class Recorder(ABC):
|
||||||
|
|
||||||
|
|
||||||
class QtAudioInputRecorder(Recorder):
|
class QtAudioInputRecorder(Recorder):
|
||||||
def __init__(self, output_path: str, mw: aqt.AnkiQt, parent: QWidget):
|
def __init__(self, output_path: str, mw: aqt.AnkiQt, parent: QWidget) -> None:
|
||||||
super().__init__(output_path)
|
super().__init__(output_path)
|
||||||
|
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
|
@ -567,11 +567,11 @@ class QtAudioInputRecorder(Recorder):
|
||||||
self._iodevice.readyRead.connect(self._on_read_ready) # type: ignore
|
self._iodevice.readyRead.connect(self._on_read_ready) # type: ignore
|
||||||
super().start(on_done)
|
super().start(on_done)
|
||||||
|
|
||||||
def _on_read_ready(self):
|
def _on_read_ready(self) -> None:
|
||||||
self._buffer += self._iodevice.readAll()
|
self._buffer += self._iodevice.readAll()
|
||||||
|
|
||||||
def stop(self, on_done: Callable[[str], None]):
|
def stop(self, on_done: Callable[[str], None]) -> None:
|
||||||
def on_stop_timer():
|
def on_stop_timer() -> None:
|
||||||
# read anything remaining in buffer & stop
|
# read anything remaining in buffer & stop
|
||||||
self._on_read_ready()
|
self._on_read_ready()
|
||||||
self._audio_input.stop()
|
self._audio_input.stop()
|
||||||
|
@ -580,7 +580,7 @@ class QtAudioInputRecorder(Recorder):
|
||||||
showWarning(f"recording failed: {err}")
|
showWarning(f"recording failed: {err}")
|
||||||
return
|
return
|
||||||
|
|
||||||
def write_file():
|
def write_file() -> None:
|
||||||
# swallow the first 300ms to allow audio device to quiesce
|
# swallow the first 300ms to allow audio device to quiesce
|
||||||
wait = int(44100 * self.STARTUP_DELAY)
|
wait = int(44100 * self.STARTUP_DELAY)
|
||||||
if len(self._buffer) <= wait:
|
if len(self._buffer) <= wait:
|
||||||
|
@ -595,7 +595,7 @@ class QtAudioInputRecorder(Recorder):
|
||||||
wf.writeframes(self._buffer)
|
wf.writeframes(self._buffer)
|
||||||
wf.close()
|
wf.close()
|
||||||
|
|
||||||
def and_then(fut):
|
def and_then(fut) -> None:
|
||||||
fut.result()
|
fut.result()
|
||||||
Recorder.stop(self, on_done)
|
Recorder.stop(self, on_done)
|
||||||
|
|
||||||
|
@ -672,7 +672,7 @@ class PyAudioThreadedRecorder(threading.Thread):
|
||||||
|
|
||||||
|
|
||||||
class PyAudioRecorder(Recorder):
|
class PyAudioRecorder(Recorder):
|
||||||
def __init__(self, mw: aqt.AnkiQt, output_path: str):
|
def __init__(self, mw: aqt.AnkiQt, output_path: str) -> None:
|
||||||
super().__init__(output_path)
|
super().__init__(output_path)
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
|
|
||||||
|
@ -686,7 +686,7 @@ class PyAudioRecorder(Recorder):
|
||||||
while self.duration() < 1:
|
while self.duration() < 1:
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
|
|
||||||
def func(fut):
|
def func(fut) -> None:
|
||||||
Recorder.stop(self, on_done)
|
Recorder.stop(self, on_done)
|
||||||
|
|
||||||
self.thread.finish = True
|
self.thread.finish = True
|
||||||
|
@ -715,7 +715,7 @@ class RecordDialog(QDialog):
|
||||||
self._start_recording()
|
self._start_recording()
|
||||||
self._setup_dialog()
|
self._setup_dialog()
|
||||||
|
|
||||||
def _setup_dialog(self):
|
def _setup_dialog(self) -> None:
|
||||||
self.setWindowTitle("Anki")
|
self.setWindowTitle("Anki")
|
||||||
icon = QLabel()
|
icon = QLabel()
|
||||||
icon.setPixmap(QPixmap(":/icons/media-record.png"))
|
icon.setPixmap(QPixmap(":/icons/media-record.png"))
|
||||||
|
@ -740,10 +740,10 @@ class RecordDialog(QDialog):
|
||||||
restoreGeom(self, "audioRecorder2")
|
restoreGeom(self, "audioRecorder2")
|
||||||
self.show()
|
self.show()
|
||||||
|
|
||||||
def _save_diag(self):
|
def _save_diag(self) -> None:
|
||||||
saveGeom(self, "audioRecorder2")
|
saveGeom(self, "audioRecorder2")
|
||||||
|
|
||||||
def _start_recording(self):
|
def _start_recording(self) -> None:
|
||||||
driver = self.mw.pm.recording_driver()
|
driver = self.mw.pm.recording_driver()
|
||||||
if driver is RecordingDriver.PyAudio:
|
if driver is RecordingDriver.PyAudio:
|
||||||
self._recorder = PyAudioRecorder(self.mw, namedtmp("rec.wav"))
|
self._recorder = PyAudioRecorder(self.mw, namedtmp("rec.wav"))
|
||||||
|
@ -755,18 +755,18 @@ class RecordDialog(QDialog):
|
||||||
assert_exhaustive(driver)
|
assert_exhaustive(driver)
|
||||||
self._recorder.start(self._start_timer)
|
self._recorder.start(self._start_timer)
|
||||||
|
|
||||||
def _start_timer(self):
|
def _start_timer(self) -> None:
|
||||||
self._timer = t = QTimer(self._parent)
|
self._timer = t = QTimer(self._parent)
|
||||||
t.timeout.connect(self._on_timer) # type: ignore
|
t.timeout.connect(self._on_timer) # type: ignore
|
||||||
t.setSingleShot(False)
|
t.setSingleShot(False)
|
||||||
t.start(100)
|
t.start(100)
|
||||||
|
|
||||||
def _on_timer(self):
|
def _on_timer(self) -> None:
|
||||||
self._recorder.on_timer()
|
self._recorder.on_timer()
|
||||||
duration = self._recorder.duration()
|
duration = self._recorder.duration()
|
||||||
self.label.setText(tr(TR.MEDIA_RECORDINGTIME, secs="%0.1f" % duration))
|
self.label.setText(tr(TR.MEDIA_RECORDINGTIME, secs="%0.1f" % duration))
|
||||||
|
|
||||||
def accept(self):
|
def accept(self) -> None:
|
||||||
self._timer.stop()
|
self._timer.stop()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -775,10 +775,10 @@ class RecordDialog(QDialog):
|
||||||
finally:
|
finally:
|
||||||
QDialog.accept(self)
|
QDialog.accept(self)
|
||||||
|
|
||||||
def reject(self):
|
def reject(self) -> None:
|
||||||
self._timer.stop()
|
self._timer.stop()
|
||||||
|
|
||||||
def cleanup(out: str):
|
def cleanup(out: str) -> None:
|
||||||
os.unlink(out)
|
os.unlink(out)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -790,7 +790,7 @@ class RecordDialog(QDialog):
|
||||||
def record_audio(
|
def record_audio(
|
||||||
parent: QWidget, mw: aqt.AnkiQt, encode: bool, on_done: Callable[[str], None]
|
parent: QWidget, mw: aqt.AnkiQt, encode: bool, on_done: Callable[[str], None]
|
||||||
):
|
):
|
||||||
def after_record(path: str):
|
def after_record(path: str) -> None:
|
||||||
if not encode:
|
if not encode:
|
||||||
on_done(path)
|
on_done(path)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -25,7 +25,7 @@ from aqt.utils import (
|
||||||
class NewDeckStats(QDialog):
|
class NewDeckStats(QDialog):
|
||||||
"""New deck stats."""
|
"""New deck stats."""
|
||||||
|
|
||||||
def __init__(self, mw: aqt.main.AnkiQt):
|
def __init__(self, mw: aqt.main.AnkiQt) -> None:
|
||||||
QDialog.__init__(self, mw, Qt.Window)
|
QDialog.__init__(self, mw, Qt.Window)
|
||||||
mw.setupDialogGC(self)
|
mw.setupDialogGC(self)
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
|
@ -54,17 +54,17 @@ class NewDeckStats(QDialog):
|
||||||
self.form.web.set_bridge_command(self._on_bridge_cmd, self)
|
self.form.web.set_bridge_command(self._on_bridge_cmd, self)
|
||||||
self.activateWindow()
|
self.activateWindow()
|
||||||
|
|
||||||
def reject(self):
|
def reject(self) -> None:
|
||||||
self.form.web = None
|
self.form.web = None
|
||||||
saveGeom(self, self.name)
|
saveGeom(self, self.name)
|
||||||
aqt.dialogs.markClosed("NewDeckStats")
|
aqt.dialogs.markClosed("NewDeckStats")
|
||||||
QDialog.reject(self)
|
QDialog.reject(self)
|
||||||
|
|
||||||
def closeWithCallback(self, callback):
|
def closeWithCallback(self, callback) -> None:
|
||||||
self.reject()
|
self.reject()
|
||||||
callback()
|
callback()
|
||||||
|
|
||||||
def _imagePath(self):
|
def _imagePath(self) -> str:
|
||||||
name = time.strftime("-%Y-%m-%d@%H-%M-%S.pdf", time.localtime(time.time()))
|
name = time.strftime("-%Y-%m-%d@%H-%M-%S.pdf", time.localtime(time.time()))
|
||||||
name = "anki-" + tr(TR.STATISTICS_STATS) + name
|
name = "anki-" + tr(TR.STATISTICS_STATS) + name
|
||||||
file = getSaveFile(
|
file = getSaveFile(
|
||||||
|
@ -77,17 +77,17 @@ class NewDeckStats(QDialog):
|
||||||
)
|
)
|
||||||
return file
|
return file
|
||||||
|
|
||||||
def saveImage(self):
|
def saveImage(self) -> None:
|
||||||
path = self._imagePath()
|
path = self._imagePath()
|
||||||
if not path:
|
if not path:
|
||||||
return
|
return
|
||||||
self.form.web.page().printToPdf(path)
|
self.form.web.page().printToPdf(path)
|
||||||
tooltip(tr(TR.STATISTICS_SAVED))
|
tooltip(tr(TR.STATISTICS_SAVED))
|
||||||
|
|
||||||
def changePeriod(self, n):
|
def changePeriod(self, n) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def changeScope(self, type):
|
def changeScope(self, type) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _on_bridge_cmd(self, cmd: str) -> bool:
|
def _on_bridge_cmd(self, cmd: str) -> bool:
|
||||||
|
@ -98,14 +98,14 @@ class NewDeckStats(QDialog):
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def refresh(self):
|
def refresh(self) -> None:
|
||||||
self.form.web.load_ts_page("graphs")
|
self.form.web.load_ts_page("graphs")
|
||||||
|
|
||||||
|
|
||||||
class DeckStats(QDialog):
|
class DeckStats(QDialog):
|
||||||
"""Legacy deck stats, used by some add-ons."""
|
"""Legacy deck stats, used by some add-ons."""
|
||||||
|
|
||||||
def __init__(self, mw):
|
def __init__(self, mw: aqt.main.AnkiQt) -> None:
|
||||||
QDialog.__init__(self, mw, Qt.Window)
|
QDialog.__init__(self, mw, Qt.Window)
|
||||||
mw.setupDialogGC(self)
|
mw.setupDialogGC(self)
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
|
@ -143,17 +143,17 @@ class DeckStats(QDialog):
|
||||||
self.refresh()
|
self.refresh()
|
||||||
self.activateWindow()
|
self.activateWindow()
|
||||||
|
|
||||||
def reject(self):
|
def reject(self) -> None:
|
||||||
self.form.web = None
|
self.form.web = None
|
||||||
saveGeom(self, self.name)
|
saveGeom(self, self.name)
|
||||||
aqt.dialogs.markClosed("DeckStats")
|
aqt.dialogs.markClosed("DeckStats")
|
||||||
QDialog.reject(self)
|
QDialog.reject(self)
|
||||||
|
|
||||||
def closeWithCallback(self, callback):
|
def closeWithCallback(self, callback) -> None:
|
||||||
self.reject()
|
self.reject()
|
||||||
callback()
|
callback()
|
||||||
|
|
||||||
def _imagePath(self):
|
def _imagePath(self) -> str:
|
||||||
name = time.strftime("-%Y-%m-%d@%H-%M-%S.pdf", time.localtime(time.time()))
|
name = time.strftime("-%Y-%m-%d@%H-%M-%S.pdf", time.localtime(time.time()))
|
||||||
name = "anki-" + tr(TR.STATISTICS_STATS) + name
|
name = "anki-" + tr(TR.STATISTICS_STATS) + name
|
||||||
file = getSaveFile(
|
file = getSaveFile(
|
||||||
|
@ -166,22 +166,22 @@ class DeckStats(QDialog):
|
||||||
)
|
)
|
||||||
return file
|
return file
|
||||||
|
|
||||||
def saveImage(self):
|
def saveImage(self) -> None:
|
||||||
path = self._imagePath()
|
path = self._imagePath()
|
||||||
if not path:
|
if not path:
|
||||||
return
|
return
|
||||||
self.form.web.page().printToPdf(path)
|
self.form.web.page().printToPdf(path)
|
||||||
tooltip(tr(TR.STATISTICS_SAVED))
|
tooltip(tr(TR.STATISTICS_SAVED))
|
||||||
|
|
||||||
def changePeriod(self, n):
|
def changePeriod(self, n: int) -> None:
|
||||||
self.period = n
|
self.period = n
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
def changeScope(self, type):
|
def changeScope(self, type: str) -> None:
|
||||||
self.wholeCollection = type == "collection"
|
self.wholeCollection = type == "collection"
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
def refresh(self):
|
def refresh(self) -> None:
|
||||||
self.mw.progress.start(parent=self)
|
self.mw.progress.start(parent=self)
|
||||||
stats = self.mw.col.stats()
|
stats = self.mw.col.stats()
|
||||||
stats.wholeCollection = self.wholeCollection
|
stats.wholeCollection = self.wholeCollection
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from aqt import gui_hooks
|
from aqt import gui_hooks
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
|
@ -109,7 +111,7 @@ class StudyDeck(QDialog):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def redraw(self, filt, focus=None):
|
def redraw(self, filt: str, focus: Optional[str] = None) -> None:
|
||||||
self.filt = filt
|
self.filt = filt
|
||||||
self.focus = focus
|
self.focus = focus
|
||||||
self.names = [n for n in self.origNames if self._matches(n, filt)]
|
self.names = [n for n in self.origNames if self._matches(n, filt)]
|
||||||
|
@ -123,7 +125,7 @@ class StudyDeck(QDialog):
|
||||||
l.setCurrentRow(idx)
|
l.setCurrentRow(idx)
|
||||||
l.scrollToItem(l.item(idx), QAbstractItemView.PositionAtCenter)
|
l.scrollToItem(l.item(idx), QAbstractItemView.PositionAtCenter)
|
||||||
|
|
||||||
def _matches(self, name, filt):
|
def _matches(self, name: str, filt: str) -> bool:
|
||||||
name = name.lower()
|
name = name.lower()
|
||||||
filt = filt.lower()
|
filt = filt.lower()
|
||||||
if not filt:
|
if not filt:
|
||||||
|
@ -133,7 +135,7 @@ class StudyDeck(QDialog):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def onReset(self):
|
def onReset(self) -> None:
|
||||||
# model updated?
|
# model updated?
|
||||||
if self.nameFunc:
|
if self.nameFunc:
|
||||||
self.origNames = self.nameFunc()
|
self.origNames = self.nameFunc()
|
||||||
|
|
|
@ -40,12 +40,14 @@ class FullSyncChoice(enum.Enum):
|
||||||
DOWNLOAD = 2
|
DOWNLOAD = 2
|
||||||
|
|
||||||
|
|
||||||
def get_sync_status(mw: aqt.main.AnkiQt, callback: Callable[[SyncStatus], None]):
|
def get_sync_status(
|
||||||
|
mw: aqt.main.AnkiQt, callback: Callable[[SyncStatus], None]
|
||||||
|
) -> None:
|
||||||
auth = mw.pm.sync_auth()
|
auth = mw.pm.sync_auth()
|
||||||
if not auth:
|
if not auth:
|
||||||
return SyncStatus(required=SyncStatus.NO_CHANGES) # pylint:disable=no-member
|
callback(SyncStatus(required=SyncStatus.NO_CHANGES)) # pylint:disable=no-member
|
||||||
|
|
||||||
def on_future_done(fut):
|
def on_future_done(fut) -> None:
|
||||||
try:
|
try:
|
||||||
out = fut.result()
|
out = fut.result()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -57,7 +59,7 @@ def get_sync_status(mw: aqt.main.AnkiQt, callback: Callable[[SyncStatus], None])
|
||||||
mw.taskman.run_in_background(lambda: mw.col.sync_status(auth), on_future_done)
|
mw.taskman.run_in_background(lambda: mw.col.sync_status(auth), on_future_done)
|
||||||
|
|
||||||
|
|
||||||
def handle_sync_error(mw: aqt.main.AnkiQt, err: Exception):
|
def handle_sync_error(mw: aqt.main.AnkiQt, err: Exception) -> None:
|
||||||
if isinstance(err, SyncError):
|
if isinstance(err, SyncError):
|
||||||
if err.is_auth_error():
|
if err.is_auth_error():
|
||||||
mw.pm.clear_sync_auth()
|
mw.pm.clear_sync_auth()
|
||||||
|
@ -87,14 +89,14 @@ def sync_collection(mw: aqt.main.AnkiQt, on_done: Callable[[], None]) -> None:
|
||||||
auth = mw.pm.sync_auth()
|
auth = mw.pm.sync_auth()
|
||||||
assert auth
|
assert auth
|
||||||
|
|
||||||
def on_timer():
|
def on_timer() -> None:
|
||||||
on_normal_sync_timer(mw)
|
on_normal_sync_timer(mw)
|
||||||
|
|
||||||
timer = QTimer(mw)
|
timer = QTimer(mw)
|
||||||
qconnect(timer.timeout, on_timer)
|
qconnect(timer.timeout, on_timer)
|
||||||
timer.start(150)
|
timer.start(150)
|
||||||
|
|
||||||
def on_future_done(fut):
|
def on_future_done(fut) -> None:
|
||||||
mw.col.db.begin()
|
mw.col.db.begin()
|
||||||
timer.stop()
|
timer.stop()
|
||||||
try:
|
try:
|
||||||
|
@ -171,14 +173,14 @@ def on_full_sync_timer(mw: aqt.main.AnkiQt) -> None:
|
||||||
def full_download(mw: aqt.main.AnkiQt, on_done: Callable[[], None]) -> None:
|
def full_download(mw: aqt.main.AnkiQt, on_done: Callable[[], None]) -> None:
|
||||||
mw.col.close_for_full_sync()
|
mw.col.close_for_full_sync()
|
||||||
|
|
||||||
def on_timer():
|
def on_timer() -> None:
|
||||||
on_full_sync_timer(mw)
|
on_full_sync_timer(mw)
|
||||||
|
|
||||||
timer = QTimer(mw)
|
timer = QTimer(mw)
|
||||||
qconnect(timer.timeout, on_timer)
|
qconnect(timer.timeout, on_timer)
|
||||||
timer.start(150)
|
timer.start(150)
|
||||||
|
|
||||||
def on_future_done(fut):
|
def on_future_done(fut) -> None:
|
||||||
timer.stop()
|
timer.stop()
|
||||||
mw.col.reopen(after_full_sync=True)
|
mw.col.reopen(after_full_sync=True)
|
||||||
mw.reset()
|
mw.reset()
|
||||||
|
@ -199,14 +201,14 @@ def full_download(mw: aqt.main.AnkiQt, on_done: Callable[[], None]) -> None:
|
||||||
def full_upload(mw: aqt.main.AnkiQt, on_done: Callable[[], None]) -> None:
|
def full_upload(mw: aqt.main.AnkiQt, on_done: Callable[[], None]) -> None:
|
||||||
mw.col.close_for_full_sync()
|
mw.col.close_for_full_sync()
|
||||||
|
|
||||||
def on_timer():
|
def on_timer() -> None:
|
||||||
on_full_sync_timer(mw)
|
on_full_sync_timer(mw)
|
||||||
|
|
||||||
timer = QTimer(mw)
|
timer = QTimer(mw)
|
||||||
qconnect(timer.timeout, on_timer)
|
qconnect(timer.timeout, on_timer)
|
||||||
timer.start(150)
|
timer.start(150)
|
||||||
|
|
||||||
def on_future_done(fut):
|
def on_future_done(fut) -> None:
|
||||||
timer.stop()
|
timer.stop()
|
||||||
mw.col.reopen(after_full_sync=True)
|
mw.col.reopen(after_full_sync=True)
|
||||||
mw.reset()
|
mw.reset()
|
||||||
|
@ -235,7 +237,7 @@ def sync_login(
|
||||||
if username and password:
|
if username and password:
|
||||||
break
|
break
|
||||||
|
|
||||||
def on_future_done(fut):
|
def on_future_done(fut) -> None:
|
||||||
try:
|
try:
|
||||||
auth = fut.result()
|
auth = fut.result()
|
||||||
except SyncError as e:
|
except SyncError as e:
|
||||||
|
|
|
@ -4,7 +4,9 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
from typing import Iterable, List, Optional, Union
|
||||||
|
|
||||||
|
from anki.collection import Collection
|
||||||
from aqt import gui_hooks
|
from aqt import gui_hooks
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
|
|
||||||
|
@ -15,9 +17,9 @@ class TagEdit(QLineEdit):
|
||||||
lostFocus = pyqtSignal()
|
lostFocus = pyqtSignal()
|
||||||
|
|
||||||
# 0 = tags, 1 = decks
|
# 0 = tags, 1 = decks
|
||||||
def __init__(self, parent, type=0):
|
def __init__(self, parent: QDialog, type: int = 0) -> None:
|
||||||
QLineEdit.__init__(self, parent)
|
QLineEdit.__init__(self, parent)
|
||||||
self.col = None
|
self.col: Optional[Collection] = None
|
||||||
self.model = QStringListModel()
|
self.model = QStringListModel()
|
||||||
self.type = type
|
self.type = type
|
||||||
if type == 0:
|
if type == 0:
|
||||||
|
@ -28,19 +30,20 @@ class TagEdit(QLineEdit):
|
||||||
self.completer.setCaseSensitivity(Qt.CaseInsensitive)
|
self.completer.setCaseSensitivity(Qt.CaseInsensitive)
|
||||||
self.setCompleter(self.completer)
|
self.setCompleter(self.completer)
|
||||||
|
|
||||||
def setCol(self, col):
|
def setCol(self, col: Collection) -> None:
|
||||||
"Set the current col, updating list of available tags."
|
"Set the current col, updating list of available tags."
|
||||||
self.col = col
|
self.col = col
|
||||||
|
l: Iterable[str]
|
||||||
if self.type == 0:
|
if self.type == 0:
|
||||||
l = self.col.tags.all()
|
l = self.col.tags.all()
|
||||||
else:
|
else:
|
||||||
l = (d.name for d in self.col.decks.all_names_and_ids())
|
l = (d.name for d in self.col.decks.all_names_and_ids())
|
||||||
self.model.setStringList(l)
|
self.model.setStringList(l)
|
||||||
|
|
||||||
def focusInEvent(self, evt):
|
def focusInEvent(self, evt: QFocusEvent) -> None:
|
||||||
QLineEdit.focusInEvent(self, evt)
|
QLineEdit.focusInEvent(self, evt)
|
||||||
|
|
||||||
def keyPressEvent(self, evt):
|
def keyPressEvent(self, evt: QKeyEvent) -> None:
|
||||||
if evt.key() in (Qt.Key_Up, Qt.Key_Down):
|
if evt.key() in (Qt.Key_Up, Qt.Key_Down):
|
||||||
# show completer on arrow key up/down
|
# show completer on arrow key up/down
|
||||||
if not self.completer.popup().isVisible():
|
if not self.completer.popup().isVisible():
|
||||||
|
@ -85,7 +88,7 @@ class TagEdit(QLineEdit):
|
||||||
self.showCompleter()
|
self.showCompleter()
|
||||||
gui_hooks.tag_editor_did_process_key(self, evt)
|
gui_hooks.tag_editor_did_process_key(self, evt)
|
||||||
|
|
||||||
def showCompleter(self):
|
def showCompleter(self) -> None:
|
||||||
self.completer.setCompletionPrefix(self.text())
|
self.completer.setCompletionPrefix(self.text())
|
||||||
self.completer.complete()
|
self.completer.complete()
|
||||||
|
|
||||||
|
@ -94,20 +97,26 @@ class TagEdit(QLineEdit):
|
||||||
self.lostFocus.emit() # type: ignore
|
self.lostFocus.emit() # type: ignore
|
||||||
self.completer.popup().hide()
|
self.completer.popup().hide()
|
||||||
|
|
||||||
def hideCompleter(self):
|
def hideCompleter(self) -> None:
|
||||||
if sip.isdeleted(self.completer):
|
if sip.isdeleted(self.completer):
|
||||||
return
|
return
|
||||||
self.completer.popup().hide()
|
self.completer.popup().hide()
|
||||||
|
|
||||||
|
|
||||||
class TagCompleter(QCompleter):
|
class TagCompleter(QCompleter):
|
||||||
def __init__(self, model, parent, edit, *args):
|
def __init__(
|
||||||
|
self,
|
||||||
|
model: QStringListModel,
|
||||||
|
parent: QWidget,
|
||||||
|
edit: TagEdit,
|
||||||
|
*args,
|
||||||
|
) -> None:
|
||||||
QCompleter.__init__(self, model, parent)
|
QCompleter.__init__(self, model, parent)
|
||||||
self.tags = []
|
self.tags: List[str] = []
|
||||||
self.edit = edit
|
self.edit = edit
|
||||||
self.cursor = None
|
self.cursor: Optional[int] = None
|
||||||
|
|
||||||
def splitPath(self, tags):
|
def splitPath(self, tags: str) -> List[str]:
|
||||||
stripped_tags = tags.strip()
|
stripped_tags = tags.strip()
|
||||||
stripped_tags = re.sub(" +", " ", stripped_tags)
|
stripped_tags = re.sub(" +", " ", stripped_tags)
|
||||||
self.tags = self.edit.col.tags.split(stripped_tags)
|
self.tags = self.edit.col.tags.split(stripped_tags)
|
||||||
|
@ -119,7 +128,7 @@ class TagCompleter(QCompleter):
|
||||||
self.cursor = stripped_tags.count(" ", 0, p)
|
self.cursor = stripped_tags.count(" ", 0, p)
|
||||||
return [self.tags[self.cursor]]
|
return [self.tags[self.cursor]]
|
||||||
|
|
||||||
def pathFromIndex(self, idx):
|
def pathFromIndex(self, idx: QModelIndex) -> str:
|
||||||
if self.cursor is None:
|
if self.cursor is None:
|
||||||
return self.edit.text()
|
return self.edit.text()
|
||||||
ret = QCompleter.pathFromIndex(self, idx)
|
ret = QCompleter.pathFromIndex(self, idx)
|
||||||
|
|
|
@ -3,14 +3,17 @@
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
|
from aqt.customstudy import CustomStudy
|
||||||
|
from aqt.main import AnkiQt
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.utils import disable_help_button, restoreGeom, saveGeom
|
from aqt.utils import disable_help_button, restoreGeom, saveGeom
|
||||||
|
|
||||||
|
|
||||||
class TagLimit(QDialog):
|
class TagLimit(QDialog):
|
||||||
def __init__(self, mw, parent):
|
def __init__(self, mw: AnkiQt, parent: CustomStudy) -> None:
|
||||||
QDialog.__init__(self, parent, Qt.Window)
|
QDialog.__init__(self, parent, Qt.Window)
|
||||||
self.tags: Union[str, List] = ""
|
self.tags: str = ""
|
||||||
|
self.tags_list: List[str] = []
|
||||||
self.mw = mw
|
self.mw = mw
|
||||||
self.parent: Optional[QWidget] = parent
|
self.parent: Optional[QWidget] = parent
|
||||||
self.deck = self.parent.deck
|
self.deck = self.parent.deck
|
||||||
|
@ -29,7 +32,7 @@ class TagLimit(QDialog):
|
||||||
restoreGeom(self, "tagLimit")
|
restoreGeom(self, "tagLimit")
|
||||||
self.exec_()
|
self.exec_()
|
||||||
|
|
||||||
def rebuildTagList(self):
|
def rebuildTagList(self) -> None:
|
||||||
usertags = self.mw.col.tags.byDeck(self.deck["id"], True)
|
usertags = self.mw.col.tags.byDeck(self.deck["id"], True)
|
||||||
yes = self.deck.get("activeTags", [])
|
yes = self.deck.get("activeTags", [])
|
||||||
no = self.deck.get("inactiveTags", [])
|
no = self.deck.get("inactiveTags", [])
|
||||||
|
@ -42,10 +45,10 @@ class TagLimit(QDialog):
|
||||||
groupedTags = []
|
groupedTags = []
|
||||||
usertags.sort()
|
usertags.sort()
|
||||||
groupedTags.append(usertags)
|
groupedTags.append(usertags)
|
||||||
self.tags = []
|
self.tags_list = []
|
||||||
for tags in groupedTags:
|
for tags in groupedTags:
|
||||||
for t in tags:
|
for t in tags:
|
||||||
self.tags.append(t)
|
self.tags_list.append(t)
|
||||||
item = QListWidgetItem(t.replace("_", " "))
|
item = QListWidgetItem(t.replace("_", " "))
|
||||||
self.dialog.activeList.addItem(item)
|
self.dialog.activeList.addItem(item)
|
||||||
if t in yesHash:
|
if t in yesHash:
|
||||||
|
@ -65,11 +68,11 @@ class TagLimit(QDialog):
|
||||||
idx = self.dialog.inactiveList.indexFromItem(item)
|
idx = self.dialog.inactiveList.indexFromItem(item)
|
||||||
self.dialog.inactiveList.selectionModel().select(idx, mode)
|
self.dialog.inactiveList.selectionModel().select(idx, mode)
|
||||||
|
|
||||||
def reject(self):
|
def reject(self) -> None:
|
||||||
self.tags = ""
|
self.tags = ""
|
||||||
QDialog.reject(self)
|
QDialog.reject(self)
|
||||||
|
|
||||||
def accept(self):
|
def accept(self) -> None:
|
||||||
self.hide()
|
self.hide()
|
||||||
# gather yes/no tags
|
# gather yes/no tags
|
||||||
yes = []
|
yes = []
|
||||||
|
@ -80,12 +83,12 @@ class TagLimit(QDialog):
|
||||||
item = self.dialog.activeList.item(c)
|
item = self.dialog.activeList.item(c)
|
||||||
idx = self.dialog.activeList.indexFromItem(item)
|
idx = self.dialog.activeList.indexFromItem(item)
|
||||||
if self.dialog.activeList.selectionModel().isSelected(idx):
|
if self.dialog.activeList.selectionModel().isSelected(idx):
|
||||||
yes.append(self.tags[c])
|
yes.append(self.tags_list[c])
|
||||||
# inactive
|
# inactive
|
||||||
item = self.dialog.inactiveList.item(c)
|
item = self.dialog.inactiveList.item(c)
|
||||||
idx = self.dialog.inactiveList.indexFromItem(item)
|
idx = self.dialog.inactiveList.indexFromItem(item)
|
||||||
if self.dialog.inactiveList.selectionModel().isSelected(idx):
|
if self.dialog.inactiveList.selectionModel().isSelected(idx):
|
||||||
no.append(self.tags[c])
|
no.append(self.tags_list[c])
|
||||||
# save in the deck for future invocations
|
# save in the deck for future invocations
|
||||||
self.deck["activeTags"] = yes
|
self.deck["activeTags"] = yes
|
||||||
self.deck["inactiveTags"] = no
|
self.deck["inactiveTags"] = no
|
||||||
|
|
|
@ -31,7 +31,7 @@ class TaskManager(QObject):
|
||||||
self._closures_lock = Lock()
|
self._closures_lock = Lock()
|
||||||
qconnect(self._closures_pending, self._on_closures_pending)
|
qconnect(self._closures_pending, self._on_closures_pending)
|
||||||
|
|
||||||
def run_on_main(self, closure: Closure):
|
def run_on_main(self, closure: Closure) -> None:
|
||||||
"Run the provided closure on the main thread."
|
"Run the provided closure on the main thread."
|
||||||
with self._closures_lock:
|
with self._closures_lock:
|
||||||
self._closures.append(closure)
|
self._closures.append(closure)
|
||||||
|
@ -71,14 +71,14 @@ class TaskManager(QObject):
|
||||||
):
|
):
|
||||||
self.mw.progress.start(parent=parent, label=label, immediate=immediate)
|
self.mw.progress.start(parent=parent, label=label, immediate=immediate)
|
||||||
|
|
||||||
def wrapped_done(fut):
|
def wrapped_done(fut) -> None:
|
||||||
self.mw.progress.finish()
|
self.mw.progress.finish()
|
||||||
if on_done:
|
if on_done:
|
||||||
on_done(fut)
|
on_done(fut)
|
||||||
|
|
||||||
self.run_in_background(task, wrapped_done)
|
self.run_in_background(task, wrapped_done)
|
||||||
|
|
||||||
def _on_closures_pending(self):
|
def _on_closures_pending(self) -> None:
|
||||||
"""Run any pending closures. This runs in the main thread."""
|
"""Run any pending closures. This runs in the main thread."""
|
||||||
with self._closures_lock:
|
with self._closures_lock:
|
||||||
closures = self._closures
|
closures = self._closures
|
||||||
|
|
|
@ -481,7 +481,7 @@ if isWin:
|
||||||
return []
|
return []
|
||||||
return list(map(self._voice_to_object, self.speaker.GetVoices()))
|
return list(map(self._voice_to_object, self.speaker.GetVoices()))
|
||||||
|
|
||||||
def _voice_to_object(self, voice: Any):
|
def _voice_to_object(self, voice: Any) -> WindowsVoice:
|
||||||
lang = voice.GetAttribute("language")
|
lang = voice.GetAttribute("language")
|
||||||
lang = lcid_hex_str_to_lang_code(lang)
|
lang = lcid_hex_str_to_lang_code(lang)
|
||||||
name = self._tidy_name(voice.GetAttribute("name"))
|
name = self._tidy_name(voice.GetAttribute("name"))
|
||||||
|
@ -525,7 +525,7 @@ if isWin:
|
||||||
id: Any
|
id: Any
|
||||||
|
|
||||||
class WindowsRTTTSFilePlayer(TTSProcessPlayer):
|
class WindowsRTTTSFilePlayer(TTSProcessPlayer):
|
||||||
voice_list = None
|
voice_list: List[Any] = []
|
||||||
tmppath = os.path.join(tmpdir(), "tts.wav")
|
tmppath = os.path.join(tmpdir(), "tts.wav")
|
||||||
|
|
||||||
def import_voices(self) -> None:
|
def import_voices(self) -> None:
|
||||||
|
@ -561,7 +561,7 @@ if isWin:
|
||||||
)
|
)
|
||||||
asyncio.run(self.speakText(tag, voice.id))
|
asyncio.run(self.speakText(tag, voice.id))
|
||||||
|
|
||||||
def _on_done(self, ret: Future, cb: OnDoneCallback):
|
def _on_done(self, ret: Future, cb: OnDoneCallback) -> None:
|
||||||
ret.result()
|
ret.result()
|
||||||
|
|
||||||
# inject file into the top of the audio queue
|
# inject file into the top of the audio queue
|
||||||
|
@ -572,7 +572,7 @@ if isWin:
|
||||||
# then tell player to advance, which will cause the file to be played
|
# then tell player to advance, which will cause the file to be played
|
||||||
cb()
|
cb()
|
||||||
|
|
||||||
async def speakText(self, tag: TTSTag, voice_id):
|
async def speakText(self, tag: TTSTag, voice_id) -> None:
|
||||||
import winrt.windows.media.speechsynthesis as speechsynthesis # type: ignore
|
import winrt.windows.media.speechsynthesis as speechsynthesis # type: ignore
|
||||||
import winrt.windows.storage.streams as streams # type: ignore
|
import winrt.windows.storage.streams as streams # type: ignore
|
||||||
|
|
||||||
|
|
|
@ -2,11 +2,13 @@
|
||||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki.utils import platDesc, versionWithBuild
|
from anki.utils import platDesc, versionWithBuild
|
||||||
|
from aqt.main import AnkiQt
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.utils import TR, openLink, showText, tr
|
from aqt.utils import TR, openLink, showText, tr
|
||||||
|
|
||||||
|
@ -17,12 +19,12 @@ class LatestVersionFinder(QThread):
|
||||||
newMsg = pyqtSignal(dict)
|
newMsg = pyqtSignal(dict)
|
||||||
clockIsOff = pyqtSignal(float)
|
clockIsOff = pyqtSignal(float)
|
||||||
|
|
||||||
def __init__(self, main):
|
def __init__(self, main: AnkiQt) -> None:
|
||||||
QThread.__init__(self)
|
QThread.__init__(self)
|
||||||
self.main = main
|
self.main = main
|
||||||
self.config = main.pm.meta
|
self.config = main.pm.meta
|
||||||
|
|
||||||
def _data(self):
|
def _data(self) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"ver": versionWithBuild(),
|
"ver": versionWithBuild(),
|
||||||
"os": platDesc(),
|
"os": platDesc(),
|
||||||
|
@ -31,7 +33,7 @@ class LatestVersionFinder(QThread):
|
||||||
"crt": self.config["created"],
|
"crt": self.config["created"],
|
||||||
}
|
}
|
||||||
|
|
||||||
def run(self):
|
def run(self) -> None:
|
||||||
if not self.config["updates"]:
|
if not self.config["updates"]:
|
||||||
return
|
return
|
||||||
d = self._data()
|
d = self._data()
|
||||||
|
@ -54,7 +56,7 @@ class LatestVersionFinder(QThread):
|
||||||
self.clockIsOff.emit(diff) # type: ignore
|
self.clockIsOff.emit(diff) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
def askAndUpdate(mw, ver):
|
def askAndUpdate(mw, ver) -> None:
|
||||||
baseStr = tr(TR.QT_MISC_ANKI_UPDATEDANKI_HAS_BEEN_RELEASED, val=ver)
|
baseStr = tr(TR.QT_MISC_ANKI_UPDATEDANKI_HAS_BEEN_RELEASED, val=ver)
|
||||||
msg = QMessageBox(mw)
|
msg = QMessageBox(mw)
|
||||||
msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No) # type: ignore
|
msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No) # type: ignore
|
||||||
|
@ -71,6 +73,6 @@ def askAndUpdate(mw, ver):
|
||||||
openLink(aqt.appWebsite)
|
openLink(aqt.appWebsite)
|
||||||
|
|
||||||
|
|
||||||
def showMessages(mw, data):
|
def showMessages(mw, data) -> None:
|
||||||
showText(data["msg"], parent=mw, type="html")
|
showText(data["msg"], parent=mw, type="html")
|
||||||
mw.pm.meta["lastMsg"] = data["msgId"]
|
mw.pm.meta["lastMsg"] = data["msgId"]
|
||||||
|
|
296
qt/aqt/utils.py
296
qt/aqt/utils.py
|
@ -8,12 +8,35 @@ import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import TYPE_CHECKING, Any, List, Literal, Optional, Union, cast
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Any,
|
||||||
|
Callable,
|
||||||
|
List,
|
||||||
|
Literal,
|
||||||
|
Optional,
|
||||||
|
Sequence,
|
||||||
|
Tuple,
|
||||||
|
Union,
|
||||||
|
cast,
|
||||||
|
)
|
||||||
|
|
||||||
from markdown import markdown
|
from markdown import markdown
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QAction,
|
||||||
|
QDialog,
|
||||||
|
QDialogButtonBox,
|
||||||
|
QFileDialog,
|
||||||
|
QHeaderView,
|
||||||
|
QMenu,
|
||||||
|
QPushButton,
|
||||||
|
QSplitter,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
import anki
|
import anki
|
||||||
import aqt
|
import aqt
|
||||||
|
from anki import Collection
|
||||||
from anki.errors import InvalidInput
|
from anki.errors import InvalidInput
|
||||||
from anki.lang import TR # pylint: disable=unused-import
|
from anki.lang import TR # pylint: disable=unused-import
|
||||||
from anki.utils import invalidFilename, isMac, isWin, noBundledLibs, versionWithBuild
|
from anki.utils import invalidFilename, isMac, isWin, noBundledLibs, versionWithBuild
|
||||||
|
@ -77,7 +100,7 @@ argument. However, add-on may use string, and we want to accept this.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def openHelp(section: HelpPageArgument):
|
def openHelp(section: HelpPageArgument) -> None:
|
||||||
link = aqt.appHelpSite
|
link = aqt.appHelpSite
|
||||||
if section:
|
if section:
|
||||||
if isinstance(section, HelpPage):
|
if isinstance(section, HelpPage):
|
||||||
|
@ -87,27 +110,35 @@ def openHelp(section: HelpPageArgument):
|
||||||
openLink(link)
|
openLink(link)
|
||||||
|
|
||||||
|
|
||||||
def openLink(link):
|
def openLink(link: str) -> None:
|
||||||
tooltip(tr(TR.QT_MISC_LOADING), period=1000)
|
tooltip(tr(TR.QT_MISC_LOADING), period=1000)
|
||||||
with noBundledLibs():
|
with noBundledLibs():
|
||||||
QDesktopServices.openUrl(QUrl(link))
|
QDesktopServices.openUrl(QUrl(link))
|
||||||
|
|
||||||
|
|
||||||
def showWarning(
|
def showWarning(
|
||||||
text, parent=None, help="", title="Anki", textFormat: Optional[TextFormat] = None
|
text: str,
|
||||||
):
|
parent: Optional[QDialog] = None,
|
||||||
|
help: HelpPageArgument = "",
|
||||||
|
title: str = "Anki",
|
||||||
|
textFormat: Optional[TextFormat] = None,
|
||||||
|
) -> int:
|
||||||
"Show a small warning with an OK button."
|
"Show a small warning with an OK button."
|
||||||
return showInfo(text, parent, help, "warning", title=title, textFormat=textFormat)
|
return showInfo(text, parent, help, "warning", title=title, textFormat=textFormat)
|
||||||
|
|
||||||
|
|
||||||
def showCritical(
|
def showCritical(
|
||||||
text, parent=None, help="", title="Anki", textFormat: Optional[TextFormat] = None
|
text: str,
|
||||||
):
|
parent: Optional[QDialog] = None,
|
||||||
|
help: str = "",
|
||||||
|
title: str = "Anki",
|
||||||
|
textFormat: Optional[TextFormat] = None,
|
||||||
|
) -> int:
|
||||||
"Show a small critical error with an OK button."
|
"Show a small critical error with an OK button."
|
||||||
return showInfo(text, parent, help, "critical", title=title, textFormat=textFormat)
|
return showInfo(text, parent, help, "critical", title=title, textFormat=textFormat)
|
||||||
|
|
||||||
|
|
||||||
def show_invalid_search_error(err: Exception):
|
def show_invalid_search_error(err: Exception) -> None:
|
||||||
"Render search errors in markdown, then display a warning."
|
"Render search errors in markdown, then display a warning."
|
||||||
text = str(err)
|
text = str(err)
|
||||||
if isinstance(err, InvalidInput):
|
if isinstance(err, InvalidInput):
|
||||||
|
@ -116,24 +147,27 @@ def show_invalid_search_error(err: Exception):
|
||||||
|
|
||||||
|
|
||||||
def showInfo(
|
def showInfo(
|
||||||
text,
|
text: str,
|
||||||
parent=False,
|
parent: Union[Literal[False], QDialog] = False,
|
||||||
help="",
|
help: HelpPageArgument = "",
|
||||||
type="info",
|
type: str = "info",
|
||||||
title="Anki",
|
title: str = "Anki",
|
||||||
textFormat: Optional[TextFormat] = None,
|
textFormat: Optional[TextFormat] = None,
|
||||||
customBtns=None,
|
customBtns: Optional[List[QMessageBox.StandardButton]] = None,
|
||||||
) -> int:
|
) -> int:
|
||||||
"Show a small info window with an OK button."
|
"Show a small info window with an OK button."
|
||||||
|
parent_widget: QWidget
|
||||||
if parent is False:
|
if parent is False:
|
||||||
parent = aqt.mw.app.activeWindow() or aqt.mw
|
parent_widget = aqt.mw.app.activeWindow() or aqt.mw
|
||||||
|
else:
|
||||||
|
parent_widget = parent
|
||||||
if type == "warning":
|
if type == "warning":
|
||||||
icon = QMessageBox.Warning
|
icon = QMessageBox.Warning
|
||||||
elif type == "critical":
|
elif type == "critical":
|
||||||
icon = QMessageBox.Critical
|
icon = QMessageBox.Critical
|
||||||
else:
|
else:
|
||||||
icon = QMessageBox.Information
|
icon = QMessageBox.Information
|
||||||
mb = QMessageBox(parent)
|
mb = QMessageBox(parent_widget) #
|
||||||
if textFormat == "plain":
|
if textFormat == "plain":
|
||||||
mb.setTextFormat(Qt.PlainText)
|
mb.setTextFormat(Qt.PlainText)
|
||||||
elif textFormat == "rich":
|
elif textFormat == "rich":
|
||||||
|
@ -161,16 +195,16 @@ def showInfo(
|
||||||
|
|
||||||
|
|
||||||
def showText(
|
def showText(
|
||||||
txt,
|
txt: str,
|
||||||
parent=None,
|
parent: Optional[QWidget] = None,
|
||||||
type="text",
|
type: str = "text",
|
||||||
run=True,
|
run: bool = True,
|
||||||
geomKey=None,
|
geomKey: Optional[str] = None,
|
||||||
minWidth=500,
|
minWidth: int = 500,
|
||||||
minHeight=400,
|
minHeight: int = 400,
|
||||||
title="Anki",
|
title: str = "Anki",
|
||||||
copyBtn=False,
|
copyBtn: bool = False,
|
||||||
):
|
) -> Optional[Tuple[QDialog, QDialogButtonBox]]:
|
||||||
if not parent:
|
if not parent:
|
||||||
parent = aqt.mw.app.activeWindow() or aqt.mw
|
parent = aqt.mw.app.activeWindow() or aqt.mw
|
||||||
diag = QDialog(parent)
|
diag = QDialog(parent)
|
||||||
|
@ -189,21 +223,21 @@ def showText(
|
||||||
layout.addWidget(box)
|
layout.addWidget(box)
|
||||||
if copyBtn:
|
if copyBtn:
|
||||||
|
|
||||||
def onCopy():
|
def onCopy() -> None:
|
||||||
QApplication.clipboard().setText(text.toPlainText())
|
QApplication.clipboard().setText(text.toPlainText())
|
||||||
|
|
||||||
btn = QPushButton(tr(TR.QT_MISC_COPY_TO_CLIPBOARD))
|
btn = QPushButton(tr(TR.QT_MISC_COPY_TO_CLIPBOARD))
|
||||||
qconnect(btn.clicked, onCopy)
|
qconnect(btn.clicked, onCopy)
|
||||||
box.addButton(btn, QDialogButtonBox.ActionRole)
|
box.addButton(btn, QDialogButtonBox.ActionRole)
|
||||||
|
|
||||||
def onReject():
|
def onReject() -> None:
|
||||||
if geomKey:
|
if geomKey:
|
||||||
saveGeom(diag, geomKey)
|
saveGeom(diag, geomKey)
|
||||||
QDialog.reject(diag)
|
QDialog.reject(diag)
|
||||||
|
|
||||||
qconnect(box.rejected, onReject)
|
qconnect(box.rejected, onReject)
|
||||||
|
|
||||||
def onFinish():
|
def onFinish() -> None:
|
||||||
if geomKey:
|
if geomKey:
|
||||||
saveGeom(diag, geomKey)
|
saveGeom(diag, geomKey)
|
||||||
|
|
||||||
|
@ -214,18 +248,19 @@ def showText(
|
||||||
restoreGeom(diag, geomKey)
|
restoreGeom(diag, geomKey)
|
||||||
if run:
|
if run:
|
||||||
diag.exec_()
|
diag.exec_()
|
||||||
|
return None
|
||||||
else:
|
else:
|
||||||
return diag, box
|
return diag, box
|
||||||
|
|
||||||
|
|
||||||
def askUser(
|
def askUser(
|
||||||
text,
|
text: str,
|
||||||
parent=None,
|
parent: QDialog = None,
|
||||||
help: HelpPageArgument = None,
|
help: HelpPageArgument = None,
|
||||||
defaultno=False,
|
defaultno: bool = False,
|
||||||
msgfunc=None,
|
msgfunc: Optional[Callable] = None,
|
||||||
title="Anki",
|
title: str = "Anki",
|
||||||
):
|
) -> bool:
|
||||||
"Show a yes/no question. Return true if yes."
|
"Show a yes/no question. Return true if yes."
|
||||||
if not parent:
|
if not parent:
|
||||||
parent = aqt.mw.app.activeWindow()
|
parent = aqt.mw.app.activeWindow()
|
||||||
|
@ -239,7 +274,7 @@ def askUser(
|
||||||
default = QMessageBox.No
|
default = QMessageBox.No
|
||||||
else:
|
else:
|
||||||
default = QMessageBox.Yes
|
default = QMessageBox.Yes
|
||||||
r = msgfunc(parent, title, text, sb, default)
|
r = msgfunc(parent, title, text, cast(QMessageBox.StandardButtons, sb), default)
|
||||||
if r == QMessageBox.Help:
|
if r == QMessageBox.Help:
|
||||||
|
|
||||||
openHelp(help)
|
openHelp(help)
|
||||||
|
@ -250,10 +285,15 @@ def askUser(
|
||||||
|
|
||||||
class ButtonedDialog(QMessageBox):
|
class ButtonedDialog(QMessageBox):
|
||||||
def __init__(
|
def __init__(
|
||||||
self, text, buttons, parent=None, help: HelpPageArgument = None, title="Anki"
|
self,
|
||||||
|
text: str,
|
||||||
|
buttons: List[str],
|
||||||
|
parent: Optional[QDialog] = None,
|
||||||
|
help: HelpPageArgument = None,
|
||||||
|
title: str = "Anki",
|
||||||
):
|
):
|
||||||
QMessageBox.__init__(self, parent)
|
QMessageBox.__init__(self, parent)
|
||||||
self._buttons = []
|
self._buttons: List[QPushButton] = []
|
||||||
self.setWindowTitle(title)
|
self.setWindowTitle(title)
|
||||||
self.help = help
|
self.help = help
|
||||||
self.setIcon(QMessageBox.Warning)
|
self.setIcon(QMessageBox.Warning)
|
||||||
|
@ -264,7 +304,7 @@ class ButtonedDialog(QMessageBox):
|
||||||
self.addButton(tr(TR.ACTIONS_HELP), QMessageBox.HelpRole)
|
self.addButton(tr(TR.ACTIONS_HELP), QMessageBox.HelpRole)
|
||||||
buttons.append(tr(TR.ACTIONS_HELP))
|
buttons.append(tr(TR.ACTIONS_HELP))
|
||||||
|
|
||||||
def run(self):
|
def run(self) -> str:
|
||||||
self.exec_()
|
self.exec_()
|
||||||
but = self.clickedButton().text()
|
but = self.clickedButton().text()
|
||||||
if but == "Help":
|
if but == "Help":
|
||||||
|
@ -274,13 +314,17 @@ class ButtonedDialog(QMessageBox):
|
||||||
# work around KDE 'helpfully' adding accelerators to button text of Qt apps
|
# work around KDE 'helpfully' adding accelerators to button text of Qt apps
|
||||||
return txt.replace("&", "")
|
return txt.replace("&", "")
|
||||||
|
|
||||||
def setDefault(self, idx):
|
def setDefault(self, idx: int) -> None:
|
||||||
self.setDefaultButton(self._buttons[idx])
|
self.setDefaultButton(self._buttons[idx])
|
||||||
|
|
||||||
|
|
||||||
def askUserDialog(
|
def askUserDialog(
|
||||||
text, buttons, parent=None, help: HelpPageArgument = None, title="Anki"
|
text: str,
|
||||||
):
|
buttons: List[str],
|
||||||
|
parent: Optional[QDialog] = None,
|
||||||
|
help: HelpPageArgument = None,
|
||||||
|
title: str = "Anki",
|
||||||
|
) -> ButtonedDialog:
|
||||||
if not parent:
|
if not parent:
|
||||||
parent = aqt.mw
|
parent = aqt.mw
|
||||||
diag = ButtonedDialog(text, buttons, parent, help, title=title)
|
diag = ButtonedDialog(text, buttons, parent, help, title=title)
|
||||||
|
@ -290,14 +334,14 @@ def askUserDialog(
|
||||||
class GetTextDialog(QDialog):
|
class GetTextDialog(QDialog):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
parent,
|
parent: Optional[QDialog],
|
||||||
question,
|
question: str,
|
||||||
help: HelpPageArgument = None,
|
help: HelpPageArgument = None,
|
||||||
edit=None,
|
edit: Optional[QLineEdit] = None,
|
||||||
default="",
|
default: str = "",
|
||||||
title="Anki",
|
title: str = "Anki",
|
||||||
minWidth=400,
|
minWidth: int = 400,
|
||||||
):
|
) -> None:
|
||||||
QDialog.__init__(self, parent)
|
QDialog.__init__(self, parent)
|
||||||
self.setWindowTitle(title)
|
self.setWindowTitle(title)
|
||||||
disable_help_button(self)
|
disable_help_button(self)
|
||||||
|
@ -325,26 +369,26 @@ class GetTextDialog(QDialog):
|
||||||
if help:
|
if help:
|
||||||
qconnect(b.button(QDialogButtonBox.Help).clicked, self.helpRequested)
|
qconnect(b.button(QDialogButtonBox.Help).clicked, self.helpRequested)
|
||||||
|
|
||||||
def accept(self):
|
def accept(self) -> None:
|
||||||
return QDialog.accept(self)
|
return QDialog.accept(self)
|
||||||
|
|
||||||
def reject(self):
|
def reject(self) -> None:
|
||||||
return QDialog.reject(self)
|
return QDialog.reject(self)
|
||||||
|
|
||||||
def helpRequested(self):
|
def helpRequested(self) -> None:
|
||||||
openHelp(self.help)
|
openHelp(self.help)
|
||||||
|
|
||||||
|
|
||||||
def getText(
|
def getText(
|
||||||
prompt,
|
prompt: str,
|
||||||
parent=None,
|
parent: Optional[QDialog] = None,
|
||||||
help: HelpPageArgument = None,
|
help: HelpPageArgument = None,
|
||||||
edit=None,
|
edit: Optional[QLineEdit] = None,
|
||||||
default="",
|
default: str = "",
|
||||||
title="Anki",
|
title: str = "Anki",
|
||||||
geomKey=None,
|
geomKey: Optional[str] = None,
|
||||||
**kwargs,
|
**kwargs: Any,
|
||||||
):
|
) -> Tuple[str, int]:
|
||||||
if not parent:
|
if not parent:
|
||||||
parent = aqt.mw.app.activeWindow() or aqt.mw
|
parent = aqt.mw.app.activeWindow() or aqt.mw
|
||||||
d = GetTextDialog(
|
d = GetTextDialog(
|
||||||
|
@ -359,7 +403,7 @@ def getText(
|
||||||
return (str(d.l.text()), ret)
|
return (str(d.l.text()), ret)
|
||||||
|
|
||||||
|
|
||||||
def getOnlyText(*args, **kwargs):
|
def getOnlyText(*args: Any, **kwargs: Any) -> str:
|
||||||
(s, r) = getText(*args, **kwargs)
|
(s, r) = getText(*args, **kwargs)
|
||||||
if r:
|
if r:
|
||||||
return s
|
return s
|
||||||
|
@ -368,7 +412,10 @@ def getOnlyText(*args, **kwargs):
|
||||||
|
|
||||||
|
|
||||||
# fixme: these utilities could be combined into a single base class
|
# fixme: these utilities could be combined into a single base class
|
||||||
def chooseList(prompt, choices, startrow=0, parent=None):
|
# unused by Anki, but used by add-ons
|
||||||
|
def chooseList(
|
||||||
|
prompt: str, choices: List[str], startrow: int = 0, parent: Any = None
|
||||||
|
) -> int:
|
||||||
if not parent:
|
if not parent:
|
||||||
parent = aqt.mw.app.activeWindow()
|
parent = aqt.mw.app.activeWindow()
|
||||||
d = QDialog(parent)
|
d = QDialog(parent)
|
||||||
|
@ -389,7 +436,9 @@ def chooseList(prompt, choices, startrow=0, parent=None):
|
||||||
return c.currentRow()
|
return c.currentRow()
|
||||||
|
|
||||||
|
|
||||||
def getTag(parent, deck, question, tags="user", **kwargs):
|
def getTag(
|
||||||
|
parent: QDialog, deck: Collection, question: str, **kwargs: Any
|
||||||
|
) -> Tuple[str, int]:
|
||||||
from aqt.tagedit import TagEdit
|
from aqt.tagedit import TagEdit
|
||||||
|
|
||||||
te = TagEdit(parent)
|
te = TagEdit(parent)
|
||||||
|
@ -409,7 +458,15 @@ def disable_help_button(widget: QWidget) -> None:
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
|
|
||||||
def getFile(parent, title, cb, filter="*.*", dir=None, key=None, multi=False):
|
def getFile(
|
||||||
|
parent: QDialog,
|
||||||
|
title: str,
|
||||||
|
cb: Optional[Callable[[Union[str, Sequence[str]]], None]],
|
||||||
|
filter: str = "*.*",
|
||||||
|
dir: Optional[str] = None,
|
||||||
|
key: Optional[str] = None,
|
||||||
|
multi: bool = False, # controls whether a single or multiple files is returned
|
||||||
|
) -> Optional[Union[Sequence[str], str]]:
|
||||||
"Ask the user for a file."
|
"Ask the user for a file."
|
||||||
assert not dir or not key
|
assert not dir or not key
|
||||||
if not dir:
|
if not dir:
|
||||||
|
@ -426,7 +483,7 @@ def getFile(parent, title, cb, filter="*.*", dir=None, key=None, multi=False):
|
||||||
d.setNameFilter(filter)
|
d.setNameFilter(filter)
|
||||||
ret = []
|
ret = []
|
||||||
|
|
||||||
def accept():
|
def accept() -> None:
|
||||||
files = list(d.selectedFiles())
|
files = list(d.selectedFiles())
|
||||||
if dirkey:
|
if dirkey:
|
||||||
dir = os.path.dirname(files[0])
|
dir = os.path.dirname(files[0])
|
||||||
|
@ -442,10 +499,17 @@ def getFile(parent, title, cb, filter="*.*", dir=None, key=None, multi=False):
|
||||||
d.exec_()
|
d.exec_()
|
||||||
if key:
|
if key:
|
||||||
saveState(d, key)
|
saveState(d, key)
|
||||||
return ret and ret[0]
|
return ret[0] if ret else None
|
||||||
|
|
||||||
|
|
||||||
def getSaveFile(parent, title, dir_description, key, ext, fname=None):
|
def getSaveFile(
|
||||||
|
parent: QDialog,
|
||||||
|
title: str,
|
||||||
|
dir_description: str,
|
||||||
|
key: str,
|
||||||
|
ext: str,
|
||||||
|
fname: Optional[str] = None,
|
||||||
|
) -> str:
|
||||||
"""Ask the user for a file to save. Use DIR_DESCRIPTION as config
|
"""Ask the user for a file to save. Use DIR_DESCRIPTION as config
|
||||||
variable. The file dialog will default to open with FNAME."""
|
variable. The file dialog will default to open with FNAME."""
|
||||||
config_key = dir_description + "Directory"
|
config_key = dir_description + "Directory"
|
||||||
|
@ -474,7 +538,7 @@ def getSaveFile(parent, title, dir_description, key, ext, fname=None):
|
||||||
return file
|
return file
|
||||||
|
|
||||||
|
|
||||||
def saveGeom(widget, key: str):
|
def saveGeom(widget: QDialog, key: str) -> None:
|
||||||
key += "Geom"
|
key += "Geom"
|
||||||
if isMac and widget.windowState() & Qt.WindowFullScreen:
|
if isMac and widget.windowState() & Qt.WindowFullScreen:
|
||||||
geom = None
|
geom = None
|
||||||
|
@ -483,7 +547,9 @@ def saveGeom(widget, key: str):
|
||||||
aqt.mw.pm.profile[key] = geom
|
aqt.mw.pm.profile[key] = geom
|
||||||
|
|
||||||
|
|
||||||
def restoreGeom(widget, key: str, offset=None, adjustSize=False):
|
def restoreGeom(
|
||||||
|
widget: QWidget, key: str, offset: Optional[int] = None, adjustSize: bool = False
|
||||||
|
) -> None:
|
||||||
key += "Geom"
|
key += "Geom"
|
||||||
if aqt.mw.pm.profile.get(key):
|
if aqt.mw.pm.profile.get(key):
|
||||||
widget.restoreGeometry(aqt.mw.pm.profile[key])
|
widget.restoreGeometry(aqt.mw.pm.profile[key])
|
||||||
|
@ -498,7 +564,7 @@ def restoreGeom(widget, key: str, offset=None, adjustSize=False):
|
||||||
widget.adjustSize()
|
widget.adjustSize()
|
||||||
|
|
||||||
|
|
||||||
def ensureWidgetInScreenBoundaries(widget):
|
def ensureWidgetInScreenBoundaries(widget: QWidget) -> None:
|
||||||
handle = widget.window().windowHandle()
|
handle = widget.window().windowHandle()
|
||||||
if not handle:
|
if not handle:
|
||||||
# window has not yet been shown, retry later
|
# window has not yet been shown, retry later
|
||||||
|
@ -524,58 +590,60 @@ def ensureWidgetInScreenBoundaries(widget):
|
||||||
widget.move(x, y)
|
widget.move(x, y)
|
||||||
|
|
||||||
|
|
||||||
def saveState(widget, key: str):
|
def saveState(widget: QFileDialog, key: str) -> None:
|
||||||
key += "State"
|
key += "State"
|
||||||
aqt.mw.pm.profile[key] = widget.saveState()
|
aqt.mw.pm.profile[key] = widget.saveState()
|
||||||
|
|
||||||
|
|
||||||
def restoreState(widget, key: str):
|
def restoreState(widget: Union[aqt.AnkiQt, QFileDialog], key: str) -> None:
|
||||||
key += "State"
|
key += "State"
|
||||||
if aqt.mw.pm.profile.get(key):
|
if aqt.mw.pm.profile.get(key):
|
||||||
widget.restoreState(aqt.mw.pm.profile[key])
|
widget.restoreState(aqt.mw.pm.profile[key])
|
||||||
|
|
||||||
|
|
||||||
def saveSplitter(widget, key):
|
def saveSplitter(widget: QSplitter, key: str) -> None:
|
||||||
key += "Splitter"
|
key += "Splitter"
|
||||||
aqt.mw.pm.profile[key] = widget.saveState()
|
aqt.mw.pm.profile[key] = widget.saveState()
|
||||||
|
|
||||||
|
|
||||||
def restoreSplitter(widget, key):
|
def restoreSplitter(widget: QSplitter, key: str) -> None:
|
||||||
key += "Splitter"
|
key += "Splitter"
|
||||||
if aqt.mw.pm.profile.get(key):
|
if aqt.mw.pm.profile.get(key):
|
||||||
widget.restoreState(aqt.mw.pm.profile[key])
|
widget.restoreState(aqt.mw.pm.profile[key])
|
||||||
|
|
||||||
|
|
||||||
def saveHeader(widget, key):
|
def saveHeader(widget: QHeaderView, key: str) -> None:
|
||||||
key += "Header"
|
key += "Header"
|
||||||
aqt.mw.pm.profile[key] = widget.saveState()
|
aqt.mw.pm.profile[key] = widget.saveState()
|
||||||
|
|
||||||
|
|
||||||
def restoreHeader(widget, key):
|
def restoreHeader(widget: QHeaderView, key: str) -> None:
|
||||||
key += "Header"
|
key += "Header"
|
||||||
if aqt.mw.pm.profile.get(key):
|
if aqt.mw.pm.profile.get(key):
|
||||||
widget.restoreState(aqt.mw.pm.profile[key])
|
widget.restoreState(aqt.mw.pm.profile[key])
|
||||||
|
|
||||||
|
|
||||||
def save_is_checked(widget, key: str):
|
def save_is_checked(widget: QWidget, key: str) -> None:
|
||||||
key += "IsChecked"
|
key += "IsChecked"
|
||||||
aqt.mw.pm.profile[key] = widget.isChecked()
|
aqt.mw.pm.profile[key] = widget.isChecked()
|
||||||
|
|
||||||
|
|
||||||
def restore_is_checked(widget, key: str):
|
def restore_is_checked(widget: QWidget, key: str) -> None:
|
||||||
key += "IsChecked"
|
key += "IsChecked"
|
||||||
if aqt.mw.pm.profile.get(key) is not None:
|
if aqt.mw.pm.profile.get(key) is not None:
|
||||||
widget.setChecked(aqt.mw.pm.profile[key])
|
widget.setChecked(aqt.mw.pm.profile[key])
|
||||||
|
|
||||||
|
|
||||||
def save_combo_index_for_session(widget: QComboBox, key: str):
|
def save_combo_index_for_session(widget: QComboBox, key: str) -> None:
|
||||||
textKey = key + "ComboActiveText"
|
textKey = key + "ComboActiveText"
|
||||||
indexKey = key + "ComboActiveIndex"
|
indexKey = key + "ComboActiveIndex"
|
||||||
aqt.mw.pm.session[textKey] = widget.currentText()
|
aqt.mw.pm.session[textKey] = widget.currentText()
|
||||||
aqt.mw.pm.session[indexKey] = widget.currentIndex()
|
aqt.mw.pm.session[indexKey] = widget.currentIndex()
|
||||||
|
|
||||||
|
|
||||||
def restore_combo_index_for_session(widget: QComboBox, history: List[str], key: str):
|
def restore_combo_index_for_session(
|
||||||
|
widget: QComboBox, history: List[str], key: str
|
||||||
|
) -> None:
|
||||||
textKey = key + "ComboActiveText"
|
textKey = key + "ComboActiveText"
|
||||||
indexKey = key + "ComboActiveIndex"
|
indexKey = key + "ComboActiveIndex"
|
||||||
text = aqt.mw.pm.session.get(textKey)
|
text = aqt.mw.pm.session.get(textKey)
|
||||||
|
@ -585,7 +653,7 @@ def restore_combo_index_for_session(widget: QComboBox, history: List[str], key:
|
||||||
widget.setCurrentIndex(index)
|
widget.setCurrentIndex(index)
|
||||||
|
|
||||||
|
|
||||||
def save_combo_history(comboBox: QComboBox, history: List[str], name: str):
|
def save_combo_history(comboBox: QComboBox, history: List[str], name: str) -> str:
|
||||||
name += "BoxHistory"
|
name += "BoxHistory"
|
||||||
text_input = comboBox.lineEdit().text()
|
text_input = comboBox.lineEdit().text()
|
||||||
if text_input in history:
|
if text_input in history:
|
||||||
|
@ -599,7 +667,7 @@ def save_combo_history(comboBox: QComboBox, history: List[str], name: str):
|
||||||
return text_input
|
return text_input
|
||||||
|
|
||||||
|
|
||||||
def restore_combo_history(comboBox: QComboBox, name: str):
|
def restore_combo_history(comboBox: QComboBox, name: str) -> List[str]:
|
||||||
name += "BoxHistory"
|
name += "BoxHistory"
|
||||||
history = aqt.mw.pm.profile.get(name, [])
|
history = aqt.mw.pm.profile.get(name, [])
|
||||||
comboBox.addItems([""] + history)
|
comboBox.addItems([""] + history)
|
||||||
|
@ -611,13 +679,13 @@ def restore_combo_history(comboBox: QComboBox, name: str):
|
||||||
return history
|
return history
|
||||||
|
|
||||||
|
|
||||||
def mungeQA(col, txt):
|
def mungeQA(col: Collection, txt: str) -> str:
|
||||||
print("mungeQA() deprecated; use mw.prepare_card_text_for_display()")
|
print("mungeQA() deprecated; use mw.prepare_card_text_for_display()")
|
||||||
txt = col.media.escape_media_filenames(txt)
|
txt = col.media.escape_media_filenames(txt)
|
||||||
return txt
|
return txt
|
||||||
|
|
||||||
|
|
||||||
def openFolder(path):
|
def openFolder(path: str) -> None:
|
||||||
if isWin:
|
if isWin:
|
||||||
subprocess.Popen(["explorer", "file://" + path])
|
subprocess.Popen(["explorer", "file://" + path])
|
||||||
else:
|
else:
|
||||||
|
@ -625,27 +693,27 @@ def openFolder(path):
|
||||||
QDesktopServices.openUrl(QUrl("file://" + path))
|
QDesktopServices.openUrl(QUrl("file://" + path))
|
||||||
|
|
||||||
|
|
||||||
def shortcut(key):
|
def shortcut(key: str) -> str:
|
||||||
if isMac:
|
if isMac:
|
||||||
return re.sub("(?i)ctrl", "Command", key)
|
return re.sub("(?i)ctrl", "Command", key)
|
||||||
return key
|
return key
|
||||||
|
|
||||||
|
|
||||||
def maybeHideClose(bbox):
|
def maybeHideClose(bbox: QDialogButtonBox) -> None:
|
||||||
if isMac:
|
if isMac:
|
||||||
b = bbox.button(QDialogButtonBox.Close)
|
b = bbox.button(QDialogButtonBox.Close)
|
||||||
if b:
|
if b:
|
||||||
bbox.removeButton(b)
|
bbox.removeButton(b)
|
||||||
|
|
||||||
|
|
||||||
def addCloseShortcut(widg):
|
def addCloseShortcut(widg: QDialog) -> None:
|
||||||
if not isMac:
|
if not isMac:
|
||||||
return
|
return
|
||||||
widg._closeShortcut = QShortcut(QKeySequence("Ctrl+W"), widg)
|
widg._closeShortcut = QShortcut(QKeySequence("Ctrl+W"), widg)
|
||||||
qconnect(widg._closeShortcut.activated, widg.reject)
|
qconnect(widg._closeShortcut.activated, widg.reject)
|
||||||
|
|
||||||
|
|
||||||
def downArrow():
|
def downArrow() -> str:
|
||||||
if isWin:
|
if isWin:
|
||||||
return "▼"
|
return "▼"
|
||||||
# windows 10 is lacking the smaller arrow on English installs
|
# windows 10 is lacking the smaller arrow on English installs
|
||||||
|
@ -659,13 +727,19 @@ _tooltipTimer: Optional[QTimer] = None
|
||||||
_tooltipLabel: Optional[QLabel] = None
|
_tooltipLabel: Optional[QLabel] = None
|
||||||
|
|
||||||
|
|
||||||
def tooltip(msg, period=3000, parent=None, x_offset=0, y_offset=100):
|
def tooltip(
|
||||||
|
msg: str,
|
||||||
|
period: int = 3000,
|
||||||
|
parent: Optional[aqt.AnkiQt] = None,
|
||||||
|
x_offset: int = 0,
|
||||||
|
y_offset: int = 100,
|
||||||
|
) -> None:
|
||||||
global _tooltipTimer, _tooltipLabel
|
global _tooltipTimer, _tooltipLabel
|
||||||
|
|
||||||
class CustomLabel(QLabel):
|
class CustomLabel(QLabel):
|
||||||
silentlyClose = True
|
silentlyClose = True
|
||||||
|
|
||||||
def mousePressEvent(self, evt):
|
def mousePressEvent(self, evt: QMouseEvent) -> None:
|
||||||
evt.accept()
|
evt.accept()
|
||||||
self.hide()
|
self.hide()
|
||||||
|
|
||||||
|
@ -697,7 +771,7 @@ def tooltip(msg, period=3000, parent=None, x_offset=0, y_offset=100):
|
||||||
_tooltipLabel = lab
|
_tooltipLabel = lab
|
||||||
|
|
||||||
|
|
||||||
def closeTooltip():
|
def closeTooltip() -> None:
|
||||||
global _tooltipLabel, _tooltipTimer
|
global _tooltipLabel, _tooltipTimer
|
||||||
if _tooltipLabel:
|
if _tooltipLabel:
|
||||||
try:
|
try:
|
||||||
|
@ -712,7 +786,7 @@ def closeTooltip():
|
||||||
|
|
||||||
|
|
||||||
# true if invalid; print warning
|
# true if invalid; print warning
|
||||||
def checkInvalidFilename(str, dirsep=True):
|
def checkInvalidFilename(str: str, dirsep: bool = True) -> bool:
|
||||||
bad = invalidFilename(str, dirsep)
|
bad = invalidFilename(str, dirsep)
|
||||||
if bad:
|
if bad:
|
||||||
showWarning(tr(TR.QT_MISC_THE_FOLLOWING_CHARACTER_CAN_NOT_BE, val=bad))
|
showWarning(tr(TR.QT_MISC_THE_FOLLOWING_CHARACTER_CAN_NOT_BE, val=bad))
|
||||||
|
@ -723,28 +797,30 @@ def checkInvalidFilename(str, dirsep=True):
|
||||||
# Menus
|
# Menus
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
|
MenuListChild = Union["SubMenu", QAction, "MenuItem", "MenuList"]
|
||||||
|
|
||||||
|
|
||||||
class MenuList:
|
class MenuList:
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self.children = []
|
self.children: List[MenuListChild] = []
|
||||||
|
|
||||||
def addItem(self, title, func):
|
def addItem(self, title: str, func: Callable) -> MenuItem:
|
||||||
item = MenuItem(title, func)
|
item = MenuItem(title, func)
|
||||||
self.children.append(item)
|
self.children.append(item)
|
||||||
return item
|
return item
|
||||||
|
|
||||||
def addSeparator(self):
|
def addSeparator(self) -> None:
|
||||||
self.children.append(None)
|
self.children.append(None)
|
||||||
|
|
||||||
def addMenu(self, title):
|
def addMenu(self, title: str) -> SubMenu:
|
||||||
submenu = SubMenu(title)
|
submenu = SubMenu(title)
|
||||||
self.children.append(submenu)
|
self.children.append(submenu)
|
||||||
return submenu
|
return submenu
|
||||||
|
|
||||||
def addChild(self, child):
|
def addChild(self, child: Union[SubMenu, QAction, MenuList]) -> None:
|
||||||
self.children.append(child)
|
self.children.append(child)
|
||||||
|
|
||||||
def renderTo(self, qmenu):
|
def renderTo(self, qmenu: QMenu) -> None:
|
||||||
for child in self.children:
|
for child in self.children:
|
||||||
if child is None:
|
if child is None:
|
||||||
qmenu.addSeparator()
|
qmenu.addSeparator()
|
||||||
|
@ -753,33 +829,33 @@ class MenuList:
|
||||||
else:
|
else:
|
||||||
child.renderTo(qmenu)
|
child.renderTo(qmenu)
|
||||||
|
|
||||||
def popupOver(self, widget):
|
def popupOver(self, widget: QPushButton) -> None:
|
||||||
qmenu = QMenu()
|
qmenu = QMenu()
|
||||||
self.renderTo(qmenu)
|
self.renderTo(qmenu)
|
||||||
qmenu.exec_(widget.mapToGlobal(QPoint(0, 0)))
|
qmenu.exec_(widget.mapToGlobal(QPoint(0, 0)))
|
||||||
|
|
||||||
|
|
||||||
class SubMenu(MenuList):
|
class SubMenu(MenuList):
|
||||||
def __init__(self, title):
|
def __init__(self, title: str) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.title = title
|
self.title = title
|
||||||
|
|
||||||
def renderTo(self, menu):
|
def renderTo(self, menu: QMenu) -> None:
|
||||||
submenu = menu.addMenu(self.title)
|
submenu = menu.addMenu(self.title)
|
||||||
super().renderTo(submenu)
|
super().renderTo(submenu)
|
||||||
|
|
||||||
|
|
||||||
class MenuItem:
|
class MenuItem:
|
||||||
def __init__(self, title, func):
|
def __init__(self, title: str, func: Callable) -> None:
|
||||||
self.title = title
|
self.title = title
|
||||||
self.func = func
|
self.func = func
|
||||||
|
|
||||||
def renderTo(self, qmenu):
|
def renderTo(self, qmenu: QMenu) -> None:
|
||||||
a = qmenu.addAction(self.title)
|
a = qmenu.addAction(self.title)
|
||||||
qconnect(a.triggered, self.func)
|
qconnect(a.triggered, self.func)
|
||||||
|
|
||||||
|
|
||||||
def qtMenuShortcutWorkaround(qmenu):
|
def qtMenuShortcutWorkaround(qmenu: QMenu) -> None:
|
||||||
if qtminor < 10:
|
if qtminor < 10:
|
||||||
return
|
return
|
||||||
for act in qmenu.actions():
|
for act in qmenu.actions():
|
||||||
|
@ -789,7 +865,7 @@ def qtMenuShortcutWorkaround(qmenu):
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
|
|
||||||
def supportText():
|
def supportText() -> str:
|
||||||
import platform
|
import platform
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
@ -802,9 +878,9 @@ def supportText():
|
||||||
else:
|
else:
|
||||||
platname = "Linux"
|
platname = "Linux"
|
||||||
|
|
||||||
def schedVer():
|
def schedVer() -> str:
|
||||||
try:
|
try:
|
||||||
return mw.col.schedVer()
|
return str(mw.col.schedVer())
|
||||||
except:
|
except:
|
||||||
return "?"
|
return "?"
|
||||||
|
|
||||||
|
@ -832,7 +908,7 @@ Add-ons, last update check: {}
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
# adapted from version detection in qutebrowser
|
# adapted from version detection in qutebrowser
|
||||||
def opengl_vendor():
|
def opengl_vendor() -> Optional[str]:
|
||||||
old_context = QOpenGLContext.currentContext()
|
old_context = QOpenGLContext.currentContext()
|
||||||
old_surface = None if old_context is None else old_context.surface()
|
old_surface = None if old_context is None else old_context.surface()
|
||||||
|
|
||||||
|
@ -871,7 +947,7 @@ def opengl_vendor():
|
||||||
old_context.makeCurrent(old_surface)
|
old_context.makeCurrent(old_surface)
|
||||||
|
|
||||||
|
|
||||||
def gfxDriverIsBroken():
|
def gfxDriverIsBroken() -> bool:
|
||||||
driver = opengl_vendor()
|
driver = opengl_vendor()
|
||||||
return driver == "nouveau"
|
return driver == "nouveau"
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ serverbaseurl = re.compile(r"^.+:\/\/[^\/]+")
|
||||||
|
|
||||||
|
|
||||||
class AnkiWebPage(QWebEnginePage):
|
class AnkiWebPage(QWebEnginePage):
|
||||||
def __init__(self, onBridgeCmd):
|
def __init__(self, onBridgeCmd) -> None:
|
||||||
QWebEnginePage.__init__(self)
|
QWebEnginePage.__init__(self)
|
||||||
self._onBridgeCmd = onBridgeCmd
|
self._onBridgeCmd = onBridgeCmd
|
||||||
self._setupBridge()
|
self._setupBridge()
|
||||||
|
@ -31,7 +31,7 @@ class AnkiWebPage(QWebEnginePage):
|
||||||
def _setupBridge(self) -> None:
|
def _setupBridge(self) -> None:
|
||||||
class Bridge(QObject):
|
class Bridge(QObject):
|
||||||
@pyqtSlot(str, result=str) # type: ignore
|
@pyqtSlot(str, result=str) # type: ignore
|
||||||
def cmd(self, str):
|
def cmd(self, str) -> Any:
|
||||||
return json.dumps(self.onCmd(str))
|
return json.dumps(self.onCmd(str))
|
||||||
|
|
||||||
self._bridge = Bridge()
|
self._bridge = Bridge()
|
||||||
|
@ -74,7 +74,7 @@ class AnkiWebPage(QWebEnginePage):
|
||||||
script.setRunsOnSubFrames(False)
|
script.setRunsOnSubFrames(False)
|
||||||
self.profile().scripts().insert(script)
|
self.profile().scripts().insert(script)
|
||||||
|
|
||||||
def javaScriptConsoleMessage(self, level, msg, line, srcID):
|
def javaScriptConsoleMessage(self, level, msg, line, srcID) -> None:
|
||||||
# not translated because console usually not visible,
|
# not translated because console usually not visible,
|
||||||
# and may only accept ascii text
|
# and may only accept ascii text
|
||||||
if srcID.startswith("data"):
|
if srcID.startswith("data"):
|
||||||
|
@ -101,7 +101,7 @@ class AnkiWebPage(QWebEnginePage):
|
||||||
# https://github.com/ankitects/anki/pull/560
|
# https://github.com/ankitects/anki/pull/560
|
||||||
sys.stdout.write(buf)
|
sys.stdout.write(buf)
|
||||||
|
|
||||||
def acceptNavigationRequest(self, url, navType, isMainFrame):
|
def acceptNavigationRequest(self, url, navType, isMainFrame) -> bool:
|
||||||
if not self.open_links_externally:
|
if not self.open_links_externally:
|
||||||
return super().acceptNavigationRequest(url, navType, isMainFrame)
|
return super().acceptNavigationRequest(url, navType, isMainFrame)
|
||||||
|
|
||||||
|
@ -120,10 +120,10 @@ class AnkiWebPage(QWebEnginePage):
|
||||||
openLink(url)
|
openLink(url)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _onCmd(self, str):
|
def _onCmd(self, str) -> None:
|
||||||
return self._onBridgeCmd(str)
|
return self._onBridgeCmd(str)
|
||||||
|
|
||||||
def javaScriptAlert(self, url: QUrl, text: str):
|
def javaScriptAlert(self, url: QUrl, text: str) -> None:
|
||||||
showInfo(text)
|
showInfo(text)
|
||||||
|
|
||||||
|
|
||||||
|
@ -150,7 +150,7 @@ class WebContent:
|
||||||
You should avoid overwriting or interfering with existing data as much
|
You should avoid overwriting or interfering with existing data as much
|
||||||
as possible, instead opting to append your own changes, e.g.:
|
as possible, instead opting to append your own changes, e.g.:
|
||||||
|
|
||||||
def on_webview_will_set_content(web_content: WebContent, context):
|
def on_webview_will_set_content(web_content: WebContent, context) -> None:
|
||||||
web_content.body += "<my_html>"
|
web_content.body += "<my_html>"
|
||||||
web_content.head += "<my_head>"
|
web_content.head += "<my_head>"
|
||||||
|
|
||||||
|
@ -173,7 +173,7 @@ class WebContent:
|
||||||
Then append the subpaths to the corresponding web_content fields
|
Then append the subpaths to the corresponding web_content fields
|
||||||
within a function subscribing to gui_hooks.webview_will_set_content:
|
within a function subscribing to gui_hooks.webview_will_set_content:
|
||||||
|
|
||||||
def on_webview_will_set_content(web_content: WebContent, context):
|
def on_webview_will_set_content(web_content: WebContent, context) -> None:
|
||||||
addon_package = mw.addonManager.addonFromModule(__name__)
|
addon_package = mw.addonManager.addonFromModule(__name__)
|
||||||
web_content.css.append(
|
web_content.css.append(
|
||||||
f"/_addons/{addon_package}/web/my-addon.css")
|
f"/_addons/{addon_package}/web/my-addon.css")
|
||||||
|
@ -251,7 +251,7 @@ class AnkiWebView(QWebEngineView):
|
||||||
def set_open_links_externally(self, enable: bool) -> None:
|
def set_open_links_externally(self, enable: bool) -> None:
|
||||||
self._page.open_links_externally = enable
|
self._page.open_links_externally = enable
|
||||||
|
|
||||||
def onEsc(self):
|
def onEsc(self) -> None:
|
||||||
w = self.parent()
|
w = self.parent()
|
||||||
while w:
|
while w:
|
||||||
if isinstance(w, QDialog) or isinstance(w, QMainWindow):
|
if isinstance(w, QDialog) or isinstance(w, QMainWindow):
|
||||||
|
@ -266,7 +266,7 @@ class AnkiWebView(QWebEngineView):
|
||||||
break
|
break
|
||||||
w = w.parent()
|
w = w.parent()
|
||||||
|
|
||||||
def onCopy(self):
|
def onCopy(self) -> None:
|
||||||
if not self.selectedText():
|
if not self.selectedText():
|
||||||
ctx = self._page.contextMenuData()
|
ctx = self._page.contextMenuData()
|
||||||
if ctx and ctx.mediaType() == QWebEngineContextMenuData.MediaTypeImage:
|
if ctx and ctx.mediaType() == QWebEngineContextMenuData.MediaTypeImage:
|
||||||
|
@ -274,16 +274,16 @@ class AnkiWebView(QWebEngineView):
|
||||||
else:
|
else:
|
||||||
self.triggerPageAction(QWebEnginePage.Copy)
|
self.triggerPageAction(QWebEnginePage.Copy)
|
||||||
|
|
||||||
def onCut(self):
|
def onCut(self) -> None:
|
||||||
self.triggerPageAction(QWebEnginePage.Cut)
|
self.triggerPageAction(QWebEnginePage.Cut)
|
||||||
|
|
||||||
def onPaste(self):
|
def onPaste(self) -> None:
|
||||||
self.triggerPageAction(QWebEnginePage.Paste)
|
self.triggerPageAction(QWebEnginePage.Paste)
|
||||||
|
|
||||||
def onMiddleClickPaste(self):
|
def onMiddleClickPaste(self) -> None:
|
||||||
self.triggerPageAction(QWebEnginePage.Paste)
|
self.triggerPageAction(QWebEnginePage.Paste)
|
||||||
|
|
||||||
def onSelectAll(self):
|
def onSelectAll(self) -> None:
|
||||||
self.triggerPageAction(QWebEnginePage.SelectAll)
|
self.triggerPageAction(QWebEnginePage.SelectAll)
|
||||||
|
|
||||||
def contextMenuEvent(self, evt: QContextMenuEvent) -> None:
|
def contextMenuEvent(self, evt: QContextMenuEvent) -> None:
|
||||||
|
@ -293,7 +293,7 @@ class AnkiWebView(QWebEngineView):
|
||||||
gui_hooks.webview_will_show_context_menu(self, m)
|
gui_hooks.webview_will_show_context_menu(self, m)
|
||||||
m.popup(QCursor.pos())
|
m.popup(QCursor.pos())
|
||||||
|
|
||||||
def dropEvent(self, evt):
|
def dropEvent(self, evt) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def setHtml(self, html: str) -> None: # type: ignore
|
def setHtml(self, html: str) -> None: # type: ignore
|
||||||
|
@ -312,7 +312,7 @@ class AnkiWebView(QWebEngineView):
|
||||||
if oldFocus:
|
if oldFocus:
|
||||||
oldFocus.setFocus()
|
oldFocus.setFocus()
|
||||||
|
|
||||||
def load(self, url: QUrl):
|
def load(self, url: QUrl) -> None:
|
||||||
# allow queuing actions when loading url directly
|
# allow queuing actions when loading url directly
|
||||||
self._domDone = False
|
self._domDone = False
|
||||||
super().load(url)
|
super().load(url)
|
||||||
|
@ -364,7 +364,7 @@ class AnkiWebView(QWebEngineView):
|
||||||
else:
|
else:
|
||||||
return 3
|
return 3
|
||||||
|
|
||||||
def _getWindowColor(self):
|
def _getWindowColor(self) -> QColor:
|
||||||
if theme_manager.night_mode:
|
if theme_manager.night_mode:
|
||||||
return theme_manager.qcolor("window-bg")
|
return theme_manager.qcolor("window-bg")
|
||||||
if isMac:
|
if isMac:
|
||||||
|
@ -508,7 +508,7 @@ body {{ zoom: {zoom}; background: {background}; direction: {lang_dir}; {font} }}
|
||||||
def _evalWithCallback(self, js: str, cb: Callable[[Any], Any]) -> None:
|
def _evalWithCallback(self, js: str, cb: Callable[[Any], Any]) -> None:
|
||||||
if cb:
|
if cb:
|
||||||
|
|
||||||
def handler(val):
|
def handler(val) -> None:
|
||||||
if self._shouldIgnoreWebEvent():
|
if self._shouldIgnoreWebEvent():
|
||||||
print("ignored late js callback", cb)
|
print("ignored late js callback", cb)
|
||||||
return
|
return
|
||||||
|
@ -597,18 +597,18 @@ body {{ zoom: {zoom}; background: {background}; direction: {lang_dir}; {font} }}
|
||||||
self.onBridgeCmd = func
|
self.onBridgeCmd = func
|
||||||
self._bridge_context = context
|
self._bridge_context = context
|
||||||
|
|
||||||
def hide_while_preserving_layout(self):
|
def hide_while_preserving_layout(self) -> None:
|
||||||
"Hide but keep existing size."
|
"Hide but keep existing size."
|
||||||
sp = self.sizePolicy()
|
sp = self.sizePolicy()
|
||||||
sp.setRetainSizeWhenHidden(True)
|
sp.setRetainSizeWhenHidden(True)
|
||||||
self.setSizePolicy(sp)
|
self.setSizePolicy(sp)
|
||||||
self.hide()
|
self.hide()
|
||||||
|
|
||||||
def inject_dynamic_style_and_show(self):
|
def inject_dynamic_style_and_show(self) -> None:
|
||||||
"Add dynamic styling, and reveal."
|
"Add dynamic styling, and reveal."
|
||||||
css = self.standard_css()
|
css = self.standard_css()
|
||||||
|
|
||||||
def after_style(arg):
|
def after_style(arg) -> None:
|
||||||
gui_hooks.webview_did_inject_style_into_page(self)
|
gui_hooks.webview_did_inject_style_into_page(self)
|
||||||
self.show()
|
self.show()
|
||||||
|
|
||||||
|
|
13
qt/dmypy-watch.sh
Executable file
13
qt/dmypy-watch.sh
Executable file
|
@ -0,0 +1,13 @@
|
||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# semi-working support for mypy daemon
|
||||||
|
# - install fs_watch
|
||||||
|
# - build anki/aqt wheels first
|
||||||
|
# - create a new venv and activate it
|
||||||
|
# - install the wheels
|
||||||
|
# - then run this script from this folder
|
||||||
|
|
||||||
|
(sleep 1 && touch aqt)
|
||||||
|
. ~/pyenv/bin/activate
|
||||||
|
fswatch -o aqt | xargs -n1 -I{} sh -c 'printf \\033c\\n; dmypy run aqt'
|
||||||
|
|
11
qt/mypy.ini
11
qt/mypy.ini
|
@ -1,6 +1,5 @@
|
||||||
[mypy]
|
[mypy]
|
||||||
python_version = 3.8
|
python_version = 3.8
|
||||||
pretty = true
|
|
||||||
no_strict_optional = true
|
no_strict_optional = true
|
||||||
show_error_codes = true
|
show_error_codes = true
|
||||||
disallow_untyped_decorators = True
|
disallow_untyped_decorators = True
|
||||||
|
@ -9,6 +8,16 @@ warn_unused_configs = True
|
||||||
check_untyped_defs = true
|
check_untyped_defs = true
|
||||||
strict_equality = true
|
strict_equality = true
|
||||||
|
|
||||||
|
[mypy-aqt.browser]
|
||||||
|
disallow_untyped_defs=true
|
||||||
|
[mypy-aqt.sidebar]
|
||||||
|
disallow_untyped_defs=true
|
||||||
|
[mypy-aqt.editor]
|
||||||
|
disallow_untyped_defs=true
|
||||||
|
[mypy-aqt.utils]
|
||||||
|
disallow_untyped_defs=true
|
||||||
|
|
||||||
|
|
||||||
[mypy-aqt.mpv]
|
[mypy-aqt.mpv]
|
||||||
ignore_errors=true
|
ignore_errors=true
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
load("@rules_proto//proto:defs.bzl", "proto_library")
|
load("@rules_proto//proto:defs.bzl", "proto_library")
|
||||||
load("@io_bazel_rules_rust//rust:rust.bzl", "rust_binary", "rust_library", "rust_test")
|
load("@io_bazel_rules_rust//rust:rust.bzl", "rust_binary", "rust_library", "rust_test")
|
||||||
load("@io_bazel_rules_rust//cargo:cargo_build_script.bzl", "cargo_build_script")
|
load("@io_bazel_rules_rust//cargo:cargo_build_script.bzl", "cargo_build_script")
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
syntax = "proto3";
|
syntax = "proto3";
|
||||||
|
|
||||||
package BackendProto;
|
package BackendProto;
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
pub mod mergeftl;
|
pub mod mergeftl;
|
||||||
pub mod protobuf;
|
pub mod protobuf;
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
use fluent_syntax::ast::Entry;
|
use fluent_syntax::ast::Entry;
|
||||||
use fluent_syntax::parser::Parser;
|
use fluent_syntax::parser::Parser;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::{env, fmt::Write};
|
use std::{env, fmt::Write};
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
include!("mergeftl.rs");
|
include!("mergeftl.rs");
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@generated
|
@generated
|
||||||
cargo-raze generated Bazel file.
|
cargo-raze generated Bazel file.
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Exposes a clang-format binary for formatting protobuf.
|
Exposes a clang-format binary for formatting protobuf.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
import sys, subprocess, os, difflib
|
import sys, subprocess, os, difflib
|
||||||
|
|
||||||
clang_format = sys.argv[1]
|
clang_format = sys.argv[1]
|
||||||
|
@ -33,4 +36,4 @@ for path in sys.argv[2:]:
|
||||||
found_bad = True
|
found_bad = True
|
||||||
|
|
||||||
if found_bad:
|
if found_bad:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
# Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
def _rustfmt_impl(ctx):
|
def _rustfmt_impl(ctx):
|
||||||
toolchain = ctx.toolchains["@io_bazel_rules_rust//rust:toolchain"]
|
toolchain = ctx.toolchains["@io_bazel_rules_rust//rust:toolchain"]
|
||||||
script_name = ctx.label.name + "_script"
|
script_name = ctx.label.name + "_script"
|
||||||
|
|
|
@ -1 +1,4 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/backend_proto.rs"));
|
include!(concat!(env!("OUT_DIR"), "/backend_proto.rs"));
|
||||||
|
|
|
@ -1 +1,4 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/fluent_proto.rs"));
|
include!(concat!(env!("OUT_DIR"), "/fluent_proto.rs"));
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
mod cards;
|
mod cards;
|
||||||
mod notes;
|
mod notes;
|
||||||
mod parser;
|
mod parser;
|
||||||
|
|
|
@ -18,9 +18,9 @@ sql_format_setup()
|
||||||
|
|
||||||
exports_files([
|
exports_files([
|
||||||
"tsconfig.json",
|
"tsconfig.json",
|
||||||
"d3_missing.d.ts",
|
|
||||||
".prettierrc",
|
".prettierrc",
|
||||||
"rollup.config.js",
|
"rollup.config.js",
|
||||||
|
"rollup.aqt.config.js",
|
||||||
".eslintrc.js",
|
".eslintrc.js",
|
||||||
"licenses.json",
|
"licenses.json",
|
||||||
"sql_format.ts",
|
"sql_format.ts",
|
||||||
|
|
62
ts/editor/BUILD.bazel
Normal file
62
ts/editor/BUILD.bazel
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
load("@npm//@bazel/typescript:index.bzl", "ts_library")
|
||||||
|
load("@npm//@bazel/rollup:index.bzl", "rollup_bundle")
|
||||||
|
load("//ts:prettier.bzl", "prettier_test")
|
||||||
|
load("//ts:eslint.bzl", "eslint_test")
|
||||||
|
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary")
|
||||||
|
|
||||||
|
sass_binary(
|
||||||
|
name = "editor_css",
|
||||||
|
src = "editor.scss",
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
|
|
||||||
|
sass_binary(
|
||||||
|
name = "editable_css",
|
||||||
|
src = "editable.scss",
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
|
|
||||||
|
ts_library(
|
||||||
|
name = "editor_ts",
|
||||||
|
srcs = glob(["*.ts"]),
|
||||||
|
tsconfig = "//qt/aqt/data/web/js:tsconfig.json",
|
||||||
|
deps = [
|
||||||
|
"@npm//@types/jquery",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
rollup_bundle(
|
||||||
|
name = "editor",
|
||||||
|
config_file = "//ts:rollup.aqt.config.js",
|
||||||
|
entry_point = "index.ts",
|
||||||
|
format = "iife",
|
||||||
|
link_workspace_root = True,
|
||||||
|
silent = True,
|
||||||
|
sourcemap = "false",
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
deps = [
|
||||||
|
"editor_ts",
|
||||||
|
"@npm//@rollup/plugin-commonjs",
|
||||||
|
"@npm//@rollup/plugin-node-resolve",
|
||||||
|
"@npm//rollup-plugin-terser",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
################
|
||||||
|
|
||||||
|
prettier_test(
|
||||||
|
name = "format_check",
|
||||||
|
srcs = glob([
|
||||||
|
"*.ts",
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
|
||||||
|
# eslint_test(
|
||||||
|
# name = "eslint",
|
||||||
|
# srcs = glob(
|
||||||
|
# [
|
||||||
|
# "*.ts",
|
||||||
|
# ],
|
||||||
|
# ),
|
||||||
|
# )
|
170
ts/editor/filterHtml.ts
Normal file
170
ts/editor/filterHtml.ts
Normal file
|
@ -0,0 +1,170 @@
|
||||||
|
import { nodeIsElement } from "./helpers";
|
||||||
|
|
||||||
|
export let filterHTML = function (
|
||||||
|
html: string,
|
||||||
|
internal: boolean,
|
||||||
|
extendedMode: boolean
|
||||||
|
): string {
|
||||||
|
// wrap it in <top> as we aren't allowed to change top level elements
|
||||||
|
const top = document.createElement("ankitop");
|
||||||
|
top.innerHTML = html;
|
||||||
|
|
||||||
|
if (internal) {
|
||||||
|
filterInternalNode(top);
|
||||||
|
} else {
|
||||||
|
filterNode(top, extendedMode);
|
||||||
|
}
|
||||||
|
let outHtml = top.innerHTML;
|
||||||
|
if (!extendedMode && !internal) {
|
||||||
|
// collapse whitespace
|
||||||
|
outHtml = outHtml.replace(/[\n\t ]+/g, " ");
|
||||||
|
}
|
||||||
|
outHtml = outHtml.trim();
|
||||||
|
return outHtml;
|
||||||
|
};
|
||||||
|
|
||||||
|
let allowedTagsBasic = {};
|
||||||
|
let allowedTagsExtended = {};
|
||||||
|
|
||||||
|
let TAGS_WITHOUT_ATTRS = ["P", "DIV", "BR", "SUB", "SUP"];
|
||||||
|
for (const tag of TAGS_WITHOUT_ATTRS) {
|
||||||
|
allowedTagsBasic[tag] = { attrs: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
TAGS_WITHOUT_ATTRS = [
|
||||||
|
"B",
|
||||||
|
"BLOCKQUOTE",
|
||||||
|
"CODE",
|
||||||
|
"DD",
|
||||||
|
"DL",
|
||||||
|
"DT",
|
||||||
|
"EM",
|
||||||
|
"H1",
|
||||||
|
"H2",
|
||||||
|
"H3",
|
||||||
|
"I",
|
||||||
|
"LI",
|
||||||
|
"OL",
|
||||||
|
"PRE",
|
||||||
|
"RP",
|
||||||
|
"RT",
|
||||||
|
"RUBY",
|
||||||
|
"STRONG",
|
||||||
|
"TABLE",
|
||||||
|
"U",
|
||||||
|
"UL",
|
||||||
|
];
|
||||||
|
for (const tag of TAGS_WITHOUT_ATTRS) {
|
||||||
|
allowedTagsExtended[tag] = { attrs: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
allowedTagsBasic["IMG"] = { attrs: ["SRC"] };
|
||||||
|
|
||||||
|
allowedTagsExtended["A"] = { attrs: ["HREF"] };
|
||||||
|
allowedTagsExtended["TR"] = { attrs: ["ROWSPAN"] };
|
||||||
|
allowedTagsExtended["TD"] = { attrs: ["COLSPAN", "ROWSPAN"] };
|
||||||
|
allowedTagsExtended["TH"] = { attrs: ["COLSPAN", "ROWSPAN"] };
|
||||||
|
allowedTagsExtended["FONT"] = { attrs: ["COLOR"] };
|
||||||
|
|
||||||
|
const allowedStyling = {
|
||||||
|
color: true,
|
||||||
|
"background-color": true,
|
||||||
|
"font-weight": true,
|
||||||
|
"font-style": true,
|
||||||
|
"text-decoration-line": true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let isNightMode = function (): boolean {
|
||||||
|
return document.body.classList.contains("nightMode");
|
||||||
|
};
|
||||||
|
|
||||||
|
let filterExternalSpan = function (elem: HTMLElement) {
|
||||||
|
// filter out attributes
|
||||||
|
for (const attr of [...elem.attributes]) {
|
||||||
|
const attrName = attr.name.toUpperCase();
|
||||||
|
|
||||||
|
if (attrName !== "STYLE") {
|
||||||
|
elem.removeAttributeNode(attr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter styling
|
||||||
|
for (const name of [...elem.style]) {
|
||||||
|
const value = elem.style.getPropertyValue(name);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!allowedStyling.hasOwnProperty(name) ||
|
||||||
|
// google docs adds this unnecessarily
|
||||||
|
(name === "background-color" && value === "transparent") ||
|
||||||
|
// ignore coloured text in night mode for now
|
||||||
|
(isNightMode() && (name === "background-color" || name === "color"))
|
||||||
|
) {
|
||||||
|
elem.style.removeProperty(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
allowedTagsExtended["SPAN"] = filterExternalSpan;
|
||||||
|
|
||||||
|
// add basic tags to extended
|
||||||
|
Object.assign(allowedTagsExtended, allowedTagsBasic);
|
||||||
|
|
||||||
|
function isHTMLElement(elem: Element): elem is HTMLElement {
|
||||||
|
return elem instanceof HTMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
// filtering from another field
|
||||||
|
let filterInternalNode = function (elem: Element) {
|
||||||
|
if (isHTMLElement(elem)) {
|
||||||
|
elem.style.removeProperty("background-color");
|
||||||
|
elem.style.removeProperty("font-size");
|
||||||
|
elem.style.removeProperty("font-family");
|
||||||
|
}
|
||||||
|
// recurse
|
||||||
|
for (let i = 0; i < elem.children.length; i++) {
|
||||||
|
const child = elem.children[i];
|
||||||
|
filterInternalNode(child);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// filtering from external sources
|
||||||
|
let filterNode = function (node: Node, extendedMode: boolean): void {
|
||||||
|
if (!nodeIsElement(node)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// descend first, and take a copy of the child nodes as the loop will skip
|
||||||
|
// elements due to node modifications otherwise
|
||||||
|
for (const child of [...node.children]) {
|
||||||
|
filterNode(child, extendedMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.tagName === "ANKITOP") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tag = extendedMode
|
||||||
|
? allowedTagsExtended[node.tagName]
|
||||||
|
: allowedTagsBasic[node.tagName];
|
||||||
|
|
||||||
|
if (!tag) {
|
||||||
|
if (!node.innerHTML || node.tagName === "TITLE") {
|
||||||
|
node.parentNode.removeChild(node);
|
||||||
|
} else {
|
||||||
|
node.outerHTML = node.innerHTML;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (typeof tag === "function") {
|
||||||
|
// filtering function provided
|
||||||
|
tag(node);
|
||||||
|
} else {
|
||||||
|
// allowed, filter out attributes
|
||||||
|
for (const attr of [...node.attributes]) {
|
||||||
|
const attrName = attr.name.toUpperCase();
|
||||||
|
if (tag.attrs.indexOf(attrName) === -1) {
|
||||||
|
node.removeAttributeNode(attr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
65
ts/editor/helpers.ts
Normal file
65
ts/editor/helpers.ts
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
export function nodeIsElement(node: Node): node is Element {
|
||||||
|
return node.nodeType === Node.ELEMENT_NODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
const INLINE_TAGS = [
|
||||||
|
"A",
|
||||||
|
"ABBR",
|
||||||
|
"ACRONYM",
|
||||||
|
"AUDIO",
|
||||||
|
"B",
|
||||||
|
"BDI",
|
||||||
|
"BDO",
|
||||||
|
"BIG",
|
||||||
|
"BR",
|
||||||
|
"BUTTON",
|
||||||
|
"CANVAS",
|
||||||
|
"CITE",
|
||||||
|
"CODE",
|
||||||
|
"DATA",
|
||||||
|
"DATALIST",
|
||||||
|
"DEL",
|
||||||
|
"DFN",
|
||||||
|
"EM",
|
||||||
|
"EMBED",
|
||||||
|
"I",
|
||||||
|
"IFRAME",
|
||||||
|
"IMG",
|
||||||
|
"INPUT",
|
||||||
|
"INS",
|
||||||
|
"KBD",
|
||||||
|
"LABEL",
|
||||||
|
"MAP",
|
||||||
|
"MARK",
|
||||||
|
"METER",
|
||||||
|
"NOSCRIPT",
|
||||||
|
"OBJECT",
|
||||||
|
"OUTPUT",
|
||||||
|
"PICTURE",
|
||||||
|
"PROGRESS",
|
||||||
|
"Q",
|
||||||
|
"RUBY",
|
||||||
|
"S",
|
||||||
|
"SAMP",
|
||||||
|
"SCRIPT",
|
||||||
|
"SELECT",
|
||||||
|
"SLOT",
|
||||||
|
"SMALL",
|
||||||
|
"SPAN",
|
||||||
|
"STRONG",
|
||||||
|
"SUB",
|
||||||
|
"SUP",
|
||||||
|
"SVG",
|
||||||
|
"TEMPLATE",
|
||||||
|
"TEXTAREA",
|
||||||
|
"TIME",
|
||||||
|
"U",
|
||||||
|
"TT",
|
||||||
|
"VAR",
|
||||||
|
"VIDEO",
|
||||||
|
"WBR",
|
||||||
|
];
|
||||||
|
|
||||||
|
export function nodeIsInline(node: Node): boolean {
|
||||||
|
return !nodeIsElement(node) || INLINE_TAGS.includes(node.tagName);
|
||||||
|
}
|
|
@ -1,12 +1,25 @@
|
||||||
/* Copyright: Ankitects Pty Ltd and contributors
|
/* Copyright: Ankitects Pty Ltd and contributors
|
||||||
* License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */
|
* License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */
|
||||||
|
|
||||||
|
import { filterHTML } from "./filterHtml";
|
||||||
|
import { nodeIsElement, nodeIsInline } from "./helpers";
|
||||||
|
import { bridgeCommand } from "./lib";
|
||||||
|
|
||||||
let currentField: EditingArea | null = null;
|
let currentField: EditingArea | null = null;
|
||||||
let changeTimer: number | null = null;
|
let changeTimer: number | null = null;
|
||||||
let currentNoteId: number | null = null;
|
let currentNoteId: number | null = null;
|
||||||
|
|
||||||
declare interface String {
|
declare global {
|
||||||
format(...args: string[]): string;
|
interface String {
|
||||||
|
format(...args: string[]): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Selection {
|
||||||
|
modify(s: string, t: string, u: string): void;
|
||||||
|
addRange(r: Range): void;
|
||||||
|
removeAllRanges(): void;
|
||||||
|
getRangeAt(n: number): Range;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* kept for compatibility with add-ons */
|
/* kept for compatibility with add-ons */
|
||||||
|
@ -18,11 +31,11 @@ String.prototype.format = function (...args: string[]): string {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function setFGButton(col: string): void {
|
export function setFGButton(col: string): void {
|
||||||
document.getElementById("forecolor").style.backgroundColor = col;
|
document.getElementById("forecolor").style.backgroundColor = col;
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveNow(keepFocus: boolean): void {
|
export function saveNow(keepFocus: boolean): void {
|
||||||
if (!currentField) {
|
if (!currentField) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -45,10 +58,6 @@ function triggerKeyTimer(): void {
|
||||||
}, 600);
|
}, 600);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Selection {
|
|
||||||
modify(s: string, t: string, u: string): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onKey(evt: KeyboardEvent): void {
|
function onKey(evt: KeyboardEvent): void {
|
||||||
// esc clears focus, allowing dialog to close
|
// esc clears focus, allowing dialog to close
|
||||||
if (evt.code === "Escape") {
|
if (evt.code === "Escape") {
|
||||||
|
@ -100,72 +109,6 @@ function onKeyUp(evt: KeyboardEvent): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function nodeIsElement(node: Node): node is Element {
|
|
||||||
return node.nodeType === Node.ELEMENT_NODE;
|
|
||||||
}
|
|
||||||
|
|
||||||
const INLINE_TAGS = [
|
|
||||||
"A",
|
|
||||||
"ABBR",
|
|
||||||
"ACRONYM",
|
|
||||||
"AUDIO",
|
|
||||||
"B",
|
|
||||||
"BDI",
|
|
||||||
"BDO",
|
|
||||||
"BIG",
|
|
||||||
"BR",
|
|
||||||
"BUTTON",
|
|
||||||
"CANVAS",
|
|
||||||
"CITE",
|
|
||||||
"CODE",
|
|
||||||
"DATA",
|
|
||||||
"DATALIST",
|
|
||||||
"DEL",
|
|
||||||
"DFN",
|
|
||||||
"EM",
|
|
||||||
"EMBED",
|
|
||||||
"I",
|
|
||||||
"IFRAME",
|
|
||||||
"IMG",
|
|
||||||
"INPUT",
|
|
||||||
"INS",
|
|
||||||
"KBD",
|
|
||||||
"LABEL",
|
|
||||||
"MAP",
|
|
||||||
"MARK",
|
|
||||||
"METER",
|
|
||||||
"NOSCRIPT",
|
|
||||||
"OBJECT",
|
|
||||||
"OUTPUT",
|
|
||||||
"PICTURE",
|
|
||||||
"PROGRESS",
|
|
||||||
"Q",
|
|
||||||
"RUBY",
|
|
||||||
"S",
|
|
||||||
"SAMP",
|
|
||||||
"SCRIPT",
|
|
||||||
"SELECT",
|
|
||||||
"SLOT",
|
|
||||||
"SMALL",
|
|
||||||
"SPAN",
|
|
||||||
"STRONG",
|
|
||||||
"SUB",
|
|
||||||
"SUP",
|
|
||||||
"SVG",
|
|
||||||
"TEMPLATE",
|
|
||||||
"TEXTAREA",
|
|
||||||
"TIME",
|
|
||||||
"U",
|
|
||||||
"TT",
|
|
||||||
"VAR",
|
|
||||||
"VIDEO",
|
|
||||||
"WBR",
|
|
||||||
];
|
|
||||||
|
|
||||||
function nodeIsInline(node: Node): boolean {
|
|
||||||
return !nodeIsElement(node) || INLINE_TAGS.includes(node.tagName);
|
|
||||||
}
|
|
||||||
|
|
||||||
function inListItem(): boolean {
|
function inListItem(): boolean {
|
||||||
const anchor = currentField.getSelection().anchorNode;
|
const anchor = currentField.getSelection().anchorNode;
|
||||||
|
|
||||||
|
@ -179,7 +122,7 @@ function inListItem(): boolean {
|
||||||
return inList;
|
return inList;
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertNewline(): void {
|
export function insertNewline(): void {
|
||||||
if (!inPreEnvironment()) {
|
if (!inPreEnvironment()) {
|
||||||
setFormat("insertText", "\n");
|
setFormat("insertText", "\n");
|
||||||
return;
|
return;
|
||||||
|
@ -228,12 +171,12 @@ function updateButtonState(): void {
|
||||||
// 'col': document.queryCommandValue("forecolor")
|
// 'col': document.queryCommandValue("forecolor")
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleEditorButton(buttonid: string): void {
|
export function toggleEditorButton(buttonid: string): void {
|
||||||
const button = $(buttonid)[0];
|
const button = $(buttonid)[0];
|
||||||
button.classList.toggle("highlighted");
|
button.classList.toggle("highlighted");
|
||||||
}
|
}
|
||||||
|
|
||||||
function setFormat(cmd: string, arg?: any, nosave: boolean = false): void {
|
export function setFormat(cmd: string, arg?: any, nosave: boolean = false): void {
|
||||||
document.execCommand(cmd, false, arg);
|
document.execCommand(cmd, false, arg);
|
||||||
if (!nosave) {
|
if (!nosave) {
|
||||||
saveField("key");
|
saveField("key");
|
||||||
|
@ -256,7 +199,7 @@ function onFocus(evt: FocusEvent): void {
|
||||||
}
|
}
|
||||||
elem.focusEditable();
|
elem.focusEditable();
|
||||||
currentField = elem;
|
currentField = elem;
|
||||||
pycmd(`focus:${currentField.ord}`);
|
bridgeCommand(`focus:${currentField.ord}`);
|
||||||
enableButtons();
|
enableButtons();
|
||||||
// do this twice so that there's no flicker on newer versions
|
// do this twice so that there's no flicker on newer versions
|
||||||
caretToEnd();
|
caretToEnd();
|
||||||
|
@ -279,7 +222,7 @@ function onFocus(evt: FocusEvent): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function focusField(n: number): void {
|
export function focusField(n: number): void {
|
||||||
const field = getEditorField(n);
|
const field = getEditorField(n);
|
||||||
|
|
||||||
if (field) {
|
if (field) {
|
||||||
|
@ -287,7 +230,7 @@ function focusField(n: number): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function focusIfField(x: number, y: number): boolean {
|
export function focusIfField(x: number, y: number): boolean {
|
||||||
const elements = document.elementsFromPoint(x, y);
|
const elements = document.elementsFromPoint(x, y);
|
||||||
for (let i = 0; i < elements.length; i++) {
|
for (let i = 0; i < elements.length; i++) {
|
||||||
let elem = elements[i] as EditingArea;
|
let elem = elements[i] as EditingArea;
|
||||||
|
@ -303,7 +246,7 @@ function focusIfField(x: number, y: number): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
function onPaste(): void {
|
function onPaste(): void {
|
||||||
pycmd("paste");
|
bridgeCommand("paste");
|
||||||
window.event.preventDefault();
|
window.event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -353,7 +296,9 @@ function saveField(type: "blur" | "key"): void {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pycmd(`${type}:${currentField.ord}:${currentNoteId}:${currentField.fieldHTML}`);
|
bridgeCommand(
|
||||||
|
`${type}:${currentField.ord}:${currentNoteId}:${currentField.fieldHTML}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function wrappedExceptForWhitespace(text: string, front: string, back: string): string {
|
function wrappedExceptForWhitespace(text: string, front: string, back: string): string {
|
||||||
|
@ -361,7 +306,7 @@ function wrappedExceptForWhitespace(text: string, front: string, back: string):
|
||||||
return match[1] + front + match[2] + back + match[3];
|
return match[1] + front + match[2] + back + match[3];
|
||||||
}
|
}
|
||||||
|
|
||||||
function preventButtonFocus(): void {
|
export function preventButtonFocus(): void {
|
||||||
for (const element of document.querySelectorAll("button.linkb")) {
|
for (const element of document.querySelectorAll("button.linkb")) {
|
||||||
element.addEventListener("mousedown", (evt: Event) => {
|
element.addEventListener("mousedown", (evt: Event) => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
|
@ -386,7 +331,7 @@ function maybeDisableButtons(): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function wrap(front: string, back: string): void {
|
export function wrap(front: string, back: string): void {
|
||||||
wrapInternal(front, back, false);
|
wrapInternal(front, back, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -419,7 +364,7 @@ function wrapInternal(front: string, back: string, plainText: boolean): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCutOrCopy(): boolean {
|
function onCutOrCopy(): boolean {
|
||||||
pycmd("cutOrCopy");
|
bridgeCommand("cutOrCopy");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -606,12 +551,12 @@ function adjustFieldAmount(amount: number): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEditorField(n: number): EditorField | null {
|
export function getEditorField(n: number): EditorField | null {
|
||||||
const fields = document.getElementById("fields").children;
|
const fields = document.getElementById("fields").children;
|
||||||
return (fields[n] as EditorField) ?? null;
|
return (fields[n] as EditorField) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function forEditorField<T>(
|
export function forEditorField<T>(
|
||||||
values: T[],
|
values: T[],
|
||||||
func: (field: EditorField, value: T) => void
|
func: (field: EditorField, value: T) => void
|
||||||
): void {
|
): void {
|
||||||
|
@ -622,7 +567,7 @@ function forEditorField<T>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setFields(fields: [string, string][]): void {
|
export function setFields(fields: [string, string][]): void {
|
||||||
// webengine will include the variable after enter+backspace
|
// webengine will include the variable after enter+backspace
|
||||||
// if we don't convert it to a literal colour
|
// if we don't convert it to a literal colour
|
||||||
const color = window
|
const color = window
|
||||||
|
@ -637,7 +582,7 @@ function setFields(fields: [string, string][]): void {
|
||||||
maybeDisableButtons();
|
maybeDisableButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setBackgrounds(cols: ("dupe" | "")[]) {
|
export function setBackgrounds(cols: ("dupe" | "")[]) {
|
||||||
forEditorField(cols, (field, value) =>
|
forEditorField(cols, (field, value) =>
|
||||||
field.editingArea.classList.toggle("dupe", value === "dupe")
|
field.editingArea.classList.toggle("dupe", value === "dupe")
|
||||||
);
|
);
|
||||||
|
@ -646,17 +591,17 @@ function setBackgrounds(cols: ("dupe" | "")[]) {
|
||||||
.classList.toggle("is-inactive", !cols.includes("dupe"));
|
.classList.toggle("is-inactive", !cols.includes("dupe"));
|
||||||
}
|
}
|
||||||
|
|
||||||
function setFonts(fonts: [string, number, boolean][]): void {
|
export function setFonts(fonts: [string, number, boolean][]): void {
|
||||||
forEditorField(fonts, (field, [fontFamily, fontSize, isRtl]) => {
|
forEditorField(fonts, (field, [fontFamily, fontSize, isRtl]) => {
|
||||||
field.setBaseStyling(fontFamily, `${fontSize}px`, isRtl ? "rtl" : "ltr");
|
field.setBaseStyling(fontFamily, `${fontSize}px`, isRtl ? "rtl" : "ltr");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setNoteId(id: number): void {
|
export function setNoteId(id: number): void {
|
||||||
currentNoteId = id;
|
currentNoteId = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
let pasteHTML = function (
|
export let pasteHTML = function (
|
||||||
html: string,
|
html: string,
|
||||||
internal: boolean,
|
internal: boolean,
|
||||||
extendedMode: boolean
|
extendedMode: boolean
|
||||||
|
@ -667,172 +612,3 @@ let pasteHTML = function (
|
||||||
setFormat("inserthtml", html);
|
setFormat("inserthtml", html);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let filterHTML = function (
|
|
||||||
html: string,
|
|
||||||
internal: boolean,
|
|
||||||
extendedMode: boolean
|
|
||||||
): string {
|
|
||||||
// wrap it in <top> as we aren't allowed to change top level elements
|
|
||||||
const top = document.createElement("ankitop");
|
|
||||||
top.innerHTML = html;
|
|
||||||
|
|
||||||
if (internal) {
|
|
||||||
filterInternalNode(top);
|
|
||||||
} else {
|
|
||||||
filterNode(top, extendedMode);
|
|
||||||
}
|
|
||||||
let outHtml = top.innerHTML;
|
|
||||||
if (!extendedMode && !internal) {
|
|
||||||
// collapse whitespace
|
|
||||||
outHtml = outHtml.replace(/[\n\t ]+/g, " ");
|
|
||||||
}
|
|
||||||
outHtml = outHtml.trim();
|
|
||||||
return outHtml;
|
|
||||||
};
|
|
||||||
|
|
||||||
let allowedTagsBasic = {};
|
|
||||||
let allowedTagsExtended = {};
|
|
||||||
|
|
||||||
let TAGS_WITHOUT_ATTRS = ["P", "DIV", "BR", "SUB", "SUP"];
|
|
||||||
for (const tag of TAGS_WITHOUT_ATTRS) {
|
|
||||||
allowedTagsBasic[tag] = { attrs: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
TAGS_WITHOUT_ATTRS = [
|
|
||||||
"B",
|
|
||||||
"BLOCKQUOTE",
|
|
||||||
"CODE",
|
|
||||||
"DD",
|
|
||||||
"DL",
|
|
||||||
"DT",
|
|
||||||
"EM",
|
|
||||||
"H1",
|
|
||||||
"H2",
|
|
||||||
"H3",
|
|
||||||
"I",
|
|
||||||
"LI",
|
|
||||||
"OL",
|
|
||||||
"PRE",
|
|
||||||
"RP",
|
|
||||||
"RT",
|
|
||||||
"RUBY",
|
|
||||||
"STRONG",
|
|
||||||
"TABLE",
|
|
||||||
"U",
|
|
||||||
"UL",
|
|
||||||
];
|
|
||||||
for (const tag of TAGS_WITHOUT_ATTRS) {
|
|
||||||
allowedTagsExtended[tag] = { attrs: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
allowedTagsBasic["IMG"] = { attrs: ["SRC"] };
|
|
||||||
|
|
||||||
allowedTagsExtended["A"] = { attrs: ["HREF"] };
|
|
||||||
allowedTagsExtended["TR"] = { attrs: ["ROWSPAN"] };
|
|
||||||
allowedTagsExtended["TD"] = { attrs: ["COLSPAN", "ROWSPAN"] };
|
|
||||||
allowedTagsExtended["TH"] = { attrs: ["COLSPAN", "ROWSPAN"] };
|
|
||||||
allowedTagsExtended["FONT"] = { attrs: ["COLOR"] };
|
|
||||||
|
|
||||||
const allowedStyling = {
|
|
||||||
color: true,
|
|
||||||
"background-color": true,
|
|
||||||
"font-weight": true,
|
|
||||||
"font-style": true,
|
|
||||||
"text-decoration-line": true,
|
|
||||||
};
|
|
||||||
|
|
||||||
let isNightMode = function (): boolean {
|
|
||||||
return document.body.classList.contains("nightMode");
|
|
||||||
};
|
|
||||||
|
|
||||||
let filterExternalSpan = function (elem: HTMLElement) {
|
|
||||||
// filter out attributes
|
|
||||||
for (const attr of [...elem.attributes]) {
|
|
||||||
const attrName = attr.name.toUpperCase();
|
|
||||||
|
|
||||||
if (attrName !== "STYLE") {
|
|
||||||
elem.removeAttributeNode(attr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// filter styling
|
|
||||||
for (const name of [...elem.style]) {
|
|
||||||
const value = elem.style.getPropertyValue(name);
|
|
||||||
|
|
||||||
if (
|
|
||||||
!allowedStyling.hasOwnProperty(name) ||
|
|
||||||
// google docs adds this unnecessarily
|
|
||||||
(name === "background-color" && value === "transparent") ||
|
|
||||||
// ignore coloured text in night mode for now
|
|
||||||
(isNightMode() && (name === "background-color" || name === "color"))
|
|
||||||
) {
|
|
||||||
elem.style.removeProperty(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
allowedTagsExtended["SPAN"] = filterExternalSpan;
|
|
||||||
|
|
||||||
// add basic tags to extended
|
|
||||||
Object.assign(allowedTagsExtended, allowedTagsBasic);
|
|
||||||
|
|
||||||
function isHTMLElement(elem: Element): elem is HTMLElement {
|
|
||||||
return elem instanceof HTMLElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
// filtering from another field
|
|
||||||
let filterInternalNode = function (elem: Element) {
|
|
||||||
if (isHTMLElement(elem)) {
|
|
||||||
elem.style.removeProperty("background-color");
|
|
||||||
elem.style.removeProperty("font-size");
|
|
||||||
elem.style.removeProperty("font-family");
|
|
||||||
}
|
|
||||||
// recurse
|
|
||||||
for (let i = 0; i < elem.children.length; i++) {
|
|
||||||
const child = elem.children[i];
|
|
||||||
filterInternalNode(child);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// filtering from external sources
|
|
||||||
let filterNode = function (node: Node, extendedMode: boolean): void {
|
|
||||||
if (!nodeIsElement(node)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// descend first, and take a copy of the child nodes as the loop will skip
|
|
||||||
// elements due to node modifications otherwise
|
|
||||||
for (const child of [...node.children]) {
|
|
||||||
filterNode(child, extendedMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node.tagName === "ANKITOP") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tag = extendedMode
|
|
||||||
? allowedTagsExtended[node.tagName]
|
|
||||||
: allowedTagsBasic[node.tagName];
|
|
||||||
|
|
||||||
if (!tag) {
|
|
||||||
if (!node.innerHTML || node.tagName === "TITLE") {
|
|
||||||
node.parentNode.removeChild(node);
|
|
||||||
} else {
|
|
||||||
node.outerHTML = node.innerHTML;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (typeof tag === "function") {
|
|
||||||
// filtering function provided
|
|
||||||
tag(node);
|
|
||||||
} else {
|
|
||||||
// allowed, filter out attributes
|
|
||||||
for (const attr of [...node.attributes]) {
|
|
||||||
const attrName = attr.name.toUpperCase();
|
|
||||||
if (tag.attrs.indexOf(attrName) === -1) {
|
|
||||||
node.removeAttributeNode(attr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
9
ts/editor/lib.ts
Normal file
9
ts/editor/lib.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
bridgeCommand<T>(command: string, callback?: (value: T) => void): void;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bridgeCommand<T>(command: string, callback?: (value: T) => void): void {
|
||||||
|
window.bridgeCommand<T>(command, callback);
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue