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 - Get code completion when developing add-ons
- Make command line scripts that modify .anki2 files via Anki's Python libraries - Make command line scripts that modify .anki2 files via Anki's Python libraries
You will need Python 3.8 or 3.9 installed. If you do not have Python yet, please You will need the 64 bit version of Python 3.8 or 3.9 installed. If you do not
see the platform-specific instructions in the "Building from source" section below have Python yet, please see the platform-specific instructions in the "Building
for more info. from source" section below for more info.
**Mac/Linux**: **Mac/Linux**:

View file

@ -19,8 +19,9 @@ $ brew install rsync bazelisk
**Install Python 3.8**: **Install Python 3.8**:
Install Python 3.8 from <https://python.org>. You may be able to use Install Python 3.8 from <https://python.org>. We have heard reports
the Homebrew version instead, but this is untested. of issues with pyenv and homebrew, so the package from python.org is
the only recommended approach.
Python 3.9 is not currently recommended, as pylint does not support it yet. Python 3.9 is not currently recommended, as pylint does not support it yet.

View file

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

View file

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

View file

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

View file

@ -8,6 +8,7 @@ import enum
import os import os
import pprint import pprint
import re import re
import sys
import time import time
import traceback import traceback
import weakref import weakref
@ -107,7 +108,7 @@ class Collection:
@property @property
def backend(self) -> RustBackend: def backend(self) -> RustBackend:
traceback.print_stack() traceback.print_stack(file=sys.stdout)
print() print()
print( print(
"Accessing the backend directly will break in the future. Please use the public methods on Collection instead." "Accessing the backend directly will break in the future. Please use the public methods on Collection instead."
@ -280,7 +281,7 @@ class Collection:
self.db.rollback() self.db.rollback()
self.db.begin() self.db.begin()
def reopen(self, after_full_sync=False) -> None: def reopen(self, after_full_sync: bool = False) -> None:
assert not self.db assert not self.db
assert self.path.endswith(".anki2") assert self.path.endswith(".anki2")
@ -409,7 +410,7 @@ class Collection:
def cardCount(self) -> Any: def cardCount(self) -> Any:
return self.db.scalar("select count() from cards") return self.db.scalar("select count() from cards")
def remove_cards_and_orphaned_notes(self, card_ids: Sequence[int]): def remove_cards_and_orphaned_notes(self, card_ids: Sequence[int]) -> None:
"You probably want .remove_notes_by_card() instead." "You probably want .remove_notes_by_card() instead."
self._backend.remove_cards(card_ids=card_ids) self._backend.remove_cards(card_ids=card_ids)
@ -505,7 +506,7 @@ class Collection:
dupes = [] dupes = []
fields: Dict[int, int] = {} fields: Dict[int, int] = {}
def ordForMid(mid): def ordForMid(mid: int) -> int:
if mid not in fields: if mid not in fields:
model = self.models.get(mid) model = self.models.get(mid)
for c, f in enumerate(model["flds"]): for c, f in enumerate(model["flds"]):
@ -539,7 +540,10 @@ class Collection:
########################################################################## ##########################################################################
def build_search_string( def build_search_string(
self, *terms: Union[str, SearchTerm], negate=False, match_any=False self,
*terms: Union[str, SearchTerm],
negate: bool = False,
match_any: bool = False,
) -> str: ) -> str:
"""Helper function for the backend's search string operations. """Helper function for the backend's search string operations.
@ -576,11 +580,11 @@ class Collection:
except KeyError: except KeyError:
return default return default
def set_config(self, key: str, val: Any): def set_config(self, key: str, val: Any) -> None:
self.setMod() self.setMod()
self.conf.set(key, val) self.conf.set(key, val)
def remove_config(self, key): def remove_config(self, key: str) -> None:
self.setMod() self.setMod()
self.conf.remove(key) self.conf.remove(key)
@ -779,11 +783,11 @@ table.review-log {{ {revlog_style} }}
# Logging # Logging
########################################################################## ##########################################################################
def log(self, *args, **kwargs) -> None: def log(self, *args: Any, **kwargs: Any) -> None:
if not self._should_log: if not self._should_log:
return return
def customRepr(x): def customRepr(x: Any) -> str:
if isinstance(x, str): if isinstance(x, str):
return x return x
return pprint.pformat(x) return pprint.pformat(x)
@ -865,7 +869,7 @@ table.review-log {{ {revlog_style} }}
def get_preferences(self) -> Preferences: def get_preferences(self) -> Preferences:
return self._backend.get_preferences() return self._backend.get_preferences()
def set_preferences(self, prefs: Preferences): def set_preferences(self, prefs: Preferences) -> None:
self._backend.set_preferences(prefs) self._backend.set_preferences(prefs)

View file

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

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: if col:
return col.tr return col.tr
else: else:

View file

@ -32,7 +32,7 @@ class DB:
del d["_db"] del d["_db"]
return f"{super().__repr__()} {pprint.pformat(d, width=300)}" return f"{super().__repr__()} {pprint.pformat(d, width=300)}"
def execute(self, sql: str, *a, **ka) -> Cursor: def execute(self, sql: str, *a: Any, **ka: Any) -> Cursor:
s = sql.strip().lower() s = sql.strip().lower()
# mark modified? # mark modified?
for stmt in "insert", "update", "delete": for stmt in "insert", "update", "delete":
@ -76,36 +76,36 @@ class DB:
def rollback(self) -> None: def rollback(self) -> None:
self._db.rollback() self._db.rollback()
def scalar(self, *a, **kw) -> Any: def scalar(self, *a: Any, **kw: Any) -> Any:
res = self.execute(*a, **kw).fetchone() res = self.execute(*a, **kw).fetchone()
if res: if res:
return res[0] return res[0]
return None return None
def all(self, *a, **kw) -> List: def all(self, *a: Any, **kw: Any) -> List:
return self.execute(*a, **kw).fetchall() return self.execute(*a, **kw).fetchall()
def first(self, *a, **kw) -> Any: def first(self, *a: Any, **kw: Any) -> Any:
c = self.execute(*a, **kw) c = self.execute(*a, **kw)
res = c.fetchone() res = c.fetchone()
c.close() c.close()
return res return res
def list(self, *a, **kw) -> List: def list(self, *a: Any, **kw: Any) -> List:
return [x[0] for x in self.execute(*a, **kw)] return [x[0] for x in self.execute(*a, **kw)]
def close(self) -> None: def close(self) -> None:
self._db.text_factory = None self._db.text_factory = None
self._db.close() self._db.close()
def set_progress_handler(self, *args) -> None: def set_progress_handler(self, *args: Any) -> None:
self._db.set_progress_handler(*args) self._db.set_progress_handler(*args)
def __enter__(self) -> "DB": def __enter__(self) -> "DB":
self._db.execute("begin") self._db.execute("begin")
return self return self
def __exit__(self, exc_type, *args) -> None: def __exit__(self, *args: Any) -> None:
self._db.close() self._db.close()
def totalChanges(self) -> Any: def totalChanges(self) -> Any:

View file

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

View file

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

View file

@ -3,8 +3,6 @@
from __future__ import annotations from __future__ import annotations
from typing import Any
import anki._backend.backend_pb2 as _pb import anki._backend.backend_pb2 as _pb
# fixme: notfounderror etc need to be in rsbackend.py # fixme: notfounderror etc need to be in rsbackend.py
@ -88,17 +86,15 @@ def backend_exception_to_pylib(err: _pb.BackendError) -> Exception:
return StringError(err.localized) return StringError(err.localized)
# FIXME: this is only used with "abortSchemaMod", but currently some
# add-ons depend on it
class AnkiError(Exception): class AnkiError(Exception):
def __init__(self, type, **data) -> None: def __init__(self, type: str) -> None:
super().__init__() super().__init__()
self.type = type self.type = type
self.data = data
def __str__(self) -> Any: def __str__(self) -> str:
m = self.type return self.type
if self.data:
m += ": %s" % repr(self.data)
return m
class DeckRenameError(Exception): class DeckRenameError(Exception):
@ -106,5 +102,5 @@ class DeckRenameError(Exception):
super().__init__() super().__init__()
self.description = description self.description = description
def __str__(self): def __str__(self) -> str:
return "Couldn't rename deck: " + self.description return "Couldn't rename deck: " + self.description

View file

@ -16,10 +16,10 @@ class Finder:
self.col = col.weakref() self.col = col.weakref()
print("Finder() is deprecated, please use col.find_cards() or .find_notes()") print("Finder() is deprecated, please use col.find_cards() or .find_notes()")
def findCards(self, query, order): def findCards(self, query: Any, order: Any) -> Any:
return self.col.find_cards(query, order) return self.col.find_cards(query, order)
def findNotes(self, query): def findNotes(self, query: Any) -> Any:
return self.col.find_notes(query) return self.col.find_notes(query)
@ -55,7 +55,7 @@ def fieldNamesForNotes(col: Collection, nids: List[int]) -> List[str]:
########################################################################## ##########################################################################
def fieldNames(col, downcase=True) -> List: def fieldNames(col: Collection, downcase: bool = True) -> List:
fields: Set[str] = set() fields: Set[str] = set()
for m in col.models.all(): for m in col.models.all():
for f in m["flds"]: for f in m["flds"]:

View file

@ -25,7 +25,7 @@ from anki.hooks_gen import *
_hooks: Dict[str, List[Callable[..., Any]]] = {} _hooks: Dict[str, List[Callable[..., Any]]] = {}
def runHook(hook: str, *args) -> None: def runHook(hook: str, *args: Any) -> None:
"Run all functions on hook." "Run all functions on hook."
hookFuncs = _hooks.get(hook, None) hookFuncs = _hooks.get(hook, None)
if hookFuncs: if hookFuncs:
@ -37,7 +37,7 @@ def runHook(hook: str, *args) -> None:
raise raise
def runFilter(hook: str, arg: Any, *args) -> Any: def runFilter(hook: str, arg: Any, *args: Any) -> Any:
hookFuncs = _hooks.get(hook, None) hookFuncs = _hooks.get(hook, None)
if hookFuncs: if hookFuncs:
for func in hookFuncs: for func in hookFuncs:
@ -57,7 +57,7 @@ def addHook(hook: str, func: Callable) -> None:
_hooks[hook].append(func) _hooks[hook].append(func)
def remHook(hook, func) -> None: def remHook(hook: Any, func: Any) -> None:
"Remove a function if is on hook." "Remove a function if is on hook."
hook = _hooks.get(hook, []) hook = _hooks.get(hook, [])
if func in hook: if func in hook:
@ -72,10 +72,10 @@ def remHook(hook, func) -> None:
# #
# If you call wrap() with pos='around', the original function will not be called # If you call wrap() with pos='around', the original function will not be called
# automatically but can be called with _old(). # automatically but can be called with _old().
def wrap(old, new, pos="after") -> Callable: def wrap(old: Any, new: Any, pos: str = "after") -> Callable:
"Override an existing function." "Override an existing function."
def repl(*args, **kwargs): def repl(*args: Any, **kwargs: Any) -> Any:
if pos == "after": if pos == "after":
old(*args, **kwargs) old(*args, **kwargs)
return new(*args, **kwargs) return new(*args, **kwargs)
@ -85,7 +85,7 @@ def wrap(old, new, pos="after") -> Callable:
else: else:
return new(_old=old, *args, **kwargs) return new(_old=old, *args, **kwargs)
def decorator_wrapper(f, *args, **kwargs): def decorator_wrapper(f: Any, *args: Any, **kwargs: Any) -> Any:
return repl(*args, **kwargs) return repl(*args, **kwargs)
return decorator.decorator(decorator_wrapper)(old) return decorator.decorator(decorator_wrapper)(old)

View file

@ -5,6 +5,8 @@
Wrapper for requests that adds a callback for tracking upload/download progress. Wrapper for requests that adds a callback for tracking upload/download progress.
""" """
from __future__ import annotations
import io import io
import os import os
from typing import Any, Callable, Dict, Optional from typing import Any, Callable, Dict, Optional
@ -28,24 +30,23 @@ class HttpClient:
self.progress_hook = progress_hook self.progress_hook = progress_hook
self.session = requests.Session() self.session = requests.Session()
def __enter__(self): def __enter__(self) -> HttpClient:
return self return self
def __exit__(self, *args): def __exit__(self, *args: Any) -> None:
self.close() self.close()
def close(self): def close(self) -> None:
if self.session: if self.session:
self.session.close() self.session.close()
self.session = None self.session = None
def __del__(self): def __del__(self) -> None:
self.close() self.close()
def post(self, url: str, data: Any, headers: Optional[Dict[str, str]]) -> Response: def post(
data = _MonitoringFile( self, url: str, data: bytes, headers: Optional[Dict[str, str]]
data, hook=self.progress_hook ) -> Response:
) # pytype: disable=wrong-arg-types
headers["User-Agent"] = self._agentName() headers["User-Agent"] = self._agentName()
return self.session.post( return self.session.post(
url, url,
@ -56,7 +57,7 @@ class HttpClient:
verify=self.verify, verify=self.verify,
) # pytype: disable=wrong-arg-types ) # pytype: disable=wrong-arg-types
def get(self, url, headers=None) -> Response: def get(self, url: str, headers: Dict[str, str] = None) -> Response:
if headers is None: if headers is None:
headers = {} headers = {}
headers["User-Agent"] = self._agentName() headers["User-Agent"] = self._agentName()
@ -64,7 +65,7 @@ class HttpClient:
url, stream=True, headers=headers, timeout=self.timeout, verify=self.verify url, stream=True, headers=headers, timeout=self.timeout, verify=self.verify
) )
def streamContent(self, resp) -> bytes: def streamContent(self, resp: Response) -> bytes:
resp.raise_for_status() resp.raise_for_status()
buf = io.BytesIO() buf = io.BytesIO()
@ -87,15 +88,3 @@ if os.environ.get("ANKI_NOVERIFYSSL"):
import warnings import warnings
warnings.filterwarnings("ignore") warnings.filterwarnings("ignore")
class _MonitoringFile(io.BufferedReader):
def __init__(self, raw: io.RawIOBase, hook: Optional[ProgressCallback]):
io.BufferedReader.__init__(self, raw)
self.hook = hook
def read(self, size=-1) -> bytes:
data = io.BufferedReader.read(self, HTTP_BUF_SIZE)
if self.hook:
self.hook(len(data), 0)
return data

View file

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

View file

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

View file

@ -5,7 +5,9 @@ from __future__ import annotations
import copy import copy
import pprint import pprint
import sys
import time import time
import traceback
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
import anki # pylint: disable=unused-import import anki # pylint: disable=unused-import
@ -39,36 +41,37 @@ class ModelsDictProxy:
def __init__(self, col: anki.collection.Collection): def __init__(self, col: anki.collection.Collection):
self._col = col.weakref() self._col = col.weakref()
def _warn(self): def _warn(self) -> None:
traceback.print_stack(file=sys.stdout)
print("add-on should use methods on col.models, not col.models.models dict") print("add-on should use methods on col.models, not col.models.models dict")
def __getitem__(self, item): def __getitem__(self, item: Any) -> Any:
self._warn() self._warn()
return self._col.models.get(int(item)) return self._col.models.get(int(item))
def __setitem__(self, key, val): def __setitem__(self, key: str, val: Any) -> None:
self._warn() self._warn()
self._col.models.save(val) self._col.models.save(val)
def __len__(self): def __len__(self) -> int:
self._warn() self._warn()
return len(self._col.models.all_names_and_ids()) return len(self._col.models.all_names_and_ids())
def keys(self): def keys(self) -> Any:
self._warn() self._warn()
return [str(nt.id) for nt in self._col.models.all_names_and_ids()] return [str(nt.id) for nt in self._col.models.all_names_and_ids()]
def values(self): def values(self) -> Any:
self._warn() self._warn()
return self._col.models.all() return self._col.models.all()
def items(self): def items(self) -> Any:
self._warn() self._warn()
return [(str(nt["id"]), nt) for nt in self._col.models.all()] return [(str(nt["id"]), nt) for nt in self._col.models.all()]
def __contains__(self, item): def __contains__(self, item: Any) -> bool:
self._warn() self._warn()
self._col.models.have(item) return self._col.models.have(item)
class ModelManager: class ModelManager:
@ -123,7 +126,7 @@ class ModelManager:
def _get_cached(self, ntid: int) -> Optional[NoteType]: def _get_cached(self, ntid: int) -> Optional[NoteType]:
return self._cache.get(ntid) return self._cache.get(ntid)
def _clear_cache(self): def _clear_cache(self) -> None:
self._cache = {} self._cache = {}
# Listing note types # Listing note types
@ -218,7 +221,7 @@ class ModelManager:
"Delete model, and all its cards/notes." "Delete model, and all its cards/notes."
self.remove(m["id"]) self.remove(m["id"])
def remove_all_notetypes(self): def remove_all_notetypes(self) -> None:
for nt in self.all_names_and_ids(): for nt in self.all_names_and_ids():
self._remove_from_cache(nt.id) self._remove_from_cache(nt.id)
self.col._backend.remove_notetype(nt.id) self.col._backend.remove_notetype(nt.id)
@ -236,7 +239,7 @@ class ModelManager:
if existing_id is not None and existing_id != m["id"]: if existing_id is not None and existing_id != m["id"]:
m["name"] += "-" + checksum(str(time.time()))[:5] m["name"] += "-" + checksum(str(time.time()))[:5]
def update(self, m: NoteType, preserve_usn=True) -> None: def update(self, m: NoteType, preserve_usn: bool = True) -> None:
"Add or update an existing model. Use .save() instead." "Add or update an existing model. Use .save() instead."
self._remove_from_cache(m["id"]) self._remove_from_cache(m["id"])
self.ensureNameUnique(m) self.ensureNameUnique(m)

View file

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

View file

@ -350,7 +350,7 @@ limit %d"""
lastIvl = -(self._delayForGrade(conf, lastLeft)) lastIvl = -(self._delayForGrade(conf, lastLeft))
ivl = card.ivl if leaving else -(self._delayForGrade(conf, card.left)) ivl = card.ivl if leaving else -(self._delayForGrade(conf, card.left))
def log(): def log() -> None:
self.col.db.execute( self.col.db.execute(
"insert into revlog values (?,?,?,?,?,?,?,?,?)", "insert into revlog values (?,?,?,?,?,?,?,?,?)",
int(time.time() * 1000), int(time.time() * 1000),
@ -450,7 +450,7 @@ and due <= ? limit ?)""",
self._revQueue: List[Any] = [] self._revQueue: List[Any] = []
self._revDids = self.col.decks.active()[:] self._revDids = self.col.decks.active()[:]
def _fillRev(self, recursing=False) -> bool: def _fillRev(self, recursing: bool = False) -> bool:
"True if a review card can be fetched." "True if a review card can be fetched."
if self._revQueue: if self._revQueue:
return True return True

View file

@ -166,7 +166,7 @@ class Scheduler:
self._restorePreviewCard(card) self._restorePreviewCard(card)
self._removeFromFiltered(card) self._removeFromFiltered(card)
def _reset_counts(self): def _reset_counts(self) -> None:
tree = self.deck_due_tree(self.col.decks.selected()) tree = self.deck_due_tree(self.col.decks.selected())
node = self.col.decks.find_deck_in_tree(tree, int(self.col.conf["curDeck"])) node = self.col.decks.find_deck_in_tree(tree, int(self.col.conf["curDeck"]))
if not node: if not node:
@ -187,7 +187,7 @@ class Scheduler:
new, lrn, rev = counts new, lrn, rev = counts
return (new, lrn, rev) return (new, lrn, rev)
def _is_finished(self): def _is_finished(self) -> bool:
"Don't use this, it is a stop-gap until this code is refactored." "Don't use this, it is a stop-gap until this code is refactored."
return not any((self.newCount, self.revCount, self._immediate_learn_count)) return not any((self.newCount, self.revCount, self._immediate_learn_count))
@ -229,8 +229,12 @@ order by due"""
########################################################################## ##########################################################################
def update_stats( def update_stats(
self, deck_id: int, new_delta=0, review_delta=0, milliseconds_delta=0 self,
): deck_id: int,
new_delta: int = 0,
review_delta: int = 0,
milliseconds_delta: int = 0,
) -> None:
self.col._backend.update_stats( self.col._backend.update_stats(
deck_id=deck_id, deck_id=deck_id,
new_delta=new_delta, new_delta=new_delta,
@ -321,7 +325,7 @@ order by due"""
self._newQueue: List[int] = [] self._newQueue: List[int] = []
self._updateNewCardRatio() self._updateNewCardRatio()
def _fillNew(self, recursing=False) -> bool: def _fillNew(self, recursing: bool = False) -> bool:
if self._newQueue: if self._newQueue:
return True return True
if not self.newCount: if not self.newCount:
@ -841,7 +845,7 @@ and due <= ? limit ?)"""
def _resetRev(self) -> None: def _resetRev(self) -> None:
self._revQueue: List[int] = [] self._revQueue: List[int] = []
def _fillRev(self, recursing=False) -> bool: def _fillRev(self, recursing: bool = False) -> bool:
"True if a review card can be fetched." "True if a review card can be fetched."
if self._revQueue: if self._revQueue:
return True return True
@ -947,7 +951,7 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
self._removeFromFiltered(card) self._removeFromFiltered(card)
def _logRev(self, card: Card, ease: int, delay: int, type: int) -> None: def _logRev(self, card: Card, ease: int, delay: int, type: int) -> None:
def log(): def log() -> None:
self.col.db.execute( self.col.db.execute(
"insert into revlog values (?,?,?,?,?,?,?,?,?)", "insert into revlog values (?,?,?,?,?,?,?,?,?)",
int(time.time() * 1000), int(time.time() * 1000),
@ -1344,7 +1348,7 @@ due = (case when odue>0 then odue else due end), odue = 0, odid = 0, usn = ? whe
mode = BuryOrSuspendMode.BURY_SCHED mode = BuryOrSuspendMode.BURY_SCHED
self.col._backend.bury_or_suspend_cards(card_ids=ids, mode=mode) self.col._backend.bury_or_suspend_cards(card_ids=ids, mode=mode)
def bury_note(self, note: Note): def bury_note(self, note: Note) -> None:
self.bury_cards(note.card_ids()) self.bury_cards(note.card_ids())
# legacy # legacy
@ -1472,7 +1476,7 @@ and (queue={QUEUE_TYPE_NEW} or (queue={QUEUE_TYPE_REV} and due<=?))""",
def orderCards(self, did: int) -> None: def orderCards(self, did: int) -> None:
self.col._backend.sort_deck(deck_id=did, randomize=False) self.col._backend.sort_deck(deck_id=did, randomize=False)
def resortConf(self, conf) -> None: def resortConf(self, conf: DeckConfig) -> None:
for did in self.col.decks.didsForConf(conf): for did in self.col.decks.didsForConf(conf):
if conf["new"]["order"] == 0: if conf["new"]["order"] == 0:
self.randomizeCards(did) self.randomizeCards(did)

