Merge branch 'master' into dyn-deckconf

This commit is contained in:
RumovZ 2021-02-01 23:33:41 +01:00
commit c18af2a0a9
101 changed files with 1795 additions and 1416 deletions

View file

@ -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
- 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
see the platform-specific instructions in the "Building from source" section below
for more info.
You will need the 64 bit version of Python 3.8 or 3.9 installed. If you do not
have Python yet, please see the platform-specific instructions in the "Building
from source" section below for more info.
**Mac/Linux**:

View file

@ -19,8 +19,9 @@ $ brew install rsync bazelisk
**Install Python 3.8**:
Install Python 3.8 from <https://python.org>. You may be able to use
the Homebrew version instead, but this is untested.
Install Python 3.8 from <https://python.org>. We have heard reports
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.

View file

@ -17,8 +17,8 @@ components enabled on the right.
**Python 3.8**:
Download Python 3.8 from <https://python.org>. Run the installer, and
customize the installation. Select "install for all users", and choose
Download the 64 bit Python 3.8 from <https://python.org>. Run the installer,
and customize the installation. Select "install for all users", and choose
the install path as c:\python. Currently the build scripts require
Python to be installed in that location.

View file

@ -34,7 +34,7 @@ decorator==4.4.2
# via -r requirements.in
distro==1.5.0
# via -r requirements.in
flask-cors==3.0.9
flask-cors==3.0.10
# via -r requirements.in
flask==1.1.2
# via
@ -54,7 +54,7 @@ isort==5.7.0
# pylint
itsdangerous==1.1.0
# via flask
jinja2==2.11.2
jinja2==2.11.3
# via flask
jsonschema==3.2.0
# via -r requirements.in
@ -72,13 +72,13 @@ mypy-extensions==0.4.3
# via
# black
# mypy
mypy-protobuf==1.23
mypy-protobuf==1.24
# via -r requirements.in
mypy==0.790
mypy==0.800
# via -r requirements.in
orjson==3.4.6
orjson==3.4.7
# via -r requirements.in
packaging==20.8
packaging==20.9
# via pytest
pathspec==0.8.1
# via black
@ -102,7 +102,7 @@ pyrsistent==0.17.3
# via jsonschema
pysocks==1.7.1
# via requests
pytest==6.2.1
pytest==6.2.2
# via -r requirements.in
pytoml==0.1.21
# via compare-locales
@ -142,7 +142,7 @@ typing-extensions==3.7.4.3
# via
# black
# mypy
urllib3==1.26.2
urllib3==1.26.3
# via requests
waitress==2.0.0b1
# via -r requirements.in
@ -154,9 +154,9 @@ wrapt==1.12.1
# via astroid
# The following packages are considered to be unsafe in a requirements file:
pip==20.3.3
pip==21.0.1
# via pip-tools
setuptools==51.1.1
setuptools==52.0.0
# via jsonschema
# manually added for now; ensure it and the earlier winrt are not removed on update

View file

@ -1,10 +1,7 @@
from typing import Any
def buildhash(*args, **kwargs) -> Any: ...
def open_backend(*args, **kwargs) -> Any: ...
def buildhash() -> str: ...
def open_backend(data: bytes) -> Backend: ...
class Backend:
@classmethod
def __init__(self, *args, **kwargs) -> None: ...
def command(self, *args, **kwargs) -> Any: ...
def db_command(self, *args, **kwargs) -> Any: ...
def command(self, method: int, data: bytes) -> bytes: ...
def db_command(self, data: bytes) -> bytes: ...

View file

@ -8,6 +8,7 @@ import enum
import os
import pprint
import re
import sys
import time
import traceback
import weakref
@ -107,7 +108,7 @@ class Collection:
@property
def backend(self) -> RustBackend:
traceback.print_stack()
traceback.print_stack(file=sys.stdout)
print()
print(
"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.begin()
def reopen(self, after_full_sync=False) -> None:
def reopen(self, after_full_sync: bool = False) -> None:
assert not self.db
assert self.path.endswith(".anki2")
@ -409,7 +410,7 @@ class Collection:
def cardCount(self) -> Any:
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."
self._backend.remove_cards(card_ids=card_ids)
@ -505,7 +506,7 @@ class Collection:
dupes = []
fields: Dict[int, int] = {}
def ordForMid(mid):
def ordForMid(mid: int) -> int:
if mid not in fields:
model = self.models.get(mid)
for c, f in enumerate(model["flds"]):
@ -539,7 +540,10 @@ class Collection:
##########################################################################
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:
"""Helper function for the backend's search string operations.
@ -576,11 +580,11 @@ class Collection:
except KeyError:
return default
def set_config(self, key: str, val: Any):
def set_config(self, key: str, val: Any) -> None:
self.setMod()
self.conf.set(key, val)
def remove_config(self, key):
def remove_config(self, key: str) -> None:
self.setMod()
self.conf.remove(key)
@ -779,11 +783,11 @@ table.review-log {{ {revlog_style} }}
# Logging
##########################################################################
def log(self, *args, **kwargs) -> None:
def log(self, *args: Any, **kwargs: Any) -> None:
if not self._should_log:
return
def customRepr(x):
def customRepr(x: Any) -> str:
if isinstance(x, str):
return x
return pprint.pformat(x)
@ -865,7 +869,7 @@ table.review-log {{ {revlog_style} }}
def get_preferences(self) -> Preferences:
return self._backend.get_preferences()
def set_preferences(self, prefs: Preferences):
def set_preferences(self, prefs: Preferences) -> None:
self._backend.set_preferences(prefs)

View file

@ -21,6 +21,7 @@ from __future__ import annotations
import copy
import weakref
from typing import Any
from weakref import ref
import anki
from anki.errors import NotFoundError
@ -46,7 +47,7 @@ class ConfigManager:
# Legacy dict interface
#########################
def __getitem__(self, key):
def __getitem__(self, key: str) -> Any:
val = self.get_immutable(key)
if isinstance(val, list):
print(
@ -61,28 +62,28 @@ class ConfigManager:
else:
return val
def __setitem__(self, key, value):
def __setitem__(self, key: str, value: Any) -> None:
self.set(key, value)
def get(self, key, default=None):
def get(self, key: str, default: Any = None) -> Any:
try:
return self[key]
except KeyError:
return default
def setdefault(self, key, default):
def setdefault(self, key: str, default: Any) -> Any:
if key not in self:
self[key] = default
return self[key]
def __contains__(self, key):
def __contains__(self, key: str) -> bool:
try:
self.get_immutable(key)
return True
except KeyError:
return False
def __delitem__(self, key):
def __delitem__(self, key: str) -> None:
self.remove(key)
@ -95,13 +96,13 @@ class ConfigManager:
class WrappedList(list):
def __init__(self, conf, key, val):
def __init__(self, conf: ref[ConfigManager], key: str, val: Any) -> None:
self.key = key
self.conf = conf
self.orig = copy.deepcopy(val)
super().__init__(val)
def __del__(self):
def __del__(self) -> None:
cur = list(self)
conf = self.conf()
if conf and self.orig != cur:
@ -109,13 +110,13 @@ class WrappedList(list):
class WrappedDict(dict):
def __init__(self, conf, key, val):
def __init__(self, conf: ref[ConfigManager], key: str, val: Any) -> None:
self.key = key
self.conf = conf
self.orig = copy.deepcopy(val)
super().__init__(val)
def __del__(self):
def __del__(self) -> None:
cur = dict(self)
conf = self.conf()
if conf and self.orig != cur:

View file

@ -92,7 +92,7 @@ REVLOG_RESCHED = 4
##########################################################################
def _tr(col: Optional[anki.collection.Collection]):
def _tr(col: Optional[anki.collection.Collection]) -> Any:
if col:
return col.tr
else:

View file

@ -32,7 +32,7 @@ class DB:
del d["_db"]
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()
# mark modified?
for stmt in "insert", "update", "delete":
@ -76,36 +76,36 @@ class DB:
def rollback(self) -> None:
self._db.rollback()
def scalar(self, *a, **kw) -> Any:
def scalar(self, *a: Any, **kw: Any) -> Any:
res = self.execute(*a, **kw).fetchone()
if res:
return res[0]
return None
def all(self, *a, **kw) -> List:
def all(self, *a: Any, **kw: Any) -> List:
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)
res = c.fetchone()
c.close()
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)]
def close(self) -> None:
self._db.text_factory = None
self._db.close()
def set_progress_handler(self, *args) -> None:
def set_progress_handler(self, *args: Any) -> None:
self._db.set_progress_handler(*args)
def __enter__(self) -> "DB":
self._db.execute("begin")
return self
def __exit__(self, exc_type, *args) -> None:
def __exit__(self, *args: Any) -> None:
self._db.close()
def totalChanges(self) -> Any:

View file

@ -4,6 +4,7 @@
from __future__ import annotations
import re
from re import Match
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Union
import anki
@ -43,7 +44,11 @@ class DBProxy:
################
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]:
# mark modified?
s = sql.strip().lower()
@ -57,20 +62,22 @@ class DBProxy:
# Query shortcuts
###################
def all(self, sql: str, *args: ValueForDB, **kwargs) -> List[Row]:
return self._query(sql, *args, **kwargs)
def all(self, sql: str, *args: ValueForDB, **kwargs: ValueForDB) -> List[Row]:
return self._query(sql, *args, first_row_only=False, **kwargs)
def list(self, sql: str, *args: ValueForDB, **kwargs) -> List[ValueFromDB]:
return [x[0] for x in self._query(sql, *args, **kwargs)]
def list(
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)
if rows:
return rows[0]
else:
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)
if rows:
return rows[0][0]
@ -109,7 +116,7 @@ def emulate_named_args(
n = len(args2)
arg_num[key] = n
# update refs
def repl(m):
def repl(m: Match) -> str:
arg = m.group(1)
return f"?{arg_num[arg]}"

View file

@ -5,6 +5,8 @@ from __future__ import annotations
import copy
import pprint
import sys
import traceback
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Union
import anki # pylint: disable=unused-import
@ -43,36 +45,37 @@ class DecksDictProxy:
def __init__(self, col: anki.collection.Collection):
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")
def __getitem__(self, item):
def __getitem__(self, item: Any) -> Any:
self._warn()
return self._col.decks.get(int(item))
def __setitem__(self, key, val):
def __setitem__(self, key: Any, val: Any) -> None:
self._warn()
self._col.decks.save(val)
def __len__(self):
def __len__(self) -> int:
self._warn()
return len(self._col.decks.all_names_and_ids())
def keys(self):
def keys(self) -> Any:
self._warn()
return [str(nt.id) for nt in self._col.decks.all_names_and_ids()]
def values(self):
def values(self) -> Any:
self._warn()
return self._col.decks.all()
def items(self):
def items(self) -> Any:
self._warn()
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._col.decks.have(item)
return self._col.decks.have(item)
class DeckManager:
@ -97,7 +100,7 @@ class DeckManager:
self.update(g, preserve_usn=False)
# legacy
def flush(self):
def flush(self) -> None:
pass
def __repr__(self) -> str:
@ -135,7 +138,7 @@ class DeckManager:
self.col._backend.remove_deck(did)
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]:
"A sorted sequence of deck names and IDs."
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["collapsed"] = not deck["collapsed"]
self.save(deck)
def collapseBrowser(self, did) -> None:
def collapseBrowser(self, did: int) -> None:
deck = self.get(did)
collapsed = deck.get("browserCollapsed", False)
deck["browserCollapsed"] = not collapsed
@ -241,7 +244,7 @@ class DeckManager:
return self.get_legacy(id)
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."
try:
g["id"] = self.col._backend.add_or_update_deck_legacy(
@ -303,7 +306,7 @@ class DeckManager:
except NotFoundError:
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(
config=to_json_bytes(conf), preserve_usn_and_mtime=preserve_usn
)
@ -325,7 +328,7 @@ class DeckManager:
) -> int:
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."
self.col.modSchema(check=True)
for g in self.all():
@ -341,14 +344,14 @@ class DeckManager:
grp["conf"] = id
self.save(grp)
def didsForConf(self, conf) -> List[int]:
def didsForConf(self, conf: DeckConfig) -> List[int]:
dids = []
for deck in self.all():
if "conf" in deck and deck["conf"] == conf["id"]:
dids.append(deck["id"])
return dids
def restoreToDefault(self, conf) -> None:
def restoreToDefault(self, conf: DeckConfig) -> None:
oldOrder = conf["new"]["order"]
new = from_json_bytes(self.col._backend.new_deck_config_legacy())
new["id"] = conf["id"]
@ -380,7 +383,7 @@ class DeckManager:
return deck["name"]
return None
def setDeck(self, cids, did) -> None:
def setDeck(self, cids: List[int], did: int) -> None:
self.col.db.execute(
"update cards set did=?,usn=?,mod=? where id in " + ids2str(cids),
did,
@ -424,7 +427,7 @@ class DeckManager:
self.col.conf["activeDecks"] = active
# don't use this, it will likely go away
def update_active(self):
def update_active(self) -> None:
self.select(self.current()["id"])
# Parents/children
@ -480,7 +483,7 @@ class DeckManager:
# Change to Dict[int, "DeckManager.childMapNode"] when MyPy allow recursive type
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():
arr.append(did)
gather(child, arr)

View file

@ -3,8 +3,6 @@
from __future__ import annotations
from typing import Any
import anki._backend.backend_pb2 as _pb
# 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)
# FIXME: this is only used with "abortSchemaMod", but currently some
# add-ons depend on it
class AnkiError(Exception):
def __init__(self, type, **data) -> None:
def __init__(self, type: str) -> None:
super().__init__()
self.type = type
self.data = data
def __str__(self) -> Any:
m = self.type
if self.data:
m += ": %s" % repr(self.data)
return m
def __str__(self) -> str:
return self.type
class DeckRenameError(Exception):
@ -106,5 +102,5 @@ class DeckRenameError(Exception):
super().__init__()
self.description = description
def __str__(self):
def __str__(self) -> str:
return "Couldn't rename deck: " + self.description

View file

@ -16,10 +16,10 @@ class Finder:
self.col = col.weakref()
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)
def findNotes(self, query):
def findNotes(self, query: Any) -> Any:
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()
for m in col.models.all():
for f in m["flds"]:

View file

@ -25,7 +25,7 @@ from anki.hooks_gen import *
_hooks: Dict[str, List[Callable[..., Any]]] = {}
def runHook(hook: str, *args) -> None:
def runHook(hook: str, *args: Any) -> None:
"Run all functions on hook."
hookFuncs = _hooks.get(hook, None)
if hookFuncs:
@ -37,7 +37,7 @@ def runHook(hook: str, *args) -> None:
raise
def runFilter(hook: str, arg: Any, *args) -> Any:
def runFilter(hook: str, arg: Any, *args: Any) -> Any:
hookFuncs = _hooks.get(hook, None)
if hookFuncs:
for func in hookFuncs:
@ -57,7 +57,7 @@ def addHook(hook: str, func: Callable) -> None:
_hooks[hook].append(func)
def remHook(hook, func) -> None:
def remHook(hook: Any, func: Any) -> None:
"Remove a function if is on hook."
hook = _hooks.get(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
# 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."
def repl(*args, **kwargs):
def repl(*args: Any, **kwargs: Any) -> Any:
if pos == "after":
old(*args, **kwargs)
return new(*args, **kwargs)
@ -85,7 +85,7 @@ def wrap(old, new, pos="after") -> Callable:
else:
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 decorator.decorator(decorator_wrapper)(old)

View file

@ -5,6 +5,8 @@
Wrapper for requests that adds a callback for tracking upload/download progress.
"""
from __future__ import annotations
import io
import os
from typing import Any, Callable, Dict, Optional
@ -28,24 +30,23 @@ class HttpClient:
self.progress_hook = progress_hook
self.session = requests.Session()
def __enter__(self):
def __enter__(self) -> HttpClient:
return self
def __exit__(self, *args):
def __exit__(self, *args: Any) -> None:
self.close()
def close(self):
def close(self) -> None:
if self.session:
self.session.close()
self.session = None
def __del__(self):
def __del__(self) -> None:
self.close()
def post(self, url: str, data: Any, headers: Optional[Dict[str, str]]) -> Response:
data = _MonitoringFile(
data, hook=self.progress_hook
) # pytype: disable=wrong-arg-types
def post(
self, url: str, data: bytes, headers: Optional[Dict[str, str]]
) -> Response:
headers["User-Agent"] = self._agentName()
return self.session.post(
url,
@ -56,7 +57,7 @@ class HttpClient:
verify=self.verify,
) # 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:
headers = {}
headers["User-Agent"] = self._agentName()
@ -64,7 +65,7 @@ class HttpClient:
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()
buf = io.BytesIO()
@ -87,15 +88,3 @@ if os.environ.get("ANKI_NOVERIFYSSL"):
import warnings
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

View file

@ -5,7 +5,7 @@ from __future__ import annotations
import locale
import re
from typing import TYPE_CHECKING, Optional, Tuple
from typing import TYPE_CHECKING, Any, Optional, Tuple
import anki
import anki._backend.backend_pb2 as _pb
@ -169,7 +169,7 @@ def ngettext(single: str, plural: str, n: int) -> str:
return plural
def tr_legacyglobal(*args, **kwargs) -> str:
def tr_legacyglobal(*args: Any, **kwargs: Any) -> str:
"Should use col.tr() instead."
if current_i18n:
return current_i18n.translate(*args, **kwargs)

View file

@ -11,7 +11,7 @@ import time
import urllib.error
import urllib.parse
import urllib.request
from typing import Any, Callable, List, Optional, Tuple
from typing import Any, Callable, List, Match, Optional, Tuple
import anki
import anki._backend.backend_pb2 as _pb
@ -197,7 +197,7 @@ class MediaManager:
else:
fn = urllib.parse.quote
def repl(match):
def repl(match: Match) -> str:
tag = match.group(0)
fname = match.group("fname")
if re.match("(https?|ftp)://", fname):

View file

@ -5,7 +5,9 @@ from __future__ import annotations
import copy
import pprint
import sys
import time
import traceback
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
import anki # pylint: disable=unused-import
@ -39,36 +41,37 @@ class ModelsDictProxy:
def __init__(self, col: anki.collection.Collection):
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")
def __getitem__(self, item):
def __getitem__(self, item: Any) -> Any:
self._warn()
return self._col.models.get(int(item))
def __setitem__(self, key, val):
def __setitem__(self, key: str, val: Any) -> None:
self._warn()
self._col.models.save(val)
def __len__(self):
def __len__(self) -> int:
self._warn()
return len(self._col.models.all_names_and_ids())
def keys(self):
def keys(self) -> Any:
self._warn()
return [str(nt.id) for nt in self._col.models.all_names_and_ids()]
def values(self):
def values(self) -> Any:
self._warn()
return self._col.models.all()
def items(self):
def items(self) -> Any:
self._warn()
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._col.models.have(item)
return self._col.models.have(item)
class ModelManager:
@ -123,7 +126,7 @@ class ModelManager:
def _get_cached(self, ntid: int) -> Optional[NoteType]:
return self._cache.get(ntid)
def _clear_cache(self):
def _clear_cache(self) -> None:
self._cache = {}
# Listing note types
@ -218,7 +221,7 @@ class ModelManager:
"Delete model, and all its cards/notes."
self.remove(m["id"])
def remove_all_notetypes(self):
def remove_all_notetypes(self) -> None:
for nt in self.all_names_and_ids():
self._remove_from_cache(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"]:
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."
self._remove_from_cache(m["id"])
self.ensureNameUnique(m)

View file

@ -113,7 +113,7 @@ class Note:
def __setitem__(self, key: str, value: str) -> None:
self.fields[self._fieldOrd(key)] = value
def __contains__(self, key) -> bool:
def __contains__(self, key: str) -> bool:
return key in self._fmap
# Tags

View file

@ -350,7 +350,7 @@ limit %d"""
lastIvl = -(self._delayForGrade(conf, lastLeft))
ivl = card.ivl if leaving else -(self._delayForGrade(conf, card.left))
def log():
def log() -> None:
self.col.db.execute(
"insert into revlog values (?,?,?,?,?,?,?,?,?)",
int(time.time() * 1000),
@ -450,7 +450,7 @@ and due <= ? limit ?)""",
self._revQueue: List[Any] = []
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."
if self._revQueue:
return True

View file

@ -166,7 +166,7 @@ class Scheduler:
self._restorePreviewCard(card)
self._removeFromFiltered(card)
def _reset_counts(self):
def _reset_counts(self) -> None:
tree = self.deck_due_tree(self.col.decks.selected())
node = self.col.decks.find_deck_in_tree(tree, int(self.col.conf["curDeck"]))
if not node:
@ -187,7 +187,7 @@ class Scheduler:
new, lrn, rev = counts
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."
return not any((self.newCount, self.revCount, self._immediate_learn_count))
@ -229,8 +229,12 @@ order by due"""
##########################################################################
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(
deck_id=deck_id,
new_delta=new_delta,
@ -321,7 +325,7 @@ order by due"""
self._newQueue: List[int] = []
self._updateNewCardRatio()
def _fillNew(self, recursing=False) -> bool:
def _fillNew(self, recursing: bool = False) -> bool:
if self._newQueue:
return True
if not self.newCount:
@ -841,7 +845,7 @@ and due <= ? limit ?)"""
def _resetRev(self) -> None:
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."
if self._revQueue:
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)
def _logRev(self, card: Card, ease: int, delay: int, type: int) -> None:
def log():
def log() -> None:
self.col.db.execute(
"insert into revlog values (?,?,?,?,?,?,?,?,?)",
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
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())
# legacy
@ -1472,7 +1476,7 @@ and (queue={QUEUE_TYPE_NEW} or (queue={QUEUE_TYPE_REV} and due<=?))""",
def orderCards(self, did: int) -> None:
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):
if conf["new"]["order"] == 0:
self.randomizeCards(did)

View file

@ -140,7 +140,7 @@ from revlog where id > ? """
relrn = relrn or 0
filt = filt or 0
# studied
def bold(s):
def bold(s: str) -> str:
return "<b>" + str(s) + "</b>"
if cards:
@ -298,7 +298,7 @@ group by day order by day"""
# pylint: disable=invalid-unary-operand-type
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(
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
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(
id, data=data, conf=conf, xunit=chunk, ylabel=ylabel, ylabel2=ylabel2
)

View file

@ -11,9 +11,10 @@ import os
import socket
import sys
import time
from http import HTTPStatus
from io import BytesIO
from tempfile import NamedTemporaryFile
from typing import Optional
from typing import Iterable, Optional
try:
import flask
@ -89,7 +90,7 @@ def handle_sync_request(method_str: str) -> Response:
elif method == Method.FULL_DOWNLOAD:
path = outdata.decode("utf8")
def stream_reply():
def stream_reply() -> Iterable[bytes]:
with open(path, "rb") as f:
while chunk := f.read(16 * 1024):
yield chunk
@ -106,7 +107,7 @@ def handle_sync_request(method_str: str) -> Response:
return resp
def after_full_sync():
def after_full_sync() -> None:
# the server methods do not reopen the collection after a full sync,
# so we need to
col.reopen(after_full_sync=False)
@ -146,15 +147,17 @@ def get_method(
@app.route("/<path:pathin>", methods=["POST"])
def handle_request(pathin: str):
def handle_request(pathin: str) -> Response:
path = pathin
print(int(time.time()), flask.request.remote_addr, path)
if path.startswith("sync/"):
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"))
if not os.path.exists(folder):
print("creating", folder)
@ -162,11 +165,11 @@ def folder():
return folder
def col_path():
def col_path() -> str:
return os.path.join(folder(), "collection.server.anki2")
def serve():
def serve() -> None:
global col
col = Collection(col_path(), server=True)

View file

@ -13,7 +13,7 @@ from __future__ import annotations
import pprint
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._backend.backend_pb2 as _pb
@ -48,7 +48,7 @@ class TagManager:
#############################################################
def register(
self, tags: Collection[str], usn: Optional[int] = None, clear=False
self, tags: Collection[str], usn: Optional[int] = None, clear: bool = False
) -> None:
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."
self.clear_unused_tags()
def clear_unused_tags(self):
def clear_unused_tags(self) -> None:
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"
if not children:
query = basequery + " AND c.did=?"
@ -72,7 +72,7 @@ class TagManager:
res = self.col.db.list(query)
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."
self.col._backend.set_tag_collapsed(name=tag, collapsed=collapsed)
@ -139,9 +139,9 @@ class TagManager:
def remFromStr(self, deltags: str, tags: str) -> str:
"Delete tags if they exist."
def wildcard(pat, str):
def wildcard(pat: str, repl: str) -> Match:
pat = re.escape(pat).replace("\\*", ".*")
return re.match("^" + pat + "$", str, re.IGNORECASE)
return re.match("^" + pat + "$", repl, re.IGNORECASE)
currentTags = self.split(tags)
for tag in self.split(deltags):

View file

@ -3,9 +3,7 @@
from __future__ import annotations
# some add-ons expect json to be in the utils module
import json # pylint: disable=unused-import
import locale
import json as _json
import os
import platform
import random
@ -20,7 +18,7 @@ import traceback
from contextlib import contextmanager
from hashlib import sha1
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
@ -34,8 +32,17 @@ try:
from_json_bytes = orjson.loads
except:
print("orjson is missing; DB operations will be slower")
to_json_bytes = lambda obj: json.dumps(obj).encode("utf8") # type: ignore
from_json_bytes = json.loads
to_json_bytes = lambda obj: _json.dumps(obj).encode("utf8") # type: ignore
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
##############################################################################
@ -46,22 +53,6 @@ def intTime(scale: int = 1) -> int:
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
##############################################################################
reComment = re.compile("(?s)<!--.*?-->")
@ -114,7 +105,7 @@ def entsToTxt(html: str) -> str:
# replace it first
html = html.replace("&nbsp;", " ")
def fixup(m):
def fixup(m: Match) -> str:
text = m.group(0)
if text[:2] == "&#":
# 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:
"""Given a list of integers, return a string '(int1,int2,...)'."""
return "(%s)" % ",".join(str(i) for i in ids)
@ -195,23 +178,6 @@ def guid64() -> str:
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
##############################################################################
@ -250,7 +216,7 @@ def tmpdir() -> str:
global _tmpdir
if not _tmpdir:
def cleanup():
def cleanup() -> None:
if os.path.exists(_tmpdir):
shutil.rmtree(_tmpdir)
@ -294,7 +260,7 @@ def noBundledLibs() -> Iterator[None]:
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."
# ensure we don't open a separate window for forking process on windows
if isWin:
@ -338,7 +304,7 @@ devMode = os.getenv("ANKIDEV", "")
invalidFilenameChars = ':*?"<>|'
def invalidFilename(str, dirsep=True) -> Optional[str]:
def invalidFilename(str: str, dirsep: bool = True) -> Optional[str]:
for c in invalidFilenameChars:
if c in str:
return c
@ -384,7 +350,7 @@ class TimedLog:
def __init__(self) -> None:
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]
sys.stderr.write(
"%5dms: %s(): %s\n" % ((time.time() - self._last) * 1000, fn, s)

View file

@ -9,6 +9,14 @@ warn_redundant_casts = True
warn_unused_configs = 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]
ignore_missing_imports = True
[mypy-win32pipe]

View file

@ -44,7 +44,7 @@ py_proto_library_typed = rule(
"mypy_protobuf": attr.label(
executable = True,
cfg = "exec",
default = Label("//pylib/tools:mypy_protobuf"),
default = Label("//pylib/tools:protoc-gen-mypy"),
),
},
)

View file

@ -2,8 +2,8 @@ load("@rules_python//python:defs.bzl", "py_binary", "py_library")
load("@py_deps//:requirements.bzl", "requirement")
py_binary(
name = "mypy_protobuf",
srcs = [requirement("mypy-protobuf").replace(":pkg", ":mypy_protobuf.py")],
name = "protoc-gen-mypy",
srcs = ["protoc-gen-mypy.py"],
visibility = [
"//visibility:public",
],
@ -17,7 +17,7 @@ py_binary(
"//visibility:public",
],
deps = [
":mypy_protobuf",
":protoc-gen-mypy",
"@rules_python//python/runfiles",
],
)

View file

@ -36,8 +36,7 @@ class Hook:
types = []
for arg in self.args or []:
(name, type) = arg.split(":")
if "." in type:
type = '"' + type.strip() + '"'
type = '"' + type.strip() + '"'
types.append(type)
types_str = ", ".join(types)
return f"Callable[[{types_str}], {self.return_type or 'None'}]"

9
pylib/tools/protoc-gen-mypy.py Executable file
View 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())

View file

@ -10,7 +10,7 @@ import os
import sys
import tempfile
import traceback
from typing import Any, Callable, Dict, Optional, Union
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
import anki.lang
from anki import version as _version
@ -102,10 +102,10 @@ class DialogManager:
self._dialogs[name][1] = instance
return instance
def markClosed(self, name: str):
def markClosed(self, name: str) -> 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())
def closeAll(self, onsuccess: Callable[[], None]) -> Optional[bool]:
@ -119,7 +119,7 @@ class DialogManager:
if not instance:
continue
def callback():
def callback() -> None:
if self.allClosed():
onsuccess()
else:
@ -189,12 +189,12 @@ def setupLangAndBackend(
pass
# add _ and ngettext globals used by legacy code
def fn__(arg):
def fn__(arg) -> None:
print("".join(traceback.format_stack()[-2]))
print("_ global will break in the future; please see anki/lang.py")
return arg
def fn_ngettext(a, b, c):
def fn_ngettext(a, b, c) -> None:
print("".join(traceback.format_stack()[-2]))
print("ngettext global will break in the future; please see anki/lang.py")
return b
@ -244,11 +244,11 @@ class AnkiApp(QApplication):
KEY = "anki" + checksum(getpass.getuser())
TMOUT = 30000
def __init__(self, argv):
def __init__(self, argv) -> None:
QApplication.__init__(self, argv)
self._argv = argv
def secondInstance(self):
def secondInstance(self) -> bool:
# we accept only one command line argument. if it's missing, send
# a blank screen to just raise the existing window
opts, args = parseArgs(self._argv)
@ -267,7 +267,7 @@ class AnkiApp(QApplication):
self._srv.listen(self.KEY)
return False
def sendMsg(self, txt):
def sendMsg(self, txt) -> bool:
sock = QLocalSocket(self)
sock.connectToServer(self.KEY, QIODevice.WriteOnly)
if not sock.waitForConnected(self.TMOUT):
@ -286,7 +286,7 @@ class AnkiApp(QApplication):
sock.disconnectFromServer()
return True
def onRecv(self):
def onRecv(self) -> None:
sock = self._srv.nextPendingConnection()
if not sock.waitForReadyRead(self.TMOUT):
sys.stderr.write(sock.errorString())
@ -298,14 +298,14 @@ class AnkiApp(QApplication):
# OS X file/url handler
##################################################
def event(self, evt):
def event(self, evt) -> bool:
if evt.type() == QEvent.FileOpen:
self.appMsg.emit(evt.file() or "raise") # type: ignore
return True
return QApplication.event(self, evt)
def parseArgs(argv):
def parseArgs(argv) -> Tuple[argparse.Namespace, List[str]]:
"Returns (opts, args)."
# py2app fails to strip this in some instances, then anki dies
# as there's no such profile
@ -330,7 +330,7 @@ def parseArgs(argv):
return parser.parse_known_args(argv[1:])
def setupGL(pm):
def setupGL(pm) -> None:
if isMac:
return
@ -343,7 +343,7 @@ def setupGL(pm):
ctypes.CDLL("libGL.so.1", ctypes.RTLD_GLOBAL)
# catch opengl errors
def msgHandler(category, ctx, msg):
def msgHandler(category, ctx, msg) -> None:
if category == QtDebugMsg:
category = "debug"
elif category == QtInfoMsg:
@ -400,7 +400,7 @@ def setupGL(pm):
PROFILE_CODE = os.environ.get("ANKI_PROFILE_CODE")
def write_profile_results():
def write_profile_results() -> None:
profiler.disable()
profiler.dump_stats("anki.prof")
@ -408,7 +408,7 @@ def write_profile_results():
print("use 'bazel run qt:profile' to explore")
def run():
def run() -> None:
try:
_run()
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.
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:
print(f"Anki {appVersion}")
return
return None
elif opts.syncserver:
from anki.syncserver import serve
serve()
return
return None
if PROFILE_CODE:
@ -465,7 +465,7 @@ def _run(argv=None, exec=True):
except AnkiRestart as error:
if error.exitcode:
sys.exit(error.exitcode)
return
return None
except:
# will handle below
traceback.print_exc()
@ -500,7 +500,7 @@ def _run(argv=None, exec=True):
app = AnkiApp(argv)
if app.secondInstance():
# we've signaled the primary instance, so we should close
return
return None
if not pm:
QMessageBox.critical(
@ -508,7 +508,7 @@ def _run(argv=None, exec=True):
tr(TR.QT_MISC_ERROR),
tr(TR.PROFILES_COULD_NOT_CREATE_DATA_FOLDER),
)
return
return None
# disable icons on mac; this must be done before window created
if isMac:
@ -548,7 +548,7 @@ def _run(argv=None, exec=True):
tr(TR.QT_MISC_ERROR),
tr(TR.QT_MISC_NO_TEMP_FOLDER),
)
return
return None
if pmLoadResult.firstTime:
pm.setDefaultLang(lang[0])
@ -590,3 +590,5 @@ def _run(argv=None, exec=True):
if PROFILE_CODE:
write_profile_results()
return None

View file

@ -13,20 +13,20 @@ from aqt.utils import TR, disable_help_button, supportText, tooltip, tr
class ClosableQDialog(QDialog):
def reject(self):
def reject(self) -> None:
aqt.dialogs.markClosed("About")
QDialog.reject(self)
def accept(self):
def accept(self) -> None:
aqt.dialogs.markClosed("About")
QDialog.accept(self)
def closeWithCallback(self, callback):
def closeWithCallback(self, callback) -> None:
self.reject()
callback()
def show(mw):
def show(mw) -> QDialog:
dialog = ClosableQDialog(mw)
disable_help_button(dialog)
mw.setupDialogGC(dialog)
@ -55,7 +55,7 @@ def show(mw):
modified = "mod"
return f"{name} ['{addon.dir_name}', {installed}, '{addon.human_version}', {modified}]"
def onCopy():
def onCopy() -> None:
addmgr = mw.addonManager
active = []
activeids = []

View file

@ -65,7 +65,7 @@ class AddCards(QDialog):
)
self.deckChooser = aqt.deckchooser.DeckChooser(self.mw, self.form.deckArea)
def helpRequested(self):
def helpRequested(self) -> None:
openHelp(HelpPage.ADDING_CARD_AND_NOTE)
def setupButtons(self) -> None:
@ -137,7 +137,7 @@ class AddCards(QDialog):
def removeTempNote(self, note: Note) -> None:
print("removeTempNote() will go away")
def addHistory(self, note):
def addHistory(self, note: Note) -> None:
self.history.insert(0, note.id)
self.history = self.history[:15]
self.historyButton.setEnabled(True)
@ -161,7 +161,7 @@ class AddCards(QDialog):
gui_hooks.add_cards_will_show_history_menu(self, m)
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),))
def addNote(self, note) -> Optional[Note]:
@ -186,10 +186,10 @@ class AddCards(QDialog):
gui_hooks.add_cards_did_add_note(note)
return note
def addCards(self):
def addCards(self) -> None:
self.editor.saveNow(self._addCards)
def _addCards(self):
def _addCards(self) -> None:
self.editor.saveAddModeVars()
if not self.addNote(self.editor.note):
return
@ -202,7 +202,7 @@ class AddCards(QDialog):
self.onReset(keep=True)
self.mw.col.autosave()
def keyPressEvent(self, evt):
def keyPressEvent(self, evt: QKeyEvent) -> None:
"Show answer on RET or register answer."
if evt.key() in (Qt.Key_Enter, Qt.Key_Return) and self.editor.tags.hasFocus():
evt.accept()
@ -225,7 +225,7 @@ class AddCards(QDialog):
QDialog.reject(self)
def ifCanClose(self, onOk: Callable) -> None:
def afterSave():
def afterSave() -> None:
ok = self.editor.fieldsAreBlank(self.previousNote) or askUser(
tr(TR.ADDING_CLOSE_AND_LOSE_CURRENT_INPUT), defaultno=True
)
@ -234,8 +234,8 @@ class AddCards(QDialog):
self.editor.saveNow(afterSave)
def closeWithCallback(self, cb):
def doClose():
def closeWithCallback(self, cb) -> None:
def doClose() -> None:
self._reject()
cb()

View file

@ -605,7 +605,7 @@ class AddonManager:
def _addon_schema_path(self, dir: str) -> str:
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)
try:
if not os.path.exists(path):
@ -867,9 +867,10 @@ class AddonsDialog(QDialog):
def onInstallFiles(self, paths: Optional[List[str]] = None) -> Optional[bool]:
if not paths:
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
)
paths = paths_ # type: ignore
if not paths:
return False

File diff suppressed because it is too large Load diff

View file