View file

@ -140,7 +140,7 @@ from revlog where id > ? """
relrn = relrn or 0 relrn = relrn or 0
filt = filt or 0 filt = filt or 0
# studied # studied
def bold(s): def bold(s: str) -> str:
return "<b>" + str(s) + "</b>" return "<b>" + str(s) + "</b>"
if cards: if cards:
@ -298,7 +298,7 @@ group by day order by day"""
# pylint: disable=invalid-unary-operand-type # pylint: disable=invalid-unary-operand-type
conf["xaxis"]["min"] = -days + 0.5 conf["xaxis"]["min"] = -days + 0.5
def plot(id, data, ylabel, ylabel2): def plot(id: str, data: Any, ylabel: str, ylabel2: str) -> str:
return self._graph( return self._graph(
id, data=data, conf=conf, xunit=chunk, ylabel=ylabel, ylabel2=ylabel2 id, data=data, conf=conf, xunit=chunk, ylabel=ylabel, ylabel2=ylabel2
) )
@ -333,7 +333,7 @@ group by day order by day"""
# pylint: disable=invalid-unary-operand-type # pylint: disable=invalid-unary-operand-type
conf["xaxis"]["min"] = -days + 0.5 conf["xaxis"]["min"] = -days + 0.5
def plot(id, data, ylabel, ylabel2): def plot(id: str, data: Any, ylabel: str, ylabel2: str) -> str:
return self._graph( return self._graph(
id, data=data, conf=conf, xunit=chunk, ylabel=ylabel, ylabel2=ylabel2 id, data=data, conf=conf, xunit=chunk, ylabel=ylabel, ylabel2=ylabel2
) )

View file

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

View file

@ -13,7 +13,7 @@ from __future__ import annotations
import pprint import pprint
import re import re
from typing import Collection, List, Optional, Sequence, Tuple from typing import Collection, List, Match, Optional, Sequence, Tuple
import anki # pylint: disable=unused-import import anki # pylint: disable=unused-import
import anki._backend.backend_pb2 as _pb import anki._backend.backend_pb2 as _pb
@ -48,7 +48,7 @@ class TagManager:
############################################################# #############################################################
def register( def register(
self, tags: Collection[str], usn: Optional[int] = None, clear=False self, tags: Collection[str], usn: Optional[int] = None, clear: bool = False
) -> None: ) -> None:
print("tags.register() is deprecated and no longer works") print("tags.register() is deprecated and no longer works")
@ -56,10 +56,10 @@ class TagManager:
"Clear unused tags and add any missing tags from notes to the tag list." "Clear unused tags and add any missing tags from notes to the tag list."
self.clear_unused_tags() self.clear_unused_tags()
def clear_unused_tags(self): def clear_unused_tags(self) -> None:
self.col._backend.clear_unused_tags() self.col._backend.clear_unused_tags()
def byDeck(self, did, children=False) -> List[str]: def byDeck(self, did: int, children: bool = False) -> List[str]:
basequery = "select n.tags from cards c, notes n WHERE c.nid = n.id" basequery = "select n.tags from cards c, notes n WHERE c.nid = n.id"
if not children: if not children:
query = basequery + " AND c.did=?" query = basequery + " AND c.did=?"
@ -72,7 +72,7 @@ class TagManager:
res = self.col.db.list(query) res = self.col.db.list(query)
return list(set(self.split(" ".join(res)))) return list(set(self.split(" ".join(res))))
def set_collapsed(self, tag: str, collapsed: bool): def set_collapsed(self, tag: str, collapsed: bool) -> None:
"Set browser collapse state for tag, registering the tag if missing." "Set browser collapse state for tag, registering the tag if missing."
self.col._backend.set_tag_collapsed(name=tag, collapsed=collapsed) self.col._backend.set_tag_collapsed(name=tag, collapsed=collapsed)
@ -139,9 +139,9 @@ class TagManager:
def remFromStr(self, deltags: str, tags: str) -> str: def remFromStr(self, deltags: str, tags: str) -> str:
"Delete tags if they exist." "Delete tags if they exist."
def wildcard(pat, str): def wildcard(pat: str, repl: str) -> Match:
pat = re.escape(pat).replace("\\*", ".*") pat = re.escape(pat).replace("\\*", ".*")
return re.match("^" + pat + "$", str, re.IGNORECASE) return re.match("^" + pat + "$", repl, re.IGNORECASE)
currentTags = self.split(tags) currentTags = self.split(tags)
for tag in self.split(deltags): for tag in self.split(deltags):

View file

@ -3,9 +3,7 @@
from __future__ import annotations from __future__ import annotations
# some add-ons expect json to be in the utils module import json as _json
import json # pylint: disable=unused-import
import locale
import os import os
import platform import platform
import random import random
@ -20,7 +18,7 @@ import traceback
from contextlib import contextmanager from contextlib import contextmanager
from hashlib import sha1 from hashlib import sha1
from html.entities import name2codepoint from html.entities import name2codepoint
from typing import Iterable, Iterator, List, Optional, Union from typing import Any, Iterable, Iterator, List, Match, Optional, Union
from anki.dbproxy import DBProxy from anki.dbproxy import DBProxy
@ -34,8 +32,17 @@ try:
from_json_bytes = orjson.loads from_json_bytes = orjson.loads
except: except:
print("orjson is missing; DB operations will be slower") print("orjson is missing; DB operations will be slower")
to_json_bytes = lambda obj: json.dumps(obj).encode("utf8") # type: ignore to_json_bytes = lambda obj: _json.dumps(obj).encode("utf8") # type: ignore
from_json_bytes = json.loads from_json_bytes = _json.loads
def __getattr__(name: str) -> Any:
if name == "json":
traceback.print_stack(file=sys.stdout)
print("add-on should import json directly, not from anki.utils")
return _json
raise AttributeError(f"module {__name__} has no attribute {name}")
# Time handling # Time handling
############################################################################## ##############################################################################
@ -46,22 +53,6 @@ def intTime(scale: int = 1) -> int:
return int(time.time() * scale) return int(time.time() * scale)
# Locale
##############################################################################
def fmtPercentage(float_value, point=1) -> str:
"Return float with percentage sign"
fmt = "%" + "0.%(b)df" % {"b": point}
return locale.format_string(fmt, float_value) + "%"
def fmtFloat(float_value, point=1) -> str:
"Return a string with decimal separator according to current locale"
fmt = "%" + "0.%(b)df" % {"b": point}
return locale.format_string(fmt, float_value)
# HTML # HTML
############################################################################## ##############################################################################
reComment = re.compile("(?s)<!--.*?-->") reComment = re.compile("(?s)<!--.*?-->")
@ -114,7 +105,7 @@ def entsToTxt(html: str) -> str:
# replace it first # replace it first
html = html.replace("&nbsp;", " ") html = html.replace("&nbsp;", " ")
def fixup(m): def fixup(m: Match) -> str:
text = m.group(0) text = m.group(0)
if text[:2] == "&#": if text[:2] == "&#":
# character reference # character reference
@ -140,14 +131,6 @@ def entsToTxt(html: str) -> str:
############################################################################## ##############################################################################
def hexifyID(id) -> str:
return "%x" % int(id)
def dehexifyID(id) -> int:
return int(id, 16)
def ids2str(ids: Iterable[Union[int, str]]) -> str: def ids2str(ids: Iterable[Union[int, str]]) -> str:
"""Given a list of integers, return a string '(int1,int2,...)'.""" """Given a list of integers, return a string '(int1,int2,...)'."""
return "(%s)" % ",".join(str(i) for i in ids) return "(%s)" % ",".join(str(i) for i in ids)
@ -195,23 +178,6 @@ def guid64() -> str:
return base91(random.randint(0, 2 ** 64 - 1)) return base91(random.randint(0, 2 ** 64 - 1))
# increment a guid by one, for note type conflicts
def incGuid(guid) -> str:
return _incGuid(guid[::-1])[::-1]
def _incGuid(guid) -> str:
s = string
table = s.ascii_letters + s.digits + _base91_extra_chars
idx = table.index(guid[0])
if idx + 1 == len(table):
# overflow
guid = table[0] + _incGuid(guid[1:])
else:
guid = table[idx + 1] + guid[1:]
return guid
# Fields # Fields
############################################################################## ##############################################################################
@ -250,7 +216,7 @@ def tmpdir() -> str:
global _tmpdir global _tmpdir
if not _tmpdir: if not _tmpdir:
def cleanup(): def cleanup() -> None:
if os.path.exists(_tmpdir): if os.path.exists(_tmpdir):
shutil.rmtree(_tmpdir) shutil.rmtree(_tmpdir)
@ -294,7 +260,7 @@ def noBundledLibs() -> Iterator[None]:
os.environ["LD_LIBRARY_PATH"] = oldlpath os.environ["LD_LIBRARY_PATH"] = oldlpath
def call(argv: List[str], wait: bool = True, **kwargs) -> int: def call(argv: List[str], wait: bool = True, **kwargs: Any) -> int:
"Execute a command. If WAIT, return exit code." "Execute a command. If WAIT, return exit code."
# ensure we don't open a separate window for forking process on windows # ensure we don't open a separate window for forking process on windows
if isWin: if isWin:
@ -338,7 +304,7 @@ devMode = os.getenv("ANKIDEV", "")
invalidFilenameChars = ':*?"<>|' invalidFilenameChars = ':*?"<>|'
def invalidFilename(str, dirsep=True) -> Optional[str]: def invalidFilename(str: str, dirsep: bool = True) -> Optional[str]:
for c in invalidFilenameChars: for c in invalidFilenameChars:
if c in str: if c in str:
return c return c
@ -384,7 +350,7 @@ class TimedLog:
def __init__(self) -> None: def __init__(self) -> None:
self._last = time.time() self._last = time.time()
def log(self, s) -> None: def log(self, s: str) -> None:
path, num, fn, y = traceback.extract_stack(limit=2)[0] path, num, fn, y = traceback.extract_stack(limit=2)[0]
sys.stderr.write( sys.stderr.write(
"%5dms: %s(): %s\n" % ((time.time() - self._last) * 1000, fn, s) "%5dms: %s(): %s\n" % ((time.time() - self._last) * 1000, fn, s)

View file

@ -9,6 +9,14 @@ warn_redundant_casts = True
warn_unused_configs = True warn_unused_configs = True
strict_equality = true strict_equality = true
[mypy-anki.*]
disallow_untyped_defs = True
[mypy-anki.importing.*]
disallow_untyped_defs = False
[mypy-anki.exporting]
disallow_untyped_defs = False
[mypy-win32file] [mypy-win32file]
ignore_missing_imports = True ignore_missing_imports = True
[mypy-win32pipe] [mypy-win32pipe]

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

@ -605,7 +605,7 @@ class AddonManager:
def _addon_schema_path(self, dir: str) -> str: def _addon_schema_path(self, dir: str) -> str:
return os.path.join(self.addonsFolder(dir), "config.schema.json") return os.path.join(self.addonsFolder(dir), "config.schema.json")
def _addon_schema(self, dir: str): def _addon_schema(self, dir: str) -> Any:
path = self._addon_schema_path(dir) path = self._addon_schema_path(dir)
try: try:
if not os.path.exists(path): if not os.path.exists(path):
@ -867,9 +867,10 @@ class AddonsDialog(QDialog):
def onInstallFiles(self, paths: Optional[List[str]] = None) -> Optional[bool]: def onInstallFiles(self, paths: Optional[List[str]] = None) -> Optional[bool]:
if not paths: if not paths:
key = tr(TR.ADDONS_PACKAGED_ANKI_ADDON) + " (*{})".format(self.mgr.ext) key = tr(TR.ADDONS_PACKAGED_ANKI_ADDON) + " (*{})".format(self.mgr.ext)
paths = getFile( paths_ = getFile(
self, tr(TR.ADDONS_INSTALL_ADDONS), None, key, key="addons", multi=True self, tr(TR.ADDONS_INSTALL_ADDONS), None, key, key="addons", multi=True
) )
paths = paths_ # type: ignore
if not paths: if not paths:
return False return False

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

@ -1,4 +1,5 @@
load("@bazel_skylib//rules:copy_file.bzl", "copy_file") load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
load("//qt/aqt/data/web/pages:defs.bzl", "copy_page")
load("compile_sass.bzl", "compile_sass") load("compile_sass.bzl", "compile_sass")
compile_sass( compile_sass(
@ -16,11 +17,21 @@ copy_file(
out = "core.css", out = "core.css",
) )
copy_page(
name = "editor",
srcs = [
"editor.css",
"editable.css",
],
package = "//ts/editor",
)
filegroup( filegroup(
name = "css", name = "css",
srcs = [ srcs = [
"core.css", "core.css",
"css_local", "css_local",
"editor",
], ],
visibility = ["//qt:__subpackages__"], visibility = ["//qt:__subpackages__"],
) )

View file

@ -1,9 +1,13 @@
load("@npm//@bazel/typescript:index.bzl", "ts_library") load("@npm//@bazel/typescript:index.bzl", "ts_library")
load("@npm//@bazel/rollup:index.bzl", "rollup_bundle")
load("//qt/aqt/data/web/pages:defs.bzl", "copy_page")
load("//ts:prettier.bzl", "prettier_test") load("//ts:prettier.bzl", "prettier_test")
load("//ts:eslint.bzl", "eslint_test")
ts_library( ts_library(
name = "pycmd", name = "pycmd",
srcs = ["pycmd.d.ts"], srcs = ["pycmd.d.ts"],
visibility = ["//qt/aqt/data/web/js:__subpackages__"],
) )
ts_library( ts_library(
@ -26,10 +30,19 @@ filegroup(
output_group = "es5_sources", output_group = "es5_sources",
) )
copy_page(
name = "editor",
srcs = [
"editor.js",
],
package = "//ts/editor",
)
filegroup( filegroup(
name = "js", name = "js",
srcs = [ srcs = [
"aqt_es5", "aqt_es5",
"editor",
"mathjax.js", "mathjax.js",
"//qt/aqt/data/web/js/vendor", "//qt/aqt/data/web/js/vendor",
], ],
@ -47,4 +60,7 @@ prettier_test(
# srcs = glob(["*.ts"]), # srcs = glob(["*.ts"]),
# ) # )
exports_files(["mathjax.js"]) exports_files([
"mathjax.js",
"tsconfig.json",
])

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

@ -6,7 +6,7 @@
Legacy support Legacy support
""" """
from typing import List from typing import Any, List
import anki import anki
import aqt import aqt
@ -31,7 +31,14 @@ def stripSounds(text) -> str:
return aqt.mw.col.media.strip_av_tags(text) return aqt.mw.col.media.strip_av_tags(text)
def fmtTimeSpan(time, pad=0, point=0, short=False, inTime=False, unit=99): def fmtTimeSpan(
time: Any,
pad: Any = 0,
point: Any = 0,
short: Any = False,
inTime: Any = False,
unit: Any = 99,
) -> Any:
print("fmtTimeSpan() has become col.format_timespan()") print("fmtTimeSpan() has become col.format_timespan()")
return aqt.mw.col.format_timespan(time) return aqt.mw.col.format_timespan(time)

View file

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

View file

@ -136,7 +136,7 @@ class MediaChecker:
diag.exec_() diag.exec_()
saveGeom(diag, "checkmediadb") saveGeom(diag, "checkmediadb")
def _on_render_latex(self): def _on_render_latex(self) -> None:
self.progress_dialog = self.mw.progress.start() self.progress_dialog = self.mw.progress.start()
try: try:
out = self.mw.col.media.render_all_latex(self._on_render_latex_progress) out = self.mw.col.media.render_all_latex(self._on_render_latex_progress)
@ -160,7 +160,7 @@ class MediaChecker:
self.mw.progress.update(tr(TR.MEDIA_CHECK_CHECKED, count=count)) self.mw.progress.update(tr(TR.MEDIA_CHECK_CHECKED, count=count))
return True return True
def _on_trash_files(self, fnames: Sequence[str]): def _on_trash_files(self, fnames: Sequence[str]) -> None:
if not askUser(tr(TR.MEDIA_CHECK_DELETE_UNUSED_CONFIRM)): if not askUser(tr(TR.MEDIA_CHECK_DELETE_UNUSED_CONFIRM)):
return return
@ -183,14 +183,14 @@ class MediaChecker:
tooltip(tr(TR.MEDIA_CHECK_DELETE_UNUSED_COMPLETE, count=total)) tooltip(tr(TR.MEDIA_CHECK_DELETE_UNUSED_COMPLETE, count=total))
def _on_empty_trash(self): def _on_empty_trash(self) -> None:
self.progress_dialog = self.mw.progress.start() self.progress_dialog = self.mw.progress.start()
self._set_progress_enabled(True) self._set_progress_enabled(True)
def empty_trash(): def empty_trash() -> None:
self.mw.col.media.empty_trash() self.mw.col.media.empty_trash()
def on_done(fut: Future): def on_done(fut: Future) -> None:
self.mw.progress.finish() self.mw.progress.finish()
self._set_progress_enabled(False) self._set_progress_enabled(False)
# check for errors # check for errors
@ -200,14 +200,14 @@ class MediaChecker:
self.mw.taskman.run_in_background(empty_trash, on_done) self.mw.taskman.run_in_background(empty_trash, on_done)
def _on_restore_trash(self): def _on_restore_trash(self) -> None:
self.progress_dialog = self.mw.progress.start() self.progress_dialog = self.mw.progress.start()
self._set_progress_enabled(True) self._set_progress_enabled(True)
def restore_trash(): def restore_trash() -> None:
self.mw.col.media.restore_trash() self.mw.col.media.restore_trash()
def on_done(fut: Future): def on_done(fut: Future) -> None:
self.mw.progress.finish() self.mw.progress.finish()
self._set_progress_enabled(False) self._set_progress_enabled(False)
# check for errors # check for errors

View file

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

View file

@ -28,14 +28,14 @@ class LogEntryWithTime:
class MediaSyncer: class MediaSyncer:
def __init__(self, mw: aqt.main.AnkiQt): def __init__(self, mw: aqt.main.AnkiQt) -> None:
self.mw = mw self.mw = mw
self._syncing: bool = False self._syncing: bool = False
self._log: List[LogEntryWithTime] = [] self._log: List[LogEntryWithTime] = []
self._progress_timer: Optional[QTimer] = None self._progress_timer: Optional[QTimer] = None
gui_hooks.media_sync_did_start_or_stop.append(self._on_start_stop) gui_hooks.media_sync_did_start_or_stop.append(self._on_start_stop)
def _on_progress(self): def _on_progress(self) -> None:
progress = self.mw.col.latest_progress() progress = self.mw.col.latest_progress()
if progress.kind != ProgressKind.MediaSync: if progress.kind != ProgressKind.MediaSync:
return return
@ -88,7 +88,7 @@ class MediaSyncer:
else: else:
self._log_and_notify(tr(TR.SYNC_MEDIA_COMPLETE)) self._log_and_notify(tr(TR.SYNC_MEDIA_COMPLETE))
def _handle_sync_error(self, exc: BaseException): def _handle_sync_error(self, exc: BaseException) -> None:
if isinstance(exc, Interrupted): if isinstance(exc, Interrupted):
self._log_and_notify(tr(TR.SYNC_MEDIA_ABORTED)) self._log_and_notify(tr(TR.SYNC_MEDIA_ABORTED))
return return
@ -116,10 +116,10 @@ class MediaSyncer:
def _on_start_stop(self, running: bool) -> None: def _on_start_stop(self, running: bool) -> None:
self.mw.toolbar.set_sync_active(running) self.mw.toolbar.set_sync_active(running)
def show_sync_log(self): def show_sync_log(self) -> None:
aqt.dialogs.open("sync_log", self.mw, self) aqt.dialogs.open("sync_log", self.mw, self)
def show_diag_until_finished(self, on_finished: Callable[[], None]): def show_diag_until_finished(self, on_finished: Callable[[], None]) -> None:
# nothing to do if not syncing # nothing to do if not syncing
if not self.is_syncing(): if not self.is_syncing():
return on_finished() return on_finished()
@ -129,7 +129,7 @@ class MediaSyncer:
timer: Optional[QTimer] = None timer: Optional[QTimer] = None
def check_finished(): def check_finished() -> None:
if not self.is_syncing(): if not self.is_syncing():
timer.stop() timer.stop()
on_finished() on_finished()
@ -197,7 +197,7 @@ class MediaSyncDialog(QDialog):
asctime = time.asctime(time.localtime(stamp)) asctime = time.asctime(time.localtime(stamp))
return f"{asctime}: {text}" return f"{asctime}: {text}"
def _entry_to_text(self, entry: LogEntryWithTime): def _entry_to_text(self, entry: LogEntryWithTime) -> str:
if isinstance(entry.entry, str): if isinstance(entry.entry, str):
txt = entry.entry txt = entry.entry
elif isinstance(entry.entry, MediaSyncProgress): elif isinstance(entry.entry, MediaSyncProgress):
@ -209,7 +209,7 @@ class MediaSyncDialog(QDialog):
def _logentry_to_text(self, e: MediaSyncProgress) -> str: def _logentry_to_text(self, e: MediaSyncProgress) -> str:
return f"{e.added}, {e.removed}, {e.checked}" return f"{e.added}, {e.removed}, {e.checked}"
def _on_log_entry(self, entry: LogEntryWithTime): def _on_log_entry(self, entry: LogEntryWithTime) -> None:
self.form.plainTextEdit.appendPlainText(self._entry_to_text(entry)) self.form.plainTextEdit.appendPlainText(self._entry_to_text(entry))
if not self._syncer.is_syncing(): if not self._syncer.is_syncing():
self.abort_button.setHidden(True) self.abort_button.setHidden(True)

View file

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright: Ankitects Pty Ltd and contributors # Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from typing import Optional from typing import List, Optional
from aqt import AnkiQt, gui_hooks from aqt import AnkiQt, gui_hooks
from aqt.qt import * from aqt.qt import *
@ -74,7 +74,7 @@ class ModelChooser(QHBoxLayout):
# edit button # edit button
edit = QPushButton(tr(TR.QT_MISC_MANAGE), clicked=self.onEdit) # type: ignore edit = QPushButton(tr(TR.QT_MISC_MANAGE), clicked=self.onEdit) # type: ignore
def nameFunc(): def nameFunc() -> List[str]:
return sorted(self.deck.models.allNames()) return sorted(self.deck.models.allNames())
ret = StudyDeck( ret = StudyDeck(

View file

@ -57,7 +57,7 @@ class Models(QDialog):
# Models # Models
########################################################################## ##########################################################################
def maybe_select_provided_notetype(self): def maybe_select_provided_notetype(self) -> None:
if not self.selected_notetype_id: if not self.selected_notetype_id:
self.form.modelsList.setCurrentRow(0) self.form.modelsList.setCurrentRow(0)
return return
@ -219,7 +219,7 @@ class Models(QDialog):
class AddModel(QDialog): class AddModel(QDialog):
model: Optional[NoteType] model: Optional[NoteType]
def __init__(self, mw: AnkiQt, parent: Optional[QWidget] = None): def __init__(self, mw: AnkiQt, parent: Optional[QWidget] = None) -> None:
self.parent_ = parent or mw self.parent_ = parent or mw
self.mw = mw self.mw = mw
self.col = mw.col self.col = mw.col

View file

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

View file

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

View file

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

View file

@ -113,17 +113,17 @@ class LoadMetaResult:
class AnkiRestart(SystemExit): class AnkiRestart(SystemExit):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs) -> None:
self.exitcode = kwargs.pop("exitcode", 0) self.exitcode = kwargs.pop("exitcode", 0)
super().__init__(*args, **kwargs) # type: ignore super().__init__(*args, **kwargs) # type: ignore
class ProfileManager: class ProfileManager:
def __init__(self, base=None): def __init__(self, base: Optional[str] = None) -> None: #
## Settings which should be forgotten each Anki restart ## Settings which should be forgotten each Anki restart
self.session = {} self.session: Dict[str, Any] = {}
self.name = None self.name: Optional[str] = None
self.db = None self.db: Optional[DB] = None
self.profile: Optional[Dict] = None self.profile: Optional[Dict] = None
# instantiate base folder # instantiate base folder
self.base: str self.base: str
@ -170,7 +170,7 @@ class ProfileManager:
return p return p
return os.path.expanduser("~/Documents/Anki") return os.path.expanduser("~/Documents/Anki")
def maybeMigrateFolder(self): def maybeMigrateFolder(self) -> None:
newBase = self.base newBase = self.base
oldBase = self._oldFolderLocation() oldBase = self._oldFolderLocation()
@ -185,7 +185,7 @@ class ProfileManager:
self.base = newBase self.base = newBase
shutil.move(oldBase, self.base) shutil.move(oldBase, self.base)
def _tryToMigrateFolder(self, oldBase): def _tryToMigrateFolder(self, oldBase) -> None:
from PyQt5 import QtGui, QtWidgets from PyQt5 import QtGui, QtWidgets
app = QtWidgets.QApplication([]) app = QtWidgets.QApplication([])
@ -206,7 +206,7 @@ class ProfileManager:
confirmation.setText( confirmation.setText(
"Anki needs to move its data folder from Documents/Anki to a new location. Proceed?" "Anki needs to move its data folder from Documents/Anki to a new location. Proceed?"
) )
retval = confirmation.exec() retval = confirmation.exec_()
if retval == QMessageBox.Ok: if retval == QMessageBox.Ok:
progress = QMessageBox() progress = QMessageBox()
@ -228,7 +228,7 @@ class ProfileManager:
completion.setWindowTitle(window_title) completion.setWindowTitle(window_title)
completion.setText("Migration complete. Please start Anki again.") completion.setText("Migration complete. Please start Anki again.")
completion.show() completion.show()
completion.exec() completion.exec_()
else: else:
diag = QMessageBox() diag = QMessageBox()
diag.setIcon(QMessageBox.Warning) diag.setIcon(QMessageBox.Warning)
@ -239,7 +239,7 @@ class ProfileManager:
"Migration aborted. If you would like to keep the old folder location, please " "Migration aborted. If you would like to keep the old folder location, please "
"see the Startup Options section of the manual. Anki will now quit." "see the Startup Options section of the manual. Anki will now quit."
) )
diag.exec() diag.exec_()
raise AnkiRestart(exitcode=0) raise AnkiRestart(exitcode=0)
@ -269,7 +269,7 @@ class ProfileManager:
fn = super().find_class(module, name) fn = super().find_class(module, name)
if module == "sip" and name == "_unpickle_type": if module == "sip" and name == "_unpickle_type":
def wrapper(mod, obj, args): def wrapper(mod, obj, args) -> Any:
if mod.startswith("PyQt4") and obj == "QByteArray": if mod.startswith("PyQt4") and obj == "QByteArray":
# can't trust str objects from python 2 # can't trust str objects from python 2
return QByteArray() return QByteArray()
@ -424,7 +424,7 @@ class ProfileManager:
os.makedirs(path) os.makedirs(path)
return path return path
def _setBaseFolder(self, cmdlineBase: None) -> None: def _setBaseFolder(self, cmdlineBase: Optional[str]) -> None:
if cmdlineBase: if cmdlineBase:
self.base = os.path.abspath(cmdlineBase) self.base = os.path.abspath(cmdlineBase)
elif os.environ.get("ANKI_BASE"): elif os.environ.get("ANKI_BASE"):
@ -534,7 +534,7 @@ create table if not exists profiles
def setDefaultLang(self, idx: int) -> None: def setDefaultLang(self, idx: int) -> None:
# create dialog # create dialog
class NoCloseDiag(QDialog): class NoCloseDiag(QDialog):
def reject(self): def reject(self) -> None:
pass pass
d = self.langDiag = NoCloseDiag() d = self.langDiag = NoCloseDiag()
@ -665,7 +665,7 @@ create table if not exists profiles
pass pass
return RecordingDriver.QtAudioInput return RecordingDriver.QtAudioInput
def set_recording_driver(self, driver: RecordingDriver): def set_recording_driver(self, driver: RecordingDriver) -> None:
self.profile["recordingDriver"] = driver.value self.profile["recordingDriver"] = driver.value
###################################################################### ######################################################################

View file

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

View file

@ -30,7 +30,7 @@ except ImportError:
import sip # type: ignore import sip # type: ignore
def debug(): def debug() -> None:
from pdb import set_trace from pdb import set_trace
pyqtRemoveInputHook() pyqtRemoveInputHook()
@ -39,7 +39,7 @@ def debug():
if os.environ.get("DEBUG"): if os.environ.get("DEBUG"):
def info(type, value, tb): def info(type, value, tb) -> None:
for line in traceback.format_exception(type, value, tb): for line in traceback.format_exception(type, value, tb):
sys.stdout.write(line) sys.stdout.write(line)
pyqtRemoveInputHook() pyqtRemoveInputHook()

View file

@ -8,7 +8,7 @@ import html
import json import json
import re import re
import unicodedata as ucd import unicodedata as ucd
from typing import Callable, List, Optional, Tuple, Union from typing import Any, Callable, List, Match, Optional, Tuple, Union
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
@ -426,7 +426,7 @@ class Reviewer:
# compare with typed answer # compare with typed answer
res = self.correct(given, cor, showBad=False) res = self.correct(given, cor, showBad=False)
# and update the type answer area # and update the type answer area
def repl(match): def repl(match: Match) -> str:
# can't pass a string in directly, and can't use re.escape as it # can't pass a string in directly, and can't use re.escape as it
# escapes too much # escapes too much
s = """ s = """
@ -448,7 +448,7 @@ class Reviewer:
if not matches: if not matches:
return None return None
def noHint(txt): def noHint(txt) -> str:
if "::" in txt: if "::" in txt:
return txt.split("::")[0] return txt.split("::")[0]
return txt return txt
@ -652,7 +652,7 @@ time = %(time)d;
def _answerButtons(self) -> str: def _answerButtons(self) -> str:
default = self._defaultEase() default = self._defaultEase()
def but(i, label): def but(i, label) -> str:
if i == default: if i == default:
extra = """id="defease" class="focus" """ extra = """id="defease" class="focus" """
else: else:
@ -697,7 +697,7 @@ time = %(time)d;
########################################################################## ##########################################################################
# note the shortcuts listed here also need to be defined above # note the shortcuts listed here also need to be defined above
def _contextMenu(self): def _contextMenu(self) -> List[Any]:
currentFlag = self.card and self.card.userFlag() currentFlag = self.card and self.card.userFlag()
opts = [ opts = [
[ [
@ -834,7 +834,7 @@ time = %(time)d;
tooltip(tr(TR.STUDYING_NOTE_BURIED)) tooltip(tr(TR.STUDYING_NOTE_BURIED))
def onRecordVoice(self) -> None: def onRecordVoice(self) -> None:
def after_record(path: str): def after_record(path: str) -> None:
self._recordedAudio = path self._recordedAudio = path
self.onReplayRecorded() self.onReplayRecorded()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -40,12 +40,14 @@ class FullSyncChoice(enum.Enum):
DOWNLOAD = 2 DOWNLOAD = 2
def get_sync_status(mw: aqt.main.AnkiQt, callback: Callable[[SyncStatus], None]): def get_sync_status(
mw: aqt.main.AnkiQt, callback: Callable[[SyncStatus], None]
) -> None:
auth = mw.pm.sync_auth() auth = mw.pm.sync_auth()
if not auth: if not auth:
return SyncStatus(required=SyncStatus.NO_CHANGES) # pylint:disable=no-member callback(SyncStatus(required=SyncStatus.NO_CHANGES)) # pylint:disable=no-member
def on_future_done(fut): def on_future_done(fut) -> None:
try: try:
out = fut.result() out = fut.result()
except Exception as e: except Exception as e:
@ -57,7 +59,7 @@ def get_sync_status(mw: aqt.main.AnkiQt, callback: Callable[[SyncStatus], None])
mw.taskman.run_in_background(lambda: mw.col.sync_status(auth), on_future_done) mw.taskman.run_in_background(lambda: mw.col.sync_status(auth), on_future_done)
def handle_sync_error(mw: aqt.main.AnkiQt, err: Exception): def handle_sync_error(mw: aqt.main.AnkiQt, err: Exception) -> None:
if isinstance(err, SyncError): if isinstance(err, SyncError):
if err.is_auth_error(): if err.is_auth_error():
mw.pm.clear_sync_auth() mw.pm.clear_sync_auth()
@ -87,14 +89,14 @@ def sync_collection(mw: aqt.main.AnkiQt, on_done: Callable[[], None]) -> None:
auth = mw.pm.sync_auth() auth = mw.pm.sync_auth()
assert auth assert auth
def on_timer(): def on_timer() -> None:
on_normal_sync_timer(mw) on_normal_sync_timer(mw)
timer = QTimer(mw) timer = QTimer(mw)
qconnect(timer.timeout, on_timer) qconnect(timer.timeout, on_timer)
timer.start(150) timer.start(150)
def on_future_done(fut): def on_future_done(fut) -> None:
mw.col.db.begin() mw.col.db.begin()
timer.stop() timer.stop()
try: try:
@ -171,14 +173,14 @@ def on_full_sync_timer(mw: aqt.main.AnkiQt) -> None:
def full_download(mw: aqt.main.AnkiQt, on_done: Callable[[], None]) -> None: def full_download(mw: aqt.main.AnkiQt, on_done: Callable[[], None]) -> None:
mw.col.close_for_full_sync() mw.col.close_for_full_sync()
def on_timer(): def on_timer() -> None:
on_full_sync_timer(mw) on_full_sync_timer(mw)
timer = QTimer(mw) timer = QTimer(mw)
qconnect(timer.timeout, on_timer) qconnect(timer.timeout, on_timer)
timer.start(150) timer.start(150)
def on_future_done(fut): def on_future_done(fut) -> None:
timer.stop() timer.stop()
mw.col.reopen(after_full_sync=True) mw.col.reopen(after_full_sync=True)
mw.reset() mw.reset()
@ -199,14 +201,14 @@ def full_download(mw: aqt.main.AnkiQt, on_done: Callable[[], None]) -> None:
def full_upload(mw: aqt.main.AnkiQt, on_done: Callable[[], None]) -> None: def full_upload(mw: aqt.main.AnkiQt, on_done: Callable[[], None]) -> None:
mw.col.close_for_full_sync() mw.col.close_for_full_sync()
def on_timer(): def on_timer() -> None:
on_full_sync_timer(mw) on_full_sync_timer(mw)
timer = QTimer(mw) timer = QTimer(mw)
qconnect(timer.timeout, on_timer) qconnect(timer.timeout, on_timer)
timer.start(150) timer.start(150)
def on_future_done(fut): def on_future_done(fut) -> None:
timer.stop() timer.stop()
mw.col.reopen(after_full_sync=True) mw.col.reopen(after_full_sync=True)
mw.reset() mw.reset()
@ -235,7 +237,7 @@ def sync_login(
if username and password: if username and password:
break break
def on_future_done(fut): def on_future_done(fut) -> None:
try: try:
auth = fut.result() auth = fut.result()
except SyncError as e: except SyncError as e:

View file

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

View file

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

View file

@ -31,7 +31,7 @@ class TaskManager(QObject):
self._closures_lock = Lock() self._closures_lock = Lock()
qconnect(self._closures_pending, self._on_closures_pending) qconnect(self._closures_pending, self._on_closures_pending)
def run_on_main(self, closure: Closure): def run_on_main(self, closure: Closure) -> None:
"Run the provided closure on the main thread." "Run the provided closure on the main thread."
with self._closures_lock: with self._closures_lock:
self._closures.append(closure) self._closures.append(closure)
@ -71,14 +71,14 @@ class TaskManager(QObject):
): ):
self.mw.progress.start(parent=parent, label=label, immediate=immediate) self.mw.progress.start(parent=parent, label=label, immediate=immediate)
def wrapped_done(fut): def wrapped_done(fut) -> None:
self.mw.progress.finish() self.mw.progress.finish()
if on_done: if on_done:
on_done(fut) on_done(fut)
self.run_in_background(task, wrapped_done) self.run_in_background(task, wrapped_done)
def _on_closures_pending(self): def _on_closures_pending(self) -> None:
"""Run any pending closures. This runs in the main thread.""" """Run any pending closures. This runs in the main thread."""
with self._closures_lock: with self._closures_lock:
closures = self._closures closures = self._closures

View file

@ -481,7 +481,7 @@ if isWin:
return [] return []
return list(map(self._voice_to_object, self.speaker.GetVoices())) return list(map(self._voice_to_object, self.speaker.GetVoices()))
def _voice_to_object(self, voice: Any): def _voice_to_object(self, voice: Any) -> WindowsVoice:
lang = voice.GetAttribute("language") lang = voice.GetAttribute("language")
lang = lcid_hex_str_to_lang_code(lang) lang = lcid_hex_str_to_lang_code(lang)
name = self._tidy_name(voice.GetAttribute("name")) name = self._tidy_name(voice.GetAttribute("name"))
@ -525,7 +525,7 @@ if isWin:
id: Any id: Any
class WindowsRTTTSFilePlayer(TTSProcessPlayer): class WindowsRTTTSFilePlayer(TTSProcessPlayer):
voice_list = None voice_list: List[Any] = []
tmppath = os.path.join(tmpdir(), "tts.wav") tmppath = os.path.join(tmpdir(), "tts.wav")
def import_voices(self) -> None: def import_voices(self) -> None:
@ -561,7 +561,7 @@ if isWin:
) )
asyncio.run(self.speakText(tag, voice.id)) asyncio.run(self.speakText(tag, voice.id))
def _on_done(self, ret: Future, cb: OnDoneCallback): def _on_done(self, ret: Future, cb: OnDoneCallback) -> None:
ret.result() ret.result()
# inject file into the top of the audio queue # inject file into the top of the audio queue
@ -572,7 +572,7 @@ if isWin:
# then tell player to advance, which will cause the file to be played # then tell player to advance, which will cause the file to be played
cb() cb()
async def speakText(self, tag: TTSTag, voice_id): async def speakText(self, tag: TTSTag, voice_id) -> None:
import winrt.windows.media.speechsynthesis as speechsynthesis # type: ignore import winrt.windows.media.speechsynthesis as speechsynthesis # type: ignore
import winrt.windows.storage.streams as streams # type: ignore import winrt.windows.storage.streams as streams # type: ignore

View file

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

View file

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

View file

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

13
qt/dmypy-watch.sh Executable file
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] [mypy]
python_version = 3.8 python_version = 3.8
pretty = true
no_strict_optional = true no_strict_optional = true
show_error_codes = true show_error_codes = true
disallow_untyped_decorators = True disallow_untyped_decorators = True
@ -9,6 +8,16 @@ warn_unused_configs = True
check_untyped_defs = true check_untyped_defs = true
strict_equality = true strict_equality = true
[mypy-aqt.browser]
disallow_untyped_defs=true
[mypy-aqt.sidebar]
disallow_untyped_defs=true
[mypy-aqt.editor]
disallow_untyped_defs=true
[mypy-aqt.utils]
disallow_untyped_defs=true
[mypy-aqt.mpv] [mypy-aqt.mpv]
ignore_errors=true ignore_errors=true

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("@rules_proto//proto:defs.bzl", "proto_library")
load("@io_bazel_rules_rust//rust:rust.bzl", "rust_binary", "rust_library", "rust_test") load("@io_bazel_rules_rust//rust:rust.bzl", "rust_binary", "rust_library", "rust_test")
load("@io_bazel_rules_rust//cargo:cargo_build_script.bzl", "cargo_build_script") load("@io_bazel_rules_rust//cargo:cargo_build_script.bzl", "cargo_build_script")

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"; syntax = "proto3";
package BackendProto; 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 mergeftl;
pub mod protobuf; 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::ast::Entry;
use fluent_syntax::parser::Parser; use fluent_syntax::parser::Parser;
use std::path::Path; 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::path::PathBuf;
use std::{env, fmt::Write}; 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"); include!("mergeftl.rs");
fn main() { 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 @generated
cargo-raze generated Bazel file. 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. 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 import sys, subprocess, os, difflib
clang_format = sys.argv[1] clang_format = sys.argv[1]
@ -33,4 +36,4 @@ for path in sys.argv[2:]:
found_bad = True found_bad = True
if found_bad: if found_bad:
sys.exit(1) sys.exit(1)

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

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

View file

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

62
ts/editor/BUILD.bazel Normal file
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 /* Copyright: Ankitects Pty Ltd and contributors
* License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */ * License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */
import { filterHTML } from "./filterHtml";
import { nodeIsElement, nodeIsInline } from "./helpers";
import { bridgeCommand } from "./lib";
let currentField: EditingArea | null = null; let currentField: EditingArea | null = null;
let changeTimer: number | null = null; let changeTimer: number | null = null;
let currentNoteId: number | null = null; let currentNoteId: number | null = null;
declare interface String { declare global {
format(...args: string[]): string; interface String {
format(...args: string[]): string;
}
interface Selection {
modify(s: string, t: string, u: string): void;
addRange(r: Range): void;
removeAllRanges(): void;
getRangeAt(n: number): Range;
}
} }
/* kept for compatibility with add-ons */ /* kept for compatibility with add-ons */
@ -18,11 +31,11 @@ String.prototype.format = function (...args: string[]): string {
}); });
}; };
function setFGButton(col: string): void { export function setFGButton(col: string): void {
document.getElementById("forecolor").style.backgroundColor = col; document.getElementById("forecolor").style.backgroundColor = col;
} }
function saveNow(keepFocus: boolean): void { export function saveNow(keepFocus: boolean): void {
if (!currentField) { if (!currentField) {
return; return;
} }
@ -45,10 +58,6 @@ function triggerKeyTimer(): void {
}, 600); }, 600);
} }
interface Selection {
modify(s: string, t: string, u: string): void;
}
function onKey(evt: KeyboardEvent): void { function onKey(evt: KeyboardEvent): void {
// esc clears focus, allowing dialog to close // esc clears focus, allowing dialog to close
if (evt.code === "Escape") { if (evt.code === "Escape") {
@ -100,72 +109,6 @@ function onKeyUp(evt: KeyboardEvent): void {
} }
} }
function nodeIsElement(node: Node): node is Element {
return node.nodeType === Node.ELEMENT_NODE;
}
const INLINE_TAGS = [
"A",
"ABBR",
"ACRONYM",
"AUDIO",
"B",
"BDI",
"BDO",
"BIG",
"BR",
"BUTTON",
"CANVAS",
"CITE",
"CODE",
"DATA",
"DATALIST",
"DEL",
"DFN",
"EM",
"EMBED",
"I",
"IFRAME",
"IMG",
"INPUT",
"INS",
"KBD",
"LABEL",
"MAP",
"MARK",
"METER",
"NOSCRIPT",
"OBJECT",
"OUTPUT",
"PICTURE",
"PROGRESS",
"Q",
"RUBY",
"S",
"SAMP",
"SCRIPT",
"SELECT",
"SLOT",
"SMALL",
"SPAN",
"STRONG",
"SUB",
"SUP",
"SVG",
"TEMPLATE",
"TEXTAREA",
"TIME",
"U",
"TT",
"VAR",
"VIDEO",
"WBR",
];
function nodeIsInline(node: Node): boolean {
return !nodeIsElement(node) || INLINE_TAGS.includes(node.tagName);
}
function inListItem(): boolean { function inListItem(): boolean {
const anchor = currentField.getSelection().anchorNode; const anchor = currentField.getSelection().anchorNode;
@ -179,7 +122,7 @@ function inListItem(): boolean {
return inList; return inList;
} }
function insertNewline(): void { export function insertNewline(): void {
if (!inPreEnvironment()) { if (!inPreEnvironment()) {
setFormat("insertText", "\n"); setFormat("insertText", "\n");
return; return;
@ -228,12 +171,12 @@ function updateButtonState(): void {
// 'col': document.queryCommandValue("forecolor") // 'col': document.queryCommandValue("forecolor")
} }
function toggleEditorButton(buttonid: string): void { export function toggleEditorButton(buttonid: string): void {
const button = $(buttonid)[0]; const button = $(buttonid)[0];
button.classList.toggle("highlighted"); button.classList.toggle("highlighted");
} }
function setFormat(cmd: string, arg?: any, nosave: boolean = false): void { export function setFormat(cmd: string, arg?: any, nosave: boolean = false): void {
document.execCommand(cmd, false, arg); document.execCommand(cmd, false, arg);
if (!nosave) { if (!nosave) {
saveField("key"); saveField("key");
@ -256,7 +199,7 @@ function onFocus(evt: FocusEvent): void {
} }
elem.focusEditable(); elem.focusEditable();
currentField = elem; currentField = elem;
pycmd(`focus:${currentField.ord}`); bridgeCommand(`focus:${currentField.ord}`);
enableButtons(); enableButtons();
// do this twice so that there's no flicker on newer versions // do this twice so that there's no flicker on newer versions
caretToEnd(); caretToEnd();
@ -279,7 +222,7 @@ function onFocus(evt: FocusEvent): void {
} }
} }
function focusField(n: number): void { export function focusField(n: number): void {
const field = getEditorField(n); const field = getEditorField(n);
if (field) { if (field) {
@ -287,7 +230,7 @@ function focusField(n: number): void {
} }
} }
function focusIfField(x: number, y: number): boolean { export function focusIfField(x: number, y: number): boolean {
const elements = document.elementsFromPoint(x, y); const elements = document.elementsFromPoint(x, y);
for (let i = 0; i < elements.length; i++) { for (let i = 0; i < elements.length; i++) {
let elem = elements[i] as EditingArea; let elem = elements[i] as EditingArea;
@ -303,7 +246,7 @@ function focusIfField(x: number, y: number): boolean {
} }
function onPaste(): void { function onPaste(): void {
pycmd("paste"); bridgeCommand("paste");
window.event.preventDefault(); window.event.preventDefault();
} }
@ -353,7 +296,9 @@ function saveField(type: "blur" | "key"): void {
return; return;
} }
pycmd(`${type}:${currentField.ord}:${currentNoteId}:${currentField.fieldHTML}`); bridgeCommand(
`${type}:${currentField.ord}:${currentNoteId}:${currentField.fieldHTML}`
);
} }
function wrappedExceptForWhitespace(text: string, front: string, back: string): string { function wrappedExceptForWhitespace(text: string, front: string, back: string): string {
@ -361,7 +306,7 @@ function wrappedExceptForWhitespace(text: string, front: string, back: string):
return match[1] + front + match[2] + back + match[3]; return match[1] + front + match[2] + back + match[3];
} }
function preventButtonFocus(): void { export function preventButtonFocus(): void {
for (const element of document.querySelectorAll("button.linkb")) { for (const element of document.querySelectorAll("button.linkb")) {
element.addEventListener("mousedown", (evt: Event) => { element.addEventListener("mousedown", (evt: Event) => {
evt.preventDefault(); evt.preventDefault();
@ -386,7 +331,7 @@ function maybeDisableButtons(): void {
} }
} }
function wrap(front: string, back: string): void { export function wrap(front: string, back: string): void {
wrapInternal(front, back, false); wrapInternal(front, back, false);
} }
@ -419,7 +364,7 @@ function wrapInternal(front: string, back: string, plainText: boolean): void {
} }
function onCutOrCopy(): boolean { function onCutOrCopy(): boolean {
pycmd("cutOrCopy"); bridgeCommand("cutOrCopy");
return true; return true;
} }
@ -606,12 +551,12 @@ function adjustFieldAmount(amount: number): void {
} }
} }
function getEditorField(n: number): EditorField | null { export function getEditorField(n: number): EditorField | null {
const fields = document.getElementById("fields").children; const fields = document.getElementById("fields").children;
return (fields[n] as EditorField) ?? null; return (fields[n] as EditorField) ?? null;
} }
function forEditorField<T>( export function forEditorField<T>(
values: T[], values: T[],
func: (field: EditorField, value: T) => void func: (field: EditorField, value: T) => void
): void { ): void {
@ -622,7 +567,7 @@ function forEditorField<T>(
} }
} }
function setFields(fields: [string, string][]): void { export function setFields(fields: [string, string][]): void {
// webengine will include the variable after enter+backspace // webengine will include the variable after enter+backspace
// if we don't convert it to a literal colour // if we don't convert it to a literal colour
const color = window const color = window
@ -637,7 +582,7 @@ function setFields(fields: [string, string][]): void {
maybeDisableButtons(); maybeDisableButtons();
} }
function setBackgrounds(cols: ("dupe" | "")[]) { export function setBackgrounds(cols: ("dupe" | "")[]) {
forEditorField(cols, (field, value) => forEditorField(cols, (field, value) =>
field.editingArea.classList.toggle("dupe", value === "dupe") field.editingArea.classList.toggle("dupe", value === "dupe")
); );
@ -646,17 +591,17 @@ function setBackgrounds(cols: ("dupe" | "")[]) {
.classList.toggle("is-inactive", !cols.includes("dupe")); .classList.toggle("is-inactive", !cols.includes("dupe"));
} }
function setFonts(fonts: [string, number, boolean][]): void { export function setFonts(fonts: [string, number, boolean][]): void {
forEditorField(fonts, (field, [fontFamily, fontSize, isRtl]) => { forEditorField(fonts, (field, [fontFamily, fontSize, isRtl]) => {
field.setBaseStyling(fontFamily, `${fontSize}px`, isRtl ? "rtl" : "ltr"); field.setBaseStyling(fontFamily, `${fontSize}px`, isRtl ? "rtl" : "ltr");
}); });
} }
function setNoteId(id: number): void { export function setNoteId(id: number): void {
currentNoteId = id; currentNoteId = id;
} }
let pasteHTML = function ( export let pasteHTML = function (
html: string, html: string,
internal: boolean, internal: boolean,
extendedMode: boolean extendedMode: boolean
@ -667,172 +612,3 @@ let pasteHTML = function (
setFormat("inserthtml", html); setFormat("inserthtml", html);
} }
}; };
let filterHTML = function (
html: string,
internal: boolean,
extendedMode: boolean
): string {
// wrap it in <top> as we aren't allowed to change top level elements
const top = document.createElement("ankitop");
top.innerHTML = html;
if (internal) {
filterInternalNode(top);
} else {
filterNode(top, extendedMode);
}
let outHtml = top.innerHTML;
if (!extendedMode && !internal) {
// collapse whitespace
outHtml = outHtml.replace(/[\n\t ]+/g, " ");
}
outHtml = outHtml.trim();
return outHtml;
};
let allowedTagsBasic = {};
let allowedTagsExtended = {};
let TAGS_WITHOUT_ATTRS = ["P", "DIV", "BR", "SUB", "SUP"];
for (const tag of TAGS_WITHOUT_ATTRS) {
allowedTagsBasic[tag] = { attrs: [] };
}
TAGS_WITHOUT_ATTRS = [
"B",
"BLOCKQUOTE",
"CODE",
"DD",
"DL",
"DT",
"EM",
"H1",
"H2",
"H3",
"I",
"LI",
"OL",
"PRE",
"RP",
"RT",
"RUBY",
"STRONG",
"TABLE",
"U",
"UL",
];
for (const tag of TAGS_WITHOUT_ATTRS) {
allowedTagsExtended[tag] = { attrs: [] };
}
allowedTagsBasic["IMG"] = { attrs: ["SRC"] };
allowedTagsExtended["A"] = { attrs: ["HREF"] };
allowedTagsExtended["TR"] = { attrs: ["ROWSPAN"] };
allowedTagsExtended["TD"] = { attrs: ["COLSPAN", "ROWSPAN"] };
allowedTagsExtended["TH"] = { attrs: ["COLSPAN", "ROWSPAN"] };
allowedTagsExtended["FONT"] = { attrs: ["COLOR"] };
const allowedStyling = {
color: true,
"background-color": true,
"font-weight": true,
"font-style": true,
"text-decoration-line": true,
};
let isNightMode = function (): boolean {
return document.body.classList.contains("nightMode");
};
let filterExternalSpan = function (elem: HTMLElement) {
// filter out attributes
for (const attr of [...elem.attributes]) {
const attrName = attr.name.toUpperCase();
if (attrName !== "STYLE") {
elem.removeAttributeNode(attr);
}
}
// filter styling
for (const name of [...elem.style]) {
const value = elem.style.getPropertyValue(name);
if (
!allowedStyling.hasOwnProperty(name) ||
// google docs adds this unnecessarily
(name === "background-color" && value === "transparent") ||
// ignore coloured text in night mode for now
(isNightMode() && (name === "background-color" || name === "color"))
) {
elem.style.removeProperty(name);
}
}
};
allowedTagsExtended["SPAN"] = filterExternalSpan;
// add basic tags to extended
Object.assign(allowedTagsExtended, allowedTagsBasic);
function isHTMLElement(elem: Element): elem is HTMLElement {
return elem instanceof HTMLElement;
}
// filtering from another field
let filterInternalNode = function (elem: Element) {
if (isHTMLElement(elem)) {
elem.style.removeProperty("background-color");
elem.style.removeProperty("font-size");
elem.style.removeProperty("font-family");
}
// recurse
for (let i = 0; i < elem.children.length; i++) {
const child = elem.children[i];
filterInternalNode(child);
}
};
// filtering from external sources
let filterNode = function (node: Node, extendedMode: boolean): void {
if (!nodeIsElement(node)) {
return;
}
// descend first, and take a copy of the child nodes as the loop will skip
// elements due to node modifications otherwise
for (const child of [...node.children]) {
filterNode(child, extendedMode);
}
if (node.tagName === "ANKITOP") {
return;
}
const tag = extendedMode
? allowedTagsExtended[node.tagName]
: allowedTagsBasic[node.tagName];
if (!tag) {
if (!node.innerHTML || node.tagName === "TITLE") {
node.parentNode.removeChild(node);
} else {
node.outerHTML = node.innerHTML;
}
} else {
if (typeof tag === "function") {
// filtering function provided
tag(node);
} else {
// allowed, filter out attributes
for (const attr of [...node.attributes]) {
const attrName = attr.name.toUpperCase();
if (tag.attrs.indexOf(attrName) === -1) {
node.removeAttributeNode(attr);
}
}
}
}
};

9
ts/editor/lib.ts Normal file
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