@ -4,7 +4,7 @@
import copy
import json
import re
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Match, Optional
import aqt
from anki.cards import Card
@ -14,6 +14,7 @@ from anki.lang import without_unicode_isolation
from anki.notes import Note
from anki.template import TemplateRenderContext
from aqt import AnkiQt, gui_hooks
from aqt.forms.browserdisp import Ui_Dialog
from aqt.qt import *
from aqt.schema_change_tracker import ChangeTracker
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
self.setFocus()
def redraw_everything(self):
def redraw_everything(self) -> None:
self.ignore_change_signals = True
self.updateTopArea()
self.ignore_change_signals = False
@ -104,13 +105,13 @@ class CardLayout(QDialog):
self.fill_fields_from_template()
self.renderPreview()
def _isCloze(self):
def _isCloze(self) -> bool:
return self.model["type"] == MODEL_CLOZE
# Top area
##########################################################################
def setupTopArea(self):
def setupTopArea(self) -> None:
self.topArea = QWidget()
self.topArea.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
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))
def updateTopArea(self):
def updateTopArea(self) -> None:
self.updateCardNames()
def updateCardNames(self):
def updateCardNames(self) -> None:
self.ignore_change_signals = True
combo = self.topAreaForm.templatesBox
combo.clear()
@ -139,7 +140,7 @@ class CardLayout(QDialog):
combo.setEnabled(not self._isCloze())
self.ignore_change_signals = False
def _summarizedName(self, idx: int, tmpl: Dict):
def _summarizedName(self, idx: int, tmpl: Dict) -> str:
return "{}: {}: {} -> {}".format(
idx + 1,
tmpl["name"],
@ -170,7 +171,7 @@ class CardLayout(QDialog):
s += "+..."
return s
def setupShortcuts(self):
def setupShortcuts(self) -> None:
self.tform.front_button.setToolTip(shortcut("Ctrl+1"))
self.tform.back_button.setToolTip(shortcut("Ctrl+2"))
self.tform.style_button.setToolTip(shortcut("Ctrl+3"))
@ -193,7 +194,7 @@ class CardLayout(QDialog):
# Main area setup
##########################################################################
def setupMainArea(self):
def setupMainArea(self) -> None:
split = self.mainArea = QSplitter()
split.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
split.setOrientation(Qt.Horizontal)
@ -216,7 +217,7 @@ class CardLayout(QDialog):
split.addWidget(right)
split.setCollapsible(1, False)
def setup_edit_area(self):
def setup_edit_area(self) -> None:
tform = self.tform
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.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)
self.pform.cloze_number_combo.addItems(names)
try:
@ -266,7 +267,7 @@ class CardLayout(QDialog):
self.have_autoplayed = False
self._renderPreview()
def on_editor_toggled(self):
def on_editor_toggled(self) -> None:
if self.tform.front_button.isChecked():
self.current_editor_index = 0
self.pform.preview_front.setChecked(True)
@ -283,7 +284,7 @@ class CardLayout(QDialog):
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
if not editor.find(text):
# try again from top
@ -293,11 +294,11 @@ class CardLayout(QDialog):
if not editor.find(text):
tooltip("No matches found.")
def on_search_next(self):
def on_search_next(self) -> None:
text = self.tform.search_edit.text()
self.on_search_changed(text)
def setup_preview(self):
def setup_preview(self) -> None:
pform = self.pform
self.preview_web = AnkiWebView(title="card layout")
pform.verticalLayout.addWidget(self.preview_web)
@ -336,19 +337,19 @@ class CardLayout(QDialog):
self.cloze_numbers = []
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.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.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.on_preview_toggled()
def on_preview_settings(self):
def on_preview_settings(self) -> None:
m = QMenu(self)
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)))
def on_preview_toggled(self):
def on_preview_toggled(self) -> None:
self.have_autoplayed = False
self._renderPreview()
@ -388,7 +389,7 @@ class CardLayout(QDialog):
# Buttons
##########################################################################
def setupButtons(self):
def setupButtons(self) -> None:
l = self.buttons = QHBoxLayout()
help = QPushButton(tr(TR.ACTIONS_HELP))
help.setAutoDefault(False)
@ -424,7 +425,7 @@ class CardLayout(QDialog):
return self.templates[0]
return self.templates[self.ord]
def fill_fields_from_template(self):
def fill_fields_from_template(self) -> None:
t = self.current_template()
self.ignore_change_signals = True
@ -438,7 +439,7 @@ class CardLayout(QDialog):
self.tform.edit_area.setPlainText(text)
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:
return
@ -458,14 +459,14 @@ class CardLayout(QDialog):
# Preview
##########################################################################
_previewTimer = None
_previewTimer: Optional[QTimer] = None
def renderPreview(self):
def renderPreview(self) -> None:
# schedule a preview when timing stops
self.cancelPreviewTimer()
self._previewTimer = self.mw.progress.timer(200, self._renderPreview, False)
def cancelPreviewTimer(self):
def cancelPreviewTimer(self) -> None:
if self._previewTimer:
self._previewTimer.stop()
self._previewTimer = None
@ -512,14 +513,14 @@ class CardLayout(QDialog):
self.updateCardNames()
def maybeTextInput(self, txt, type="q"):
def maybeTextInput(self, txt: str, type: str = "q") -> str:
if "[[type:" not in txt:
return txt
origLen = len(txt)
txt = txt.replace("<hr id=answer>", "")
hadHR = origLen != len(txt)
def answerRepl(match):
def answerRepl(match: Match) -> str:
res = self.mw.reviewer.correct("exomple", "an example")
if hadHR:
res = "<hr id=answer>" + res
@ -555,14 +556,15 @@ class CardLayout(QDialog):
# Card operations
######################################################################
def onRemove(self):
def onRemove(self) -> None:
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)
def on_done(fut):
def on_done(fut) -> None:
card_cnt = fut.result()
template = self.current_template()
@ -592,7 +594,7 @@ class CardLayout(QDialog):
self.redraw_everything()
def onRename(self):
def onRename(self) -> None:
template = self.current_template()
name = getOnlyText(tr(TR.ACTIONS_NEW_NAME), default=template["name"]).replace(
'"', ""
@ -603,18 +605,18 @@ class CardLayout(QDialog):
template["name"] = name
self.redraw_everything()
def onReorder(self):
def onReorder(self) -> None:
n = len(self.templates)
template = self.current_template()
current_pos = self.templates.index(template) + 1
pos = getOnlyText(
pos_txt = getOnlyText(
tr(TR.CARD_TEMPLATES_ENTER_NEW_CARD_POSITION_1, val=n),
default=str(current_pos),
)
if not pos:
if not pos_txt:
return
try:
pos = int(pos)
pos = int(pos_txt)
except ValueError:
return
if pos < 1 or pos > n:
@ -628,7 +630,7 @@ class CardLayout(QDialog):
self.ord = new_idx
self.redraw_everything()
def _newCardName(self):
def _newCardName(self) -> str:
n = len(self.templates) + 1
while 1:
name = without_unicode_isolation(tr(TR.CARD_TEMPLATES_CARD, val=n))
@ -637,7 +639,7 @@ class CardLayout(QDialog):
n += 1
return name
def onAddCard(self):
def onAddCard(self) -> None:
cnt = self.mw.col.models.useCount(self.model)
txt = tr(TR.CARD_TEMPLATES_THIS_WILL_CREATE_CARD_PROCEED, count=cnt)
if not askUser(txt):
@ -653,12 +655,12 @@ class CardLayout(QDialog):
self.ord = len(self.templates) - 1
self.redraw_everything()
def onFlip(self):
def onFlip(self) -> None:
old = self.current_template()
self._flipQA(old, old)
self.redraw_everything()
def _flipQA(self, src, dst):
def _flipQA(self, src, dst) -> None:
m = re.match("(?s)(.+)<hr id=answer>(.+)", src["afmt"])
if not m:
showInfo(tr(TR.CARD_TEMPLATES_ANKI_COULDNT_FIND_THE_LINE_BETWEEN))
@ -666,9 +668,8 @@ class CardLayout(QDialog):
self.change_tracker.mark_basic()
dst["afmt"] = "{{FrontSide}}\n\n<hr id=answer>\n\n%s" % src["qfmt"]
dst["qfmt"] = m.group(2).strip()
return True
def onMore(self):
def onMore(self) -> None:
m = QMenu(self)
if not self._isCloze():
@ -699,7 +700,7 @@ class CardLayout(QDialog):
m.exec_(self.topAreaForm.templateOptions.mapToGlobal(QPoint(0, 0)))
def onBrowserDisplay(self):
def onBrowserDisplay(self) -> None:
d = QDialog()
disable_help_button(d)
f = aqt.forms.browserdisp.Ui_Dialog()
@ -714,7 +715,7 @@ class CardLayout(QDialog):
qconnect(f.buttonBox.accepted, lambda: self.onBrowserDisplayOk(f))
d.exec_()
def onBrowserDisplayOk(self, f):
def onBrowserDisplayOk(self, f: Ui_Dialog) -> None:
t = self.current_template()
self.change_tracker.mark_basic()
t["bqfmt"] = f.qfmt.text().strip()
@ -727,7 +728,7 @@ class CardLayout(QDialog):
if key in t:
del t[key]
def onTargetDeck(self):
def onTargetDeck(self) -> None:
from aqt.tagedit import TagEdit
t = self.current_template()
@ -759,7 +760,7 @@ class CardLayout(QDialog):
else:
t["did"] = self.col.decks.id(te.text())
def onAddField(self):
def onAddField(self) -> None:
diag = QDialog(self)
form = aqt.forms.addfield.Ui_Dialog()
form.setupUi(diag)
@ -779,7 +780,7 @@ class CardLayout(QDialog):
form.size.value(),
)
def _addField(self, field, font, size):
def _addField(self, field, font, size) -> None:
text = self.tform.edit_area.toPlainText()
text += "\n<div style='font-family: %s; font-size: %spx;'>{{%s}}</div>\n" % (
font,
@ -794,10 +795,10 @@ class CardLayout(QDialog):
######################################################################
def accept(self) -> None:
def save():
def save() -> None:
self.mm.save(self.model)
def on_done(fut):
def on_done(fut) -> None:
try:
fut.result()
except TemplateError as e:
@ -828,5 +829,5 @@ class CardLayout(QDialog):
self.rendered_card = None
self.mw = None
def onHelp(self):
def onHelp(self) -> None:
openHelp(HelpPage.TEMPLATES)

View file

@ -1,6 +1,7 @@
# Copyright: Ankitects Pty Ltd and contributors
# -*- coding: utf-8 -*-
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import aqt
from anki.collection import SearchTerm
from anki.consts import *
@ -35,7 +36,7 @@ class CustomStudy(QDialog):
f.radioNew.click()
self.exec_()
def setupSignals(self):
def setupSignals(self) -> None:
f = self.form
qconnect(f.radioNew.clicked, lambda: self.onRadioChange(RADIO_NEW))
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.radioCram.clicked, lambda: self.onRadioChange(RADIO_CRAM))
def onRadioChange(self, idx):
def onRadioChange(self, idx: int) -> None:
f = self.form
sp = f.spin
smin = 1
@ -56,7 +57,7 @@ class CustomStudy(QDialog):
typeShow = False
ok = tr(TR.CUSTOM_STUDY_OK)
def plus(num):
def plus(num) -> str:
if num == 1000:
num = "1000+"
return "<b>" + str(num) + "</b>"
@ -123,7 +124,7 @@ class CustomStudy(QDialog):
f.buttonBox.button(QDialogButtonBox.Ok).setText(ok)
self.radioIdx = idx
def accept(self):
def accept(self) -> None:
f = self.form
i = self.radioIdx
spin = f.spin.value()
@ -132,13 +133,15 @@ class CustomStudy(QDialog):
self.mw.col.decks.save(self.deck)
self.mw.col.sched.extendLimits(spin, 0)
self.mw.reset()
return QDialog.accept(self)
QDialog.accept(self)
return
elif i == RADIO_REV:
self.deck["extendRev"] = spin
self.mw.col.decks.save(self.deck)
self.mw.col.sched.extendLimits(0, spin)
self.mw.reset()
return QDialog.accept(self)
QDialog.accept(self)
return
elif i == RADIO_CRAM:
tags = self._getTags()
# the rest create a filtered deck
@ -146,7 +149,8 @@ class CustomStudy(QDialog):
if cur:
if not cur["dyn"]:
showInfo(tr(TR.CUSTOM_STUDY_MUST_RENAME_DECK))
return QDialog.accept(self)
QDialog.accept(self)
return
else:
# safe to empty
self.mw.col.sched.empty_filtered_deck(cur["id"])
@ -211,7 +215,8 @@ class CustomStudy(QDialog):
# generate cards
self.created_custom_study = True
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")
QDialog.accept(self)
@ -222,7 +227,7 @@ class CustomStudy(QDialog):
# fixme: clean up the empty custom study deck
QDialog.reject(self)
def _getTags(self):
def _getTags(self) -> str:
from aqt.taglimit import TagLimit
return TagLimit(self.mw, self).tags

View file

@ -1,4 +1,5 @@
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")
compile_sass(
@ -16,11 +17,21 @@ copy_file(
out = "core.css",
)
copy_page(
name = "editor",
srcs = [
"editor.css",
"editable.css",
],
package = "//ts/editor",
)
filegroup(
name = "css",
srcs = [
"core.css",
"css_local",
"editor",
],
visibility = ["//qt:__subpackages__"],
)

View file

@ -1,9 +1,13 @@
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:eslint.bzl", "eslint_test")
ts_library(
name = "pycmd",
srcs = ["pycmd.d.ts"],
visibility = ["//qt/aqt/data/web/js:__subpackages__"],
)
ts_library(
@ -26,10 +30,19 @@ filegroup(
output_group = "es5_sources",
)
copy_page(
name = "editor",
srcs = [
"editor.js",
],
package = "//ts/editor",
)
filegroup(
name = "js",
srcs = [
"aqt_es5",
"editor",
"mathjax.js",
"//qt/aqt/data/web/js/vendor",
],
@ -47,4 +60,7 @@ prettier_test(
# srcs = glob(["*.ts"]),
# )
exports_files(["mathjax.js"])
exports_files([
"mathjax.js",
"tsconfig.json",
])

View file

@ -9,7 +9,7 @@ from aqt.qt import *
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()
if progress.kind != ProgressKind.DatabaseCheck:
return
@ -24,14 +24,14 @@ def on_progress(mw: aqt.main.AnkiQt):
def check_db(mw: aqt.AnkiQt) -> None:
def on_timer():
def on_timer() -> None:
on_progress(mw)
timer = QTimer(mw)
qconnect(timer.timeout, on_timer)
timer.start(100)
def on_future_done(fut):
def on_future_done(fut) -> None:
timer.stop()
ret, ok = fut.result()

View file

@ -19,7 +19,7 @@ from aqt.utils import TR, askUser, getOnlyText, openLink, shortcut, showWarning,
class DeckBrowserBottomBar:
def __init__(self, deck_browser: DeckBrowser):
def __init__(self, deck_browser: DeckBrowser) -> None:
self.deck_browser = deck_browser
@ -51,14 +51,14 @@ class DeckBrowser:
self.bottom = BottomBar(mw, mw.bottomWeb)
self.scrollPos = QPoint(0, 0)
def show(self):
def show(self) -> None:
av_player.stop_and_clear_queue()
self.web.set_bridge_command(self._linkHandler, self)
self._renderPage()
# redraw top bar for theme change
self.mw.toolbar.redraw()
def refresh(self):
def refresh(self) -> None:
self._renderPage()
# Event handlers
@ -90,7 +90,7 @@ class DeckBrowser:
self._collapse(int(arg))
return False
def _selDeck(self, did):
def _selDeck(self, did) -> None:
self.mw.col.decks.select(did)
self.mw.onOverview()
@ -108,14 +108,14 @@ class DeckBrowser:
</center>
"""
def _renderPage(self, reuse=False):
def _renderPage(self, reuse=False) -> None:
if not reuse:
self._dueTree = self.mw.col.sched.deck_due_tree()
self.__renderPage(None)
return
self.web.evalWithCallback("window.pageYOffset", self.__renderPage)
def __renderPage(self, offset):
def __renderPage(self, offset) -> None:
content = DeckBrowserContent(
tree=self._renderDeckTree(self._dueTree),
stats=self._renderStats(),
@ -137,10 +137,10 @@ class DeckBrowser:
self._scrollToOffset(offset)
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)
def _renderStats(self):
def _renderStats(self) -> str:
return '<div id="studiedToday"><span>{}</span></div>'.format(
self.mw.col.studied_today(),
)
@ -170,7 +170,7 @@ class DeckBrowser:
due = node.review_count + node.learn_count
def indent():
def indent() -> str:
return "&nbsp;" * 6 * (node.level - 1)
if node.deck_id == ctx.current_deck_id:
@ -202,7 +202,7 @@ class DeckBrowser:
node.name,
)
# due counts
def nonzeroColour(cnt, klass):
def nonzeroColour(cnt: int, klass: str) -> str:
if not cnt:
klass = "zero-count"
return f'<span class="{klass}">{cnt}</span>'
@ -222,7 +222,7 @@ class DeckBrowser:
buf += self._render_deck_node(child, ctx)
return buf
def _topLevelDragRow(self):
def _topLevelDragRow(self) -> str:
return "<tr class='top-level-drag-row'><td colspan='6'>&nbsp;</td></tr>"
# Options
@ -235,13 +235,13 @@ class DeckBrowser:
a = m.addAction(tr(TR.ACTIONS_OPTIONS))
qconnect(a.triggered, lambda b, did=did: self._options(did))
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))
qconnect(a.triggered, lambda b, did=did: self._delete(int(did)))
gui_hooks.deck_browser_will_show_options_menu(m, int(did))
m.exec_(QCursor.pos())
def _export(self, did):
def _export(self, did: int) -> None:
self.mw.onExport(did=did)
def _rename(self, did: int) -> None:
@ -256,10 +256,11 @@ class DeckBrowser:
self.mw.col.decks.rename(deck, newName)
gui_hooks.sidebar_should_refresh_decks()
except DeckRenameError as e:
return showWarning(e.description)
showWarning(e.description)
return
self.show()
def _options(self, did):
def _options(self, did) -> None:
# select the deck first, because the dyn deck conf assumes the deck
# we're editing is the current one
self.mw.col.decks.select(did)
@ -296,10 +297,10 @@ class DeckBrowser:
def _delete(self, did: int) -> None:
if self.ask_delete_deck(did):
def do_delete():
def do_delete() -> None:
return self.mw.col.decks.rem(did, True)
def on_done(fut: Future):
def on_done(fut: Future) -> None:
self.show()
res = fut.result() # Required to check for errors
@ -315,7 +316,7 @@ class DeckBrowser:
["Ctrl+Shift+I", "import", tr(TR.DECKS_IMPORT_FILE)],
]
def _drawButtons(self):
def _drawButtons(self) -> None:
buf = ""
drawLinks = deepcopy(self.drawLinks)
for b in drawLinks:
@ -331,5 +332,5 @@ class DeckBrowser:
web_context=DeckBrowserBottomBar(self),
)
def _onShared(self):
def _onShared(self) -> None:
openLink(aqt.appShared + "decks/")

View file

@ -2,12 +2,13 @@
# -*- coding: utf-8 -*-
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from operator import itemgetter
from typing import Any, Dict
from typing import Any, Dict, Optional
from PyQt5.QtWidgets import QLineEdit
import aqt
from anki.consts import NEW_CARDS_RANDOM
from anki.decks import DeckConfig
from anki.lang import without_unicode_isolation
from aqt import gui_hooks
from aqt.qt import *
@ -28,7 +29,7 @@ from aqt.utils import (
class DeckConf(QDialog):
def __init__(self, mw: aqt.AnkiQt, deck: Dict):
def __init__(self, mw: aqt.AnkiQt, deck: Dict) -> None:
QDialog.__init__(self, mw)
self.mw = mw
self.deck = deck
@ -60,7 +61,7 @@ class DeckConf(QDialog):
self.exec_()
saveGeom(self, "deckconf")
def setupCombos(self):
def setupCombos(self) -> None:
import anki.consts as cs
f = self.form
@ -70,12 +71,12 @@ class DeckConf(QDialog):
# Conf list
######################################################################
def setupConfs(self):
def setupConfs(self) -> None:
qconnect(self.form.dconf.currentIndexChanged, self.onConfChange)
self.conf = None
self.conf: Optional[DeckConfig] = None
self.loadConfs()
def loadConfs(self):
def loadConfs(self) -> None:
current = self.deck["conf"]
self.confList = self.mw.col.decks.allConf()
self.confList.sort(key=itemgetter("name"))
@ -92,7 +93,7 @@ class DeckConf(QDialog):
self._origNewOrder = self.confList[startOn]["new"]["order"]
self.onConfChange(startOn)
def confOpts(self):
def confOpts(self) -> None:
m = QMenu(self.mw)
a = m.addAction(tr(TR.ACTIONS_ADD))
qconnect(a.triggered, self.addGroup)
@ -106,7 +107,7 @@ class DeckConf(QDialog):
a.setEnabled(False)
m.exec_(QCursor.pos())
def onConfChange(self, idx):
def onConfChange(self, idx) -> None:
if self.ignoreConfChange:
return
if self.conf:
@ -159,7 +160,7 @@ class DeckConf(QDialog):
self.saveConf()
self.loadConfs()
def setChildren(self):
def setChildren(self) -> None:
if not askUser(tr(TR.SCHEDULING_SET_ALL_DECKS_BELOW_TO, val=self.deck["name"])):
return
for did in self.childDids:
@ -173,8 +174,8 @@ class DeckConf(QDialog):
# Loading
##################################################
def listToUser(self, l):
def num_to_user(n: Union[int, float]):
def listToUser(self, l) -> str:
def num_to_user(n: Union[int, float]) -> str:
if n == round(n):
return str(int(n))
else:
@ -182,7 +183,7 @@ class DeckConf(QDialog):
return " ".join(map(num_to_user, l))
def parentLimText(self, type="new"):
def parentLimText(self, type="new") -> str:
# top level?
if "::" not in self.deck["name"]:
return ""
@ -196,7 +197,7 @@ class DeckConf(QDialog):
lim = min(x, 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"])
# new
c = self.conf["new"]
@ -238,7 +239,7 @@ class DeckConf(QDialog):
f.desc.setPlainText(self.deck["desc"])
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.col.decks.restoreToDefault(self.conf)
self.mw.progress.finish()
@ -247,7 +248,7 @@ class DeckConf(QDialog):
# New order
##################################################
def onNewOrderChanged(self, new):
def onNewOrderChanged(self, new) -> None:
old = self.conf["new"]["order"]
if old == new:
return
@ -280,7 +281,7 @@ class DeckConf(QDialog):
return
conf[key] = ret
def saveConf(self):
def saveConf(self) -> None:
# new
c = self.conf["new"]
f = self.form
@ -324,10 +325,10 @@ class DeckConf(QDialog):
self.mw.col.decks.save(self.deck)
self.mw.col.decks.save(self.conf)
def reject(self):
def reject(self) -> None:
self.accept()
def accept(self):
def accept(self) -> None:
self.saveConf()
self.mw.reset()
QDialog.accept(self)

View file

@ -34,7 +34,7 @@ class DeckConf(QDialog):
search: Optional[str] = None,
search_2: Optional[str] = None,
deck: Optional[Deck] = None,
):
) -> None:
"""If 'deck' is an existing filtered deck, load and modify its settings.
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.selectAll()
def initialSetup(self):
def initialSetup(self) -> None:
import anki.consts as cs
self.form.order.addItems(list(cs.dynOrderLabels(self.mw.col).values()))
@ -165,12 +165,12 @@ class DeckConf(QDialog):
else:
aqt.dialogs.open("Browser", self.mw, search=(search,))
def _onReschedToggled(self, _state: int):
def _onReschedToggled(self, _state: int) -> None:
self.form.previewDelayWidget.setVisible(
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
d = deck or self.deck
@ -205,7 +205,7 @@ class DeckConf(QDialog):
f.secondFilter.setChecked(False)
f.filter2group.setVisible(False)
def saveConf(self):
def saveConf(self) -> None:
f = self.form
d = self.deck
@ -235,7 +235,7 @@ class DeckConf(QDialog):
self.mw.col.decks.save(d)
def reject(self):
def reject(self) -> None:
if self.did:
self.mw.col.decks.rem(self.did)
self.mw.col.decks.select(self.old_deck["id"])
@ -243,7 +243,7 @@ class DeckConf(QDialog):
QDialog.reject(self)
aqt.dialogs.markClosed("DynDeckConfDialog")
def accept(self):
def accept(self) -> None:
try:
self.saveConf()
except InvalidInput as err:

View file

@ -48,14 +48,14 @@ class EditCurrent(QDialog):
return
self.editor.setNote(n)
def reopen(self, mw):
def reopen(self, mw) -> None:
tooltip("Please finish editing the existing card first.")
self.onReset()
def reject(self):
def reject(self) -> None:
self.saveAndClose()
def saveAndClose(self):
def saveAndClose(self) -> None:
self.editor.saveNow(self._saveAndClose)
def _saveAndClose(self) -> None:
@ -74,8 +74,8 @@ class EditCurrent(QDialog):
aqt.dialogs.markClosed("EditCurrent")
QDialog.reject(self)
def closeWithCallback(self, onsuccess):
def callback():
def closeWithCallback(self, onsuccess) -> None:
def callback() -> None:
self._saveAndClose()
onsuccess()

View file

@ -12,7 +12,7 @@ import urllib.parse
import urllib.request
import warnings
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 requests
@ -92,7 +92,9 @@ _html = """
# caller is responsible for resetting note on reset
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.widget = widget
self.parentWindow = parentWindow
@ -110,7 +112,7 @@ class Editor:
# Initial setup
############################################################
def setupOuter(self):
def setupOuter(self) -> None:
l = QVBoxLayout()
l.setContentsMargins(0, 0, 0, 0)
l.setSpacing(0)
@ -229,7 +231,7 @@ class Editor:
# Top buttons
######################################################################
def resourceToData(self, path):
def resourceToData(self, path: str) -> str:
"""Convert a file (specified by a path) into a data URI."""
if not os.path.exists(path):
raise FileNotFoundError
@ -251,21 +253,21 @@ class Editor:
keys: str = None,
disables: bool = True,
rightside: bool = True,
):
) -> str:
"""Assign func to bridge cmd, register shortcut, return button"""
if func:
self._links[cmd] = func
if keys:
def on_activated():
def on_activated() -> None:
func(self)
if toggleable:
# generate a random id for triggering toggle
id = id or str(randrange(1_000_000))
def on_hotkey():
def on_hotkey() -> None:
on_activated()
self.web.eval(f'toggleEditorButton("#{id}");')
@ -383,26 +385,26 @@ class Editor:
keys, fn, _ = row
QShortcut(QKeySequence(keys), self.widget, activated=fn) # type: ignore
def _addFocusCheck(self, fn):
def checkFocus():
def _addFocusCheck(self, fn: Callable) -> Callable:
def checkFocus() -> None:
if self.currentField is None:
return
fn()
return checkFocus
def onFields(self):
def onFields(self) -> None:
self.saveNow(self._onFields)
def _onFields(self):
def _onFields(self) -> None:
from aqt.fields import FieldDialog
FieldDialog(self.mw, self.note.model(), parent=self.parentWindow)
def onCardLayout(self):
def onCardLayout(self) -> None:
self.saveNow(self._onCardLayout)
def _onCardLayout(self):
def _onCardLayout(self) -> None:
from aqt.clayout import CardLayout
if self.card:
@ -422,16 +424,16 @@ class Editor:
# JS->Python bridge
######################################################################
def onBridgeCmd(self, cmd) -> None:
def onBridgeCmd(self, cmd: str) -> None:
if not self.note:
# shutdown
return
# focus lost or key/button pressed?
if cmd.startswith("blur") or cmd.startswith("key"):
(type, ord, nid, txt) = cmd.split(":", 3)
ord = int(ord)
(type, ord_str, nid_str, txt) = cmd.split(":", 3)
ord = int(ord_str)
try:
nid = int(nid)
nid = int(nid_str)
except ValueError:
nid = 0
if nid != self.note.id:
@ -465,13 +467,15 @@ class Editor:
else:
print("uncaught cmd", cmd)
def mungeHTML(self, txt):
def mungeHTML(self, txt: str) -> str:
return gui_hooks.editor_will_munge_html(txt, self)
# 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."
self.note = note
self.currentField = None
@ -482,10 +486,10 @@ class Editor:
if hide:
self.widget.hide()
def loadNoteKeepingFocus(self):
def loadNoteKeepingFocus(self) -> None:
self.loadNote(self.currentField)
def loadNote(self, focusTo=None) -> None:
def loadNote(self, focusTo: Optional[int] = None) -> None:
if not self.note:
return
@ -496,7 +500,7 @@ class Editor:
self.widget.show()
self.updateTags()
def oncallback(arg):
def oncallback(arg: Any) -> None:
if not self.note:
return
self.setupForegroundButton()
@ -520,7 +524,7 @@ class Editor:
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()."
if not self.note:
# calling code may not expect the callback to fire immediately
@ -529,7 +533,7 @@ class Editor:
self.saveTags()
self.web.evalWithCallback("saveNow(%d)" % keepFocus, lambda res: callback())
def checkValid(self):
def checkValid(self) -> None:
cols = [""] * len(self.note.fields)
err = self.note.dupeOrEmpty()
if err == 2:
@ -537,7 +541,7 @@ class Editor:
self.web.eval("setBackgrounds(%s);" % json.dumps(cols))
def showDupes(self):
def showDupes(self) -> None:
aqt.dialogs.open(
"Browser",
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:
return True
m = self.note.model()
@ -564,7 +568,7 @@ class Editor:
return False
return True
def cleanup(self):
def cleanup(self) -> None:
self.setNote(None)
# prevent any remaining evalWithCallback() events from firing after C++ object deleted
self.web = None
@ -572,11 +576,11 @@ class Editor:
# HTML editing
######################################################################
def onHtmlEdit(self):
def onHtmlEdit(self) -> None:
field = self.currentField
self.saveNow(lambda: self._onHtmlEdit(field))
def _onHtmlEdit(self, field):
def _onHtmlEdit(self, field: int) -> None:
d = QDialog(self.widget, Qt.Window)
form = aqt.forms.edithtml.Ui_Dialog()
form.setupUi(d)
@ -609,7 +613,7 @@ class Editor:
# Tag handling
######################################################################
def setupTags(self):
def setupTags(self) -> None:
import aqt.tagedit
g = QGroupBox(self.widget)
@ -631,7 +635,7 @@ class Editor:
g.setLayout(tb)
self.outerLayout.addWidget(g)
def updateTags(self):
def updateTags(self) -> None:
if self.tags.col != self.mw.col:
self.tags.setCol(self.mw.col)
if not self.tags.text() or not self.addMode:
@ -645,44 +649,44 @@ class Editor:
self.note.flush()
gui_hooks.editor_did_update_tags(self.note)
def saveAddModeVars(self):
def saveAddModeVars(self) -> None:
if self.addMode:
# save tags to model
m = self.note.model()
m["tags"] = self.note.tags
self.mw.col.models.save(m, updateReqs=False)
def hideCompleters(self):
def hideCompleters(self) -> None:
self.tags.hideCompleter()
def onFocusTags(self):
def onFocusTags(self) -> None:
self.tags.setFocus()
# Format buttons
######################################################################
def toggleBold(self):
def toggleBold(self) -> None:
self.web.eval("setFormat('bold');")
def toggleItalic(self):
def toggleItalic(self) -> None:
self.web.eval("setFormat('italic');")
def toggleUnderline(self):
def toggleUnderline(self) -> None:
self.web.eval("setFormat('underline');")
def toggleSuper(self):
def toggleSuper(self) -> None:
self.web.eval("setFormat('superscript');")
def toggleSub(self):
def toggleSub(self) -> None:
self.web.eval("setFormat('subscript');")
def removeFormat(self):
def removeFormat(self) -> None:
self.web.eval("setFormat('removeFormat');")
def onCloze(self):
def onCloze(self) -> None:
self.saveNow(self._onCloze, keepFocus=True)
def _onCloze(self):
def _onCloze(self) -> None:
# check that the model is set up for cloze deletion
if self.note.model()["type"] != MODEL_CLOZE:
if self.addMode:
@ -706,16 +710,16 @@ class Editor:
# Foreground colour
######################################################################
def setupForegroundButton(self):
def setupForegroundButton(self) -> None:
self.fcolour = self.mw.pm.profile.get("lastColour", "#00f")
self.onColourChanged()
# use last colour
def onForeground(self):
def onForeground(self) -> None:
self._wrapWithColour(self.fcolour)
# choose new colour
def onChangeCol(self):
def onChangeCol(self) -> None:
if isLin:
new = QColorDialog.getColor(
QColor(self.fcolour), None, None, QColorDialog.DontUseNativeDialog
@ -729,32 +733,38 @@ class Editor:
self.onColourChanged()
self._wrapWithColour(self.fcolour)
def _updateForegroundButton(self):
def _updateForegroundButton(self) -> None:
self.web.eval("setFGButton('%s')" % self.fcolour)
def onColourChanged(self):
def onColourChanged(self) -> None:
self._updateForegroundButton()
self.mw.pm.profile["lastColour"] = self.fcolour
def _wrapWithColour(self, colour):
def _wrapWithColour(self, colour: str) -> None:
self.web.eval("setFormat('forecolor', '%s')" % colour)
# Audio/video/images
######################################################################
def onAddMedia(self):
def onAddMedia(self) -> None:
extension_filter = " ".join(
"*." + extension for extension in sorted(itertools.chain(pics, audio))
)
key = tr(TR.EDITING_MEDIA) + " (" + extension_filter + ")"
def accept(file):
def accept(file: str) -> None:
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()
def addMedia(self, path, canDelete=False):
def addMedia(self, path: str, canDelete: bool = False) -> None:
try:
html = self._addMedia(path, canDelete)
except Exception as e:
@ -762,7 +772,7 @@ class Editor:
return
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."
# copy to media folder
fname = self.mw.col.media.addFile(path)
@ -779,7 +789,7 @@ class Editor:
def _addMediaFromData(self, fname: str, data: bytes) -> str:
return self.mw.col.media.writeData(fname, data)
def onRecSound(self):
def onRecSound(self) -> None:
aqt.sound.record_audio(
self.parentWindow,
self.mw,
@ -813,7 +823,7 @@ class Editor:
# not a supported type
return None
def isURL(self, s):
def isURL(self, s: str) -> bool:
s = s.lower()
return (
s.startswith("http://")
@ -962,23 +972,23 @@ class Editor:
)
def doDrop(self, html: str, internal: bool, extended: bool = False) -> None:
def pasteIfField(ret):
def pasteIfField(ret: bool) -> None:
if ret:
self.doPaste(html, internal, extended)
p = self.web.mapFromGlobal(QCursor.pos())
self.web.evalWithCallback(f"focusIfField({p.x()}, {p.y()});", pasteIfField)
def onPaste(self):
def onPaste(self) -> None:
self.web.onPaste()
def onCutOrCopy(self):
def onCutOrCopy(self) -> None:
self.web.flagAnkiText()
# Advanced menu
######################################################################
def onAdvanced(self):
def onAdvanced(self) -> None:
m = QMenu(self.mw)
for text, handler, shortcut in (
@ -1005,28 +1015,28 @@ class Editor:
# LaTeX
######################################################################
def insertLatex(self):
def insertLatex(self) -> None:
self.web.eval("wrap('[latex]', '[/latex]');")
def insertLatexEqn(self):
def insertLatexEqn(self) -> None:
self.web.eval("wrap('[$]', '[/$]');")
def insertLatexMathEnv(self):
def insertLatexMathEnv(self) -> None:
self.web.eval("wrap('[$$]', '[/$$]');")
def insertMathjaxInline(self):
def insertMathjaxInline(self) -> None:
self.web.eval("wrap('\\\\(', '\\\\)');")
def insertMathjaxBlock(self):
def insertMathjaxBlock(self) -> None:
self.web.eval("wrap('\\\\[', '\\\\]');")
def insertMathjaxChemistry(self):
def insertMathjaxChemistry(self) -> None:
self.web.eval("wrap('\\\\(\\\\ce{', '}\\\\)');")
# Links from HTML
######################################################################
_links = dict(
_links: Dict[str, Callable] = dict(
fields=onFields,
cards=onCardLayout,
bold=toggleBold,
@ -1052,7 +1062,7 @@ class Editor:
class EditorWebView(AnkiWebView):
def __init__(self, parent, editor):
def __init__(self, parent: QWidget, editor: Editor) -> None:
AnkiWebView.__init__(self, title="editor")
self.editor = editor
self.strip = self.editor.mw.pm.profile["stripHTML"]
@ -1062,15 +1072,15 @@ class EditorWebView(AnkiWebView):
qconnect(clip.dataChanged, self._onClipboardChange)
gui_hooks.editor_web_view_did_init(self)
def _onClipboardChange(self):
def _onClipboardChange(self) -> None:
if self._markInternal:
self._markInternal = False
self._flagAnkiText()
def onCut(self):
def onCut(self) -> None:
self.triggerPageAction(QWebEnginePage.Cut)
def onCopy(self):
def onCopy(self) -> None:
self.triggerPageAction(QWebEnginePage.Copy)
def _wantsExtendedPaste(self) -> bool:
@ -1093,10 +1103,10 @@ class EditorWebView(AnkiWebView):
def onMiddleClickPaste(self) -> None:
self._onPaste(QClipboard.Selection)
def dragEnterEvent(self, evt):
def dragEnterEvent(self, evt: QDragEnterEvent) -> None:
evt.accept()
def dropEvent(self, evt):
def dropEvent(self, evt: QDropEvent) -> None:
extended = self._wantsExtendedPaste()
mime = evt.mimeData()
@ -1177,7 +1187,7 @@ class EditorWebView(AnkiWebView):
token = html.escape(token).replace("\t", " " * 4)
# if there's more than one consecutive space,
# use non-breaking spaces for the second one on
def repl(match):
def repl(match: Match) -> None:
return match.group(1).replace(" ", "&nbsp;") + " "
token = re.sub(" ( +)", repl, token)
@ -1223,11 +1233,11 @@ class EditorWebView(AnkiWebView):
return self.editor.fnameToLink(fname)
return None
def flagAnkiText(self):
def flagAnkiText(self) -> None:
# be ready to adjust when clipboard event fires
self._markInternal = True
def _flagAnkiText(self):
def _flagAnkiText(self) -> None:
# add a comment in the clipboard html so we can tell text is copied
# from us and doesn't need to be stripped
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"
# - there may be other cases like a trailing 'Bold' that need fixing, but will
# wait for further reports first.
def fontMungeHack(font):
def fontMungeHack(font: str) -> str:
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
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
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
return editor.mw.col.media.escape_media_filenames(txt, unescape=True)

View file

@ -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:
mw.progress.start()
def on_done(fut):
def on_done(fut) -> None:
mw.progress.finish()
report: EmptyCardsReport = fut.result()
if not report.notes:
@ -54,7 +54,7 @@ class EmptyCardsDialog(QDialog):
style = "<style>.allempty { color: red; }</style>"
self.form.webview.stdHtml(style + html, context=self)
def on_finished(code):
def on_finished(code) -> None:
saveGeom(self, "emptycards")
qconnect(self.finished, on_finished)
@ -65,16 +65,16 @@ class EmptyCardsDialog(QDialog):
self._delete_button.setAutoDefault(False)
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,))
def _on_delete(self):
def _on_delete(self) -> None:
self.mw.progress.start()
def delete():
def delete() -> int:
return self._delete_cards(self.form.keep_notes.isChecked())
def on_done(fut):
def on_done(fut) -> None:
self.mw.progress.finish()
try:
count = fut.result()

View file

@ -5,16 +5,18 @@ import html
import re
import sys
import traceback
from typing import Optional
from markdown import markdown
from aqt import mw
from aqt.main import AnkiQt
from aqt.qt import *
from aqt.utils import TR, showText, showWarning, supportText, tr
if not os.environ.get("DEBUG"):
def excepthook(etype, val, tb):
def excepthook(etype, val, tb) -> None:
sys.stderr.write(
"Caught exception:\n%s\n"
% ("".join(traceback.format_exception(etype, val, tb)))
@ -29,20 +31,20 @@ class ErrorHandler(QObject):
errorTimer = pyqtSignal()
def __init__(self, mw):
def __init__(self, mw: AnkiQt) -> None:
QObject.__init__(self, mw)
self.mw = mw
self.timer = None
self.timer: Optional[QTimer] = None
qconnect(self.errorTimer, self._setTimer)
self.pool = ""
self._oldstderr = sys.stderr
sys.stderr = self
def unload(self):
def unload(self) -> None:
sys.stderr = self._oldstderr
sys.excepthook = None
def write(self, data):
def write(self, data: str) -> None:
# dump to stdout
sys.stdout.write(data)
# save in buffer
@ -50,12 +52,12 @@ class ErrorHandler(QObject):
# and update timer
self.setTimer()
def setTimer(self):
def setTimer(self) -> None:
# we can't create a timer from a different thread, so we post a
# message to the object on the main thread
self.errorTimer.emit() # type: ignore
def _setTimer(self):
def _setTimer(self) -> None:
if not self.timer:
self.timer = QTimer(self.mw)
qconnect(self.timer.timeout, self.onTimeout)
@ -63,10 +65,10 @@ class ErrorHandler(QObject):
self.timer.setSingleShot(True)
self.timer.start()
def tempFolderMsg(self):
def tempFolderMsg(self) -> str:
return tr(TR.QT_MISC_UNABLE_TO_ACCESS_ANKI_MEDIA_FOLDER)
def onTimeout(self):
def onTimeout(self) -> None:
error = html.escape(self.pool)
self.pool = ""
self.mw.progress.clear()
@ -75,15 +77,19 @@ class ErrorHandler(QObject):
if "DeprecationWarning" in error:
return
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():
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:
return showWarning(self.tempFolderMsg())
showWarning(self.tempFolderMsg())
return
if "Beautiful Soup is not an HTTP client" in error:
return
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:
showWarning(markdown(tr(TR.ERRORS_ACCESSING_DB)))
return
@ -99,7 +105,7 @@ class ErrorHandler(QObject):
txt = txt + "<div style='white-space: pre-wrap'>" + error + "</div>"
showText(txt, type="html", copyBtn=True)
def _addonText(self, error):
def _addonText(self, error: str) -> str:
matches = re.findall(r"addons21/(.*?)/", error)
if not matches:
return ""

View file

@ -42,7 +42,7 @@ class ExportDialog(QDialog):
self.setup(did)
self.exec_()
def setup(self, did: Optional[int]):
def setup(self, did: Optional[int]) -> None:
self.exporters = exporters(self.col)
# if a deck specified, start with .apkg type selected
idx = 0
@ -71,7 +71,7 @@ class ExportDialog(QDialog):
index = self.frm.deck.findText(name)
self.frm.deck.setCurrentIndex(index)
def exporterChanged(self, idx):
def exporterChanged(self, idx: int) -> None:
self.exporter = self.exporters[idx][1](self.col)
self.isApkg = self.exporter.ext == ".apkg"
self.isVerbatim = getattr(self.exporter, "verbatim", False)
@ -94,7 +94,7 @@ class ExportDialog(QDialog):
# show deck list?
self.frm.deck.setVisible(not self.isVerbatim)
def accept(self):
def accept(self) -> None:
self.exporter.includeSched = self.frm.includeSched.isChecked()
self.exporter.includeMedia = self.frm.includeMedia.isChecked()
self.exporter.includeTags = self.frm.includeTags.isChecked()
@ -155,17 +155,17 @@ class ExportDialog(QDialog):
os.unlink(file)
# progress handler
def exported_media(cnt):
def exported_media(cnt) -> None:
self.mw.taskman.run_on_main(
lambda: self.mw.progress.update(
label=tr(TR.EXPORTING_EXPORTED_MEDIA_FILE, count=cnt)
)
)
def do_export():
def do_export() -> None:
self.exporter.exportInto(file)
def on_done(future: Future):
def on_done(future: Future) -> None:
self.mw.progress.finish()
hooks.media_files_did_export.remove(exported_media)
# raises if exporter failed
@ -177,7 +177,7 @@ class ExportDialog(QDialog):
self.mw.taskman.run_in_background(do_export, on_done)
def on_export_finished(self):
def on_export_finished(self) -> None:
if self.isVerbatim:
msg = tr(TR.EXPORTING_COLLECTION_EXPORTED)
self.mw.reopen()

View file

@ -23,7 +23,7 @@ from aqt.utils import (
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)
self.mw = mw
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.Cancel).setAutoDefault(False)
self.form.buttonBox.button(QDialogButtonBox.Save).setAutoDefault(False)
self.currentIdx = None
self.currentIdx: Optional[int] = None
self.oldSortField = self.model["sortf"]
self.fillFields()
self.setupSignals()
@ -52,13 +52,13 @@ class FieldDialog(QDialog):
##########################################################################
def fillFields(self):
def fillFields(self) -> None:
self.currentIdx = None
self.form.fieldList.clear()
for c, f in enumerate(self.model["flds"]):
self.form.fieldList.addItem("{}: {}".format(c + 1, f["name"]))
def setupSignals(self):
def setupSignals(self) -> None:
f = self.form
qconnect(f.fieldList.currentRowChanged, self.onRowChange)
qconnect(f.fieldAdd.clicked, self.onAdd)
@ -68,7 +68,7 @@ class FieldDialog(QDialog):
qconnect(f.sortField.clicked, self.onSortField)
qconnect(f.buttonBox.helpRequested, self.onHelp)
def onDrop(self, ev):
def onDrop(self, ev) -> None:
fieldList = self.form.fieldList
indicatorPos = fieldList.dropIndicatorPosition()
dropPos = fieldList.indexAt(ev.pos()).row()
@ -86,32 +86,34 @@ class FieldDialog(QDialog):
movePos -= 1
self.moveField(movePos + 1) # convert to 1 based.
def onRowChange(self, idx):
def onRowChange(self, idx: int) -> None:
if idx == -1:
return
self.saveField()
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()
if not txt:
return
return None
if txt[0] in "#^/":
showWarning(tr(TR.FIELDS_NAME_FIRST_LETTER_NOT_VALID))
return
return None
for letter in """:{"}""":
if letter in txt:
showWarning(tr(TR.FIELDS_NAME_INVALID_LETTER))
return
return None
for f in self.model["flds"]:
if ignoreOrd is not None and f["ord"] == ignoreOrd:
continue
if f["name"] == txt:
showWarning(tr(TR.FIELDS_THAT_FIELD_NAME_IS_ALREADY_USED))
return
return None
return txt
def onRename(self):
def onRename(self) -> None:
idx = self.currentIdx
f = self.model["flds"][idx]
name = self._uniqueName(tr(TR.ACTIONS_NEW_NAME), self.currentIdx, f["name"])
@ -127,7 +129,7 @@ class FieldDialog(QDialog):
self.fillFields()
self.form.fieldList.setCurrentRow(idx)
def onAdd(self):
def onAdd(self) -> None:
name = self._uniqueName(tr(TR.FIELDS_FIELD_NAME))
if not name:
return
@ -139,9 +141,10 @@ class FieldDialog(QDialog):
self.fillFields()
self.form.fieldList.setCurrentRow(len(self.model["flds"]) - 1)
def onDelete(self):
def onDelete(self) -> None:
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)
c = tr(TR.BROWSING_NOTE_COUNT, count=count)
if not askUser(tr(TR.FIELDS_DELETE_FIELD_FROM, val=c)):
@ -155,7 +158,7 @@ class FieldDialog(QDialog):
self.fillFields()
self.form.fieldList.setCurrentRow(0)
def onPosition(self, delta=-1):
def onPosition(self, delta=-1) -> None:
idx = self.currentIdx
l = len(self.model["flds"])
txt = getOnlyText(tr(TR.FIELDS_NEW_POSITION_1, val=l), default=str(idx + 1))
@ -169,23 +172,23 @@ class FieldDialog(QDialog):
return
self.moveField(pos)
def onSortField(self):
def onSortField(self) -> None:
if not self.change_tracker.mark_schema():
return False
return
# don't allow user to disable; it makes no sense
self.form.sortField.setChecked(True)
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():
return False
return
self.saveField()
f = self.model["flds"][self.currentIdx]
self.mm.reposition_field(self.model, f, pos - 1)
self.fillFields()
self.form.fieldList.setCurrentRow(pos - 1)
def loadField(self, idx):
def loadField(self, idx: int) -> None:
self.currentIdx = idx
fld = self.model["flds"][idx]
f = self.form
@ -195,7 +198,7 @@ class FieldDialog(QDialog):
f.sortField.setChecked(self.model["sortf"] == fld["ord"])
f.rtl.setChecked(fld["rtl"])
def saveField(self):
def saveField(self) -> None:
# not initialized yet?
if self.currentIdx is None:
return
@ -219,20 +222,20 @@ class FieldDialog(QDialog):
fld["rtl"] = rtl
self.change_tracker.mark_basic()
def reject(self):
def reject(self) -> None:
if self.change_tracker.changed():
if not askUser("Discard changes?"):
return
QDialog.reject(self)
def accept(self):
def accept(self) -> None:
self.saveField()
def save():
def save() -> None:
self.mm.save(self.model)
def on_done(fut):
def on_done(fut) -> None:
try:
fut.result()
except TemplateError as e:
@ -245,5 +248,5 @@ class FieldDialog(QDialog):
self.mw.taskman.with_progress(save, on_done, self)
def onHelp(self):
def onHelp(self) -> None:
openHelp(HelpPage.CUSTOMIZING_FIELDS)

View file

@ -9,12 +9,13 @@ import traceback
import unicodedata
import zipfile
from concurrent.futures import Future
from typing import Optional
from typing import Any, Optional
import anki.importing as importing
import aqt.deckchooser
import aqt.forms
import aqt.modelchooser
from anki.importing.apkg import AnkiPackageImporter
from aqt import AnkiQt, gui_hooks
from aqt.qt import *
from aqt.utils import (
@ -34,7 +35,7 @@ from aqt.utils import (
class ChangeMap(QDialog):
def __init__(self, mw: AnkiQt, model, current):
def __init__(self, mw: AnkiQt, model, current) -> None:
QDialog.__init__(self, mw, Qt.Window)
self.mw = mw
self.model = model
@ -59,11 +60,11 @@ class ChangeMap(QDialog):
self.frm.fields.setCurrentRow(n + 1)
self.field: Optional[str] = None
def getField(self):
def getField(self) -> str:
self.exec_()
return self.field
def accept(self):
def accept(self) -> None:
row = self.frm.fields.currentRow()
if row < len(self.model["flds"]):
self.field = self.model["flds"][row]["name"]
@ -73,7 +74,7 @@ class ChangeMap(QDialog):
self.field = None
QDialog.accept(self)
def reject(self):
def reject(self) -> None:
self.accept()
@ -106,19 +107,19 @@ class ImportDialog(QDialog):
self.frm.buttonBox.addButton(b, QDialogButtonBox.AcceptRole)
self.exec_()
def setupOptions(self):
def setupOptions(self) -> None:
self.model = self.mw.col.models.current()
self.modelChooser = aqt.modelchooser.ModelChooser(
self.mw, self.frm.modelArea, 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.initMapping()
self.showMapping()
def onDelimiter(self):
def onDelimiter(self) -> None:
str = (
getOnlyText(
tr(TR.IMPORTING_BY_DEFAULT_ANKI_WILL_DETECT_THE),
@ -135,14 +136,14 @@ class ImportDialog(QDialog):
return
self.hideMapping()
def updateDelim():
def updateDelim() -> None:
self.importer.delimiter = str
self.importer.updateDelimiter()
self.showMapping(hook=updateDelim)
self.updateDelimiterButtonText()
def updateDelimiterButtonText(self):
def updateDelimiterButtonText(self) -> None:
if not self.importer.needDelimiter:
return
if self.importer.delimiter:
@ -164,7 +165,7 @@ class ImportDialog(QDialog):
txt = tr(TR.IMPORTING_FIELDS_SEPARATED_BY, val=d)
self.frm.autoDetect.setText(txt)
def accept(self):
def accept(self) -> None:
self.importer.mapping = self.mapping
if not self.importer.mappingOk():
showWarning(tr(TR.IMPORTING_THE_FIRST_FIELD_OF_THE_NOTE))
@ -182,7 +183,7 @@ class ImportDialog(QDialog):
self.mw.progress.start()
self.mw.checkpoint(tr(TR.ACTIONS_IMPORT))
def on_done(future: Future):
def on_done(future: Future) -> None:
self.mw.progress.finish()
try:
@ -211,19 +212,21 @@ class ImportDialog(QDialog):
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
# to a separate object and add/remove that instead
self.frame = QFrame(self.frm.mappingArea)
self.frm.mappingArea.setWidget(self.frame)
self.mapbox = QVBoxLayout(self.frame)
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()
def showMapping(self, keepMapping=False, hook=None):
def showMapping(
self, keepMapping: bool = False, hook: Optional[Callable] = None
) -> None:
if hook:
hook()
if not keepMapping:
@ -255,7 +258,7 @@ class ImportDialog(QDialog):
self.grid.addWidget(button, num, 2)
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()
try:
# make sure we don't have it twice
@ -267,7 +270,7 @@ class ImportDialog(QDialog):
if getattr(self.importer, "delimiter", False):
self.savedDelimiter = self.importer.delimiter
def updateDelim():
def updateDelim() -> None:
self.importer.delimiter = self.savedDelimiter
self.showMapping(hook=updateDelim, keepMapping=True)
@ -280,22 +283,22 @@ class ImportDialog(QDialog):
gui_hooks.current_note_type_did_change.remove(self.modelChanged)
QDialog.reject(self)
def helpRequested(self):
def helpRequested(self) -> None:
openHelp(HelpPage.IMPORTING)
def importModeChanged(self, newImportMode):
def importModeChanged(self, newImportMode) -> None:
if newImportMode == 0:
self.frm.tagModified.setEnabled(True)
else:
self.frm.tagModified.setEnabled(False)
def showUnicodeWarning():
def showUnicodeWarning() -> None:
"""Shorthand to show a standard warning."""
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])
file = getFile(mw, tr(TR.ACTIONS_IMPORT), None, key="import", filter=filt)
if not file:
@ -314,7 +317,7 @@ def onImport(mw):
importFile(mw, file)
def importFile(mw, file):
def importFile(mw: AnkiQt, file: str) -> None:
importerClass = None
done = False
for i in importing.Importers:
@ -371,7 +374,7 @@ def importFile(mw, file):
# importing non-colpkg files
mw.progress.start(immediate=True)
def on_done(future: Future):
def on_done(future: Future) -> None:
mw.progress.finish()
try:
future.result()
@ -402,11 +405,11 @@ def importFile(mw, file):
mw.taskman.run_in_background(importer.run, on_done)
def invalidZipMsg():
def invalidZipMsg() -> str:
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()
full = (
(base == "collection.apkg")
@ -424,16 +427,17 @@ def setupApkgImport(mw, importer):
return False
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))
def _replaceWithApkg(mw, filename, backup):
def _replaceWithApkg(mw, filename, backup) -> None:
mw.progress.start(immediate=True)
def do_import():
def do_import() -> None:
z = zipfile.ZipFile(filename)
# v2 scheduler?
@ -468,7 +472,7 @@ def _replaceWithApkg(mw, filename, backup):
z.close()
def on_done(future: Future):
def on_done(future: Future) -> None:
mw.progress.finish()
try:

View file

@ -6,7 +6,7 @@
Legacy support
"""
from typing import List
from typing import Any, List
import anki
import aqt
@ -31,7 +31,14 @@ def stripSounds(text) -> str:
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()")
return aqt.mw.col.format_timespan(time)

View file

@ -14,7 +14,7 @@ import weakref
import zipfile
from argparse import Namespace
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 aqt
@ -70,6 +70,7 @@ install_pylib_legacy()
class ResetReason(enum.Enum):
Unknown = "unknown"
AddCardsAddNote = "addCardsAddNote"
EditCurrentInit = "editCurrentInit"
EditorBridgeCmd = "editorBridgeCmd"
@ -85,7 +86,7 @@ class ResetReason(enum.Enum):
class ResetRequired:
def __init__(self, mw: AnkiQt):
def __init__(self, mw: AnkiQt) -> None:
self.mw = mw
@ -137,7 +138,7 @@ class AnkiQt(QMainWindow):
else:
fn = self.setupProfile
def on_window_init():
def on_window_init() -> None:
fn()
gui_hooks.main_window_did_init()
@ -173,7 +174,7 @@ class AnkiQt(QMainWindow):
"Actions that are deferred until after add-on loading."
self.toolbar.draw()
def setupProfileAfterWebviewsLoaded(self):
def setupProfileAfterWebviewsLoaded(self) -> None:
for w in (self.web, self.bottomWeb):
if not w._domDone:
self.progress.timer(
@ -204,7 +205,7 @@ class AnkiQt(QMainWindow):
self.onClose.emit() # type: ignore
evt.accept()
def closeWithoutQuitting(self):
def closeWithoutQuitting(self) -> None:
self.closeFires = False
self.close()
self.closeFires = True
@ -273,9 +274,10 @@ class AnkiQt(QMainWindow):
name = self.pm.profiles()[n]
self.pm.load(name)
def openProfile(self):
def openProfile(self) -> None:
name = self.pm.profiles()[self.profileForm.profiles.currentRow()]
return self.pm.load(name)
self.pm.load(name)
return
def onOpenProfile(self) -> None:
self.profileDiag.hide()
@ -286,34 +288,37 @@ class AnkiQt(QMainWindow):
def profileNameOk(self, name: str) -> bool:
return not checkInvalidFilename(name) and name != "addons21"
def onAddProfile(self):
def onAddProfile(self) -> None:
name = getOnlyText(tr(TR.ACTIONS_NAME)).strip()
if name:
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):
return
self.pm.create(name)
self.pm.name = name
self.refreshProfilesList()
def onRenameProfile(self):
def onRenameProfile(self) -> None:
name = getOnlyText(tr(TR.ACTIONS_NEW_NAME), default=self.pm.name).strip()
if not name:
return
if name == self.pm.name:
return
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):
return
self.pm.rename(name)
self.refreshProfilesList()
def onRemProfile(self):
def onRemProfile(self) -> None:
profs = self.pm.profiles()
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?
if not askUser(
tr(TR.QT_MISC_ALL_CARDS_NOTES_AND_MEDIA_FOR),
@ -324,7 +329,7 @@ class AnkiQt(QMainWindow):
self.pm.remove(self.pm.name)
self.refreshProfilesList()
def onOpenBackup(self):
def onOpenBackup(self) -> None:
if not askUser(
tr(TR.QT_MISC_REPLACE_YOUR_COLLECTION_WITH_AN_EARLIER),
msgfunc=QMessageBox.warning,
@ -332,7 +337,7 @@ class AnkiQt(QMainWindow):
):
return
def doOpen(path):
def doOpen(path) -> None:
self._openBackup(path)
getFile(
@ -343,7 +348,7 @@ class AnkiQt(QMainWindow):
dir=self.pm.backupFolder(),
)
def _openBackup(self, path):
def _openBackup(self, path) -> None:
try:
# move the existing collection to the trash, as it may not open
self.pm.trashCollection()
@ -358,14 +363,14 @@ class AnkiQt(QMainWindow):
self.onOpenProfile()
def _on_downgrade(self):
def _on_downgrade(self) -> None:
self.progress.start()
profiles = self.pm.profiles()
def downgrade():
def downgrade() -> List[str]:
return self.pm.downgrade(profiles)
def on_done(future):
def on_done(future) -> None:
self.progress.finish()
problems = future.result()
if not problems:
@ -407,7 +412,7 @@ class AnkiQt(QMainWindow):
self.pendingImport = None
gui_hooks.profile_did_open()
def _onsuccess():
def _onsuccess() -> None:
self._refresh_after_sync()
if onsuccess:
onsuccess()
@ -415,7 +420,7 @@ class AnkiQt(QMainWindow):
self.maybe_auto_sync_on_open_close(_onsuccess)
def unloadProfile(self, onsuccess: Callable) -> None:
def callback():
def callback() -> None:
self._unloadProfile()
onsuccess()
@ -445,7 +450,7 @@ class AnkiQt(QMainWindow):
def unloadProfileAndExit(self) -> None:
self.unloadProfile(self.cleanupAndExit)
def unloadProfileAndShowProfileManager(self):
def unloadProfileAndShowProfileManager(self) -> None:
self.unloadProfile(self.showProfileManager)
def cleanupAndExit(self) -> None:
@ -509,23 +514,23 @@ class AnkiQt(QMainWindow):
return True
def _loadCollection(self):
def _loadCollection(self) -> None:
cpath = self.pm.collectionPath()
self.col = Collection(cpath, backend=self.backend, log=True)
self.setEnabled(True)
def reopen(self):
def reopen(self) -> None:
self.col.reopen()
def unloadCollection(self, onsuccess: Callable) -> None:
def after_media_sync():
def after_media_sync() -> None:
self._unloadCollection()
onsuccess()
def after_sync():
def after_sync() -> None:
self.media_syncer.show_diag_until_finished(after_media_sync)
def before_sync():
def before_sync() -> None:
self.setEnabled(False)
self.maybe_auto_sync_on_open_close(after_sync)
@ -563,7 +568,7 @@ class AnkiQt(QMainWindow):
##########################################################################
class BackupThread(Thread):
def __init__(self, path, data):
def __init__(self, path, data) -> None:
Thread.__init__(self)
self.path = path
self.data = data
@ -572,7 +577,7 @@ class AnkiQt(QMainWindow):
with open(self.path, "wb") as file:
pass
def run(self):
def run(self) -> None:
z = zipfile.ZipFile(self.path, "w", zipfile.ZIP_DEFLATED)
z.writestr("collection.anki2", self.data)
z.writestr("media", "{}")
@ -655,10 +660,10 @@ class AnkiQt(QMainWindow):
return self.moveToState("deckBrowser")
self.overview.show()
def _reviewState(self, oldState):
def _reviewState(self, oldState: str) -> None:
self.reviewer.show()
def _reviewCleanup(self, newState):
def _reviewCleanup(self, newState: str) -> None:
if newState != "resetRequired" and newState != "review":
self.reviewer.cleanup()
@ -674,7 +679,12 @@ class AnkiQt(QMainWindow):
self.maybeEnableUndo()
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."
self.autosave()
self.resetModal = modal
@ -683,7 +693,7 @@ class AnkiQt(QMainWindow):
):
self.moveToState("resetRequired")
def interactiveState(self):
def interactiveState(self) -> bool:
"True if not in profile manager, syncing, etc."
return self.state in ("overview", "review", "deckBrowser")
@ -693,7 +703,7 @@ class AnkiQt(QMainWindow):
self.state = self.returnState
self.reset()
def delayedMaybeReset(self):
def delayedMaybeReset(self) -> None:
# if we redraw the page in a button click event it will often crash on
# windows
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.SIGTERM, self.onUnixSignal)
def onUnixSignal(self, signum, frame):
def onUnixSignal(self, signum, frame) -> None:
# schedule a rollback & quit
def quit():
def quit() -> None:
self.col.db.rollback()
self.close()
@ -822,7 +832,7 @@ title="%s" %s>%s</button>""" % (
self.addonManager.loadAddons()
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()
elap = intTime() - last_check
@ -865,7 +875,7 @@ title="%s" %s>%s</button>""" % (
# Syncing
##########################################################################
def on_sync_button_clicked(self):
def on_sync_button_clicked(self) -> None:
if self.media_syncer.is_syncing():
self.media_syncer.show_sync_log()
else:
@ -878,16 +888,16 @@ title="%s" %s>%s</button>""" % (
else:
self._sync_collection_and_media(self._refresh_after_sync)
def _refresh_after_sync(self):
def _refresh_after_sync(self) -> None:
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."
# start media sync if not already running
if not self.media_syncer.is_syncing():
self.media_syncer.start()
def on_collection_sync_finished():
def on_collection_sync_finished() -> None:
self.col.clearUndo()
self.col.models._clear_cache()
gui_hooks.sync_did_finish()
@ -920,7 +930,7 @@ title="%s" %s>%s</button>""" % (
)
# legacy
def _sync(self):
def _sync(self) -> None:
pass
onSync = on_sync_button_clicked
@ -1027,7 +1037,7 @@ title="%s" %s>%s</button>""" % (
self.form.actionUndo.setEnabled(False)
gui_hooks.undo_state_did_change(False)
def checkpoint(self, name):
def checkpoint(self, name: str) -> None:
self.col.save(name)
self.maybeEnableUndo()
@ -1046,10 +1056,10 @@ title="%s" %s>%s</button>""" % (
def onBrowse(self) -> None:
aqt.dialogs.open("Browser", self, card=self.reviewer.card)
def onEditCurrent(self):
def onEditCurrent(self) -> None:
aqt.dialogs.open("EditCurrent", self)
def onDeckConf(self, deck=None):
def onDeckConf(self, deck: Optional[Deck] = None) -> None:
import aqt.deckconf
if not deck:
@ -1059,11 +1069,11 @@ title="%s" %s>%s</button>""" % (
else:
aqt.deckconf.DeckConf(self, deck)
def onOverview(self):
def onOverview(self) -> None:
self.col.reset()
self.moveToState("overview")
def onStats(self):
def onStats(self) -> None:
deck = self._selectedDeck()
if not deck:
return
@ -1073,21 +1083,21 @@ title="%s" %s>%s</button>""" % (
else:
aqt.dialogs.open("NewDeckStats", self)
def onPrefs(self):
def onPrefs(self) -> None:
aqt.dialogs.open("Preferences", self)
def onNoteTypes(self):
def onNoteTypes(self) -> None:
import aqt.models
aqt.models.Models(self, self, fromMain=True)
def onAbout(self):
def onAbout(self) -> None:
aqt.dialogs.open("About", self)
def onDonate(self):
def onDonate(self) -> None:
openLink(aqt.appDonate)
def onDocumentation(self):
def onDocumentation(self) -> None:
openHelp(HelpPage.INDEX)
# Importing & exporting
@ -1103,12 +1113,12 @@ title="%s" %s>%s</button>""" % (
aqt.importing.importFile(self, path)
return None
def onImport(self):
def onImport(self) -> None:
import aqt.importing
aqt.importing.onImport(self)
def onExport(self, did=None):
def onExport(self, did: Optional[int] = None) -> None:
import aqt.exporting
aqt.exporting.ExportDialog(self, did=did)
@ -1116,7 +1126,7 @@ title="%s" %s>%s</button>""" % (
# 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
installAddonPackages(
@ -1131,7 +1141,7 @@ title="%s" %s>%s</button>""" % (
# Cramming
##########################################################################
def onCram(self):
def onCram(self) -> None:
aqt.dialogs.open("DynDeckConfDialog", self)
# Menu, title bar & status
@ -1174,14 +1184,14 @@ title="%s" %s>%s</button>""" % (
qconnect(self.autoUpdate.clockIsOff, self.clockIsOff)
self.autoUpdate.start()
def newVerAvail(self, ver):
def newVerAvail(self, ver) -> None:
if self.pm.meta.get("suppressUpdate", None) != ver:
aqt.update.askAndUpdate(self, ver)
def newMsg(self, data):
def newMsg(self, data) -> None:
aqt.update.showMessages(self, data)
def clockIsOff(self, diff):
def clockIsOff(self, diff) -> None:
if devMode:
print("clock is off; ignoring")
return
@ -1202,13 +1212,13 @@ title="%s" %s>%s</button>""" % (
# SIGINT/SIGTERM is processed without a long delay
self.progress.timer(1000, lambda: None, True, False)
def onRefreshTimer(self):
def onRefreshTimer(self) -> None:
if self.state == "deckBrowser":
self.deckBrowser.refresh()
elif self.state == "overview":
self.overview.refresh()
def on_autosync_timer(self):
def on_autosync_timer(self) -> None:
elap = self.media_syncer.seconds_since_last_sync()
minutes = self.pm.auto_sync_media_minutes()
if not minutes:
@ -1229,7 +1239,7 @@ title="%s" %s>%s</button>""" % (
self._activeWindowOnPlay: Optional[QWidget] = None
def onOdueInvalid(self):
def onOdueInvalid(self) -> None:
showWarning(tr(TR.QT_MISC_INVALID_PROPERTY_FOUND_ON_CARD_PLEASE))
def _isVideo(self, tag: AVTag) -> bool:
@ -1274,7 +1284,7 @@ title="%s" %s>%s</button>""" % (
##########################################################################
# this will gradually be phased out
def onSchemaMod(self, arg):
def onSchemaMod(self, arg: bool) -> bool:
assert self.inMainThread()
progress_shown = self.progress.busy()
if progress_shown:
@ -1295,14 +1305,14 @@ title="%s" %s>%s</button>""" % (
# Advanced features
##########################################################################
def onCheckDB(self):
def onCheckDB(self) -> None:
check_db(self)
def on_check_media_db(self) -> None:
gui_hooks.media_check_will_start()
check_media_db(self)
def onStudyDeck(self):
def onStudyDeck(self) -> None:
from aqt.studydeck import StudyDeck
ret = StudyDeck(self, dyn=True, current=self.col.decks.current()["name"])
@ -1316,11 +1326,11 @@ title="%s" %s>%s</button>""" % (
# Debugging
######################################################################
def onDebug(self):
def onDebug(self) -> None:
frm = self.debug_diag_form = aqt.forms.debug.Ui_Dialog()
class DebugDialog(QDialog):
def reject(self):
def reject(self) -> None:
super().reject()
saveSplitter(frm.splitter, "DebugConsoleWindow")
saveGeom(self, "DebugConsoleWindow")
@ -1356,7 +1366,7 @@ title="%s" %s>%s</button>""" % (
a = menu.addAction("Clear Code")
a.setShortcuts(QKeySequence("ctrl+shift+l"))
qconnect(a.triggered, frm.text.clear)
menu.exec(QCursor.pos())
menu.exec_(QCursor.pos())
frm.log.contextMenuEvent = lambda ev: addContextMenu(ev, "log")
frm.text.contextMenuEvent = lambda ev: addContextMenu(ev, "text")
@ -1367,7 +1377,7 @@ title="%s" %s>%s</button>""" % (
mw = self
class Stream:
def write(self, data):
def write(self, data) -> None:
mw._output += data
if on:
@ -1418,7 +1428,7 @@ title="%s" %s>%s</button>""" % (
self._card_repr(card)
return card
def onDebugPrint(self, frm):
def onDebugPrint(self, frm) -> None:
cursor = frm.text.textCursor()
position = cursor.position()
cursor.select(QTextCursor.LineUnderCursor)
@ -1431,7 +1441,7 @@ title="%s" %s>%s</button>""" % (
frm.text.setTextCursor(cursor)
self.onDebugRet(frm)
def onDebugRet(self, frm):
def onDebugRet(self, frm) -> None:
import pprint
import traceback
@ -1509,14 +1519,15 @@ title="%s" %s>%s</button>""" % (
def setupAppMsg(self) -> None:
qconnect(self.app.appMsg, self.onAppMsg)
def onAppMsg(self, buf: str) -> Optional[QTimer]:
def onAppMsg(self, buf: str) -> None:
is_addon = self._isAddon(buf)
if self.state == "startup":
# try again in a second
return self.progress.timer(
self.progress.timer(
1000, lambda: self.onAppMsg(buf), False, requiresCollection=False
)
return
elif self.state == "profileManager":
# can't raise window while in profile manager
if buf == "raise":
@ -1526,7 +1537,8 @@ title="%s" %s>%s</button>""" % (
msg = tr(TR.QT_MISC_ADDON_WILL_BE_INSTALLED_WHEN_A)
else:
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():
# we can't raise the main window while in profile dialog, syncing, etc
if buf != "raise":

View file

@ -136,7 +136,7 @@ class MediaChecker:
diag.exec_()
saveGeom(diag, "checkmediadb")
def _on_render_latex(self):
def _on_render_latex(self) -> None:
self.progress_dialog = self.mw.progress.start()
try:
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))
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)):
return
@ -183,14 +183,14 @@ class MediaChecker:
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._set_progress_enabled(True)
def empty_trash():
def empty_trash() -> None:
self.mw.col.media.empty_trash()
def on_done(fut: Future):
def on_done(fut: Future) -> None:
self.mw.progress.finish()
self._set_progress_enabled(False)
# check for errors
@ -200,14 +200,14 @@ class MediaChecker:
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._set_progress_enabled(True)
def restore_trash():
def restore_trash() -> None:
self.mw.col.media.restore_trash()
def on_done(fut: Future):
def on_done(fut: Future) -> None:
self.mw.progress.finish()
self._set_progress_enabled(False)
# check for errors

View file

@ -12,6 +12,7 @@ import threading
import time
import traceback
from http import HTTPStatus
from typing import Tuple
import flask
import flask_cors # type: ignore
@ -26,7 +27,7 @@ from aqt.qt import *
from aqt.utils import aqt_data_folder
def _getExportFolder():
def _getExportFolder() -> str:
data_folder = aqt_data_folder()
webInSrcFolder = os.path.abspath(os.path.join(data_folder, "web"))
if os.path.exists(webInSrcFolder):
@ -52,11 +53,11 @@ class MediaServer(threading.Thread):
_ready = threading.Event()
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)
self.is_shutdown = False
def run(self):
def run(self) -> None:
try:
if devMode:
# idempotent if logging has already been set up
@ -83,7 +84,7 @@ class MediaServer(threading.Thread):
if not self.is_shutdown:
raise
def shutdown(self):
def shutdown(self) -> None:
self.is_shutdown = True
sockets = list(self.server._map.values()) # type: ignore
for socket in sockets:
@ -91,13 +92,13 @@ class MediaServer(threading.Thread):
# https://github.com/Pylons/webtest/blob/4b8a3ebf984185ff4fefb31b4d0cf82682e1fcf7/webtest/http.py#L93-L104
self.server.task_dispatcher.shutdown()
def getPort(self):
def getPort(self) -> int:
self._ready.wait()
return int(self.server.effective_port) # type: ignore
@app.route("/<path:pathin>", methods=["GET", "POST"])
def allroutes(pathin):
def allroutes(pathin) -> Response:
try:
directory, path = _redirectWebExports(pathin)
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
targetPath = "_anki/"
if path.startswith(targetPath):

View file

@ -28,14 +28,14 @@ class LogEntryWithTime:
class MediaSyncer:
def __init__(self, mw: aqt.main.AnkiQt):
def __init__(self, mw: aqt.main.AnkiQt) -> None:
self.mw = mw
self._syncing: bool = False
self._log: List[LogEntryWithTime] = []
self._progress_timer: Optional[QTimer] = None
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()
if progress.kind != ProgressKind.MediaSync:
return
@ -88,7 +88,7 @@ class MediaSyncer:
else:
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):
self._log_and_notify(tr(TR.SYNC_MEDIA_ABORTED))
return
@ -116,10 +116,10 @@ class MediaSyncer:
def _on_start_stop(self, running: bool) -> None:
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)
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
if not self.is_syncing():
return on_finished()
@ -129,7 +129,7 @@ class MediaSyncer:
timer: Optional[QTimer] = None
def check_finished():
def check_finished() -> None:
if not self.is_syncing():
timer.stop()
on_finished()
@ -197,7 +197,7 @@ class MediaSyncDialog(QDialog):
asctime = time.asctime(time.localtime(stamp))
return f"{asctime}: {text}"
def _entry_to_text(self, entry: LogEntryWithTime):
def _entry_to_text(self, entry: LogEntryWithTime) -> str:
if isinstance(entry.entry, str):
txt = entry.entry
elif isinstance(entry.entry, MediaSyncProgress):
@ -209,7 +209,7 @@ class MediaSyncDialog(QDialog):
def _logentry_to_text(self, e: MediaSyncProgress) -> str:
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))
if not self._syncer.is_syncing():
self.abort_button.setHidden(True)

View file

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Copyright: Ankitects Pty Ltd and contributors
# 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.qt import *
@ -74,7 +74,7 @@ class ModelChooser(QHBoxLayout):
# edit button
edit = QPushButton(tr(TR.QT_MISC_MANAGE), clicked=self.onEdit) # type: ignore
def nameFunc():
def nameFunc() -> List[str]:
return sorted(self.deck.models.allNames())
ret = StudyDeck(

View file

@ -57,7 +57,7 @@ class Models(QDialog):
# Models
##########################################################################
def maybe_select_provided_notetype(self):
def maybe_select_provided_notetype(self) -> None:
if not self.selected_notetype_id:
self.form.modelsList.setCurrentRow(0)
return
@ -219,7 +219,7 @@ class Models(QDialog):
class AddModel(QDialog):
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.mw = mw
self.col = mw.col

View file

@ -4,7 +4,7 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Optional
from typing import Any, Callable, Dict, List, Optional, Tuple
import aqt
from aqt import gui_hooks
@ -14,7 +14,7 @@ from aqt.utils import TR, askUserDialog, openLink, shortcut, tooltip, tr
class OverviewBottomBar:
def __init__(self, overview: Overview):
def __init__(self, overview: Overview) -> None:
self.overview = overview
@ -44,13 +44,13 @@ class Overview:
self.web = mw.web
self.bottom = BottomBar(mw, mw.bottomWeb)
def show(self):
def show(self) -> None:
av_player.stop_and_clear_queue()
self.web.set_bridge_command(self._linkHandler, self)
self.mw.setStateShortcuts(self._shortcutKeys())
self.refresh()
def refresh(self):
def refresh(self) -> None:
self.mw.col.reset()
self._renderPage()
self._renderBottom()
@ -60,7 +60,7 @@ class Overview:
# Handlers
############################################################
def _linkHandler(self, url):
def _linkHandler(self, url: str) -> bool:
if url == "study":
self.mw.col.startTimebox()
self.mw.moveToState("review")
@ -90,7 +90,7 @@ class Overview:
openLink(url)
return False
def _shortcutKeys(self):
def _shortcutKeys(self) -> List[Tuple[str, Callable]]:
return [
("o", self.mw.onDeckConf),
("r", self.onRebuildKey),
@ -99,24 +99,24 @@ class Overview:
("u", self.onUnbury),
]
def _filteredDeck(self):
def _filteredDeck(self) -> int:
return self.mw.col.decks.current()["dyn"]
def onRebuildKey(self):
def onRebuildKey(self) -> None:
if self._filteredDeck():
self.mw.col.sched.rebuild_filtered_deck(self.mw.col.decks.selected())
self.mw.reset()
def onEmptyKey(self):
def onEmptyKey(self) -> None:
if self._filteredDeck():
self.mw.col.sched.empty_filtered_deck(self.mw.col.decks.selected())
self.mw.reset()
def onCustomStudyKey(self):
def onCustomStudyKey(self) -> None:
if not self._filteredDeck():
self.onStudyMore()
def onUnbury(self):
def onUnbury(self) -> None:
if self.mw.col.schedVer() == 1:
self.mw.col.sched.unburyCardsForDeck()
self.mw.reset()
@ -148,7 +148,7 @@ class Overview:
# HTML
############################################################
def _renderPage(self):
def _renderPage(self) -> None:
but = self.mw.button
deck = self.mw.col.decks.current()
self.sid = deck.get("sharedFrom")
@ -175,10 +175,10 @@ class Overview:
context=self,
)
def _show_finished_screen(self):
def _show_finished_screen(self) -> None:
self.web.load_ts_page("congrats")
def _desc(self, deck):
def _desc(self, deck: Dict[str, Any]) -> str:
if deck["dyn"]:
desc = tr(TR.STUDYING_THIS_IS_A_SPECIAL_DECK_FOR)
desc += " " + tr(TR.STUDYING_CARDS_WILL_BE_AUTOMATICALLY_RETURNED_TO)
@ -227,7 +227,7 @@ class Overview:
# Bottom area
######################################################################
def _renderBottom(self):
def _renderBottom(self) -> None:
links = [
["O", "opts", tr(TR.ACTIONS_OPTIONS)],
]
@ -254,7 +254,7 @@ class Overview:
# Studying more
######################################################################
def onStudyMore(self):
def onStudyMore(self) -> None:
import aqt.customstudy
aqt.customstudy.CustomStudy(self.mw)

View file

@ -34,7 +34,7 @@ def video_driver_name_for_platform(driver: VideoDriver) -> str:
class Preferences(QDialog):
def __init__(self, mw: AnkiQt):
def __init__(self, mw: AnkiQt) -> None:
QDialog.__init__(self, mw, Qt.Window)
self.mw = mw
self.prof = self.mw.pm.profile
@ -55,7 +55,7 @@ class Preferences(QDialog):
self.setupOptions()
self.show()
def accept(self):
def accept(self) -> None:
# avoid exception if main window is already closed
if not self.mw.col:
return
@ -68,19 +68,19 @@ class Preferences(QDialog):
self.done(0)
aqt.dialogs.markClosed("Preferences")
def reject(self):
def reject(self) -> None:
self.accept()
# Language
######################################################################
def setupLang(self):
def setupLang(self) -> None:
f = self.form
f.lang.addItems([x[0] for x in anki.lang.langs])
f.lang.setCurrentIndex(self.langIdx())
qconnect(f.lang.currentIndexChanged, self.onLangIdxChanged)
def langIdx(self):
def langIdx(self) -> int:
codes = [x[1] for x in anki.lang.langs]
lang = anki.lang.currentLang
if lang in anki.lang.compatMap:
@ -92,7 +92,7 @@ class Preferences(QDialog):
except:
return codes.index("en_US")
def onLangIdxChanged(self, idx):
def onLangIdxChanged(self, idx) -> None:
code = anki.lang.langs[idx][1]
self.mw.pm.setLang(code)
showInfo(
@ -102,7 +102,7 @@ class Preferences(QDialog):
# Collection options
######################################################################
def setupCollection(self):
def setupCollection(self) -> None:
import anki.consts as c
f = self.form
@ -130,7 +130,7 @@ class Preferences(QDialog):
f.newSched.setChecked(True)
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()
names = [
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())
)
def update_video_driver(self):
def update_video_driver(self) -> None:
new_driver = self.video_drivers[self.form.video_driver.currentIndex()]
if new_driver != self.mw.pm.video_driver():
self.mw.pm.set_video_driver(new_driver)
showInfo(tr(TR.PREFERENCES_CHANGES_WILL_TAKE_EFFECT_WHEN_YOU))
def updateCollection(self):
def updateCollection(self) -> None:
f = self.form
d = self.mw.col
@ -176,7 +176,7 @@ class Preferences(QDialog):
# Scheduler version
######################################################################
def _updateSchedVer(self, wantNew):
def _updateSchedVer(self, wantNew) -> None:
haveNew = self.mw.col.schedVer() == 2
# nothing to do?
@ -194,7 +194,7 @@ class Preferences(QDialog):
# Network
######################################################################
def setupNetwork(self):
def setupNetwork(self) -> None:
self.form.media_log.setText(tr(TR.SYNC_MEDIA_LOG_BUTTON))
qconnect(self.form.media_log.clicked, self.on_media_log)
self.form.syncOnProgramOpen.setChecked(self.prof["autoSync"])
@ -207,10 +207,10 @@ class Preferences(QDialog):
qconnect(self.form.syncDeauth.clicked, self.onSyncDeauth)
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()
def _hideAuth(self):
def _hideAuth(self) -> None:
self.form.syncDeauth.setVisible(False)
self.form.syncUser.setText("")
self.form.syncLabel.setText(
@ -225,7 +225,7 @@ class Preferences(QDialog):
self.mw.col.media.force_resync()
self._hideAuth()
def updateNetwork(self):
def updateNetwork(self) -> None:
self.prof["autoSync"] = self.form.syncOnProgramOpen.isChecked()
self.prof["syncMedia"] = self.form.syncMedia.isChecked()
self.mw.pm.set_auto_sync_media_minutes(
@ -238,16 +238,16 @@ class Preferences(QDialog):
# Backup
######################################################################
def setupBackup(self):
def setupBackup(self) -> None:
self.form.numBackups.setValue(self.prof["numBackups"])
def updateBackup(self):
def updateBackup(self) -> None:
self.prof["numBackups"] = self.form.numBackups.value()
# Basic & Advanced Options
######################################################################
def setupOptions(self):
def setupOptions(self) -> None:
self.form.pastePNG.setChecked(self.prof.get("pastePNG", False))
self.form.uiScale.setValue(int(self.mw.pm.uiScale() * 100))
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())
)
def updateOptions(self):
def updateOptions(self) -> None:
restart_required = False
self.prof["pastePNG"] = self.form.pastePNG.isChecked()

View file

@ -4,7 +4,7 @@
import json
import re
import time
from typing import Any, Callable, Optional, Union
from typing import Any, Callable, Optional, Tuple, Union
from anki.cards import Card
from anki.collection import ConfigBoolKey
@ -19,6 +19,7 @@ from aqt.qt import (
QPixmap,
QShortcut,
Qt,
QTimer,
QVBoxLayout,
QWidget,
qconnect,
@ -29,15 +30,19 @@ from aqt.theme import theme_manager
from aqt.utils import TR, disable_help_button, restoreGeom, saveGeom, tr
from aqt.webview import AnkiWebView
LastStateAndMod = Tuple[str, int, int]
class Previewer(QDialog):
_last_state = None
_last_state: Optional[LastStateAndMod] = None
_card_changed = False
_last_render: Union[int, float] = 0
_timer = None
_timer: Optional[QTimer] = None
_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)
self._open = True
self._parent = parent
@ -54,7 +59,7 @@ class Previewer(QDialog):
def card_changed(self) -> bool:
raise NotImplementedError
def open(self):
def open(self) -> None:
self._state = "question"
self._last_state = None
self._create_gui()
@ -62,7 +67,7 @@ class Previewer(QDialog):
self.render_card()
self.show()
def _create_gui(self):
def _create_gui(self) -> None:
self.setWindowTitle(tr(TR.ACTIONS_PREVIEW))
self.close_shortcut = QShortcut(QKeySequence("Ctrl+Shift+P"), self)
@ -98,25 +103,25 @@ class Previewer(QDialog):
self.setLayout(self.vbox)
restoreGeom(self, "preview")
def _on_finished(self, ok):
def _on_finished(self, ok: int) -> None:
saveGeom(self, "preview")
self.mw.progress.timer(100, self._on_close, False)
def _on_replay_audio(self):
def _on_replay_audio(self) -> None:
if self._state == "question":
replay_audio(self.card(), True)
elif self._state == "answer":
replay_audio(self.card(), False)
def close(self):
def close(self) -> None:
self._on_close()
super().close()
def _on_close(self):
def _on_close(self) -> None:
self._open = False
self._close_callback()
def _setup_web_view(self):
def _setup_web_view(self) -> None:
jsinc = [
"js/vendor/jquery.min.js",
"js/vendor/css_browser_selector.min.js",
@ -136,7 +141,7 @@ class Previewer(QDialog):
if cmd.startswith("play:"):
play_clicked_audio(cmd, self.card())
def render_card(self):
def render_card(self) -> None:
self.cancel_timer()
# Keep track of whether render() has ever been called
# with cardChanged=True since the last successful render
@ -151,7 +156,7 @@ class Previewer(QDialog):
else:
self._render_scheduled()
def cancel_timer(self):
def cancel_timer(self) -> None:
if self._timer:
self._timer.stop()
self._timer = None
@ -214,7 +219,7 @@ class Previewer(QDialog):
self._web.eval("{}({},'{}');".format(func, json.dumps(txt), bodyclass))
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.mw.col.set_config_bool(ConfigBoolKey.PREVIEW_BOTH_SIDES, toggle)
self.mw.col.setMod()
@ -222,7 +227,7 @@ class Previewer(QDialog):
self._state = "question"
self.render_card()
def _state_and_mod(self):
def _state_and_mod(self) -> Tuple[str, int, int]:
c = self.card()
n = c.note()
n.load()
@ -241,7 +246,7 @@ class MultiCardPreviewer(Previewer):
# need to state explicitly it's not implement to avoid W0223
raise NotImplementedError
def _create_gui(self):
def _create_gui(self) -> None:
super()._create_gui()
self._prev = self.bbox.addButton("<", QDialogButtonBox.ActionRole)
self._prev.setAutoDefault(False)
@ -256,39 +261,39 @@ class MultiCardPreviewer(Previewer):
qconnect(self._prev.clicked, self._on_prev)
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:
self._state = "question"
self.render_card()
else:
self._on_prev_card()
def _on_prev_card(self):
def _on_prev_card(self) -> None:
pass
def _on_next(self):
def _on_next(self) -> None:
if self._state == "question":
self._state = "answer"
self.render_card()
else:
self._on_next_card()
def _on_next_card(self):
def _on_next_card(self) -> None:
pass
def _updateButtons(self):
def _updateButtons(self) -> None:
if not self._open:
return
self._prev.setEnabled(self._should_enable_prev())
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
def _should_enable_next(self):
def _should_enable_next(self) -> bool:
return self._state == "question"
def _on_close(self):
def _on_close(self) -> None:
super()._on_close()
self._prev = None
self._next = None
@ -312,20 +317,20 @@ class BrowserPreviewer(MultiCardPreviewer):
self._last_card_id = c.id
return changed
def _on_prev_card(self):
def _on_prev_card(self) -> None:
self._parent.editor.saveNow(
lambda: self._parent._moveCur(QAbstractItemView.MoveUp)
)
def _on_next_card(self):
def _on_next_card(self) -> None:
self._parent.editor.saveNow(
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
def _should_enable_next(self):
def _should_enable_next(self) -> bool:
return (
super()._should_enable_next()
or self._parent.currentRow() < self._parent.model.rowCount(None) - 1

View file

@ -113,17 +113,17 @@ class LoadMetaResult:
class AnkiRestart(SystemExit):
def __init__(self, *args, **kwargs):
def __init__(self, *args, **kwargs) -> None:
self.exitcode = kwargs.pop("exitcode", 0)
super().__init__(*args, **kwargs) # type: ignore
class ProfileManager:
def __init__(self, base=None):
def __init__(self, base: Optional[str] = None) -> None: #
## Settings which should be forgotten each Anki restart
self.session = {}
self.name = None
self.db = None
self.session: Dict[str, Any] = {}
self.name: Optional[str] = None
self.db: Optional[DB] = None
self.profile: Optional[Dict] = None
# instantiate base folder
self.base: str
@ -170,7 +170,7 @@ class ProfileManager:
return p
return os.path.expanduser("~/Documents/Anki")
def maybeMigrateFolder(self):
def maybeMigrateFolder(self) -> None:
newBase = self.base
oldBase = self._oldFolderLocation()
@ -185,7 +185,7 @@ class ProfileManager:
self.base = newBase
shutil.move(oldBase, self.base)
def _tryToMigrateFolder(self, oldBase):
def _tryToMigrateFolder(self, oldBase) -> None:
from PyQt5 import QtGui, QtWidgets
app = QtWidgets.QApplication([])
@ -206,7 +206,7 @@ class ProfileManager:
confirmation.setText(
"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:
progress = QMessageBox()
@ -228,7 +228,7 @@ class ProfileManager:
completion.setWindowTitle(window_title)
completion.setText("Migration complete. Please start Anki again.")
completion.show()
completion.exec()
completion.exec_()
else:
diag = QMessageBox()
diag.setIcon(QMessageBox.Warning)
@ -239,7 +239,7 @@ class ProfileManager:
"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."
)
diag.exec()
diag.exec_()
raise AnkiRestart(exitcode=0)
@ -269,7 +269,7 @@ class ProfileManager:
fn = super().find_class(module, name)
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":
# can't trust str objects from python 2
return QByteArray()
@ -424,7 +424,7 @@ class ProfileManager:
os.makedirs(path)
return path
def _setBaseFolder(self, cmdlineBase: None) -> None:
def _setBaseFolder(self, cmdlineBase: Optional[str]) -> None:
if cmdlineBase:
self.base = os.path.abspath(cmdlineBase)
elif os.environ.get("ANKI_BASE"):
@ -534,7 +534,7 @@ create table if not exists profiles
def setDefaultLang(self, idx: int) -> None:
# create dialog
class NoCloseDiag(QDialog):
def reject(self):
def reject(self) -> None:
pass
d = self.langDiag = NoCloseDiag()
@ -665,7 +665,7 @@ create table if not exists profiles
pass
return RecordingDriver.QtAudioInput
def set_recording_driver(self, driver: RecordingDriver):
def set_recording_driver(self, driver: RecordingDriver) -> None:
self.profile["recordingDriver"] = driver.value
######################################################################

View file

@ -4,7 +4,7 @@
from __future__ import annotations
import time
from typing import Optional
from typing import Callable, Optional
import aqt.forms
from aqt.qt import *
@ -15,13 +15,13 @@ from aqt.utils import TR, disable_help_button, tr
class ProgressManager:
def __init__(self, mw):
def __init__(self, mw: aqt.AnkiQt) -> None:
self.mw = mw
self.app = QApplication.instance()
self.inDB = False
self.blockUpdates = False
self._show_timer: Optional[QTimer] = None
self._win = None
self._win: Optional[ProgressDialog] = None
self._levels = 0
# Safer timers
@ -29,7 +29,9 @@ class ProgressManager:
# A custom timer which avoids firing while a progress dialog is active
# (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.
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
only fire when there is no current progress dialog."""
def handler():
def handler() -> None:
if requiresCollection and not self.mw.col:
# no current collection; timer is no longer valid
print("Ignored progress func as collection unloaded: %s" % repr(func))
@ -136,7 +138,7 @@ class ProgressManager:
self._updating = False
self._lastUpdate = time.time()
def finish(self):
def finish(self) -> None:
self._levels -= 1
self._levels = max(0, self._levels)
if self._levels == 0:
@ -147,13 +149,13 @@ class ProgressManager:
self._show_timer.stop()
self._show_timer = None
def clear(self):
def clear(self) -> None:
"Restore the interface after an error."
if self._levels:
self._levels = 1
self.finish()
def _maybeShow(self):
def _maybeShow(self) -> None:
if not self._levels:
return
if self._shown:
@ -181,17 +183,17 @@ class ProgressManager:
self._win = None
self._shown = 0
def _setBusy(self):
def _setBusy(self) -> None:
self.mw.app.setOverrideCursor(QCursor(Qt.WaitCursor))
def _unsetBusy(self):
def _unsetBusy(self) -> None:
self.app.restoreOverrideCursor()
def busy(self):
def busy(self) -> int:
"True if processing."
return self._levels
def _on_show_timer(self):
def _on_show_timer(self) -> None:
self._show_timer = None
self._showWin()
@ -209,7 +211,7 @@ class ProgressManager:
class ProgressDialog(QDialog):
def __init__(self, parent):
def __init__(self, parent: QWidget) -> None:
QDialog.__init__(self, parent)
disable_help_button(self)
self.form = aqt.forms.progress.Ui_Dialog()
@ -219,18 +221,18 @@ class ProgressDialog(QDialog):
# required for smooth progress bars
self.form.progressBar.setStyleSheet("QProgressBar::chunk { width: 1px; }")
def cancel(self):
def cancel(self) -> None:
self._closingDown = True
self.hide()
def closeEvent(self, evt):
def closeEvent(self, evt) -> None:
if self._closingDown:
evt.accept()
else:
self.wantCancel = True
evt.ignore()
def keyPressEvent(self, evt):
def keyPressEvent(self, evt) -> None:
if evt.key() == Qt.Key_Escape:
evt.ignore()
self.wantCancel = True

View file

@ -30,7 +30,7 @@ except ImportError:
import sip # type: ignore
def debug():
def debug() -> None:
from pdb import set_trace
pyqtRemoveInputHook()
@ -39,7 +39,7 @@ def 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):
sys.stdout.write(line)
pyqtRemoveInputHook()

View file

@ -8,7 +8,7 @@ import html
import json
import re
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
@ -426,7 +426,7 @@ class Reviewer:
# compare with typed answer
res = self.correct(given, cor, showBad=False)
# 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
# escapes too much
s = """
@ -448,7 +448,7 @@ class Reviewer:
if not matches:
return None
def noHint(txt):
def noHint(txt) -> str:
if "::" in txt:
return txt.split("::")[0]
return txt
@ -652,7 +652,7 @@ time = %(time)d;
def _answerButtons(self) -> str:
default = self._defaultEase()
def but(i, label):
def but(i, label) -> str:
if i == default:
extra = """id="defease" class="focus" """
else:
@ -697,7 +697,7 @@ time = %(time)d;
##########################################################################
# 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()
opts = [
[
@ -834,7 +834,7 @@ time = %(time)d;
tooltip(tr(TR.STUDYING_NOTE_BURIED))
def onRecordVoice(self) -> None:
def after_record(path: str):
def after_record(path: str) -> None:
self._recordedAudio = path
self.onReplayRecorded()

View file

@ -17,10 +17,10 @@ class Change(enum.Enum):
class ChangeTracker:
_changed = Change.NO_CHANGE
def __init__(self, mw: AnkiQt):
def __init__(self, mw: AnkiQt) -> None:
self.mw = mw
def mark_basic(self):
def mark_basic(self) -> None:
if self._changed == Change.NO_CHANGE:
self._changed = Change.BASIC_CHANGE

View file

@ -6,7 +6,17 @@ from __future__ import annotations
from concurrent.futures import Future
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
from anki.collection import ConfigBoolKey, SearchTerm
@ -101,7 +111,7 @@ class SidebarModel(QAbstractItemModel):
self.root = root
self._cache_rows(root)
def _cache_rows(self, node: SidebarItem):
def _cache_rows(self, node: SidebarItem) -> None:
"Cache index of children in parent."
for row, item in enumerate(node.children):
item.row_in_parent = row
@ -168,12 +178,12 @@ class SidebarModel(QAbstractItemModel):
else:
return QVariant(theme_manager.icon_from_resources(item.icon))
def supportedDropActions(self):
return Qt.MoveAction
def supportedDropActions(self) -> Qt.DropActions:
return cast(Qt.DropActions, Qt.MoveAction)
def flags(self, index: QModelIndex):
def flags(self, index: QModelIndex) -> Qt.ItemFlags:
if not index.isValid():
return Qt.ItemIsEnabled
return cast(Qt.ItemFlags, Qt.ItemIsEnabled)
flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
item: SidebarItem = index.internalPointer()
@ -183,7 +193,7 @@ class SidebarModel(QAbstractItemModel):
):
flags |= Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled
return flags
return cast(Qt.ItemFlags, flags)
# Helpers
######################################################################
@ -193,7 +203,9 @@ class SidebarModel(QAbstractItemModel):
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()
for row in range(model.rowCount(parent)):
idx = model.index(row, 0, parent)
@ -213,7 +225,7 @@ class FilterModel(QSortFilterProxyModel):
class SidebarSearchBar(QLineEdit):
def __init__(self, sidebar: SidebarTreeView):
def __init__(self, sidebar: SidebarTreeView) -> None:
QLineEdit.__init__(self, sidebar)
self.setPlaceholderText(sidebar.col.tr(TR.BROWSING_SIDEBAR_FILTER))
self.sidebar = sidebar
@ -223,14 +235,14 @@ class SidebarSearchBar(QLineEdit):
qconnect(self.timer.timeout, self.onSearch)
qconnect(self.textChanged, self.onTextChanged)
def onTextChanged(self, text: str):
def onTextChanged(self, text: str) -> None:
if not self.timer.isActive():
self.timer.start()
def onSearch(self):
def onSearch(self) -> None:
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):
self.sidebar.setFocus()
elif evt.key() in (Qt.Key_Enter, Qt.Key_Return):
@ -293,7 +305,7 @@ class SidebarTreeView(QTreeView):
if not self.isVisible():
return
def on_done(fut: Future):
def on_done(fut: Future) -> None:
root = fut.result()
model = SidebarModel(root)
@ -308,7 +320,7 @@ class SidebarTreeView(QTreeView):
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():
self.current_search = None
self.refresh()
@ -331,7 +343,7 @@ class SidebarTreeView(QTreeView):
def drawRow(
self, painter: QPainter, options: QStyleOptionViewItem, idx: QModelIndex
):
) -> None:
if self.current_search is None:
return super().drawRow(painter, options, idx)
if not (item := self.model().item_for_index(idx)):
@ -364,7 +376,7 @@ class SidebarTreeView(QTreeView):
if not source_ids:
return False
def on_done(fut):
def on_done(fut: Future) -> None:
fut.result()
self.refresh()
@ -444,7 +456,7 @@ class SidebarTreeView(QTreeView):
collapse_key: ConfigBoolKeyValue,
type: Optional[SidebarItemType] = None,
) -> SidebarItem:
def update(expanded: bool):
def update(expanded: bool) -> None:
self.col.set_config_bool(collapse_key, not expanded)
top = SidebarItem(
@ -486,7 +498,7 @@ class SidebarTreeView(QTreeView):
type=SidebarItemType.SAVED_SEARCH_ROOT,
)
def on_click():
def on_click() -> None:
self.show_context_menu(root, None)
root.onClick = on_click
@ -503,10 +515,12 @@ class SidebarTreeView(QTreeView):
def _tag_tree(self, root: SidebarItem) -> None:
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:
def toggle_expand():
def toggle_expand() -> Callable[[bool], None]:
full_name = head + node.name # pylint: disable=cell-var-from-loop
return lambda expanded: self.mw.col.tags.set_collapsed(
full_name, not expanded
@ -537,10 +551,12 @@ class SidebarTreeView(QTreeView):
def _deck_tree(self, root: SidebarItem) -> None:
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:
def toggle_expand():
def toggle_expand() -> Callable[[bool], None]:
did = node.deck_id # pylint: disable=cell-var-from-loop
return lambda _: self.mw.col.decks.collapseBrowser(did)
@ -613,7 +629,7 @@ class SidebarTreeView(QTreeView):
return
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()
if item.item_type in self.context_menus:
@ -680,7 +696,8 @@ class SidebarTreeView(QTreeView):
try:
self.mw.col.decks.rename(deck, new_name)
except DeckRenameError as e:
return showWarning(e.description)
showWarning(e.description)
return
self.refresh()
self.mw.deckBrowser.refresh()
@ -690,11 +707,11 @@ class SidebarTreeView(QTreeView):
def _remove_tag(self, item: "aqt.browser.SidebarItem") -> None:
old_name = item.full_name
def do_remove():
def do_remove() -> None:
self.mw.col.tags.remove(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.browser.model.endReset()
fut.result()
@ -713,11 +730,11 @@ class SidebarTreeView(QTreeView):
if new_name == old_name or not new_name:
return
def do_rename():
def do_rename() -> int:
self.mw.col.tags.remove(old_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.browser.model.endReset()
@ -739,10 +756,10 @@ class SidebarTreeView(QTreeView):
did = item.id
if self.mw.deckBrowser.ask_delete_deck(did):
def do_delete():
def do_delete() -> None:
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.browser.search()
self.browser.model.endReset()
@ -777,7 +794,7 @@ class SidebarTreeView(QTreeView):
self.col.set_config("savedFilters", conf)
self.refresh()
def save_current_search(self, _item=None) -> None:
def save_current_search(self, _item: Any = None) -> None:
try:
filt = self.col.build_search_string(
self.browser.form.searchEdit.lineEdit().text()

View file

@ -438,7 +438,7 @@ class MpvManager(MPV, SoundOrVideoPlayer):
class SimpleMplayerSlaveModePlayer(SimpleMplayerPlayer):
def __init__(self, taskman: TaskManager):
def __init__(self, taskman: TaskManager) -> None:
super().__init__(taskman)
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."
dst_mp3 = src_wav.replace(".wav", "%d.mp3" % time.time())
def _on_done(fut: Future):
def _on_done(fut: Future) -> None:
fut.result()
on_done(dst_mp3)
@ -509,7 +509,7 @@ class Recorder(ABC):
# seconds to wait before recording
STARTUP_DELAY = 0.3
def __init__(self, output_path: str):
def __init__(self, output_path: str) -> None:
self.output_path = output_path
def start(self, on_done: Callable[[], None]) -> None:
@ -517,7 +517,7 @@ class Recorder(ABC):
self._started_at = time.time()
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."
on_done(self.output_path)
@ -525,7 +525,7 @@ class Recorder(ABC):
"Seconds since recording started."
return time.time() - self._started_at
def on_timer(self):
def on_timer(self) -> None:
"Will be called periodically."
@ -534,7 +534,7 @@ class Recorder(ABC):
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)
self.mw = mw
@ -567,11 +567,11 @@ class QtAudioInputRecorder(Recorder):
self._iodevice.readyRead.connect(self._on_read_ready) # type: ignore
super().start(on_done)
def _on_read_ready(self):
def _on_read_ready(self) -> None:
self._buffer += self._iodevice.readAll()
def stop(self, on_done: Callable[[str], None]):
def on_stop_timer():
def stop(self, on_done: Callable[[str], None]) -> None:
def on_stop_timer() -> None:
# read anything remaining in buffer & stop
self._on_read_ready()
self._audio_input.stop()
@ -580,7 +580,7 @@ class QtAudioInputRecorder(Recorder):
showWarning(f"recording failed: {err}")
return
def write_file():
def write_file() -> None:
# swallow the first 300ms to allow audio device to quiesce
wait = int(44100 * self.STARTUP_DELAY)
if len(self._buffer) <= wait:
@ -595,7 +595,7 @@ class QtAudioInputRecorder(Recorder):
wf.writeframes(self._buffer)
wf.close()
def and_then(fut):
def and_then(fut) -> None:
fut.result()
Recorder.stop(self, on_done)
@ -672,7 +672,7 @@ class PyAudioThreadedRecorder(threading.Thread):
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)
self.mw = mw
@ -686,7 +686,7 @@ class PyAudioRecorder(Recorder):
while self.duration() < 1:
time.sleep(0.1)
def func(fut):
def func(fut) -> None:
Recorder.stop(self, on_done)
self.thread.finish = True
@ -715,7 +715,7 @@ class RecordDialog(QDialog):
self._start_recording()
self._setup_dialog()
def _setup_dialog(self):
def _setup_dialog(self) -> None:
self.setWindowTitle("Anki")
icon = QLabel()
icon.setPixmap(QPixmap(":/icons/media-record.png"))
@ -740,10 +740,10 @@ class RecordDialog(QDialog):
restoreGeom(self, "audioRecorder2")
self.show()
def _save_diag(self):
def _save_diag(self) -> None:
saveGeom(self, "audioRecorder2")
def _start_recording(self):
def _start_recording(self) -> None:
driver = self.mw.pm.recording_driver()
if driver is RecordingDriver.PyAudio:
self._recorder = PyAudioRecorder(self.mw, namedtmp("rec.wav"))
@ -755,18 +755,18 @@ class RecordDialog(QDialog):
assert_exhaustive(driver)
self._recorder.start(self._start_timer)
def _start_timer(self):
def _start_timer(self) -> None:
self._timer = t = QTimer(self._parent)
t.timeout.connect(self._on_timer) # type: ignore
t.setSingleShot(False)
t.start(100)
def _on_timer(self):
def _on_timer(self) -> None:
self._recorder.on_timer()
duration = self._recorder.duration()
self.label.setText(tr(TR.MEDIA_RECORDINGTIME, secs="%0.1f" % duration))
def accept(self):
def accept(self) -> None:
self._timer.stop()
try:
@ -775,10 +775,10 @@ class RecordDialog(QDialog):
finally:
QDialog.accept(self)
def reject(self):
def reject(self) -> None:
self._timer.stop()
def cleanup(out: str):
def cleanup(out: str) -> None:
os.unlink(out)
try:
@ -790,7 +790,7 @@ class RecordDialog(QDialog):
def record_audio(
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:
on_done(path)
else:

View file

@ -25,7 +25,7 @@ from aqt.utils import (
class NewDeckStats(QDialog):
"""New deck stats."""
def __init__(self, mw: aqt.main.AnkiQt):
def __init__(self, mw: aqt.main.AnkiQt) -> None:
QDialog.__init__(self, mw, Qt.Window)
mw.setupDialogGC(self)
self.mw = mw
@ -54,17 +54,17 @@ class NewDeckStats(QDialog):
self.form.web.set_bridge_command(self._on_bridge_cmd, self)
self.activateWindow()
def reject(self):
def reject(self) -> None:
self.form.web = None
saveGeom(self, self.name)
aqt.dialogs.markClosed("NewDeckStats")
QDialog.reject(self)
def closeWithCallback(self, callback):
def closeWithCallback(self, callback) -> None:
self.reject()
callback()
def _imagePath(self):
def _imagePath(self) -> str:
name = time.strftime("-%Y-%m-%d@%H-%M-%S.pdf", time.localtime(time.time()))
name = "anki-" + tr(TR.STATISTICS_STATS) + name
file = getSaveFile(
@ -77,17 +77,17 @@ class NewDeckStats(QDialog):
)
return file
def saveImage(self):
def saveImage(self) -> None:
path = self._imagePath()
if not path:
return
self.form.web.page().printToPdf(path)
tooltip(tr(TR.STATISTICS_SAVED))
def changePeriod(self, n):
def changePeriod(self, n) -> None:
pass
def changeScope(self, type):
def changeScope(self, type) -> None:
pass
def _on_bridge_cmd(self, cmd: str) -> bool:
@ -98,14 +98,14 @@ class NewDeckStats(QDialog):
return False
def refresh(self):
def refresh(self) -> None:
self.form.web.load_ts_page("graphs")
class DeckStats(QDialog):
"""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)
mw.setupDialogGC(self)
self.mw = mw
@ -143,17 +143,17 @@ class DeckStats(QDialog):
self.refresh()
self.activateWindow()
def reject(self):
def reject(self) -> None:
self.form.web = None
saveGeom(self, self.name)
aqt.dialogs.markClosed("DeckStats")
QDialog.reject(self)
def closeWithCallback(self, callback):
def closeWithCallback(self, callback) -> None:
self.reject()
callback()
def _imagePath(self):
def _imagePath(self) -> str:
name = time.strftime("-%Y-%m-%d@%H-%M-%S.pdf", time.localtime(time.time()))
name = "anki-" + tr(TR.STATISTICS_STATS) + name
file = getSaveFile(
@ -166,22 +166,22 @@ class DeckStats(QDialog):
)
return file
def saveImage(self):
def saveImage(self) -> None:
path = self._imagePath()
if not path:
return
self.form.web.page().printToPdf(path)
tooltip(tr(TR.STATISTICS_SAVED))
def changePeriod(self, n):
def changePeriod(self, n: int) -> None:
self.period = n
self.refresh()
def changeScope(self, type):
def changeScope(self, type: str) -> None:
self.wholeCollection = type == "collection"
self.refresh()
def refresh(self):
def refresh(self) -> None:
self.mw.progress.start(parent=self)
stats = self.mw.col.stats()
stats.wholeCollection = self.wholeCollection

View file

@ -2,6 +2,8 @@
# -*- coding: utf-8 -*-
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from typing import Optional
import aqt
from aqt import gui_hooks
from aqt.qt import *
@ -109,7 +111,7 @@ class StudyDeck(QDialog):
return True
return False
def redraw(self, filt, focus=None):
def redraw(self, filt: str, focus: Optional[str] = None) -> None:
self.filt = filt
self.focus = focus
self.names = [n for n in self.origNames if self._matches(n, filt)]
@ -123,7 +125,7 @@ class StudyDeck(QDialog):
l.setCurrentRow(idx)
l.scrollToItem(l.item(idx), QAbstractItemView.PositionAtCenter)
def _matches(self, name, filt):
def _matches(self, name: str, filt: str) -> bool:
name = name.lower()
filt = filt.lower()
if not filt:
@ -133,7 +135,7 @@ class StudyDeck(QDialog):
return False
return True
def onReset(self):
def onReset(self) -> None:
# model updated?
if self.nameFunc:
self.origNames = self.nameFunc()

View file

@ -40,12 +40,14 @@ class FullSyncChoice(enum.Enum):
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()
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:
out = fut.result()
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)
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 err.is_auth_error():
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()
assert auth
def on_timer():
def on_timer() -> None:
on_normal_sync_timer(mw)
timer = QTimer(mw)
qconnect(timer.timeout, on_timer)
timer.start(150)
def on_future_done(fut):
def on_future_done(fut) -> None:
mw.col.db.begin()
timer.stop()
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:
mw.col.close_for_full_sync()
def on_timer():
def on_timer() -> None:
on_full_sync_timer(mw)
timer = QTimer(mw)
qconnect(timer.timeout, on_timer)
timer.start(150)
def on_future_done(fut):
def on_future_done(fut) -> None:
timer.stop()
mw.col.reopen(after_full_sync=True)
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:
mw.col.close_for_full_sync()
def on_timer():
def on_timer() -> None:
on_full_sync_timer(mw)
timer = QTimer(mw)
qconnect(timer.timeout, on_timer)
timer.start(150)
def on_future_done(fut):
def on_future_done(fut) -> None:
timer.stop()
mw.col.reopen(after_full_sync=True)
mw.reset()
@ -235,7 +237,7 @@ def sync_login(
if username and password:
break
def on_future_done(fut):
def on_future_done(fut) -> None:
try:
auth = fut.result()
except SyncError as e:

View file

@ -4,7 +4,9 @@
from __future__ import annotations
import re
from typing import Iterable, List, Optional, Union
from anki.collection import Collection
from aqt import gui_hooks
from aqt.qt import *
@ -15,9 +17,9 @@ class TagEdit(QLineEdit):
lostFocus = pyqtSignal()
# 0 = tags, 1 = decks
def __init__(self, parent, type=0):
def __init__(self, parent: QDialog, type: int = 0) -> None:
QLineEdit.__init__(self, parent)
self.col = None
self.col: Optional[Collection] = None
self.model = QStringListModel()
self.type = type
if type == 0:
@ -28,19 +30,20 @@ class TagEdit(QLineEdit):
self.completer.setCaseSensitivity(Qt.CaseInsensitive)
self.setCompleter(self.completer)
def setCol(self, col):
def setCol(self, col: Collection) -> None:
"Set the current col, updating list of available tags."
self.col = col
l: Iterable[str]
if self.type == 0:
l = self.col.tags.all()
else:
l = (d.name for d in self.col.decks.all_names_and_ids())
self.model.setStringList(l)
def focusInEvent(self, evt):
def focusInEvent(self, evt: QFocusEvent) -> None:
QLineEdit.focusInEvent(self, evt)
def keyPressEvent(self, evt):
def keyPressEvent(self, evt: QKeyEvent) -> None:
if evt.key() in (Qt.Key_Up, Qt.Key_Down):
# show completer on arrow key up/down
if not self.completer.popup().isVisible():
@ -85,7 +88,7 @@ class TagEdit(QLineEdit):
self.showCompleter()
gui_hooks.tag_editor_did_process_key(self, evt)
def showCompleter(self):
def showCompleter(self) -> None:
self.completer.setCompletionPrefix(self.text())
self.completer.complete()
@ -94,20 +97,26 @@ class TagEdit(QLineEdit):
self.lostFocus.emit() # type: ignore
self.completer.popup().hide()
def hideCompleter(self):
def hideCompleter(self) -> None:
if sip.isdeleted(self.completer):
return
self.completer.popup().hide()
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)
self.tags = []
self.tags: List[str] = []
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 = re.sub(" +", " ", 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)
return [self.tags[self.cursor]]
def pathFromIndex(self, idx):
def pathFromIndex(self, idx: QModelIndex) -> str:
if self.cursor is None:
return self.edit.text()
ret = QCompleter.pathFromIndex(self, idx)

View file

@ -3,14 +3,17 @@
from typing import List, Optional
import aqt
from aqt.customstudy import CustomStudy
from aqt.main import AnkiQt
from aqt.qt import *
from aqt.utils import disable_help_button, restoreGeom, saveGeom
class TagLimit(QDialog):
def __init__(self, mw, parent):
def __init__(self, mw: AnkiQt, parent: CustomStudy) -> None:
QDialog.__init__(self, parent, Qt.Window)
self.tags: Union[str, List] = ""
self.tags: str = ""
self.tags_list: List[str] = []
self.mw = mw
self.parent: Optional[QWidget] = parent
self.deck = self.parent.deck
@ -29,7 +32,7 @@ class TagLimit(QDialog):
restoreGeom(self, "tagLimit")
self.exec_()
def rebuildTagList(self):
def rebuildTagList(self) -> None:
usertags = self.mw.col.tags.byDeck(self.deck["id"], True)
yes = self.deck.get("activeTags", [])
no = self.deck.get("inactiveTags", [])
@ -42,10 +45,10 @@ class TagLimit(QDialog):
groupedTags = []
usertags.sort()
groupedTags.append(usertags)
self.tags = []
self.tags_list = []
for tags in groupedTags:
for t in tags:
self.tags.append(t)
self.tags_list.append(t)
item = QListWidgetItem(t.replace("_", " "))
self.dialog.activeList.addItem(item)
if t in yesHash:
@ -65,11 +68,11 @@ class TagLimit(QDialog):
idx = self.dialog.inactiveList.indexFromItem(item)
self.dialog.inactiveList.selectionModel().select(idx, mode)
def reject(self):
def reject(self) -> None:
self.tags = ""
QDialog.reject(self)
def accept(self):
def accept(self) -> None:
self.hide()
# gather yes/no tags
yes = []
@ -80,12 +83,12 @@ class TagLimit(QDialog):
item = self.dialog.activeList.item(c)
idx = self.dialog.activeList.indexFromItem(item)
if self.dialog.activeList.selectionModel().isSelected(idx):
yes.append(self.tags[c])
yes.append(self.tags_list[c])
# inactive
item = self.dialog.inactiveList.item(c)
idx = self.dialog.inactiveList.indexFromItem(item)
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
self.deck["activeTags"] = yes
self.deck["inactiveTags"] = no

View file

@ -31,7 +31,7 @@ class TaskManager(QObject):
self._closures_lock = Lock()
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."
with self._closures_lock:
self._closures.append(closure)
@ -71,14 +71,14 @@ class TaskManager(QObject):
):
self.mw.progress.start(parent=parent, label=label, immediate=immediate)
def wrapped_done(fut):
def wrapped_done(fut) -> None:
self.mw.progress.finish()
if on_done:
on_done(fut)
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."""
with self._closures_lock:
closures = self._closures

View file

@ -481,7 +481,7 @@ if isWin:
return []
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 = lcid_hex_str_to_lang_code(lang)
name = self._tidy_name(voice.GetAttribute("name"))
@ -525,7 +525,7 @@ if isWin:
id: Any
class WindowsRTTTSFilePlayer(TTSProcessPlayer):
voice_list = None
voice_list: List[Any] = []
tmppath = os.path.join(tmpdir(), "tts.wav")
def import_voices(self) -> None:
@ -561,7 +561,7 @@ if isWin:
)
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()
# 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
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.storage.streams as streams # type: ignore

View file

@ -2,11 +2,13 @@
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import time
from typing import Any, Dict
import requests
import aqt
from anki.utils import platDesc, versionWithBuild
from aqt.main import AnkiQt
from aqt.qt import *
from aqt.utils import TR, openLink, showText, tr
@ -17,12 +19,12 @@ class LatestVersionFinder(QThread):
newMsg = pyqtSignal(dict)
clockIsOff = pyqtSignal(float)
def __init__(self, main):
def __init__(self, main: AnkiQt) -> None:
QThread.__init__(self)
self.main = main
self.config = main.pm.meta
def _data(self):
def _data(self) -> Dict[str, Any]:
return {
"ver": versionWithBuild(),
"os": platDesc(),
@ -31,7 +33,7 @@ class LatestVersionFinder(QThread):
"crt": self.config["created"],
}
def run(self):
def run(self) -> None:
if not self.config["updates"]:
return
d = self._data()
@ -54,7 +56,7 @@ class LatestVersionFinder(QThread):
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)
msg = QMessageBox(mw)
msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No) # type: ignore
@ -71,6 +73,6 @@ def askAndUpdate(mw, ver):
openLink(aqt.appWebsite)
def showMessages(mw, data):
def showMessages(mw, data) -> None:
showText(data["msg"], parent=mw, type="html")
mw.pm.meta["lastMsg"] = data["msgId"]

View file

@ -8,12 +8,35 @@ import re
import subprocess
import sys
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 PyQt5.QtWidgets import (
QAction,
QDialog,
QDialogButtonBox,
QFileDialog,
QHeaderView,
QMenu,
QPushButton,
QSplitter,
QWidget,
)
import anki
import aqt
from anki import Collection
from anki.errors import InvalidInput
from anki.lang import TR # pylint: disable=unused-import
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
if section:
if isinstance(section, HelpPage):
@ -87,27 +110,35 @@ def openHelp(section: HelpPageArgument):
openLink(link)
def openLink(link):
def openLink(link: str) -> None:
tooltip(tr(TR.QT_MISC_LOADING), period=1000)
with noBundledLibs():
QDesktopServices.openUrl(QUrl(link))
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."
return showInfo(text, parent, help, "warning", title=title, textFormat=textFormat)
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."
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."
text = str(err)
if isinstance(err, InvalidInput):
@ -116,24 +147,27 @@ def show_invalid_search_error(err: Exception):
def showInfo(
text,
parent=False,
help="",
type="info",
title="Anki",
text: str,
parent: Union[Literal[False], QDialog] = False,
help: HelpPageArgument = "",
type: str = "info",
title: str = "Anki",
textFormat: Optional[TextFormat] = None,
customBtns=None,
customBtns: Optional[List[QMessageBox.StandardButton]] = None,
) -> int:
"Show a small info window with an OK button."
parent_widget: QWidget
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":
icon = QMessageBox.Warning
elif type == "critical":
icon = QMessageBox.Critical
else:
icon = QMessageBox.Information
mb = QMessageBox(parent)
mb = QMessageBox(parent_widget) #
if textFormat == "plain":
mb.setTextFormat(Qt.PlainText)
elif textFormat == "rich":
@ -161,16 +195,16 @@ def showInfo(
def showText(
txt,
parent=None,
type="text",
run=True,
geomKey=None,
minWidth=500,
minHeight=400,
title="Anki",
copyBtn=False,
):
txt: str,
parent: Optional[QWidget] = None,
type: str = "text",
run: bool = True,
geomKey: Optional[str] = None,
minWidth: int = 500,
minHeight: int = 400,
title: str = "Anki",
copyBtn: bool = False,
) -> Optional[Tuple[QDialog, QDialogButtonBox]]:
if not parent:
parent = aqt.mw.app.activeWindow() or aqt.mw
diag = QDialog(parent)
@ -189,21 +223,21 @@ def showText(
layout.addWidget(box)
if copyBtn:
def onCopy():
def onCopy() -> None:
QApplication.clipboard().setText(text.toPlainText())
btn = QPushButton(tr(TR.QT_MISC_COPY_TO_CLIPBOARD))
qconnect(btn.clicked, onCopy)
box.addButton(btn, QDialogButtonBox.ActionRole)
def onReject():
def onReject() -> None:
if geomKey:
saveGeom(diag, geomKey)
QDialog.reject(diag)
qconnect(box.rejected, onReject)
def onFinish():
def onFinish() -> None:
if geomKey:
saveGeom(diag, geomKey)
@ -214,18 +248,19 @@ def showText(
restoreGeom(diag, geomKey)
if run:
diag.exec_()
return None
else:
return diag, box
def askUser(
text,
parent=None,
text: str,
parent: QDialog = None,
help: HelpPageArgument = None,
defaultno=False,
msgfunc=None,
title="Anki",
):
defaultno: bool = False,
msgfunc: Optional[Callable] = None,
title: str = "Anki",
) -> bool:
"Show a yes/no question. Return true if yes."
if not parent:
parent = aqt.mw.app.activeWindow()
@ -239,7 +274,7 @@ def askUser(
default = QMessageBox.No
else:
default = QMessageBox.Yes
r = msgfunc(parent, title, text, sb, default)
r = msgfunc(parent, title, text, cast(QMessageBox.StandardButtons, sb), default)
if r == QMessageBox.Help:
openHelp(help)
@ -250,10 +285,15 @@ def askUser(
class ButtonedDialog(QMessageBox):
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)
self._buttons = []
self._buttons: List[QPushButton] = []
self.setWindowTitle(title)
self.help = help
self.setIcon(QMessageBox.Warning)
@ -264,7 +304,7 @@ class ButtonedDialog(QMessageBox):
self.addButton(tr(TR.ACTIONS_HELP), QMessageBox.HelpRole)
buttons.append(tr(TR.ACTIONS_HELP))
def run(self):
def run(self) -> str:
self.exec_()
but = self.clickedButton().text()
if but == "Help":
@ -274,13 +314,17 @@ class ButtonedDialog(QMessageBox):
# work around KDE 'helpfully' adding accelerators to button text of Qt apps
return txt.replace("&", "")
def setDefault(self, idx):
def setDefault(self, idx: int) -> None:
self.setDefaultButton(self._buttons[idx])
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:
parent = aqt.mw
diag = ButtonedDialog(text, buttons, parent, help, title=title)
@ -290,14 +334,14 @@ def askUserDialog(
class GetTextDialog(QDialog):
def __init__(
self,
parent,
question,
parent: Optional[QDialog],
question: str,
help: HelpPageArgument = None,
edit=None,
default="",
title="Anki",
minWidth=400,
):
edit: Optional[QLineEdit] = None,
default: str = "",
title: str = "Anki",
minWidth: int = 400,
) -> None:
QDialog.__init__(self, parent)
self.setWindowTitle(title)
disable_help_button(self)
@ -325,26 +369,26 @@ class GetTextDialog(QDialog):
if help:
qconnect(b.button(QDialogButtonBox.Help).clicked, self.helpRequested)
def accept(self):
def accept(self) -> None:
return QDialog.accept(self)
def reject(self):
def reject(self) -> None:
return QDialog.reject(self)
def helpRequested(self):
def helpRequested(self) -> None:
openHelp(self.help)
def getText(
prompt,
parent=None,
prompt: str,
parent: Optional[QDialog] = None,
help: HelpPageArgument = None,
edit=None,
default="",
title="Anki",
geomKey=None,
**kwargs,
):
edit: Optional[QLineEdit] = None,
default: str = "",
title: str = "Anki",
geomKey: Optional[str] = None,
**kwargs: Any,
) -> Tuple[str, int]:
if not parent:
parent = aqt.mw.app.activeWindow() or aqt.mw
d = GetTextDialog(
@ -359,7 +403,7 @@ def getText(
return (str(d.l.text()), ret)
def getOnlyText(*args, **kwargs):
def getOnlyText(*args: Any, **kwargs: Any) -> str:
(s, r) = getText(*args, **kwargs)
if r:
return s
@ -368,7 +412,10 @@ def getOnlyText(*args, **kwargs):
# 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:
parent = aqt.mw.app.activeWindow()
d = QDialog(parent)
@ -389,7 +436,9 @@ def chooseList(prompt, choices, startrow=0, parent=None):
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
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."
assert not dir or not key
if not dir:
@ -426,7 +483,7 @@ def getFile(parent, title, cb, filter="*.*", dir=None, key=None, multi=False):
d.setNameFilter(filter)
ret = []
def accept():
def accept() -> None:
files = list(d.selectedFiles())
if dirkey:
dir = os.path.dirname(files[0])
@ -442,10 +499,17 @@ def getFile(parent, title, cb, filter="*.*", dir=None, key=None, multi=False):
d.exec_()
if 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
variable. The file dialog will default to open with FNAME."""
config_key = dir_description + "Directory"
@ -474,7 +538,7 @@ def getSaveFile(parent, title, dir_description, key, ext, fname=None):
return file
def saveGeom(widget, key: str):
def saveGeom(widget: QDialog, key: str) -> None:
key += "Geom"
if isMac and widget.windowState() & Qt.WindowFullScreen:
geom = None
@ -483,7 +547,9 @@ def saveGeom(widget, key: str):
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"
if aqt.mw.pm.profile.get(key):
widget.restoreGeometry(aqt.mw.pm.profile[key])
@ -498,7 +564,7 @@ def restoreGeom(widget, key: str, offset=None, adjustSize=False):
widget.adjustSize()
def ensureWidgetInScreenBoundaries(widget):
def ensureWidgetInScreenBoundaries(widget: QWidget) -> None:
handle = widget.window().windowHandle()
if not handle:
# window has not yet been shown, retry later
@ -524,58 +590,60 @@ def ensureWidgetInScreenBoundaries(widget):
widget.move(x, y)
def saveState(widget, key: str):
def saveState(widget: QFileDialog, key: str) -> None:
key += "State"
aqt.mw.pm.profile[key] = widget.saveState()
def restoreState(widget, key: str):
def restoreState(widget: Union[aqt.AnkiQt, QFileDialog], key: str) -> None:
key += "State"
if aqt.mw.pm.profile.get(key):
widget.restoreState(aqt.mw.pm.profile[key])
def saveSplitter(widget, key):
def saveSplitter(widget: QSplitter, key: str) -> None:
key += "Splitter"
aqt.mw.pm.profile[key] = widget.saveState()
def restoreSplitter(widget, key):
def restoreSplitter(widget: QSplitter, key: str) -> None:
key += "Splitter"
if aqt.mw.pm.profile.get(key):
widget.restoreState(aqt.mw.pm.profile[key])
def saveHeader(widget, key):
def saveHeader(widget: QHeaderView, key: str) -> None:
key += "Header"
aqt.mw.pm.profile[key] = widget.saveState()
def restoreHeader(widget, key):
def restoreHeader(widget: QHeaderView, key: str) -> None:
key += "Header"
if aqt.mw.pm.profile.get(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"
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"
if aqt.mw.pm.profile.get(key) is not None:
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"
indexKey = key + "ComboActiveIndex"
aqt.mw.pm.session[textKey] = widget.currentText()
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"
indexKey = key + "ComboActiveIndex"
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)
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"
text_input = comboBox.lineEdit().text()
if text_input in history:
@ -599,7 +667,7 @@ def save_combo_history(comboBox: QComboBox, history: List[str], name: str):
return text_input
def restore_combo_history(comboBox: QComboBox, name: str):
def restore_combo_history(comboBox: QComboBox, name: str) -> List[str]:
name += "BoxHistory"
history = aqt.mw.pm.profile.get(name, [])
comboBox.addItems([""] + history)
@ -611,13 +679,13 @@ def restore_combo_history(comboBox: QComboBox, name: str):
return history
def mungeQA(col, txt):
def mungeQA(col: Collection, txt: str) -> str:
print("mungeQA() deprecated; use mw.prepare_card_text_for_display()")
txt = col.media.escape_media_filenames(txt)
return txt
def openFolder(path):
def openFolder(path: str) -> None:
if isWin:
subprocess.Popen(["explorer", "file://" + path])
else:
@ -625,27 +693,27 @@ def openFolder(path):
QDesktopServices.openUrl(QUrl("file://" + path))
def shortcut(key):
def shortcut(key: str) -> str:
if isMac:
return re.sub("(?i)ctrl", "Command", key)
return key
def maybeHideClose(bbox):
def maybeHideClose(bbox: QDialogButtonBox) -> None:
if isMac:
b = bbox.button(QDialogButtonBox.Close)
if b:
bbox.removeButton(b)
def addCloseShortcut(widg):
def addCloseShortcut(widg: QDialog) -> None:
if not isMac:
return
widg._closeShortcut = QShortcut(QKeySequence("Ctrl+W"), widg)
qconnect(widg._closeShortcut.activated, widg.reject)
def downArrow():
def downArrow() -> str:
if isWin:
return ""
# windows 10 is lacking the smaller arrow on English installs
@ -659,13 +727,19 @@ _tooltipTimer: Optional[QTimer] = 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
class CustomLabel(QLabel):
silentlyClose = True
def mousePressEvent(self, evt):
def mousePressEvent(self, evt: QMouseEvent) -> None:
evt.accept()
self.hide()
@ -697,7 +771,7 @@ def tooltip(msg, period=3000, parent=None, x_offset=0, y_offset=100):
_tooltipLabel = lab
def closeTooltip():
def closeTooltip() -> None:
global _tooltipLabel, _tooltipTimer
if _tooltipLabel:
try:
@ -712,7 +786,7 @@ def closeTooltip():
# true if invalid; print warning
def checkInvalidFilename(str, dirsep=True):
def checkInvalidFilename(str: str, dirsep: bool = True) -> bool:
bad = invalidFilename(str, dirsep)
if bad:
showWarning(tr(TR.QT_MISC_THE_FOLLOWING_CHARACTER_CAN_NOT_BE, val=bad))
@ -723,28 +797,30 @@ def checkInvalidFilename(str, dirsep=True):
# Menus
######################################################################
MenuListChild = Union["SubMenu", QAction, "MenuItem", "MenuList"]
class MenuList:
def __init__(self):
self.children = []
def __init__(self) -> None:
self.children: List[MenuListChild] = []
def addItem(self, title, func):
def addItem(self, title: str, func: Callable) -> MenuItem:
item = MenuItem(title, func)
self.children.append(item)
return item
def addSeparator(self):
def addSeparator(self) -> None:
self.children.append(None)
def addMenu(self, title):
def addMenu(self, title: str) -> SubMenu:
submenu = SubMenu(title)
self.children.append(submenu)
return submenu
def addChild(self, child):
def addChild(self, child: Union[SubMenu, QAction, MenuList]) -> None:
self.children.append(child)
def renderTo(self, qmenu):
def renderTo(self, qmenu: QMenu) -> None:
for child in self.children:
if child is None:
qmenu.addSeparator()
@ -753,33 +829,33 @@ class MenuList:
else:
child.renderTo(qmenu)
def popupOver(self, widget):
def popupOver(self, widget: QPushButton) -> None:
qmenu = QMenu()
self.renderTo(qmenu)
qmenu.exec_(widget.mapToGlobal(QPoint(0, 0)))
class SubMenu(MenuList):
def __init__(self, title):
def __init__(self, title: str) -> None:
super().__init__()
self.title = title
def renderTo(self, menu):
def renderTo(self, menu: QMenu) -> None:
submenu = menu.addMenu(self.title)
super().renderTo(submenu)
class MenuItem:
def __init__(self, title, func):
def __init__(self, title: str, func: Callable) -> None:
self.title = title
self.func = func
def renderTo(self, qmenu):
def renderTo(self, qmenu: QMenu) -> None:
a = qmenu.addAction(self.title)
qconnect(a.triggered, self.func)
def qtMenuShortcutWorkaround(qmenu):
def qtMenuShortcutWorkaround(qmenu: QMenu) -> None:
if qtminor < 10:
return
for act in qmenu.actions():
@ -789,7 +865,7 @@ def qtMenuShortcutWorkaround(qmenu):
######################################################################
def supportText():
def supportText() -> str:
import platform
import time
@ -802,9 +878,9 @@ def supportText():
else:
platname = "Linux"
def schedVer():
def schedVer() -> str:
try:
return mw.col.schedVer()
return str(mw.col.schedVer())
except:
return "?"
@ -832,7 +908,7 @@ Add-ons, last update check: {}
######################################################################
# adapted from version detection in qutebrowser
def opengl_vendor():
def opengl_vendor() -> Optional[str]:
old_context = QOpenGLContext.currentContext()
old_surface = None if old_context is None else old_context.surface()
@ -871,7 +947,7 @@ def opengl_vendor():
old_context.makeCurrent(old_surface)
def gfxDriverIsBroken():
def gfxDriverIsBroken() -> bool:
driver = opengl_vendor()
return driver == "nouveau"

View file

@ -22,7 +22,7 @@ serverbaseurl = re.compile(r"^.+:\/\/[^\/]+")
class AnkiWebPage(QWebEnginePage):
def __init__(self, onBridgeCmd):
def __init__(self, onBridgeCmd) -> None:
QWebEnginePage.__init__(self)
self._onBridgeCmd = onBridgeCmd
self._setupBridge()
@ -31,7 +31,7 @@ class AnkiWebPage(QWebEnginePage):
def _setupBridge(self) -> None:
class Bridge(QObject):
@pyqtSlot(str, result=str) # type: ignore
def cmd(self, str):
def cmd(self, str) -> Any:
return json.dumps(self.onCmd(str))
self._bridge = Bridge()
@ -74,7 +74,7 @@ class AnkiWebPage(QWebEnginePage):
script.setRunsOnSubFrames(False)
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,
# and may only accept ascii text
if srcID.startswith("data"):
@ -101,7 +101,7 @@ class AnkiWebPage(QWebEnginePage):
# https://github.com/ankitects/anki/pull/560
sys.stdout.write(buf)
def acceptNavigationRequest(self, url, navType, isMainFrame):
def acceptNavigationRequest(self, url, navType, isMainFrame) -> bool:
if not self.open_links_externally:
return super().acceptNavigationRequest(url, navType, isMainFrame)
@ -120,10 +120,10 @@ class AnkiWebPage(QWebEnginePage):
openLink(url)
return False
def _onCmd(self, str):
def _onCmd(self, str) -> None:
return self._onBridgeCmd(str)
def javaScriptAlert(self, url: QUrl, text: str):
def javaScriptAlert(self, url: QUrl, text: str) -> None:
showInfo(text)
@ -150,7 +150,7 @@ class WebContent:
You should avoid overwriting or interfering with existing data as much
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.head += "<my_head>"
@ -173,7 +173,7 @@ class WebContent:
Then append the subpaths to the corresponding web_content fields
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__)
web_content.css.append(
f"/_addons/{addon_package}/web/my-addon.css")
@ -251,7 +251,7 @@ class AnkiWebView(QWebEngineView):
def set_open_links_externally(self, enable: bool) -> None:
self._page.open_links_externally = enable
def onEsc(self):
def onEsc(self) -> None:
w = self.parent()
while w:
if isinstance(w, QDialog) or isinstance(w, QMainWindow):
@ -266,7 +266,7 @@ class AnkiWebView(QWebEngineView):
break
w = w.parent()
def onCopy(self):
def onCopy(self) -> None:
if not self.selectedText():
ctx = self._page.contextMenuData()
if ctx and ctx.mediaType() == QWebEngineContextMenuData.MediaTypeImage:
@ -274,16 +274,16 @@ class AnkiWebView(QWebEngineView):
else:
self.triggerPageAction(QWebEnginePage.Copy)
def onCut(self):
def onCut(self) -> None:
self.triggerPageAction(QWebEnginePage.Cut)
def onPaste(self):
def onPaste(self) -> None:
self.triggerPageAction(QWebEnginePage.Paste)
def onMiddleClickPaste(self):
def onMiddleClickPaste(self) -> None:
self.triggerPageAction(QWebEnginePage.Paste)
def onSelectAll(self):
def onSelectAll(self) -> None:
self.triggerPageAction(QWebEnginePage.SelectAll)
def contextMenuEvent(self, evt: QContextMenuEvent) -> None:
@ -293,7 +293,7 @@ class AnkiWebView(QWebEngineView):
gui_hooks.webview_will_show_context_menu(self, m)
m.popup(QCursor.pos())
def dropEvent(self, evt):
def dropEvent(self, evt) -> None:
pass
def setHtml(self, html: str) -> None: # type: ignore
@ -312,7 +312,7 @@ class AnkiWebView(QWebEngineView):
if oldFocus:
oldFocus.setFocus()
def load(self, url: QUrl):
def load(self, url: QUrl) -> None:
# allow queuing actions when loading url directly
self._domDone = False
super().load(url)
@ -364,7 +364,7 @@ class AnkiWebView(QWebEngineView):
else:
return 3
def _getWindowColor(self):
def _getWindowColor(self) -> QColor:
if theme_manager.night_mode:
return theme_manager.qcolor("window-bg")
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:
if cb:
def handler(val):
def handler(val) -> None:
if self._shouldIgnoreWebEvent():
print("ignored late js callback", cb)
return
@ -597,18 +597,18 @@ body {{ zoom: {zoom}; background: {background}; direction: {lang_dir}; {font} }}
self.onBridgeCmd = func
self._bridge_context = context
def hide_while_preserving_layout(self):
def hide_while_preserving_layout(self) -> None:
"Hide but keep existing size."
sp = self.sizePolicy()
sp.setRetainSizeWhenHidden(True)
self.setSizePolicy(sp)
self.hide()
def inject_dynamic_style_and_show(self):
def inject_dynamic_style_and_show(self) -> None:
"Add dynamic styling, and reveal."
css = self.standard_css()
def after_style(arg):
def after_style(arg) -> None:
gui_hooks.webview_did_inject_style_into_page(self)
self.show()

13
qt/dmypy-watch.sh Executable file
View 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'

View file

@ -1,6 +1,5 @@
[mypy]
python_version = 3.8
pretty = true
no_strict_optional = true
show_error_codes = true
disallow_untyped_decorators = True
@ -9,6 +8,16 @@ warn_unused_configs = True
check_untyped_defs = 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]
ignore_errors=true

View 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
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//cargo:cargo_build_script.bzl", "cargo_build_script")

View file

@ -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";
package BackendProto;

View 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
pub mod mergeftl;
pub mod protobuf;

View 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
use fluent_syntax::ast::Entry;
use fluent_syntax::parser::Parser;
use std::path::Path;

View 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
use std::path::PathBuf;
use std::{env, fmt::Write};

View 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
include!("mergeftl.rs");
fn main() {

View 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
"""
@generated
cargo-raze generated Bazel file.

View 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.
"""

View 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
import sys, subprocess, os, difflib
clang_format = sys.argv[1]
@ -33,4 +36,4 @@ for path in sys.argv[2:]:
found_bad = True
if found_bad:
sys.exit(1)
sys.exit(1)

View 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
def _rustfmt_impl(ctx):
toolchain = ctx.toolchains["@io_bazel_rules_rust//rust:toolchain"]
script_name = ctx.label.name + "_script"

View file

@ -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"));

View file

@ -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"));

View 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
mod cards;
mod notes;
mod parser;

View file

@ -18,9 +18,9 @@ sql_format_setup()
exports_files([
"tsconfig.json",
"d3_missing.d.ts",
".prettierrc",
"rollup.config.js",
"rollup.aqt.config.js",
".eslintrc.js",
"licenses.json",
"sql_format.ts",

62
ts/editor/BUILD.bazel Normal file
View 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
View 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
View 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);
}

View file

@ -1,12 +1,25 @@
/* Copyright: Ankitects Pty Ltd and contributors
* 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 changeTimer: number | null = null;
let currentNoteId: number | null = null;
declare interface String {
format(...args: string[]): string;
declare global {
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 */
@ -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;
}
function saveNow(keepFocus: boolean): void {
export function saveNow(keepFocus: boolean): void {
if (!currentField) {
return;
}
@ -45,10 +58,6 @@ function triggerKeyTimer(): void {
}, 600);
}
interface Selection {
modify(s: string, t: string, u: string): void;
}
function onKey(evt: KeyboardEvent): void {
// esc clears focus, allowing dialog to close
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 {
const anchor = currentField.getSelection().anchorNode;
@ -179,7 +122,7 @@ function inListItem(): boolean {
return inList;
}
function insertNewline(): void {
export function insertNewline(): void {
if (!inPreEnvironment()) {
setFormat("insertText", "\n");
return;
@ -228,12 +171,12 @@ function updateButtonState(): void {
// 'col': document.queryCommandValue("forecolor")
}
function toggleEditorButton(buttonid: string): void {
export function toggleEditorButton(buttonid: string): void {
const button = $(buttonid)[0];
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);
if (!nosave) {
saveField("key");
@ -256,7 +199,7 @@ function onFocus(evt: FocusEvent): void {
}
elem.focusEditable();
currentField = elem;
pycmd(`focus:${currentField.ord}`);
bridgeCommand(`focus:${currentField.ord}`);
enableButtons();
// do this twice so that there's no flicker on newer versions
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);
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);
for (let i = 0; i < elements.length; i++) {
let elem = elements[i] as EditingArea;
@ -303,7 +246,7 @@ function focusIfField(x: number, y: number): boolean {
}
function onPaste(): void {
pycmd("paste");
bridgeCommand("paste");
window.event.preventDefault();
}
@ -353,7 +296,9 @@ function saveField(type: "blur" | "key"): void {
return;
}
pycmd(`${type}:${currentField.ord}:${currentNoteId}:${currentField.fieldHTML}`);
bridgeCommand(
`${type}:${currentField.ord}:${currentNoteId}:${currentField.fieldHTML}`
);
}
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];
}
function preventButtonFocus(): void {
export function preventButtonFocus(): void {
for (const element of document.querySelectorAll("button.linkb")) {
element.addEventListener("mousedown", (evt: Event) => {
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);
}
@ -419,7 +364,7 @@ function wrapInternal(front: string, back: string, plainText: boolean): void {
}
function onCutOrCopy(): boolean {
pycmd("cutOrCopy");
bridgeCommand("cutOrCopy");
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;
return (fields[n] as EditorField) ?? null;
}
function forEditorField<T>(
export function forEditorField<T>(
values: T[],
func: (field: EditorField, value: T) => 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
// if we don't convert it to a literal colour
const color = window
@ -637,7 +582,7 @@ function setFields(fields: [string, string][]): void {
maybeDisableButtons();
}
function setBackgrounds(cols: ("dupe" | "")[]) {
export function setBackgrounds(cols: ("dupe" | "")[]) {
forEditorField(cols, (field, value) =>
field.editingArea.classList.toggle("dupe", value === "dupe")
);
@ -646,17 +591,17 @@ function setBackgrounds(cols: ("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]) => {
field.setBaseStyling(fontFamily, `${fontSize}px`, isRtl ? "rtl" : "ltr");
});
}
function setNoteId(id: number): void {
export function setNoteId(id: number): void {
currentNoteId = id;
}
let pasteHTML = function (
export let pasteHTML = function (
html: string,
internal: boolean,
extendedMode: boolean
@ -667,172 +612,3 @@ let pasteHTML = function (
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
View 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