mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
Merge branch 'master' into dyn-deckconf
This commit is contained in:
commit
c18af2a0a9
101 changed files with 1795 additions and 1416 deletions
|
@ -15,9 +15,9 @@ Pre-built Python packages are available on PyPI. They are useful if you wish to:
|
|||
- Get code completion when developing add-ons
|
||||
- Make command line scripts that modify .anki2 files via Anki's Python libraries
|
||||
|
||||
You will need Python 3.8 or 3.9 installed. If you do not have Python yet, please
|
||||
see the platform-specific instructions in the "Building from source" section below
|
||||
for more info.
|
||||
You will need the 64 bit version of Python 3.8 or 3.9 installed. If you do not
|
||||
have Python yet, please see the platform-specific instructions in the "Building
|
||||
from source" section below for more info.
|
||||
|
||||
**Mac/Linux**:
|
||||
|
||||
|
|
|
@ -19,8 +19,9 @@ $ brew install rsync bazelisk
|
|||
|
||||
**Install Python 3.8**:
|
||||
|
||||
Install Python 3.8 from <https://python.org>. You may be able to use
|
||||
the Homebrew version instead, but this is untested.
|
||||
Install Python 3.8 from <https://python.org>. We have heard reports
|
||||
of issues with pyenv and homebrew, so the package from python.org is
|
||||
the only recommended approach.
|
||||
|
||||
Python 3.9 is not currently recommended, as pylint does not support it yet.
|
||||
|
||||
|
|
|
@ -17,8 +17,8 @@ components enabled on the right.
|
|||
|
||||
**Python 3.8**:
|
||||
|
||||
Download Python 3.8 from <https://python.org>. Run the installer, and
|
||||
customize the installation. Select "install for all users", and choose
|
||||
Download the 64 bit Python 3.8 from <https://python.org>. Run the installer,
|
||||
and customize the installation. Select "install for all users", and choose
|
||||
the install path as c:\python. Currently the build scripts require
|
||||
Python to be installed in that location.
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ decorator==4.4.2
|
|||
# via -r requirements.in
|
||||
distro==1.5.0
|
||||
# via -r requirements.in
|
||||
flask-cors==3.0.9
|
||||
flask-cors==3.0.10
|
||||
# via -r requirements.in
|
||||
flask==1.1.2
|
||||
# via
|
||||
|
@ -54,7 +54,7 @@ isort==5.7.0
|
|||
# pylint
|
||||
itsdangerous==1.1.0
|
||||
# via flask
|
||||
jinja2==2.11.2
|
||||
jinja2==2.11.3
|
||||
# via flask
|
||||
jsonschema==3.2.0
|
||||
# via -r requirements.in
|
||||
|
@ -72,13 +72,13 @@ mypy-extensions==0.4.3
|
|||
# via
|
||||
# black
|
||||
# mypy
|
||||
mypy-protobuf==1.23
|
||||
mypy-protobuf==1.24
|
||||
# via -r requirements.in
|
||||
mypy==0.790
|
||||
mypy==0.800
|
||||
# via -r requirements.in
|
||||
orjson==3.4.6
|
||||
orjson==3.4.7
|
||||
# via -r requirements.in
|
||||
packaging==20.8
|
||||
packaging==20.9
|
||||
# via pytest
|
||||
pathspec==0.8.1
|
||||
# via black
|
||||
|
@ -102,7 +102,7 @@ pyrsistent==0.17.3
|
|||
# via jsonschema
|
||||
pysocks==1.7.1
|
||||
# via requests
|
||||
pytest==6.2.1
|
||||
pytest==6.2.2
|
||||
# via -r requirements.in
|
||||
pytoml==0.1.21
|
||||
# via compare-locales
|
||||
|
@ -142,7 +142,7 @@ typing-extensions==3.7.4.3
|
|||
# via
|
||||
# black
|
||||
# mypy
|
||||
urllib3==1.26.2
|
||||
urllib3==1.26.3
|
||||
# via requests
|
||||
waitress==2.0.0b1
|
||||
# via -r requirements.in
|
||||
|
@ -154,9 +154,9 @@ wrapt==1.12.1
|
|||
# via astroid
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
pip==20.3.3
|
||||
pip==21.0.1
|
||||
# via pip-tools
|
||||
setuptools==51.1.1
|
||||
setuptools==52.0.0
|
||||
# via jsonschema
|
||||
|
||||
# manually added for now; ensure it and the earlier winrt are not removed on update
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
from typing import Any
|
||||
|
||||
def buildhash(*args, **kwargs) -> Any: ...
|
||||
def open_backend(*args, **kwargs) -> Any: ...
|
||||
def buildhash() -> str: ...
|
||||
def open_backend(data: bytes) -> Backend: ...
|
||||
|
||||
class Backend:
|
||||
@classmethod
|
||||
def __init__(self, *args, **kwargs) -> None: ...
|
||||
def command(self, *args, **kwargs) -> Any: ...
|
||||
def db_command(self, *args, **kwargs) -> Any: ...
|
||||
def command(self, method: int, data: bytes) -> bytes: ...
|
||||
def db_command(self, data: bytes) -> bytes: ...
|
||||
|
|
|
@ -8,6 +8,7 @@ import enum
|
|||
import os
|
||||
import pprint
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
import weakref
|
||||
|
@ -107,7 +108,7 @@ class Collection:
|
|||
|
||||
@property
|
||||
def backend(self) -> RustBackend:
|
||||
traceback.print_stack()
|
||||
traceback.print_stack(file=sys.stdout)
|
||||
print()
|
||||
print(
|
||||
"Accessing the backend directly will break in the future. Please use the public methods on Collection instead."
|
||||
|
@ -280,7 +281,7 @@ class Collection:
|
|||
self.db.rollback()
|
||||
self.db.begin()
|
||||
|
||||
def reopen(self, after_full_sync=False) -> None:
|
||||
def reopen(self, after_full_sync: bool = False) -> None:
|
||||
assert not self.db
|
||||
assert self.path.endswith(".anki2")
|
||||
|
||||
|
@ -409,7 +410,7 @@ class Collection:
|
|||
def cardCount(self) -> Any:
|
||||
return self.db.scalar("select count() from cards")
|
||||
|
||||
def remove_cards_and_orphaned_notes(self, card_ids: Sequence[int]):
|
||||
def remove_cards_and_orphaned_notes(self, card_ids: Sequence[int]) -> None:
|
||||
"You probably want .remove_notes_by_card() instead."
|
||||
self._backend.remove_cards(card_ids=card_ids)
|
||||
|
||||
|
@ -505,7 +506,7 @@ class Collection:
|
|||
dupes = []
|
||||
fields: Dict[int, int] = {}
|
||||
|
||||
def ordForMid(mid):
|
||||
def ordForMid(mid: int) -> int:
|
||||
if mid not in fields:
|
||||
model = self.models.get(mid)
|
||||
for c, f in enumerate(model["flds"]):
|
||||
|
@ -539,7 +540,10 @@ class Collection:
|
|||
##########################################################################
|
||||
|
||||
def build_search_string(
|
||||
self, *terms: Union[str, SearchTerm], negate=False, match_any=False
|
||||
self,
|
||||
*terms: Union[str, SearchTerm],
|
||||
negate: bool = False,
|
||||
match_any: bool = False,
|
||||
) -> str:
|
||||
"""Helper function for the backend's search string operations.
|
||||
|
||||
|
@ -576,11 +580,11 @@ class Collection:
|
|||
except KeyError:
|
||||
return default
|
||||
|
||||
def set_config(self, key: str, val: Any):
|
||||
def set_config(self, key: str, val: Any) -> None:
|
||||
self.setMod()
|
||||
self.conf.set(key, val)
|
||||
|
||||
def remove_config(self, key):
|
||||
def remove_config(self, key: str) -> None:
|
||||
self.setMod()
|
||||
self.conf.remove(key)
|
||||
|
||||
|
@ -779,11 +783,11 @@ table.review-log {{ {revlog_style} }}
|
|||
# Logging
|
||||
##########################################################################
|
||||
|
||||
def log(self, *args, **kwargs) -> None:
|
||||
def log(self, *args: Any, **kwargs: Any) -> None:
|
||||
if not self._should_log:
|
||||
return
|
||||
|
||||
def customRepr(x):
|
||||
def customRepr(x: Any) -> str:
|
||||
if isinstance(x, str):
|
||||
return x
|
||||
return pprint.pformat(x)
|
||||
|
@ -865,7 +869,7 @@ table.review-log {{ {revlog_style} }}
|
|||
def get_preferences(self) -> Preferences:
|
||||
return self._backend.get_preferences()
|
||||
|
||||
def set_preferences(self, prefs: Preferences):
|
||||
def set_preferences(self, prefs: Preferences) -> None:
|
||||
self._backend.set_preferences(prefs)
|
||||
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ from __future__ import annotations
|
|||
import copy
|
||||
import weakref
|
||||
from typing import Any
|
||||
from weakref import ref
|
||||
|
||||
import anki
|
||||
from anki.errors import NotFoundError
|
||||
|
@ -46,7 +47,7 @@ class ConfigManager:
|
|||
# Legacy dict interface
|
||||
#########################
|
||||
|
||||
def __getitem__(self, key):
|
||||
def __getitem__(self, key: str) -> Any:
|
||||
val = self.get_immutable(key)
|
||||
if isinstance(val, list):
|
||||
print(
|
||||
|
@ -61,28 +62,28 @@ class ConfigManager:
|
|||
else:
|
||||
return val
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
def __setitem__(self, key: str, value: Any) -> None:
|
||||
self.set(key, value)
|
||||
|
||||
def get(self, key, default=None):
|
||||
def get(self, key: str, default: Any = None) -> Any:
|
||||
try:
|
||||
return self[key]
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def setdefault(self, key, default):
|
||||
def setdefault(self, key: str, default: Any) -> Any:
|
||||
if key not in self:
|
||||
self[key] = default
|
||||
return self[key]
|
||||
|
||||
def __contains__(self, key):
|
||||
def __contains__(self, key: str) -> bool:
|
||||
try:
|
||||
self.get_immutable(key)
|
||||
return True
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
def __delitem__(self, key):
|
||||
def __delitem__(self, key: str) -> None:
|
||||
self.remove(key)
|
||||
|
||||
|
||||
|
@ -95,13 +96,13 @@ class ConfigManager:
|
|||
|
||||
|
||||
class WrappedList(list):
|
||||
def __init__(self, conf, key, val):
|
||||
def __init__(self, conf: ref[ConfigManager], key: str, val: Any) -> None:
|
||||
self.key = key
|
||||
self.conf = conf
|
||||
self.orig = copy.deepcopy(val)
|
||||
super().__init__(val)
|
||||
|
||||
def __del__(self):
|
||||
def __del__(self) -> None:
|
||||
cur = list(self)
|
||||
conf = self.conf()
|
||||
if conf and self.orig != cur:
|
||||
|
@ -109,13 +110,13 @@ class WrappedList(list):
|
|||
|
||||
|
||||
class WrappedDict(dict):
|
||||
def __init__(self, conf, key, val):
|
||||
def __init__(self, conf: ref[ConfigManager], key: str, val: Any) -> None:
|
||||
self.key = key
|
||||
self.conf = conf
|
||||
self.orig = copy.deepcopy(val)
|
||||
super().__init__(val)
|
||||
|
||||
def __del__(self):
|
||||
def __del__(self) -> None:
|
||||
cur = dict(self)
|
||||
conf = self.conf()
|
||||
if conf and self.orig != cur:
|
||||
|
|
|
@ -92,7 +92,7 @@ REVLOG_RESCHED = 4
|
|||
##########################################################################
|
||||
|
||||
|
||||
def _tr(col: Optional[anki.collection.Collection]):
|
||||
def _tr(col: Optional[anki.collection.Collection]) -> Any:
|
||||
if col:
|
||||
return col.tr
|
||||
else:
|
||||
|
|
|
@ -32,7 +32,7 @@ class DB:
|
|||
del d["_db"]
|
||||
return f"{super().__repr__()} {pprint.pformat(d, width=300)}"
|
||||
|
||||
def execute(self, sql: str, *a, **ka) -> Cursor:
|
||||
def execute(self, sql: str, *a: Any, **ka: Any) -> Cursor:
|
||||
s = sql.strip().lower()
|
||||
# mark modified?
|
||||
for stmt in "insert", "update", "delete":
|
||||
|
@ -76,36 +76,36 @@ class DB:
|
|||
def rollback(self) -> None:
|
||||
self._db.rollback()
|
||||
|
||||
def scalar(self, *a, **kw) -> Any:
|
||||
def scalar(self, *a: Any, **kw: Any) -> Any:
|
||||
res = self.execute(*a, **kw).fetchone()
|
||||
if res:
|
||||
return res[0]
|
||||
return None
|
||||
|
||||
def all(self, *a, **kw) -> List:
|
||||
def all(self, *a: Any, **kw: Any) -> List:
|
||||
return self.execute(*a, **kw).fetchall()
|
||||
|
||||
def first(self, *a, **kw) -> Any:
|
||||
def first(self, *a: Any, **kw: Any) -> Any:
|
||||
c = self.execute(*a, **kw)
|
||||
res = c.fetchone()
|
||||
c.close()
|
||||
return res
|
||||
|
||||
def list(self, *a, **kw) -> List:
|
||||
def list(self, *a: Any, **kw: Any) -> List:
|
||||
return [x[0] for x in self.execute(*a, **kw)]
|
||||
|
||||
def close(self) -> None:
|
||||
self._db.text_factory = None
|
||||
self._db.close()
|
||||
|
||||
def set_progress_handler(self, *args) -> None:
|
||||
def set_progress_handler(self, *args: Any) -> None:
|
||||
self._db.set_progress_handler(*args)
|
||||
|
||||
def __enter__(self) -> "DB":
|
||||
self._db.execute("begin")
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, *args) -> None:
|
||||
def __exit__(self, *args: Any) -> None:
|
||||
self._db.close()
|
||||
|
||||
def totalChanges(self) -> Any:
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from re import Match
|
||||
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Union
|
||||
|
||||
import anki
|
||||
|
@ -43,7 +44,11 @@ class DBProxy:
|
|||
################
|
||||
|
||||
def _query(
|
||||
self, sql: str, *args: ValueForDB, first_row_only: bool = False, **kwargs
|
||||
self,
|
||||
sql: str,
|
||||
*args: ValueForDB,
|
||||
first_row_only: bool = False,
|
||||
**kwargs: ValueForDB,
|
||||
) -> List[Row]:
|
||||
# mark modified?
|
||||
s = sql.strip().lower()
|
||||
|
@ -57,20 +62,22 @@ class DBProxy:
|
|||
# Query shortcuts
|
||||
###################
|
||||
|
||||
def all(self, sql: str, *args: ValueForDB, **kwargs) -> List[Row]:
|
||||
return self._query(sql, *args, **kwargs)
|
||||
def all(self, sql: str, *args: ValueForDB, **kwargs: ValueForDB) -> List[Row]:
|
||||
return self._query(sql, *args, first_row_only=False, **kwargs)
|
||||
|
||||
def list(self, sql: str, *args: ValueForDB, **kwargs) -> List[ValueFromDB]:
|
||||
return [x[0] for x in self._query(sql, *args, **kwargs)]
|
||||
def list(
|
||||
self, sql: str, *args: ValueForDB, **kwargs: ValueForDB
|
||||
) -> List[ValueFromDB]:
|
||||
return [x[0] for x in self._query(sql, *args, first_row_only=False, **kwargs)]
|
||||
|
||||
def first(self, sql: str, *args: ValueForDB, **kwargs) -> Optional[Row]:
|
||||
def first(self, sql: str, *args: ValueForDB, **kwargs: ValueForDB) -> Optional[Row]:
|
||||
rows = self._query(sql, *args, first_row_only=True, **kwargs)
|
||||
if rows:
|
||||
return rows[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
def scalar(self, sql: str, *args: ValueForDB, **kwargs) -> ValueFromDB:
|
||||
def scalar(self, sql: str, *args: ValueForDB, **kwargs: ValueForDB) -> ValueFromDB:
|
||||
rows = self._query(sql, *args, first_row_only=True, **kwargs)
|
||||
if rows:
|
||||
return rows[0][0]
|
||||
|
@ -109,7 +116,7 @@ def emulate_named_args(
|
|||
n = len(args2)
|
||||
arg_num[key] = n
|
||||
# update refs
|
||||
def repl(m):
|
||||
def repl(m: Match) -> str:
|
||||
arg = m.group(1)
|
||||
return f"?{arg_num[arg]}"
|
||||
|
||||
|
|
|
@ -5,6 +5,8 @@ from __future__ import annotations
|
|||
|
||||
import copy
|
||||
import pprint
|
||||
import sys
|
||||
import traceback
|
||||
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Union
|
||||
|
||||
import anki # pylint: disable=unused-import
|
||||
|
@ -43,36 +45,37 @@ class DecksDictProxy:
|
|||
def __init__(self, col: anki.collection.Collection):
|
||||
self._col = col.weakref()
|
||||
|
||||
def _warn(self):
|
||||
def _warn(self) -> None:
|
||||
traceback.print_stack(file=sys.stdout)
|
||||
print("add-on should use methods on col.decks, not col.decks.decks dict")
|
||||
|
||||
def __getitem__(self, item):
|
||||
def __getitem__(self, item: Any) -> Any:
|
||||
self._warn()
|
||||
return self._col.decks.get(int(item))
|
||||
|
||||
def __setitem__(self, key, val):
|
||||
def __setitem__(self, key: Any, val: Any) -> None:
|
||||
self._warn()
|
||||
self._col.decks.save(val)
|
||||
|
||||
def __len__(self):
|
||||
def __len__(self) -> int:
|
||||
self._warn()
|
||||
return len(self._col.decks.all_names_and_ids())
|
||||
|
||||
def keys(self):
|
||||
def keys(self) -> Any:
|
||||
self._warn()
|
||||
return [str(nt.id) for nt in self._col.decks.all_names_and_ids()]
|
||||
|
||||
def values(self):
|
||||
def values(self) -> Any:
|
||||
self._warn()
|
||||
return self._col.decks.all()
|
||||
|
||||
def items(self):
|
||||
def items(self) -> Any:
|
||||
self._warn()
|
||||
return [(str(nt["id"]), nt) for nt in self._col.decks.all()]
|
||||
|
||||
def __contains__(self, item):
|
||||
def __contains__(self, item: Any) -> bool:
|
||||
self._warn()
|
||||
self._col.decks.have(item)
|
||||
return self._col.decks.have(item)
|
||||
|
||||
|
||||
class DeckManager:
|
||||
|
@ -97,7 +100,7 @@ class DeckManager:
|
|||
self.update(g, preserve_usn=False)
|
||||
|
||||
# legacy
|
||||
def flush(self):
|
||||
def flush(self) -> None:
|
||||
pass
|
||||
|
||||
def __repr__(self) -> str:
|
||||
|
@ -135,7 +138,7 @@ class DeckManager:
|
|||
self.col._backend.remove_deck(did)
|
||||
|
||||
def all_names_and_ids(
|
||||
self, skip_empty_default=False, include_filtered=True
|
||||
self, skip_empty_default: bool = False, include_filtered: bool = True
|
||||
) -> Sequence[DeckNameID]:
|
||||
"A sorted sequence of deck names and IDs."
|
||||
return self.col._backend.get_deck_names(
|
||||
|
@ -195,12 +198,12 @@ class DeckManager:
|
|||
)
|
||||
]
|
||||
|
||||
def collapse(self, did) -> None:
|
||||
def collapse(self, did: int) -> None:
|
||||
deck = self.get(did)
|
||||
deck["collapsed"] = not deck["collapsed"]
|
||||
self.save(deck)
|
||||
|
||||
def collapseBrowser(self, did) -> None:
|
||||
def collapseBrowser(self, did: int) -> None:
|
||||
deck = self.get(did)
|
||||
collapsed = deck.get("browserCollapsed", False)
|
||||
deck["browserCollapsed"] = not collapsed
|
||||
|
@ -241,7 +244,7 @@ class DeckManager:
|
|||
return self.get_legacy(id)
|
||||
return None
|
||||
|
||||
def update(self, g: Deck, preserve_usn=True) -> None:
|
||||
def update(self, g: Deck, preserve_usn: bool = True) -> None:
|
||||
"Add or update an existing deck. Used for syncing and merging."
|
||||
try:
|
||||
g["id"] = self.col._backend.add_or_update_deck_legacy(
|
||||
|
@ -303,7 +306,7 @@ class DeckManager:
|
|||
except NotFoundError:
|
||||
return None
|
||||
|
||||
def update_config(self, conf: DeckConfig, preserve_usn=False) -> None:
|
||||
def update_config(self, conf: DeckConfig, preserve_usn: bool = False) -> None:
|
||||
conf["id"] = self.col._backend.add_or_update_deck_config_legacy(
|
||||
config=to_json_bytes(conf), preserve_usn_and_mtime=preserve_usn
|
||||
)
|
||||
|
@ -325,7 +328,7 @@ class DeckManager:
|
|||
) -> int:
|
||||
return self.add_config(name, clone_from)["id"]
|
||||
|
||||
def remove_config(self, id) -> None:
|
||||
def remove_config(self, id: int) -> None:
|
||||
"Remove a configuration and update all decks using it."
|
||||
self.col.modSchema(check=True)
|
||||
for g in self.all():
|
||||
|
@ -341,14 +344,14 @@ class DeckManager:
|
|||
grp["conf"] = id
|
||||
self.save(grp)
|
||||
|
||||
def didsForConf(self, conf) -> List[int]:
|
||||
def didsForConf(self, conf: DeckConfig) -> List[int]:
|
||||
dids = []
|
||||
for deck in self.all():
|
||||
if "conf" in deck and deck["conf"] == conf["id"]:
|
||||
dids.append(deck["id"])
|
||||
return dids
|
||||
|
||||
def restoreToDefault(self, conf) -> None:
|
||||
def restoreToDefault(self, conf: DeckConfig) -> None:
|
||||
oldOrder = conf["new"]["order"]
|
||||
new = from_json_bytes(self.col._backend.new_deck_config_legacy())
|
||||
new["id"] = conf["id"]
|
||||
|
@ -380,7 +383,7 @@ class DeckManager:
|
|||
return deck["name"]
|
||||
return None
|
||||
|
||||
def setDeck(self, cids, did) -> None:
|
||||
def setDeck(self, cids: List[int], did: int) -> None:
|
||||
self.col.db.execute(
|
||||
"update cards set did=?,usn=?,mod=? where id in " + ids2str(cids),
|
||||
did,
|
||||
|
@ -424,7 +427,7 @@ class DeckManager:
|
|||
self.col.conf["activeDecks"] = active
|
||||
|
||||
# don't use this, it will likely go away
|
||||
def update_active(self):
|
||||
def update_active(self) -> None:
|
||||
self.select(self.current()["id"])
|
||||
|
||||
# Parents/children
|
||||
|
@ -480,7 +483,7 @@ class DeckManager:
|
|||
# Change to Dict[int, "DeckManager.childMapNode"] when MyPy allow recursive type
|
||||
|
||||
def childDids(self, did: int, childMap: DeckManager.childMapNode) -> List:
|
||||
def gather(node: DeckManager.childMapNode, arr):
|
||||
def gather(node: DeckManager.childMapNode, arr: List) -> None:
|
||||
for did, child in node.items():
|
||||
arr.append(did)
|
||||
gather(child, arr)
|
||||
|
|
|
@ -3,8 +3,6 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import anki._backend.backend_pb2 as _pb
|
||||
|
||||
# fixme: notfounderror etc need to be in rsbackend.py
|
||||
|
@ -88,17 +86,15 @@ def backend_exception_to_pylib(err: _pb.BackendError) -> Exception:
|
|||
return StringError(err.localized)
|
||||
|
||||
|
||||
# FIXME: this is only used with "abortSchemaMod", but currently some
|
||||
# add-ons depend on it
|
||||
class AnkiError(Exception):
|
||||
def __init__(self, type, **data) -> None:
|
||||
def __init__(self, type: str) -> None:
|
||||
super().__init__()
|
||||
self.type = type
|
||||
self.data = data
|
||||
|
||||
def __str__(self) -> Any:
|
||||
m = self.type
|
||||
if self.data:
|
||||
m += ": %s" % repr(self.data)
|
||||
return m
|
||||
def __str__(self) -> str:
|
||||
return self.type
|
||||
|
||||
|
||||
class DeckRenameError(Exception):
|
||||
|
@ -106,5 +102,5 @@ class DeckRenameError(Exception):
|
|||
super().__init__()
|
||||
self.description = description
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
return "Couldn't rename deck: " + self.description
|
||||
|
|
|
@ -16,10 +16,10 @@ class Finder:
|
|||
self.col = col.weakref()
|
||||
print("Finder() is deprecated, please use col.find_cards() or .find_notes()")
|
||||
|
||||
def findCards(self, query, order):
|
||||
def findCards(self, query: Any, order: Any) -> Any:
|
||||
return self.col.find_cards(query, order)
|
||||
|
||||
def findNotes(self, query):
|
||||
def findNotes(self, query: Any) -> Any:
|
||||
return self.col.find_notes(query)
|
||||
|
||||
|
||||
|
@ -55,7 +55,7 @@ def fieldNamesForNotes(col: Collection, nids: List[int]) -> List[str]:
|
|||
##########################################################################
|
||||
|
||||
|
||||
def fieldNames(col, downcase=True) -> List:
|
||||
def fieldNames(col: Collection, downcase: bool = True) -> List:
|
||||
fields: Set[str] = set()
|
||||
for m in col.models.all():
|
||||
for f in m["flds"]:
|
||||
|
|
|
@ -25,7 +25,7 @@ from anki.hooks_gen import *
|
|||
_hooks: Dict[str, List[Callable[..., Any]]] = {}
|
||||
|
||||
|
||||
def runHook(hook: str, *args) -> None:
|
||||
def runHook(hook: str, *args: Any) -> None:
|
||||
"Run all functions on hook."
|
||||
hookFuncs = _hooks.get(hook, None)
|
||||
if hookFuncs:
|
||||
|
@ -37,7 +37,7 @@ def runHook(hook: str, *args) -> None:
|
|||
raise
|
||||
|
||||
|
||||
def runFilter(hook: str, arg: Any, *args) -> Any:
|
||||
def runFilter(hook: str, arg: Any, *args: Any) -> Any:
|
||||
hookFuncs = _hooks.get(hook, None)
|
||||
if hookFuncs:
|
||||
for func in hookFuncs:
|
||||
|
@ -57,7 +57,7 @@ def addHook(hook: str, func: Callable) -> None:
|
|||
_hooks[hook].append(func)
|
||||
|
||||
|
||||
def remHook(hook, func) -> None:
|
||||
def remHook(hook: Any, func: Any) -> None:
|
||||
"Remove a function if is on hook."
|
||||
hook = _hooks.get(hook, [])
|
||||
if func in hook:
|
||||
|
@ -72,10 +72,10 @@ def remHook(hook, func) -> None:
|
|||
#
|
||||
# If you call wrap() with pos='around', the original function will not be called
|
||||
# automatically but can be called with _old().
|
||||
def wrap(old, new, pos="after") -> Callable:
|
||||
def wrap(old: Any, new: Any, pos: str = "after") -> Callable:
|
||||
"Override an existing function."
|
||||
|
||||
def repl(*args, **kwargs):
|
||||
def repl(*args: Any, **kwargs: Any) -> Any:
|
||||
if pos == "after":
|
||||
old(*args, **kwargs)
|
||||
return new(*args, **kwargs)
|
||||
|
@ -85,7 +85,7 @@ def wrap(old, new, pos="after") -> Callable:
|
|||
else:
|
||||
return new(_old=old, *args, **kwargs)
|
||||
|
||||
def decorator_wrapper(f, *args, **kwargs):
|
||||
def decorator_wrapper(f: Any, *args: Any, **kwargs: Any) -> Any:
|
||||
return repl(*args, **kwargs)
|
||||
|
||||
return decorator.decorator(decorator_wrapper)(old)
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
Wrapper for requests that adds a callback for tracking upload/download progress.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import os
|
||||
from typing import Any, Callable, Dict, Optional
|
||||
|
@ -28,24 +30,23 @@ class HttpClient:
|
|||
self.progress_hook = progress_hook
|
||||
self.session = requests.Session()
|
||||
|
||||
def __enter__(self):
|
||||
def __enter__(self) -> HttpClient:
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
def __exit__(self, *args: Any) -> None:
|
||||
self.close()
|
||||
|
||||
def close(self):
|
||||
def close(self) -> None:
|
||||
if self.session:
|
||||
self.session.close()
|
||||
self.session = None
|
||||
|
||||
def __del__(self):
|
||||
def __del__(self) -> None:
|
||||
self.close()
|
||||
|
||||
def post(self, url: str, data: Any, headers: Optional[Dict[str, str]]) -> Response:
|
||||
data = _MonitoringFile(
|
||||
data, hook=self.progress_hook
|
||||
) # pytype: disable=wrong-arg-types
|
||||
def post(
|
||||
self, url: str, data: bytes, headers: Optional[Dict[str, str]]
|
||||
) -> Response:
|
||||
headers["User-Agent"] = self._agentName()
|
||||
return self.session.post(
|
||||
url,
|
||||
|
@ -56,7 +57,7 @@ class HttpClient:
|
|||
verify=self.verify,
|
||||
) # pytype: disable=wrong-arg-types
|
||||
|
||||
def get(self, url, headers=None) -> Response:
|
||||
def get(self, url: str, headers: Dict[str, str] = None) -> Response:
|
||||
if headers is None:
|
||||
headers = {}
|
||||
headers["User-Agent"] = self._agentName()
|
||||
|
@ -64,7 +65,7 @@ class HttpClient:
|
|||
url, stream=True, headers=headers, timeout=self.timeout, verify=self.verify
|
||||
)
|
||||
|
||||
def streamContent(self, resp) -> bytes:
|
||||
def streamContent(self, resp: Response) -> bytes:
|
||||
resp.raise_for_status()
|
||||
|
||||
buf = io.BytesIO()
|
||||
|
@ -87,15 +88,3 @@ if os.environ.get("ANKI_NOVERIFYSSL"):
|
|||
import warnings
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
|
||||
class _MonitoringFile(io.BufferedReader):
|
||||
def __init__(self, raw: io.RawIOBase, hook: Optional[ProgressCallback]):
|
||||
io.BufferedReader.__init__(self, raw)
|
||||
self.hook = hook
|
||||
|
||||
def read(self, size=-1) -> bytes:
|
||||
data = io.BufferedReader.read(self, HTTP_BUF_SIZE)
|
||||
if self.hook:
|
||||
self.hook(len(data), 0)
|
||||
return data
|
||||
|
|
|
@ -5,7 +5,7 @@ from __future__ import annotations
|
|||
|
||||
import locale
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Optional, Tuple
|
||||
from typing import TYPE_CHECKING, Any, Optional, Tuple
|
||||
|
||||
import anki
|
||||
import anki._backend.backend_pb2 as _pb
|
||||
|
@ -169,7 +169,7 @@ def ngettext(single: str, plural: str, n: int) -> str:
|
|||
return plural
|
||||
|
||||
|
||||
def tr_legacyglobal(*args, **kwargs) -> str:
|
||||
def tr_legacyglobal(*args: Any, **kwargs: Any) -> str:
|
||||
"Should use col.tr() instead."
|
||||
if current_i18n:
|
||||
return current_i18n.translate(*args, **kwargs)
|
||||
|
|
|
@ -11,7 +11,7 @@ import time
|
|||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from typing import Any, Callable, List, Optional, Tuple
|
||||
from typing import Any, Callable, List, Match, Optional, Tuple
|
||||
|
||||
import anki
|
||||
import anki._backend.backend_pb2 as _pb
|
||||
|
@ -197,7 +197,7 @@ class MediaManager:
|
|||
else:
|
||||
fn = urllib.parse.quote
|
||||
|
||||
def repl(match):
|
||||
def repl(match: Match) -> str:
|
||||
tag = match.group(0)
|
||||
fname = match.group("fname")
|
||||
if re.match("(https?|ftp)://", fname):
|
||||
|
|
|
@ -5,7 +5,9 @@ from __future__ import annotations
|
|||
|
||||
import copy
|
||||
import pprint
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
|
||||
|
||||
import anki # pylint: disable=unused-import
|
||||
|
@ -39,36 +41,37 @@ class ModelsDictProxy:
|
|||
def __init__(self, col: anki.collection.Collection):
|
||||
self._col = col.weakref()
|
||||
|
||||
def _warn(self):
|
||||
def _warn(self) -> None:
|
||||
traceback.print_stack(file=sys.stdout)
|
||||
print("add-on should use methods on col.models, not col.models.models dict")
|
||||
|
||||
def __getitem__(self, item):
|
||||
def __getitem__(self, item: Any) -> Any:
|
||||
self._warn()
|
||||
return self._col.models.get(int(item))
|
||||
|
||||
def __setitem__(self, key, val):
|
||||
def __setitem__(self, key: str, val: Any) -> None:
|
||||
self._warn()
|
||||
self._col.models.save(val)
|
||||
|
||||
def __len__(self):
|
||||
def __len__(self) -> int:
|
||||
self._warn()
|
||||
return len(self._col.models.all_names_and_ids())
|
||||
|
||||
def keys(self):
|
||||
def keys(self) -> Any:
|
||||
self._warn()
|
||||
return [str(nt.id) for nt in self._col.models.all_names_and_ids()]
|
||||
|
||||
def values(self):
|
||||
def values(self) -> Any:
|
||||
self._warn()
|
||||
return self._col.models.all()
|
||||
|
||||
def items(self):
|
||||
def items(self) -> Any:
|
||||
self._warn()
|
||||
return [(str(nt["id"]), nt) for nt in self._col.models.all()]
|
||||
|
||||
def __contains__(self, item):
|
||||
def __contains__(self, item: Any) -> bool:
|
||||
self._warn()
|
||||
self._col.models.have(item)
|
||||
return self._col.models.have(item)
|
||||
|
||||
|
||||
class ModelManager:
|
||||
|
@ -123,7 +126,7 @@ class ModelManager:
|
|||
def _get_cached(self, ntid: int) -> Optional[NoteType]:
|
||||
return self._cache.get(ntid)
|
||||
|
||||
def _clear_cache(self):
|
||||
def _clear_cache(self) -> None:
|
||||
self._cache = {}
|
||||
|
||||
# Listing note types
|
||||
|
@ -218,7 +221,7 @@ class ModelManager:
|
|||
"Delete model, and all its cards/notes."
|
||||
self.remove(m["id"])
|
||||
|
||||
def remove_all_notetypes(self):
|
||||
def remove_all_notetypes(self) -> None:
|
||||
for nt in self.all_names_and_ids():
|
||||
self._remove_from_cache(nt.id)
|
||||
self.col._backend.remove_notetype(nt.id)
|
||||
|
@ -236,7 +239,7 @@ class ModelManager:
|
|||
if existing_id is not None and existing_id != m["id"]:
|
||||
m["name"] += "-" + checksum(str(time.time()))[:5]
|
||||
|
||||
def update(self, m: NoteType, preserve_usn=True) -> None:
|
||||
def update(self, m: NoteType, preserve_usn: bool = True) -> None:
|
||||
"Add or update an existing model. Use .save() instead."
|
||||
self._remove_from_cache(m["id"])
|
||||
self.ensureNameUnique(m)
|
||||
|
|
|
@ -113,7 +113,7 @@ class Note:
|
|||
def __setitem__(self, key: str, value: str) -> None:
|
||||
self.fields[self._fieldOrd(key)] = value
|
||||
|
||||
def __contains__(self, key) -> bool:
|
||||
def __contains__(self, key: str) -> bool:
|
||||
return key in self._fmap
|
||||
|
||||
# Tags
|
||||
|
|
|
@ -350,7 +350,7 @@ limit %d"""
|
|||
lastIvl = -(self._delayForGrade(conf, lastLeft))
|
||||
ivl = card.ivl if leaving else -(self._delayForGrade(conf, card.left))
|
||||
|
||||
def log():
|
||||
def log() -> None:
|
||||
self.col.db.execute(
|
||||
"insert into revlog values (?,?,?,?,?,?,?,?,?)",
|
||||
int(time.time() * 1000),
|
||||
|
@ -450,7 +450,7 @@ and due <= ? limit ?)""",
|
|||
self._revQueue: List[Any] = []
|
||||
self._revDids = self.col.decks.active()[:]
|
||||
|
||||
def _fillRev(self, recursing=False) -> bool:
|
||||
def _fillRev(self, recursing: bool = False) -> bool:
|
||||
"True if a review card can be fetched."
|
||||
if self._revQueue:
|
||||
return True
|
||||
|
|
|
@ -166,7 +166,7 @@ class Scheduler:
|
|||
self._restorePreviewCard(card)
|
||||
self._removeFromFiltered(card)
|
||||
|
||||
def _reset_counts(self):
|
||||
def _reset_counts(self) -> None:
|
||||
tree = self.deck_due_tree(self.col.decks.selected())
|
||||
node = self.col.decks.find_deck_in_tree(tree, int(self.col.conf["curDeck"]))
|
||||
if not node:
|
||||
|
@ -187,7 +187,7 @@ class Scheduler:
|
|||
new, lrn, rev = counts
|
||||
return (new, lrn, rev)
|
||||
|
||||
def _is_finished(self):
|
||||
def _is_finished(self) -> bool:
|
||||
"Don't use this, it is a stop-gap until this code is refactored."
|
||||
return not any((self.newCount, self.revCount, self._immediate_learn_count))
|
||||
|
||||
|
@ -229,8 +229,12 @@ order by due"""
|
|||
##########################################################################
|
||||
|
||||
def update_stats(
|
||||
self, deck_id: int, new_delta=0, review_delta=0, milliseconds_delta=0
|
||||
):
|
||||
self,
|
||||
deck_id: int,
|
||||
new_delta: int = 0,
|
||||
review_delta: int = 0,
|
||||
milliseconds_delta: int = 0,
|
||||
) -> None:
|
||||
self.col._backend.update_stats(
|
||||
deck_id=deck_id,
|
||||
new_delta=new_delta,
|
||||
|
@ -321,7 +325,7 @@ order by due"""
|
|||
self._newQueue: List[int] = []
|
||||
self._updateNewCardRatio()
|
||||
|
||||
def _fillNew(self, recursing=False) -> bool:
|
||||
def _fillNew(self, recursing: bool = False) -> bool:
|
||||
if self._newQueue:
|
||||
return True
|
||||
if not self.newCount:
|
||||
|
@ -841,7 +845,7 @@ and due <= ? limit ?)"""
|
|||
def _resetRev(self) -> None:
|
||||
self._revQueue: List[int] = []
|
||||
|
||||
def _fillRev(self, recursing=False) -> bool:
|
||||
def _fillRev(self, recursing: bool = False) -> bool:
|
||||
"True if a review card can be fetched."
|
||||
if self._revQueue:
|
||||
return True
|
||||
|
@ -947,7 +951,7 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
|
|||
self._removeFromFiltered(card)
|
||||
|
||||
def _logRev(self, card: Card, ease: int, delay: int, type: int) -> None:
|
||||
def log():
|
||||
def log() -> None:
|
||||
self.col.db.execute(
|
||||
"insert into revlog values (?,?,?,?,?,?,?,?,?)",
|
||||
int(time.time() * 1000),
|
||||
|
@ -1344,7 +1348,7 @@ due = (case when odue>0 then odue else due end), odue = 0, odid = 0, usn = ? whe
|
|||
mode = BuryOrSuspendMode.BURY_SCHED
|
||||
self.col._backend.bury_or_suspend_cards(card_ids=ids, mode=mode)
|
||||
|
||||
def bury_note(self, note: Note):
|
||||
def bury_note(self, note: Note) -> None:
|
||||
self.bury_cards(note.card_ids())
|
||||
|
||||
# legacy
|
||||
|
@ -1472,7 +1476,7 @@ and (queue={QUEUE_TYPE_NEW} or (queue={QUEUE_TYPE_REV} and due<=?))""",
|
|||
def orderCards(self, did: int) -> None:
|
||||
self.col._backend.sort_deck(deck_id=did, randomize=False)
|
||||
|
||||
def resortConf(self, conf) -> None:
|
||||
def resortConf(self, conf: DeckConfig) -> None:
|
||||
for did in self.col.decks.didsForConf(conf):
|
||||
if conf["new"]["order"] == 0:
|
||||
self.randomizeCards(did)
|
||||
|
|
|
@ -140,7 +140,7 @@ from revlog where id > ? """
|
|||
relrn = relrn or 0
|
||||
filt = filt or 0
|
||||
# studied
|
||||
def bold(s):
|
||||
def bold(s: str) -> str:
|
||||
return "<b>" + str(s) + "</b>"
|
||||
|
||||
if cards:
|
||||
|
@ -298,7 +298,7 @@ group by day order by day"""
|
|||
# pylint: disable=invalid-unary-operand-type
|
||||
conf["xaxis"]["min"] = -days + 0.5
|
||||
|
||||
def plot(id, data, ylabel, ylabel2):
|
||||
def plot(id: str, data: Any, ylabel: str, ylabel2: str) -> str:
|
||||
return self._graph(
|
||||
id, data=data, conf=conf, xunit=chunk, ylabel=ylabel, ylabel2=ylabel2
|
||||
)
|
||||
|
@ -333,7 +333,7 @@ group by day order by day"""
|
|||
# pylint: disable=invalid-unary-operand-type
|
||||
conf["xaxis"]["min"] = -days + 0.5
|
||||
|
||||
def plot(id, data, ylabel, ylabel2):
|
||||
def plot(id: str, data: Any, ylabel: str, ylabel2: str) -> str:
|
||||
return self._graph(
|
||||
id, data=data, conf=conf, xunit=chunk, ylabel=ylabel, ylabel2=ylabel2
|
||||
)
|
||||
|
|
|
@ -11,9 +11,10 @@ import os
|
|||
import socket
|
||||
import sys
|
||||
import time
|
||||
from http import HTTPStatus
|
||||
from io import BytesIO
|
||||
from tempfile import NamedTemporaryFile
|
||||
from typing import Optional
|
||||
from typing import Iterable, Optional
|
||||
|
||||
try:
|
||||
import flask
|
||||
|
@ -89,7 +90,7 @@ def handle_sync_request(method_str: str) -> Response:
|
|||
elif method == Method.FULL_DOWNLOAD:
|
||||
path = outdata.decode("utf8")
|
||||
|
||||
def stream_reply():
|
||||
def stream_reply() -> Iterable[bytes]:
|
||||
with open(path, "rb") as f:
|
||||
while chunk := f.read(16 * 1024):
|
||||
yield chunk
|
||||
|
@ -106,7 +107,7 @@ def handle_sync_request(method_str: str) -> Response:
|
|||
return resp
|
||||
|
||||
|
||||
def after_full_sync():
|
||||
def after_full_sync() -> None:
|
||||
# the server methods do not reopen the collection after a full sync,
|
||||
# so we need to
|
||||
col.reopen(after_full_sync=False)
|
||||
|
@ -146,15 +147,17 @@ def get_method(
|
|||
|
||||
|
||||
@app.route("/<path:pathin>", methods=["POST"])
|
||||
def handle_request(pathin: str):
|
||||
def handle_request(pathin: str) -> Response:
|
||||
path = pathin
|
||||
print(int(time.time()), flask.request.remote_addr, path)
|
||||
|
||||
if path.startswith("sync/"):
|
||||
return handle_sync_request(path.split("/", maxsplit=1)[1])
|
||||
else:
|
||||
return flask.make_response("not found", HTTPStatus.NOT_FOUND)
|
||||
|
||||
|
||||
def folder():
|
||||
def folder() -> str:
|
||||
folder = os.getenv("FOLDER", os.path.expanduser("~/.syncserver"))
|
||||
if not os.path.exists(folder):
|
||||
print("creating", folder)
|
||||
|
@ -162,11 +165,11 @@ def folder():
|
|||
return folder
|
||||
|
||||
|
||||
def col_path():
|
||||
def col_path() -> str:
|
||||
return os.path.join(folder(), "collection.server.anki2")
|
||||
|
||||
|
||||
def serve():
|
||||
def serve() -> None:
|
||||
global col
|
||||
|
||||
col = Collection(col_path(), server=True)
|
||||
|
|
|
@ -13,7 +13,7 @@ from __future__ import annotations
|
|||
|
||||
import pprint
|
||||
import re
|
||||
from typing import Collection, List, Optional, Sequence, Tuple
|
||||
from typing import Collection, List, Match, Optional, Sequence, Tuple
|
||||
|
||||
import anki # pylint: disable=unused-import
|
||||
import anki._backend.backend_pb2 as _pb
|
||||
|
@ -48,7 +48,7 @@ class TagManager:
|
|||
#############################################################
|
||||
|
||||
def register(
|
||||
self, tags: Collection[str], usn: Optional[int] = None, clear=False
|
||||
self, tags: Collection[str], usn: Optional[int] = None, clear: bool = False
|
||||
) -> None:
|
||||
print("tags.register() is deprecated and no longer works")
|
||||
|
||||
|
@ -56,10 +56,10 @@ class TagManager:
|
|||
"Clear unused tags and add any missing tags from notes to the tag list."
|
||||
self.clear_unused_tags()
|
||||
|
||||
def clear_unused_tags(self):
|
||||
def clear_unused_tags(self) -> None:
|
||||
self.col._backend.clear_unused_tags()
|
||||
|
||||
def byDeck(self, did, children=False) -> List[str]:
|
||||
def byDeck(self, did: int, children: bool = False) -> List[str]:
|
||||
basequery = "select n.tags from cards c, notes n WHERE c.nid = n.id"
|
||||
if not children:
|
||||
query = basequery + " AND c.did=?"
|
||||
|
@ -72,7 +72,7 @@ class TagManager:
|
|||
res = self.col.db.list(query)
|
||||
return list(set(self.split(" ".join(res))))
|
||||
|
||||
def set_collapsed(self, tag: str, collapsed: bool):
|
||||
def set_collapsed(self, tag: str, collapsed: bool) -> None:
|
||||
"Set browser collapse state for tag, registering the tag if missing."
|
||||
self.col._backend.set_tag_collapsed(name=tag, collapsed=collapsed)
|
||||
|
||||
|
@ -139,9 +139,9 @@ class TagManager:
|
|||
def remFromStr(self, deltags: str, tags: str) -> str:
|
||||
"Delete tags if they exist."
|
||||
|
||||
def wildcard(pat, str):
|
||||
def wildcard(pat: str, repl: str) -> Match:
|
||||
pat = re.escape(pat).replace("\\*", ".*")
|
||||
return re.match("^" + pat + "$", str, re.IGNORECASE)
|
||||
return re.match("^" + pat + "$", repl, re.IGNORECASE)
|
||||
|
||||
currentTags = self.split(tags)
|
||||
for tag in self.split(deltags):
|
||||
|
|
|
@ -3,9 +3,7 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
# some add-ons expect json to be in the utils module
|
||||
import json # pylint: disable=unused-import
|
||||
import locale
|
||||
import json as _json
|
||||
import os
|
||||
import platform
|
||||
import random
|
||||
|
@ -20,7 +18,7 @@ import traceback
|
|||
from contextlib import contextmanager
|
||||
from hashlib import sha1
|
||||
from html.entities import name2codepoint
|
||||
from typing import Iterable, Iterator, List, Optional, Union
|
||||
from typing import Any, Iterable, Iterator, List, Match, Optional, Union
|
||||
|
||||
from anki.dbproxy import DBProxy
|
||||
|
||||
|
@ -34,8 +32,17 @@ try:
|
|||
from_json_bytes = orjson.loads
|
||||
except:
|
||||
print("orjson is missing; DB operations will be slower")
|
||||
to_json_bytes = lambda obj: json.dumps(obj).encode("utf8") # type: ignore
|
||||
from_json_bytes = json.loads
|
||||
to_json_bytes = lambda obj: _json.dumps(obj).encode("utf8") # type: ignore
|
||||
from_json_bytes = _json.loads
|
||||
|
||||
|
||||
def __getattr__(name: str) -> Any:
|
||||
if name == "json":
|
||||
traceback.print_stack(file=sys.stdout)
|
||||
print("add-on should import json directly, not from anki.utils")
|
||||
return _json
|
||||
raise AttributeError(f"module {__name__} has no attribute {name}")
|
||||
|
||||
|
||||
# Time handling
|
||||
##############################################################################
|
||||
|
@ -46,22 +53,6 @@ def intTime(scale: int = 1) -> int:
|
|||
return int(time.time() * scale)
|
||||
|
||||
|
||||
# Locale
|
||||
##############################################################################
|
||||
|
||||
|
||||
def fmtPercentage(float_value, point=1) -> str:
|
||||
"Return float with percentage sign"
|
||||
fmt = "%" + "0.%(b)df" % {"b": point}
|
||||
return locale.format_string(fmt, float_value) + "%"
|
||||
|
||||
|
||||
def fmtFloat(float_value, point=1) -> str:
|
||||
"Return a string with decimal separator according to current locale"
|
||||
fmt = "%" + "0.%(b)df" % {"b": point}
|
||||
return locale.format_string(fmt, float_value)
|
||||
|
||||
|
||||
# HTML
|
||||
##############################################################################
|
||||
reComment = re.compile("(?s)<!--.*?-->")
|
||||
|
@ -114,7 +105,7 @@ def entsToTxt(html: str) -> str:
|
|||
# replace it first
|
||||
html = html.replace(" ", " ")
|
||||
|
||||
def fixup(m):
|
||||
def fixup(m: Match) -> str:
|
||||
text = m.group(0)
|
||||
if text[:2] == "&#":
|
||||
# character reference
|
||||
|
@ -140,14 +131,6 @@ def entsToTxt(html: str) -> str:
|
|||
##############################################################################
|
||||
|
||||
|
||||
def hexifyID(id) -> str:
|
||||
return "%x" % int(id)
|
||||
|
||||
|
||||
def dehexifyID(id) -> int:
|
||||
return int(id, 16)
|
||||
|
||||
|
||||
def ids2str(ids: Iterable[Union[int, str]]) -> str:
|
||||
"""Given a list of integers, return a string '(int1,int2,...)'."""
|
||||
return "(%s)" % ",".join(str(i) for i in ids)
|
||||
|
@ -195,23 +178,6 @@ def guid64() -> str:
|
|||
return base91(random.randint(0, 2 ** 64 - 1))
|
||||
|
||||
|
||||
# increment a guid by one, for note type conflicts
|
||||
def incGuid(guid) -> str:
|
||||
return _incGuid(guid[::-1])[::-1]
|
||||
|
||||
|
||||
def _incGuid(guid) -> str:
|
||||
s = string
|
||||
table = s.ascii_letters + s.digits + _base91_extra_chars
|
||||
idx = table.index(guid[0])
|
||||
if idx + 1 == len(table):
|
||||
# overflow
|
||||
guid = table[0] + _incGuid(guid[1:])
|
||||
else:
|
||||
guid = table[idx + 1] + guid[1:]
|
||||
return guid
|
||||
|
||||
|
||||
# Fields
|
||||
##############################################################################
|
||||
|
||||
|
@ -250,7 +216,7 @@ def tmpdir() -> str:
|
|||
global _tmpdir
|
||||
if not _tmpdir:
|
||||
|
||||
def cleanup():
|
||||
def cleanup() -> None:
|
||||
if os.path.exists(_tmpdir):
|
||||
shutil.rmtree(_tmpdir)
|
||||
|
||||
|
@ -294,7 +260,7 @@ def noBundledLibs() -> Iterator[None]:
|
|||
os.environ["LD_LIBRARY_PATH"] = oldlpath
|
||||
|
||||
|
||||
def call(argv: List[str], wait: bool = True, **kwargs) -> int:
|
||||
def call(argv: List[str], wait: bool = True, **kwargs: Any) -> int:
|
||||
"Execute a command. If WAIT, return exit code."
|
||||
# ensure we don't open a separate window for forking process on windows
|
||||
if isWin:
|
||||
|
@ -338,7 +304,7 @@ devMode = os.getenv("ANKIDEV", "")
|
|||
invalidFilenameChars = ':*?"<>|'
|
||||
|
||||
|
||||
def invalidFilename(str, dirsep=True) -> Optional[str]:
|
||||
def invalidFilename(str: str, dirsep: bool = True) -> Optional[str]:
|
||||
for c in invalidFilenameChars:
|
||||
if c in str:
|
||||
return c
|
||||
|
@ -384,7 +350,7 @@ class TimedLog:
|
|||
def __init__(self) -> None:
|
||||
self._last = time.time()
|
||||
|
||||
def log(self, s) -> None:
|
||||
def log(self, s: str) -> None:
|
||||
path, num, fn, y = traceback.extract_stack(limit=2)[0]
|
||||
sys.stderr.write(
|
||||
"%5dms: %s(): %s\n" % ((time.time() - self._last) * 1000, fn, s)
|
||||
|
|
|
@ -9,6 +9,14 @@ warn_redundant_casts = True
|
|||
warn_unused_configs = True
|
||||
strict_equality = true
|
||||
|
||||
[mypy-anki.*]
|
||||
disallow_untyped_defs = True
|
||||
[mypy-anki.importing.*]
|
||||
disallow_untyped_defs = False
|
||||
[mypy-anki.exporting]
|
||||
disallow_untyped_defs = False
|
||||
|
||||
|
||||
[mypy-win32file]
|
||||
ignore_missing_imports = True
|
||||
[mypy-win32pipe]
|
||||
|
|
|
@ -44,7 +44,7 @@ py_proto_library_typed = rule(
|
|||
"mypy_protobuf": attr.label(
|
||||
executable = True,
|
||||
cfg = "exec",
|
||||
default = Label("//pylib/tools:mypy_protobuf"),
|
||||
default = Label("//pylib/tools:protoc-gen-mypy"),
|
||||
),
|
||||
},
|
||||
)
|
||||
|
|
|
@ -2,8 +2,8 @@ load("@rules_python//python:defs.bzl", "py_binary", "py_library")
|
|||
load("@py_deps//:requirements.bzl", "requirement")
|
||||
|
||||
py_binary(
|
||||
name = "mypy_protobuf",
|
||||
srcs = [requirement("mypy-protobuf").replace(":pkg", ":mypy_protobuf.py")],
|
||||
name = "protoc-gen-mypy",
|
||||
srcs = ["protoc-gen-mypy.py"],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
|
@ -17,7 +17,7 @@ py_binary(
|
|||
"//visibility:public",
|
||||
],
|
||||
deps = [
|
||||
":mypy_protobuf",
|
||||
":protoc-gen-mypy",
|
||||
"@rules_python//python/runfiles",
|
||||
],
|
||||
)
|
||||
|
|
|
@ -36,8 +36,7 @@ class Hook:
|
|||
types = []
|
||||
for arg in self.args or []:
|
||||
(name, type) = arg.split(":")
|
||||
if "." in type:
|
||||
type = '"' + type.strip() + '"'
|
||||
type = '"' + type.strip() + '"'
|
||||
types.append(type)
|
||||
types_str = ", ".join(types)
|
||||
return f"Callable[[{types_str}], {self.return_type or 'None'}]"
|
||||
|
|
9
pylib/tools/protoc-gen-mypy.py
Executable file
9
pylib/tools/protoc-gen-mypy.py
Executable file
|
@ -0,0 +1,9 @@
|
|||
# copied from mypy_protobuf:bin - simple launch wrapper
|
||||
import re
|
||||
import sys
|
||||
|
||||
from mypy_protobuf import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0])
|
||||
sys.exit(main())
|
|
@ -10,7 +10,7 @@ import os
|
|||
import sys
|
||||
import tempfile
|
||||
import traceback
|
||||
from typing import Any, Callable, Dict, Optional, Union
|
||||
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
||||
|
||||
import anki.lang
|
||||
from anki import version as _version
|
||||
|
@ -102,10 +102,10 @@ class DialogManager:
|
|||
self._dialogs[name][1] = instance
|
||||
return instance
|
||||
|
||||
def markClosed(self, name: str):
|
||||
def markClosed(self, name: str) -> None:
|
||||
self._dialogs[name] = [self._dialogs[name][0], None]
|
||||
|
||||
def allClosed(self):
|
||||
def allClosed(self) -> bool:
|
||||
return not any(x[1] for x in self._dialogs.values())
|
||||
|
||||
def closeAll(self, onsuccess: Callable[[], None]) -> Optional[bool]:
|
||||
|
@ -119,7 +119,7 @@ class DialogManager:
|
|||
if not instance:
|
||||
continue
|
||||
|
||||
def callback():
|
||||
def callback() -> None:
|
||||
if self.allClosed():
|
||||
onsuccess()
|
||||
else:
|
||||
|
@ -189,12 +189,12 @@ def setupLangAndBackend(
|
|||
pass
|
||||
|
||||
# add _ and ngettext globals used by legacy code
|
||||
def fn__(arg):
|
||||
def fn__(arg) -> None:
|
||||
print("".join(traceback.format_stack()[-2]))
|
||||
print("_ global will break in the future; please see anki/lang.py")
|
||||
return arg
|
||||
|
||||
def fn_ngettext(a, b, c):
|
||||
def fn_ngettext(a, b, c) -> None:
|
||||
print("".join(traceback.format_stack()[-2]))
|
||||
print("ngettext global will break in the future; please see anki/lang.py")
|
||||
return b
|
||||
|
@ -244,11 +244,11 @@ class AnkiApp(QApplication):
|
|||
KEY = "anki" + checksum(getpass.getuser())
|
||||
TMOUT = 30000
|
||||
|
||||
def __init__(self, argv):
|
||||
def __init__(self, argv) -> None:
|
||||
QApplication.__init__(self, argv)
|
||||
self._argv = argv
|
||||
|
||||
def secondInstance(self):
|
||||
def secondInstance(self) -> bool:
|
||||
# we accept only one command line argument. if it's missing, send
|
||||
# a blank screen to just raise the existing window
|
||||
opts, args = parseArgs(self._argv)
|
||||
|
@ -267,7 +267,7 @@ class AnkiApp(QApplication):
|
|||
self._srv.listen(self.KEY)
|
||||
return False
|
||||
|
||||
def sendMsg(self, txt):
|
||||
def sendMsg(self, txt) -> bool:
|
||||
sock = QLocalSocket(self)
|
||||
sock.connectToServer(self.KEY, QIODevice.WriteOnly)
|
||||
if not sock.waitForConnected(self.TMOUT):
|
||||
|
@ -286,7 +286,7 @@ class AnkiApp(QApplication):
|
|||
sock.disconnectFromServer()
|
||||
return True
|
||||
|
||||
def onRecv(self):
|
||||
def onRecv(self) -> None:
|
||||
sock = self._srv.nextPendingConnection()
|
||||
if not sock.waitForReadyRead(self.TMOUT):
|
||||
sys.stderr.write(sock.errorString())
|
||||
|
@ -298,14 +298,14 @@ class AnkiApp(QApplication):
|
|||
# OS X file/url handler
|
||||
##################################################
|
||||
|
||||
def event(self, evt):
|
||||
def event(self, evt) -> bool:
|
||||
if evt.type() == QEvent.FileOpen:
|
||||
self.appMsg.emit(evt.file() or "raise") # type: ignore
|
||||
return True
|
||||
return QApplication.event(self, evt)
|
||||
|
||||
|
||||
def parseArgs(argv):
|
||||
def parseArgs(argv) -> Tuple[argparse.Namespace, List[str]]:
|
||||
"Returns (opts, args)."
|
||||
# py2app fails to strip this in some instances, then anki dies
|
||||
# as there's no such profile
|
||||
|
@ -330,7 +330,7 @@ def parseArgs(argv):
|
|||
return parser.parse_known_args(argv[1:])
|
||||
|
||||
|
||||
def setupGL(pm):
|
||||
def setupGL(pm) -> None:
|
||||
if isMac:
|
||||
return
|
||||
|
||||
|
@ -343,7 +343,7 @@ def setupGL(pm):
|
|||
ctypes.CDLL("libGL.so.1", ctypes.RTLD_GLOBAL)
|
||||
|
||||
# catch opengl errors
|
||||
def msgHandler(category, ctx, msg):
|
||||
def msgHandler(category, ctx, msg) -> None:
|
||||
if category == QtDebugMsg:
|
||||
category = "debug"
|
||||
elif category == QtInfoMsg:
|
||||
|
@ -400,7 +400,7 @@ def setupGL(pm):
|
|||
PROFILE_CODE = os.environ.get("ANKI_PROFILE_CODE")
|
||||
|
||||
|
||||
def write_profile_results():
|
||||
def write_profile_results() -> None:
|
||||
|
||||
profiler.disable()
|
||||
profiler.dump_stats("anki.prof")
|
||||
|
@ -408,7 +408,7 @@ def write_profile_results():
|
|||
print("use 'bazel run qt:profile' to explore")
|
||||
|
||||
|
||||
def run():
|
||||
def run() -> None:
|
||||
try:
|
||||
_run()
|
||||
except Exception as e:
|
||||
|
@ -420,7 +420,7 @@ def run():
|
|||
)
|
||||
|
||||
|
||||
def _run(argv=None, exec=True):
|
||||
def _run(argv=None, exec=True) -> Optional[AnkiApp]:
|
||||
"""Start AnkiQt application or reuse an existing instance if one exists.
|
||||
|
||||
If the function is invoked with exec=False, the AnkiQt will not enter
|
||||
|
@ -441,12 +441,12 @@ def _run(argv=None, exec=True):
|
|||
|
||||
if opts.version:
|
||||
print(f"Anki {appVersion}")
|
||||
return
|
||||
return None
|
||||
elif opts.syncserver:
|
||||
from anki.syncserver import serve
|
||||
|
||||
serve()
|
||||
return
|
||||
return None
|
||||
|
||||
if PROFILE_CODE:
|
||||
|
||||
|
@ -465,7 +465,7 @@ def _run(argv=None, exec=True):
|
|||
except AnkiRestart as error:
|
||||
if error.exitcode:
|
||||
sys.exit(error.exitcode)
|
||||
return
|
||||
return None
|
||||
except:
|
||||
# will handle below
|
||||
traceback.print_exc()
|
||||
|
@ -500,7 +500,7 @@ def _run(argv=None, exec=True):
|
|||
app = AnkiApp(argv)
|
||||
if app.secondInstance():
|
||||
# we've signaled the primary instance, so we should close
|
||||
return
|
||||
return None
|
||||
|
||||
if not pm:
|
||||
QMessageBox.critical(
|
||||
|
@ -508,7 +508,7 @@ def _run(argv=None, exec=True):
|
|||
tr(TR.QT_MISC_ERROR),
|
||||
tr(TR.PROFILES_COULD_NOT_CREATE_DATA_FOLDER),
|
||||
)
|
||||
return
|
||||
return None
|
||||
|
||||
# disable icons on mac; this must be done before window created
|
||||
if isMac:
|
||||
|
@ -548,7 +548,7 @@ def _run(argv=None, exec=True):
|
|||
tr(TR.QT_MISC_ERROR),
|
||||
tr(TR.QT_MISC_NO_TEMP_FOLDER),
|
||||
)
|
||||
return
|
||||
return None
|
||||
|
||||
if pmLoadResult.firstTime:
|
||||
pm.setDefaultLang(lang[0])
|
||||
|
@ -590,3 +590,5 @@ def _run(argv=None, exec=True):
|
|||
|
||||
if PROFILE_CODE:
|
||||
write_profile_results()
|
||||
|
||||
return None
|
||||
|
|
|
@ -13,20 +13,20 @@ from aqt.utils import TR, disable_help_button, supportText, tooltip, tr
|
|||
|
||||
|
||||
class ClosableQDialog(QDialog):
|
||||
def reject(self):
|
||||
def reject(self) -> None:
|
||||
aqt.dialogs.markClosed("About")
|
||||
QDialog.reject(self)
|
||||
|
||||
def accept(self):
|
||||
def accept(self) -> None:
|
||||
aqt.dialogs.markClosed("About")
|
||||
QDialog.accept(self)
|
||||
|
||||
def closeWithCallback(self, callback):
|
||||
def closeWithCallback(self, callback) -> None:
|
||||
self.reject()
|
||||
callback()
|
||||
|
||||
|
||||
def show(mw):
|
||||
def show(mw) -> QDialog:
|
||||
dialog = ClosableQDialog(mw)
|
||||
disable_help_button(dialog)
|
||||
mw.setupDialogGC(dialog)
|
||||
|
@ -55,7 +55,7 @@ def show(mw):
|
|||
modified = "mod"
|
||||
return f"{name} ['{addon.dir_name}', {installed}, '{addon.human_version}', {modified}]"
|
||||
|
||||
def onCopy():
|
||||
def onCopy() -> None:
|
||||
addmgr = mw.addonManager
|
||||
active = []
|
||||
activeids = []
|
||||
|
|
|
@ -65,7 +65,7 @@ class AddCards(QDialog):
|
|||
)
|
||||
self.deckChooser = aqt.deckchooser.DeckChooser(self.mw, self.form.deckArea)
|
||||
|
||||
def helpRequested(self):
|
||||
def helpRequested(self) -> None:
|
||||
openHelp(HelpPage.ADDING_CARD_AND_NOTE)
|
||||
|
||||
def setupButtons(self) -> None:
|
||||
|
@ -137,7 +137,7 @@ class AddCards(QDialog):
|
|||
def removeTempNote(self, note: Note) -> None:
|
||||
print("removeTempNote() will go away")
|
||||
|
||||
def addHistory(self, note):
|
||||
def addHistory(self, note: Note) -> None:
|
||||
self.history.insert(0, note.id)
|
||||
self.history = self.history[:15]
|
||||
self.historyButton.setEnabled(True)
|
||||
|
@ -161,7 +161,7 @@ class AddCards(QDialog):
|
|||
gui_hooks.add_cards_will_show_history_menu(self, m)
|
||||
m.exec_(self.historyButton.mapToGlobal(QPoint(0, 0)))
|
||||
|
||||
def editHistory(self, nid):
|
||||
def editHistory(self, nid) -> None:
|
||||
aqt.dialogs.open("Browser", self.mw, search=(SearchTerm(nid=nid),))
|
||||
|
||||
def addNote(self, note) -> Optional[Note]:
|
||||
|
@ -186,10 +186,10 @@ class AddCards(QDialog):
|
|||
gui_hooks.add_cards_did_add_note(note)
|
||||
return note
|
||||
|
||||
def addCards(self):
|
||||
def addCards(self) -> None:
|
||||
self.editor.saveNow(self._addCards)
|
||||
|
||||
def _addCards(self):
|
||||
def _addCards(self) -> None:
|
||||
self.editor.saveAddModeVars()
|
||||
if not self.addNote(self.editor.note):
|
||||
return
|
||||
|
@ -202,7 +202,7 @@ class AddCards(QDialog):
|
|||
self.onReset(keep=True)
|
||||
self.mw.col.autosave()
|
||||
|
||||
def keyPressEvent(self, evt):
|
||||
def keyPressEvent(self, evt: QKeyEvent) -> None:
|
||||
"Show answer on RET or register answer."
|
||||
if evt.key() in (Qt.Key_Enter, Qt.Key_Return) and self.editor.tags.hasFocus():
|
||||
evt.accept()
|
||||
|
@ -225,7 +225,7 @@ class AddCards(QDialog):
|
|||
QDialog.reject(self)
|
||||
|
||||
def ifCanClose(self, onOk: Callable) -> None:
|
||||
def afterSave():
|
||||
def afterSave() -> None:
|
||||
ok = self.editor.fieldsAreBlank(self.previousNote) or askUser(
|
||||
tr(TR.ADDING_CLOSE_AND_LOSE_CURRENT_INPUT), defaultno=True
|
||||
)
|
||||
|
@ -234,8 +234,8 @@ class AddCards(QDialog):
|
|||
|
||||
self.editor.saveNow(afterSave)
|
||||
|
||||
def closeWithCallback(self, cb):
|
||||
def doClose():
|
||||
def closeWithCallback(self, cb) -> None:
|
||||
def doClose() -> None:
|
||||
self._reject()
|
||||
cb()
|
||||
|
||||
|
|
|
@ -605,7 +605,7 @@ class AddonManager:
|
|||
def _addon_schema_path(self, dir: str) -> str:
|
||||
return os.path.join(self.addonsFolder(dir), "config.schema.json")
|
||||
|
||||
def _addon_schema(self, dir: str):
|
||||
def _addon_schema(self, dir: str) -> Any:
|
||||
path = self._addon_schema_path(dir)
|
||||
try:
|
||||
if not os.path.exists(path):
|
||||
|
@ -867,9 +867,10 @@ class AddonsDialog(QDialog):
|
|||
def onInstallFiles(self, paths: Optional[List[str]] = None) -> Optional[bool]:
|
||||
if not paths:
|
||||
key = tr(TR.ADDONS_PACKAGED_ANKI_ADDON) + " (*{})".format(self.mgr.ext)
|
||||
paths = getFile(
|
||||
paths_ = getFile(
|
||||
self, tr(TR.ADDONS_INSTALL_ADDONS), None, key, key="addons", multi=True
|
||||
)
|
||||
paths = paths_ # type: ignore
|
||||
if not paths:
|
||||
return False
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -4,7 +4,7 @@
|
|||
import copy
|
||||
import json
|
||||
import re
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List, Match, Optional
|
||||
|
||||
import aqt
|
||||
from anki.cards import Card
|
||||
|
@ -14,6 +14,7 @@ from anki.lang import without_unicode_isolation
|
|||
from anki.notes import Note
|
||||
from anki.template import TemplateRenderContext
|
||||
from aqt import AnkiQt, gui_hooks
|
||||
from aqt.forms.browserdisp import Ui_Dialog
|
||||
from aqt.qt import *
|
||||
from aqt.schema_change_tracker import ChangeTracker
|
||||
from aqt.sound import av_player, play_clicked_audio
|
||||
|
@ -90,7 +91,7 @@ class CardLayout(QDialog):
|
|||
# as users tend to accidentally type into the template
|
||||
self.setFocus()
|
||||
|
||||
def redraw_everything(self):
|
||||
def redraw_everything(self) -> None:
|
||||
self.ignore_change_signals = True
|
||||
self.updateTopArea()
|
||||
self.ignore_change_signals = False
|
||||
|
@ -104,13 +105,13 @@ class CardLayout(QDialog):
|
|||
self.fill_fields_from_template()
|
||||
self.renderPreview()
|
||||
|
||||
def _isCloze(self):
|
||||
def _isCloze(self) -> bool:
|
||||
return self.model["type"] == MODEL_CLOZE
|
||||
|
||||
# Top area
|
||||
##########################################################################
|
||||
|
||||
def setupTopArea(self):
|
||||
def setupTopArea(self) -> None:
|
||||
self.topArea = QWidget()
|
||||
self.topArea.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
|
||||
self.topAreaForm = aqt.forms.clayout_top.Ui_Form()
|
||||
|
@ -125,10 +126,10 @@ class CardLayout(QDialog):
|
|||
)
|
||||
self.topAreaForm.card_type_label.setText(tr(TR.CARD_TEMPLATES_CARD_TYPE))
|
||||
|
||||
def updateTopArea(self):
|
||||
def updateTopArea(self) -> None:
|
||||
self.updateCardNames()
|
||||
|
||||
def updateCardNames(self):
|
||||
def updateCardNames(self) -> None:
|
||||
self.ignore_change_signals = True
|
||||
combo = self.topAreaForm.templatesBox
|
||||
combo.clear()
|
||||
|
@ -139,7 +140,7 @@ class CardLayout(QDialog):
|
|||
combo.setEnabled(not self._isCloze())
|
||||
self.ignore_change_signals = False
|
||||
|
||||
def _summarizedName(self, idx: int, tmpl: Dict):
|
||||
def _summarizedName(self, idx: int, tmpl: Dict) -> str:
|
||||
return "{}: {}: {} -> {}".format(
|
||||
idx + 1,
|
||||
tmpl["name"],
|
||||
|
@ -170,7 +171,7 @@ class CardLayout(QDialog):
|
|||
s += "+..."
|
||||
return s
|
||||
|
||||
def setupShortcuts(self):
|
||||
def setupShortcuts(self) -> None:
|
||||
self.tform.front_button.setToolTip(shortcut("Ctrl+1"))
|
||||
self.tform.back_button.setToolTip(shortcut("Ctrl+2"))
|
||||
self.tform.style_button.setToolTip(shortcut("Ctrl+3"))
|
||||
|
@ -193,7 +194,7 @@ class CardLayout(QDialog):
|
|||
# Main area setup
|
||||
##########################################################################
|
||||
|
||||
def setupMainArea(self):
|
||||
def setupMainArea(self) -> None:
|
||||
split = self.mainArea = QSplitter()
|
||||
split.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
split.setOrientation(Qt.Horizontal)
|
||||
|
@ -216,7 +217,7 @@ class CardLayout(QDialog):
|
|||
split.addWidget(right)
|
||||
split.setCollapsible(1, False)
|
||||
|
||||
def setup_edit_area(self):
|
||||
def setup_edit_area(self) -> None:
|
||||
tform = self.tform
|
||||
|
||||
tform.front_button.setText(tr(TR.CARD_TEMPLATES_FRONT_TEMPLATE))
|
||||
|
@ -248,7 +249,7 @@ class CardLayout(QDialog):
|
|||
qconnect(widg.textChanged, self.on_search_changed)
|
||||
qconnect(widg.returnPressed, self.on_search_next)
|
||||
|
||||
def setup_cloze_number_box(self):
|
||||
def setup_cloze_number_box(self) -> None:
|
||||
names = (tr(TR.CARD_TEMPLATES_CLOZE, val=n) for n in self.cloze_numbers)
|
||||
self.pform.cloze_number_combo.addItems(names)
|
||||
try:
|
||||
|
@ -266,7 +267,7 @@ class CardLayout(QDialog):
|
|||
self.have_autoplayed = False
|
||||
self._renderPreview()
|
||||
|
||||
def on_editor_toggled(self):
|
||||
def on_editor_toggled(self) -> None:
|
||||
if self.tform.front_button.isChecked():
|
||||
self.current_editor_index = 0
|
||||
self.pform.preview_front.setChecked(True)
|
||||
|
@ -283,7 +284,7 @@ class CardLayout(QDialog):
|
|||
|
||||
self.fill_fields_from_template()
|
||||
|
||||
def on_search_changed(self, text: str):
|
||||
def on_search_changed(self, text: str) -> None:
|
||||
editor = self.tform.edit_area
|
||||
if not editor.find(text):
|
||||
# try again from top
|
||||
|
@ -293,11 +294,11 @@ class CardLayout(QDialog):
|
|||
if not editor.find(text):
|
||||
tooltip("No matches found.")
|
||||
|
||||
def on_search_next(self):
|
||||
def on_search_next(self) -> None:
|
||||
text = self.tform.search_edit.text()
|
||||
self.on_search_changed(text)
|
||||
|
||||
def setup_preview(self):
|
||||
def setup_preview(self) -> None:
|
||||
pform = self.pform
|
||||
self.preview_web = AnkiWebView(title="card layout")
|
||||
pform.verticalLayout.addWidget(self.preview_web)
|
||||
|
@ -336,19 +337,19 @@ class CardLayout(QDialog):
|
|||
self.cloze_numbers = []
|
||||
self.pform.cloze_number_combo.setHidden(True)
|
||||
|
||||
def on_fill_empty_action_toggled(self):
|
||||
def on_fill_empty_action_toggled(self) -> None:
|
||||
self.fill_empty_action_toggled = not self.fill_empty_action_toggled
|
||||
self.on_preview_toggled()
|
||||
|
||||
def on_night_mode_action_toggled(self):
|
||||
def on_night_mode_action_toggled(self) -> None:
|
||||
self.night_mode_is_enabled = not self.night_mode_is_enabled
|
||||
self.on_preview_toggled()
|
||||
|
||||
def on_mobile_class_action_toggled(self):
|
||||
def on_mobile_class_action_toggled(self) -> None:
|
||||
self.mobile_emulation_enabled = not self.mobile_emulation_enabled
|
||||
self.on_preview_toggled()
|
||||
|
||||
def on_preview_settings(self):
|
||||
def on_preview_settings(self) -> None:
|
||||
m = QMenu(self)
|
||||
|
||||
a = m.addAction(tr(TR.CARD_TEMPLATES_FILL_EMPTY))
|
||||
|
@ -370,7 +371,7 @@ class CardLayout(QDialog):
|
|||
|
||||
m.exec_(self.pform.preview_settings.mapToGlobal(QPoint(0, 0)))
|
||||
|
||||
def on_preview_toggled(self):
|
||||
def on_preview_toggled(self) -> None:
|
||||
self.have_autoplayed = False
|
||||
self._renderPreview()
|
||||
|
||||
|
@ -388,7 +389,7 @@ class CardLayout(QDialog):
|
|||
# Buttons
|
||||
##########################################################################
|
||||
|
||||
def setupButtons(self):
|
||||
def setupButtons(self) -> None:
|
||||
l = self.buttons = QHBoxLayout()
|
||||
help = QPushButton(tr(TR.ACTIONS_HELP))
|
||||
help.setAutoDefault(False)
|
||||
|
@ -424,7 +425,7 @@ class CardLayout(QDialog):
|
|||
return self.templates[0]
|
||||
return self.templates[self.ord]
|
||||
|
||||
def fill_fields_from_template(self):
|
||||
def fill_fields_from_template(self) -> None:
|
||||
t = self.current_template()
|
||||
self.ignore_change_signals = True
|
||||
|
||||
|
@ -438,7 +439,7 @@ class CardLayout(QDialog):
|
|||
self.tform.edit_area.setPlainText(text)
|
||||
self.ignore_change_signals = False
|
||||
|
||||
def write_edits_to_template_and_redraw(self):
|
||||
def write_edits_to_template_and_redraw(self) -> None:
|
||||
if self.ignore_change_signals:
|
||||
return
|
||||
|
||||
|
@ -458,14 +459,14 @@ class CardLayout(QDialog):
|
|||
# Preview
|
||||
##########################################################################
|
||||
|
||||
_previewTimer = None
|
||||
_previewTimer: Optional[QTimer] = None
|
||||
|
||||
def renderPreview(self):
|
||||
def renderPreview(self) -> None:
|
||||
# schedule a preview when timing stops
|
||||
self.cancelPreviewTimer()
|
||||
self._previewTimer = self.mw.progress.timer(200, self._renderPreview, False)
|
||||
|
||||
def cancelPreviewTimer(self):
|
||||
def cancelPreviewTimer(self) -> None:
|
||||
if self._previewTimer:
|
||||
self._previewTimer.stop()
|
||||
self._previewTimer = None
|
||||
|
@ -512,14 +513,14 @@ class CardLayout(QDialog):
|
|||
|
||||
self.updateCardNames()
|
||||
|
||||
def maybeTextInput(self, txt, type="q"):
|
||||
def maybeTextInput(self, txt: str, type: str = "q") -> str:
|
||||
if "[[type:" not in txt:
|
||||
return txt
|
||||
origLen = len(txt)
|
||||
txt = txt.replace("<hr id=answer>", "")
|
||||
hadHR = origLen != len(txt)
|
||||
|
||||
def answerRepl(match):
|
||||
def answerRepl(match: Match) -> str:
|
||||
res = self.mw.reviewer.correct("exomple", "an example")
|
||||
if hadHR:
|
||||
res = "<hr id=answer>" + res
|
||||
|
@ -555,14 +556,15 @@ class CardLayout(QDialog):
|
|||
# Card operations
|
||||
######################################################################
|
||||
|
||||
def onRemove(self):
|
||||
def onRemove(self) -> None:
|
||||
if len(self.templates) < 2:
|
||||
return showInfo(tr(TR.CARD_TEMPLATES_AT_LEAST_ONE_CARD_TYPE_IS))
|
||||
showInfo(tr(TR.CARD_TEMPLATES_AT_LEAST_ONE_CARD_TYPE_IS))
|
||||
return
|
||||
|
||||
def get_count():
|
||||
def get_count() -> int:
|
||||
return self.mm.template_use_count(self.model["id"], self.ord)
|
||||
|
||||
def on_done(fut):
|
||||
def on_done(fut) -> None:
|
||||
card_cnt = fut.result()
|
||||
|
||||
template = self.current_template()
|
||||
|
@ -592,7 +594,7 @@ class CardLayout(QDialog):
|
|||
|
||||
self.redraw_everything()
|
||||
|
||||
def onRename(self):
|
||||
def onRename(self) -> None:
|
||||
template = self.current_template()
|
||||
name = getOnlyText(tr(TR.ACTIONS_NEW_NAME), default=template["name"]).replace(
|
||||
'"', ""
|
||||
|
@ -603,18 +605,18 @@ class CardLayout(QDialog):
|
|||
template["name"] = name
|
||||
self.redraw_everything()
|
||||
|
||||
def onReorder(self):
|
||||
def onReorder(self) -> None:
|
||||
n = len(self.templates)
|
||||
template = self.current_template()
|
||||
current_pos = self.templates.index(template) + 1
|
||||
pos = getOnlyText(
|
||||
pos_txt = getOnlyText(
|
||||
tr(TR.CARD_TEMPLATES_ENTER_NEW_CARD_POSITION_1, val=n),
|
||||
default=str(current_pos),
|
||||
)
|
||||
if not pos:
|
||||
if not pos_txt:
|
||||
return
|
||||
try:
|
||||
pos = int(pos)
|
||||
pos = int(pos_txt)
|
||||
except ValueError:
|
||||
return
|
||||
if pos < 1 or pos > n:
|
||||
|
@ -628,7 +630,7 @@ class CardLayout(QDialog):
|
|||
self.ord = new_idx
|
||||
self.redraw_everything()
|
||||
|
||||
def _newCardName(self):
|
||||
def _newCardName(self) -> str:
|
||||
n = len(self.templates) + 1
|
||||
while 1:
|
||||
name = without_unicode_isolation(tr(TR.CARD_TEMPLATES_CARD, val=n))
|
||||
|
@ -637,7 +639,7 @@ class CardLayout(QDialog):
|
|||
n += 1
|
||||
return name
|
||||
|
||||
def onAddCard(self):
|
||||
def onAddCard(self) -> None:
|
||||
cnt = self.mw.col.models.useCount(self.model)
|
||||
txt = tr(TR.CARD_TEMPLATES_THIS_WILL_CREATE_CARD_PROCEED, count=cnt)
|
||||
if not askUser(txt):
|
||||
|
@ -653,12 +655,12 @@ class CardLayout(QDialog):
|
|||
self.ord = len(self.templates) - 1
|
||||
self.redraw_everything()
|
||||
|
||||
def onFlip(self):
|
||||
def onFlip(self) -> None:
|
||||
old = self.current_template()
|
||||
self._flipQA(old, old)
|
||||
self.redraw_everything()
|
||||
|
||||
def _flipQA(self, src, dst):
|
||||
def _flipQA(self, src, dst) -> None:
|
||||
m = re.match("(?s)(.+)<hr id=answer>(.+)", src["afmt"])
|
||||
if not m:
|
||||
showInfo(tr(TR.CARD_TEMPLATES_ANKI_COULDNT_FIND_THE_LINE_BETWEEN))
|
||||
|
@ -666,9 +668,8 @@ class CardLayout(QDialog):
|
|||
self.change_tracker.mark_basic()
|
||||
dst["afmt"] = "{{FrontSide}}\n\n<hr id=answer>\n\n%s" % src["qfmt"]
|
||||
dst["qfmt"] = m.group(2).strip()
|
||||
return True
|
||||
|
||||
def onMore(self):
|
||||
def onMore(self) -> None:
|
||||
m = QMenu(self)
|
||||
|
||||
if not self._isCloze():
|
||||
|
@ -699,7 +700,7 @@ class CardLayout(QDialog):
|
|||
|
||||
m.exec_(self.topAreaForm.templateOptions.mapToGlobal(QPoint(0, 0)))
|
||||
|
||||
def onBrowserDisplay(self):
|
||||
def onBrowserDisplay(self) -> None:
|
||||
d = QDialog()
|
||||
disable_help_button(d)
|
||||
f = aqt.forms.browserdisp.Ui_Dialog()
|
||||
|
@ -714,7 +715,7 @@ class CardLayout(QDialog):
|
|||
qconnect(f.buttonBox.accepted, lambda: self.onBrowserDisplayOk(f))
|
||||
d.exec_()
|
||||
|
||||
def onBrowserDisplayOk(self, f):
|
||||
def onBrowserDisplayOk(self, f: Ui_Dialog) -> None:
|
||||
t = self.current_template()
|
||||
self.change_tracker.mark_basic()
|
||||
t["bqfmt"] = f.qfmt.text().strip()
|
||||
|
@ -727,7 +728,7 @@ class CardLayout(QDialog):
|
|||
if key in t:
|
||||
del t[key]
|
||||
|
||||
def onTargetDeck(self):
|
||||
def onTargetDeck(self) -> None:
|
||||
from aqt.tagedit import TagEdit
|
||||
|
||||
t = self.current_template()
|
||||
|
@ -759,7 +760,7 @@ class CardLayout(QDialog):
|
|||
else:
|
||||
t["did"] = self.col.decks.id(te.text())
|
||||
|
||||
def onAddField(self):
|
||||
def onAddField(self) -> None:
|
||||
diag = QDialog(self)
|
||||
form = aqt.forms.addfield.Ui_Dialog()
|
||||
form.setupUi(diag)
|
||||
|
@ -779,7 +780,7 @@ class CardLayout(QDialog):
|
|||
form.size.value(),
|
||||
)
|
||||
|
||||
def _addField(self, field, font, size):
|
||||
def _addField(self, field, font, size) -> None:
|
||||
text = self.tform.edit_area.toPlainText()
|
||||
text += "\n<div style='font-family: %s; font-size: %spx;'>{{%s}}</div>\n" % (
|
||||
font,
|
||||
|
@ -794,10 +795,10 @@ class CardLayout(QDialog):
|
|||
######################################################################
|
||||
|
||||
def accept(self) -> None:
|
||||
def save():
|
||||
def save() -> None:
|
||||
self.mm.save(self.model)
|
||||
|
||||
def on_done(fut):
|
||||
def on_done(fut) -> None:
|
||||
try:
|
||||
fut.result()
|
||||
except TemplateError as e:
|
||||
|
@ -828,5 +829,5 @@ class CardLayout(QDialog):
|
|||
self.rendered_card = None
|
||||
self.mw = None
|
||||
|
||||
def onHelp(self):
|
||||
def onHelp(self) -> None:
|
||||
openHelp(HelpPage.TEMPLATES)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# -*- coding: utf-8 -*-
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import aqt
|
||||
from anki.collection import SearchTerm
|
||||
from anki.consts import *
|
||||
|
@ -35,7 +36,7 @@ class CustomStudy(QDialog):
|
|||
f.radioNew.click()
|
||||
self.exec_()
|
||||
|
||||
def setupSignals(self):
|
||||
def setupSignals(self) -> None:
|
||||
f = self.form
|
||||
qconnect(f.radioNew.clicked, lambda: self.onRadioChange(RADIO_NEW))
|
||||
qconnect(f.radioRev.clicked, lambda: self.onRadioChange(RADIO_REV))
|
||||
|
@ -44,7 +45,7 @@ class CustomStudy(QDialog):
|
|||
qconnect(f.radioPreview.clicked, lambda: self.onRadioChange(RADIO_PREVIEW))
|
||||
qconnect(f.radioCram.clicked, lambda: self.onRadioChange(RADIO_CRAM))
|
||||
|
||||
def onRadioChange(self, idx):
|
||||
def onRadioChange(self, idx: int) -> None:
|
||||
f = self.form
|
||||
sp = f.spin
|
||||
smin = 1
|
||||
|
@ -56,7 +57,7 @@ class CustomStudy(QDialog):
|
|||
typeShow = False
|
||||
ok = tr(TR.CUSTOM_STUDY_OK)
|
||||
|
||||
def plus(num):
|
||||
def plus(num) -> str:
|
||||
if num == 1000:
|
||||
num = "1000+"
|
||||
return "<b>" + str(num) + "</b>"
|
||||
|
@ -123,7 +124,7 @@ class CustomStudy(QDialog):
|
|||
f.buttonBox.button(QDialogButtonBox.Ok).setText(ok)
|
||||
self.radioIdx = idx
|
||||
|
||||
def accept(self):
|
||||
def accept(self) -> None:
|
||||
f = self.form
|
||||
i = self.radioIdx
|
||||
spin = f.spin.value()
|
||||
|
@ -132,13 +133,15 @@ class CustomStudy(QDialog):
|
|||
self.mw.col.decks.save(self.deck)
|
||||
self.mw.col.sched.extendLimits(spin, 0)
|
||||
self.mw.reset()
|
||||
return QDialog.accept(self)
|
||||
QDialog.accept(self)
|
||||
return
|
||||
elif i == RADIO_REV:
|
||||
self.deck["extendRev"] = spin
|
||||
self.mw.col.decks.save(self.deck)
|
||||
self.mw.col.sched.extendLimits(0, spin)
|
||||
self.mw.reset()
|
||||
return QDialog.accept(self)
|
||||
QDialog.accept(self)
|
||||
return
|
||||
elif i == RADIO_CRAM:
|
||||
tags = self._getTags()
|
||||
# the rest create a filtered deck
|
||||
|
@ -146,7 +149,8 @@ class CustomStudy(QDialog):
|
|||
if cur:
|
||||
if not cur["dyn"]:
|
||||
showInfo(tr(TR.CUSTOM_STUDY_MUST_RENAME_DECK))
|
||||
return QDialog.accept(self)
|
||||
QDialog.accept(self)
|
||||
return
|
||||
else:
|
||||
# safe to empty
|
||||
self.mw.col.sched.empty_filtered_deck(cur["id"])
|
||||
|
@ -211,7 +215,8 @@ class CustomStudy(QDialog):
|
|||
# generate cards
|
||||
self.created_custom_study = True
|
||||
if not self.mw.col.sched.rebuild_filtered_deck(dyn["id"]):
|
||||
return showWarning(tr(TR.CUSTOM_STUDY_NO_CARDS_MATCHED_THE_CRITERIA_YOU))
|
||||
showWarning(tr(TR.CUSTOM_STUDY_NO_CARDS_MATCHED_THE_CRITERIA_YOU))
|
||||
return
|
||||
self.mw.moveToState("overview")
|
||||
QDialog.accept(self)
|
||||
|
||||
|
@ -222,7 +227,7 @@ class CustomStudy(QDialog):
|
|||
# fixme: clean up the empty custom study deck
|
||||
QDialog.reject(self)
|
||||
|
||||
def _getTags(self):
|
||||
def _getTags(self) -> str:
|
||||
from aqt.taglimit import TagLimit
|
||||
|
||||
return TagLimit(self.mw, self).tags
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
|
||||
load("//qt/aqt/data/web/pages:defs.bzl", "copy_page")
|
||||
load("compile_sass.bzl", "compile_sass")
|
||||
|
||||
compile_sass(
|
||||
|
@ -16,11 +17,21 @@ copy_file(
|
|||
out = "core.css",
|
||||
)
|
||||
|
||||
copy_page(
|
||||
name = "editor",
|
||||
srcs = [
|
||||
"editor.css",
|
||||
"editable.css",
|
||||
],
|
||||
package = "//ts/editor",
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "css",
|
||||
srcs = [
|
||||
"core.css",
|
||||
"css_local",
|
||||
"editor",
|
||||
],
|
||||
visibility = ["//qt:__subpackages__"],
|
||||
)
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
load("@npm//@bazel/typescript:index.bzl", "ts_library")
|
||||
load("@npm//@bazel/rollup:index.bzl", "rollup_bundle")
|
||||
load("//qt/aqt/data/web/pages:defs.bzl", "copy_page")
|
||||
load("//ts:prettier.bzl", "prettier_test")
|
||||
load("//ts:eslint.bzl", "eslint_test")
|
||||
|
||||
ts_library(
|
||||
name = "pycmd",
|
||||
srcs = ["pycmd.d.ts"],
|
||||
visibility = ["//qt/aqt/data/web/js:__subpackages__"],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
|
@ -26,10 +30,19 @@ filegroup(
|
|||
output_group = "es5_sources",
|
||||
)
|
||||
|
||||
copy_page(
|
||||
name = "editor",
|
||||
srcs = [
|
||||
"editor.js",
|
||||
],
|
||||
package = "//ts/editor",
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "js",
|
||||
srcs = [
|
||||
"aqt_es5",
|
||||
"editor",
|
||||
"mathjax.js",
|
||||
"//qt/aqt/data/web/js/vendor",
|
||||
],
|
||||
|
@ -47,4 +60,7 @@ prettier_test(
|
|||
# srcs = glob(["*.ts"]),
|
||||
# )
|
||||
|
||||
exports_files(["mathjax.js"])
|
||||
exports_files([
|
||||
"mathjax.js",
|
||||
"tsconfig.json",
|
||||
])
|
||||
|
|
|
@ -9,7 +9,7 @@ from aqt.qt import *
|
|||
from aqt.utils import showText, tooltip
|
||||
|
||||
|
||||
def on_progress(mw: aqt.main.AnkiQt):
|
||||
def on_progress(mw: aqt.main.AnkiQt) -> None:
|
||||
progress = mw.col.latest_progress()
|
||||
if progress.kind != ProgressKind.DatabaseCheck:
|
||||
return
|
||||
|
@ -24,14 +24,14 @@ def on_progress(mw: aqt.main.AnkiQt):
|
|||
|
||||
|
||||
def check_db(mw: aqt.AnkiQt) -> None:
|
||||
def on_timer():
|
||||
def on_timer() -> None:
|
||||
on_progress(mw)
|
||||
|
||||
timer = QTimer(mw)
|
||||
qconnect(timer.timeout, on_timer)
|
||||
timer.start(100)
|
||||
|
||||
def on_future_done(fut):
|
||||
def on_future_done(fut) -> None:
|
||||
timer.stop()
|
||||
ret, ok = fut.result()
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ from aqt.utils import TR, askUser, getOnlyText, openLink, shortcut, showWarning,
|
|||
|
||||
|
||||
class DeckBrowserBottomBar:
|
||||
def __init__(self, deck_browser: DeckBrowser):
|
||||
def __init__(self, deck_browser: DeckBrowser) -> None:
|
||||
self.deck_browser = deck_browser
|
||||
|
||||
|
||||
|
@ -51,14 +51,14 @@ class DeckBrowser:
|
|||
self.bottom = BottomBar(mw, mw.bottomWeb)
|
||||
self.scrollPos = QPoint(0, 0)
|
||||
|
||||
def show(self):
|
||||
def show(self) -> None:
|
||||
av_player.stop_and_clear_queue()
|
||||
self.web.set_bridge_command(self._linkHandler, self)
|
||||
self._renderPage()
|
||||
# redraw top bar for theme change
|
||||
self.mw.toolbar.redraw()
|
||||
|
||||
def refresh(self):
|
||||
def refresh(self) -> None:
|
||||
self._renderPage()
|
||||
|
||||
# Event handlers
|
||||
|
@ -90,7 +90,7 @@ class DeckBrowser:
|
|||
self._collapse(int(arg))
|
||||
return False
|
||||
|
||||
def _selDeck(self, did):
|
||||
def _selDeck(self, did) -> None:
|
||||
self.mw.col.decks.select(did)
|
||||
self.mw.onOverview()
|
||||
|
||||
|
@ -108,14 +108,14 @@ class DeckBrowser:
|
|||
</center>
|
||||
"""
|
||||
|
||||
def _renderPage(self, reuse=False):
|
||||
def _renderPage(self, reuse=False) -> None:
|
||||
if not reuse:
|
||||
self._dueTree = self.mw.col.sched.deck_due_tree()
|
||||
self.__renderPage(None)
|
||||
return
|
||||
self.web.evalWithCallback("window.pageYOffset", self.__renderPage)
|
||||
|
||||
def __renderPage(self, offset):
|
||||
def __renderPage(self, offset) -> None:
|
||||
content = DeckBrowserContent(
|
||||
tree=self._renderDeckTree(self._dueTree),
|
||||
stats=self._renderStats(),
|
||||
|
@ -137,10 +137,10 @@ class DeckBrowser:
|
|||
self._scrollToOffset(offset)
|
||||
gui_hooks.deck_browser_did_render(self)
|
||||
|
||||
def _scrollToOffset(self, offset):
|
||||
def _scrollToOffset(self, offset) -> None:
|
||||
self.web.eval("$(function() { window.scrollTo(0, %d, 'instant'); });" % offset)
|
||||
|
||||
def _renderStats(self):
|
||||
def _renderStats(self) -> str:
|
||||
return '<div id="studiedToday"><span>{}</span></div>'.format(
|
||||
self.mw.col.studied_today(),
|
||||
)
|
||||
|
@ -170,7 +170,7 @@ class DeckBrowser:
|
|||
|
||||
due = node.review_count + node.learn_count
|
||||
|
||||
def indent():
|
||||
def indent() -> str:
|
||||
return " " * 6 * (node.level - 1)
|
||||
|
||||
if node.deck_id == ctx.current_deck_id:
|
||||
|
@ -202,7 +202,7 @@ class DeckBrowser:
|
|||
node.name,
|
||||
)
|
||||
# due counts
|
||||
def nonzeroColour(cnt, klass):
|
||||
def nonzeroColour(cnt: int, klass: str) -> str:
|
||||
if not cnt:
|
||||
klass = "zero-count"
|
||||
return f'<span class="{klass}">{cnt}</span>'
|
||||
|
@ -222,7 +222,7 @@ class DeckBrowser:
|
|||
buf += self._render_deck_node(child, ctx)
|
||||
return buf
|
||||
|
||||
def _topLevelDragRow(self):
|
||||
def _topLevelDragRow(self) -> str:
|
||||
return "<tr class='top-level-drag-row'><td colspan='6'> </td></tr>"
|
||||
|
||||
# Options
|
||||
|
@ -235,13 +235,13 @@ class DeckBrowser:
|
|||
a = m.addAction(tr(TR.ACTIONS_OPTIONS))
|
||||
qconnect(a.triggered, lambda b, did=did: self._options(did))
|
||||
a = m.addAction(tr(TR.ACTIONS_EXPORT))
|
||||
qconnect(a.triggered, lambda b, did=did: self._export(did))
|
||||
qconnect(a.triggered, lambda b, did=did: self._export(int(did)))
|
||||
a = m.addAction(tr(TR.ACTIONS_DELETE))
|
||||
qconnect(a.triggered, lambda b, did=did: self._delete(int(did)))
|
||||
gui_hooks.deck_browser_will_show_options_menu(m, int(did))
|
||||
m.exec_(QCursor.pos())
|
||||
|
||||
def _export(self, did):
|
||||
def _export(self, did: int) -> None:
|
||||
self.mw.onExport(did=did)
|
||||
|
||||
def _rename(self, did: int) -> None:
|
||||
|
@ -256,10 +256,11 @@ class DeckBrowser:
|
|||
self.mw.col.decks.rename(deck, newName)
|
||||
gui_hooks.sidebar_should_refresh_decks()
|
||||
except DeckRenameError as e:
|
||||
return showWarning(e.description)
|
||||
showWarning(e.description)
|
||||
return
|
||||
self.show()
|
||||
|
||||
def _options(self, did):
|
||||
def _options(self, did) -> None:
|
||||
# select the deck first, because the dyn deck conf assumes the deck
|
||||
# we're editing is the current one
|
||||
self.mw.col.decks.select(did)
|
||||
|
@ -296,10 +297,10 @@ class DeckBrowser:
|
|||
def _delete(self, did: int) -> None:
|
||||
if self.ask_delete_deck(did):
|
||||
|
||||
def do_delete():
|
||||
def do_delete() -> None:
|
||||
return self.mw.col.decks.rem(did, True)
|
||||
|
||||
def on_done(fut: Future):
|
||||
def on_done(fut: Future) -> None:
|
||||
self.show()
|
||||
res = fut.result() # Required to check for errors
|
||||
|
||||
|
@ -315,7 +316,7 @@ class DeckBrowser:
|
|||
["Ctrl+Shift+I", "import", tr(TR.DECKS_IMPORT_FILE)],
|
||||
]
|
||||
|
||||
def _drawButtons(self):
|
||||
def _drawButtons(self) -> None:
|
||||
buf = ""
|
||||
drawLinks = deepcopy(self.drawLinks)
|
||||
for b in drawLinks:
|
||||
|
@ -331,5 +332,5 @@ class DeckBrowser:
|
|||
web_context=DeckBrowserBottomBar(self),
|
||||
)
|
||||
|
||||
def _onShared(self):
|
||||
def _onShared(self) -> None:
|
||||
openLink(aqt.appShared + "decks/")
|
||||
|
|
|
@ -2,12 +2,13 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
from operator import itemgetter
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from PyQt5.QtWidgets import QLineEdit
|
||||
|
||||
import aqt
|
||||
from anki.consts import NEW_CARDS_RANDOM
|
||||
from anki.decks import DeckConfig
|
||||
from anki.lang import without_unicode_isolation
|
||||
from aqt import gui_hooks
|
||||
from aqt.qt import *
|
||||
|
@ -28,7 +29,7 @@ from aqt.utils import (
|
|||
|
||||
|
||||
class DeckConf(QDialog):
|
||||
def __init__(self, mw: aqt.AnkiQt, deck: Dict):
|
||||
def __init__(self, mw: aqt.AnkiQt, deck: Dict) -> None:
|
||||
QDialog.__init__(self, mw)
|
||||
self.mw = mw
|
||||
self.deck = deck
|
||||
|
@ -60,7 +61,7 @@ class DeckConf(QDialog):
|
|||
self.exec_()
|
||||
saveGeom(self, "deckconf")
|
||||
|
||||
def setupCombos(self):
|
||||
def setupCombos(self) -> None:
|
||||
import anki.consts as cs
|
||||
|
||||
f = self.form
|
||||
|
@ -70,12 +71,12 @@ class DeckConf(QDialog):
|
|||
# Conf list
|
||||
######################################################################
|
||||
|
||||
def setupConfs(self):
|
||||
def setupConfs(self) -> None:
|
||||
qconnect(self.form.dconf.currentIndexChanged, self.onConfChange)
|
||||
self.conf = None
|
||||
self.conf: Optional[DeckConfig] = None
|
||||
self.loadConfs()
|
||||
|
||||
def loadConfs(self):
|
||||
def loadConfs(self) -> None:
|
||||
current = self.deck["conf"]
|
||||
self.confList = self.mw.col.decks.allConf()
|
||||
self.confList.sort(key=itemgetter("name"))
|
||||
|
@ -92,7 +93,7 @@ class DeckConf(QDialog):
|
|||
self._origNewOrder = self.confList[startOn]["new"]["order"]
|
||||
self.onConfChange(startOn)
|
||||
|
||||
def confOpts(self):
|
||||
def confOpts(self) -> None:
|
||||
m = QMenu(self.mw)
|
||||
a = m.addAction(tr(TR.ACTIONS_ADD))
|
||||
qconnect(a.triggered, self.addGroup)
|
||||
|
@ -106,7 +107,7 @@ class DeckConf(QDialog):
|
|||
a.setEnabled(False)
|
||||
m.exec_(QCursor.pos())
|
||||
|
||||
def onConfChange(self, idx):
|
||||
def onConfChange(self, idx) -> None:
|
||||
if self.ignoreConfChange:
|
||||
return
|
||||
if self.conf:
|
||||
|
@ -159,7 +160,7 @@ class DeckConf(QDialog):
|
|||
self.saveConf()
|
||||
self.loadConfs()
|
||||
|
||||
def setChildren(self):
|
||||
def setChildren(self) -> None:
|
||||
if not askUser(tr(TR.SCHEDULING_SET_ALL_DECKS_BELOW_TO, val=self.deck["name"])):
|
||||
return
|
||||
for did in self.childDids:
|
||||
|
@ -173,8 +174,8 @@ class DeckConf(QDialog):
|
|||
# Loading
|
||||
##################################################
|
||||
|
||||
def listToUser(self, l):
|
||||
def num_to_user(n: Union[int, float]):
|
||||
def listToUser(self, l) -> str:
|
||||
def num_to_user(n: Union[int, float]) -> str:
|
||||
if n == round(n):
|
||||
return str(int(n))
|
||||
else:
|
||||
|
@ -182,7 +183,7 @@ class DeckConf(QDialog):
|
|||
|
||||
return " ".join(map(num_to_user, l))
|
||||
|
||||
def parentLimText(self, type="new"):
|
||||
def parentLimText(self, type="new") -> str:
|
||||
# top level?
|
||||
if "::" not in self.deck["name"]:
|
||||
return ""
|
||||
|
@ -196,7 +197,7 @@ class DeckConf(QDialog):
|
|||
lim = min(x, lim)
|
||||
return tr(TR.SCHEDULING_PARENT_LIMIT, val=lim)
|
||||
|
||||
def loadConf(self):
|
||||
def loadConf(self) -> None:
|
||||
self.conf = self.mw.col.decks.confForDid(self.deck["id"])
|
||||
# new
|
||||
c = self.conf["new"]
|
||||
|
@ -238,7 +239,7 @@ class DeckConf(QDialog):
|
|||
f.desc.setPlainText(self.deck["desc"])
|
||||
gui_hooks.deck_conf_did_load_config(self, self.deck, self.conf)
|
||||
|
||||
def onRestore(self):
|
||||
def onRestore(self) -> None:
|
||||
self.mw.progress.start()
|
||||
self.mw.col.decks.restoreToDefault(self.conf)
|
||||
self.mw.progress.finish()
|
||||
|
@ -247,7 +248,7 @@ class DeckConf(QDialog):
|
|||
# New order
|
||||
##################################################
|
||||
|
||||
def onNewOrderChanged(self, new):
|
||||
def onNewOrderChanged(self, new) -> None:
|
||||
old = self.conf["new"]["order"]
|
||||
if old == new:
|
||||
return
|
||||
|
@ -280,7 +281,7 @@ class DeckConf(QDialog):
|
|||
return
|
||||
conf[key] = ret
|
||||
|
||||
def saveConf(self):
|
||||
def saveConf(self) -> None:
|
||||
# new
|
||||
c = self.conf["new"]
|
||||
f = self.form
|
||||
|
@ -324,10 +325,10 @@ class DeckConf(QDialog):
|
|||
self.mw.col.decks.save(self.deck)
|
||||
self.mw.col.decks.save(self.conf)
|
||||
|
||||
def reject(self):
|
||||
def reject(self) -> None:
|
||||
self.accept()
|
||||
|
||||
def accept(self):
|
||||
def accept(self) -> None:
|
||||
self.saveConf()
|
||||
self.mw.reset()
|
||||
QDialog.accept(self)
|
||||
|
|
|
@ -34,7 +34,7 @@ class DeckConf(QDialog):
|
|||
search: Optional[str] = None,
|
||||
search_2: Optional[str] = None,
|
||||
deck: Optional[Deck] = None,
|
||||
):
|
||||
) -> None:
|
||||
"""If 'deck' is an existing filtered deck, load and modify its settings.
|
||||
Otherwise, build a new one and derive settings from the current deck.
|
||||
"""
|
||||
|
@ -141,7 +141,7 @@ class DeckConf(QDialog):
|
|||
self.form.search_2.setFocus()
|
||||
self.form.search_2.selectAll()
|
||||
|
||||
def initialSetup(self):
|
||||
def initialSetup(self) -> None:
|
||||
import anki.consts as cs
|
||||
|
||||
self.form.order.addItems(list(cs.dynOrderLabels(self.mw.col).values()))
|
||||
|
@ -165,12 +165,12 @@ class DeckConf(QDialog):
|
|||
else:
|
||||
aqt.dialogs.open("Browser", self.mw, search=(search,))
|
||||
|
||||
def _onReschedToggled(self, _state: int):
|
||||
def _onReschedToggled(self, _state: int) -> None:
|
||||
self.form.previewDelayWidget.setVisible(
|
||||
not self.form.resched.isChecked() and self.mw.col.schedVer() > 1
|
||||
)
|
||||
|
||||
def loadConf(self, deck: Optional[Deck] = None):
|
||||
def loadConf(self, deck: Optional[Deck] = None) -> None:
|
||||
f = self.form
|
||||
d = deck or self.deck
|
||||
|
||||
|
@ -205,7 +205,7 @@ class DeckConf(QDialog):
|
|||
f.secondFilter.setChecked(False)
|
||||
f.filter2group.setVisible(False)
|
||||
|
||||
def saveConf(self):
|
||||
def saveConf(self) -> None:
|
||||
f = self.form
|
||||
d = self.deck
|
||||
|
||||
|
@ -235,7 +235,7 @@ class DeckConf(QDialog):
|
|||
|
||||
self.mw.col.decks.save(d)
|
||||
|
||||
def reject(self):
|
||||
def reject(self) -> None:
|
||||
if self.did:
|
||||
self.mw.col.decks.rem(self.did)
|
||||
self.mw.col.decks.select(self.old_deck["id"])
|
||||
|
@ -243,7 +243,7 @@ class DeckConf(QDialog):
|
|||
QDialog.reject(self)
|
||||
aqt.dialogs.markClosed("DynDeckConfDialog")
|
||||
|
||||
def accept(self):
|
||||
def accept(self) -> None:
|
||||
try:
|
||||
self.saveConf()
|
||||
except InvalidInput as err:
|
||||
|
|
|
@ -48,14 +48,14 @@ class EditCurrent(QDialog):
|
|||
return
|
||||
self.editor.setNote(n)
|
||||
|
||||
def reopen(self, mw):
|
||||
def reopen(self, mw) -> None:
|
||||
tooltip("Please finish editing the existing card first.")
|
||||
self.onReset()
|
||||
|
||||
def reject(self):
|
||||
def reject(self) -> None:
|
||||
self.saveAndClose()
|
||||
|
||||
def saveAndClose(self):
|
||||
def saveAndClose(self) -> None:
|
||||
self.editor.saveNow(self._saveAndClose)
|
||||
|
||||
def _saveAndClose(self) -> None:
|
||||
|
@ -74,8 +74,8 @@ class EditCurrent(QDialog):
|
|||
aqt.dialogs.markClosed("EditCurrent")
|
||||
QDialog.reject(self)
|
||||
|
||||
def closeWithCallback(self, onsuccess):
|
||||
def callback():
|
||||
def closeWithCallback(self, onsuccess) -> None:
|
||||
def callback() -> None:
|
||||
self._saveAndClose()
|
||||
onsuccess()
|
||||
|
||||
|
|
168
qt/aqt/editor.py
168
qt/aqt/editor.py
|
@ -12,7 +12,7 @@ import urllib.parse
|
|||
import urllib.request
|
||||
import warnings
|
||||
from random import randrange
|
||||
from typing import Callable, List, Optional, Tuple
|
||||
from typing import Any, Callable, Dict, List, Match, Optional, Tuple, cast
|
||||
|
||||
import bs4
|
||||
import requests
|
||||
|
@ -92,7 +92,9 @@ _html = """
|
|||
|
||||
# caller is responsible for resetting note on reset
|
||||
class Editor:
|
||||
def __init__(self, mw: AnkiQt, widget, parentWindow, addMode=False) -> None:
|
||||
def __init__(
|
||||
self, mw: AnkiQt, widget: QWidget, parentWindow: QWidget, addMode: bool = False
|
||||
) -> None:
|
||||
self.mw = mw
|
||||
self.widget = widget
|
||||
self.parentWindow = parentWindow
|
||||
|
@ -110,7 +112,7 @@ class Editor:
|
|||
# Initial setup
|
||||
############################################################
|
||||
|
||||
def setupOuter(self):
|
||||
def setupOuter(self) -> None:
|
||||
l = QVBoxLayout()
|
||||
l.setContentsMargins(0, 0, 0, 0)
|
||||
l.setSpacing(0)
|
||||
|
@ -229,7 +231,7 @@ class Editor:
|
|||
# Top buttons
|
||||
######################################################################
|
||||
|
||||
def resourceToData(self, path):
|
||||
def resourceToData(self, path: str) -> str:
|
||||
"""Convert a file (specified by a path) into a data URI."""
|
||||
if not os.path.exists(path):
|
||||
raise FileNotFoundError
|
||||
|
@ -251,21 +253,21 @@ class Editor:
|
|||
keys: str = None,
|
||||
disables: bool = True,
|
||||
rightside: bool = True,
|
||||
):
|
||||
) -> str:
|
||||
"""Assign func to bridge cmd, register shortcut, return button"""
|
||||
if func:
|
||||
self._links[cmd] = func
|
||||
|
||||
if keys:
|
||||
|
||||
def on_activated():
|
||||
def on_activated() -> None:
|
||||
func(self)
|
||||
|
||||
if toggleable:
|
||||
# generate a random id for triggering toggle
|
||||
id = id or str(randrange(1_000_000))
|
||||
|
||||
def on_hotkey():
|
||||
def on_hotkey() -> None:
|
||||
on_activated()
|
||||
self.web.eval(f'toggleEditorButton("#{id}");')
|
||||
|
||||
|
@ -383,26 +385,26 @@ class Editor:
|
|||
keys, fn, _ = row
|
||||
QShortcut(QKeySequence(keys), self.widget, activated=fn) # type: ignore
|
||||
|
||||
def _addFocusCheck(self, fn):
|
||||
def checkFocus():
|
||||
def _addFocusCheck(self, fn: Callable) -> Callable:
|
||||
def checkFocus() -> None:
|
||||
if self.currentField is None:
|
||||
return
|
||||
fn()
|
||||
|
||||
return checkFocus
|
||||
|
||||
def onFields(self):
|
||||
def onFields(self) -> None:
|
||||
self.saveNow(self._onFields)
|
||||
|
||||
def _onFields(self):
|
||||
def _onFields(self) -> None:
|
||||
from aqt.fields import FieldDialog
|
||||
|
||||
FieldDialog(self.mw, self.note.model(), parent=self.parentWindow)
|
||||
|
||||
def onCardLayout(self):
|
||||
def onCardLayout(self) -> None:
|
||||
self.saveNow(self._onCardLayout)
|
||||
|
||||
def _onCardLayout(self):
|
||||
def _onCardLayout(self) -> None:
|
||||
from aqt.clayout import CardLayout
|
||||
|
||||
if self.card:
|
||||
|
@ -422,16 +424,16 @@ class Editor:
|
|||
# JS->Python bridge
|
||||
######################################################################
|
||||
|
||||
def onBridgeCmd(self, cmd) -> None:
|
||||
def onBridgeCmd(self, cmd: str) -> None:
|
||||
if not self.note:
|
||||
# shutdown
|
||||
return
|
||||
# focus lost or key/button pressed?
|
||||
if cmd.startswith("blur") or cmd.startswith("key"):
|
||||
(type, ord, nid, txt) = cmd.split(":", 3)
|
||||
ord = int(ord)
|
||||
(type, ord_str, nid_str, txt) = cmd.split(":", 3)
|
||||
ord = int(ord_str)
|
||||
try:
|
||||
nid = int(nid)
|
||||
nid = int(nid_str)
|
||||
except ValueError:
|
||||
nid = 0
|
||||
if nid != self.note.id:
|
||||
|
@ -465,13 +467,15 @@ class Editor:
|
|||
else:
|
||||
print("uncaught cmd", cmd)
|
||||
|
||||
def mungeHTML(self, txt):
|
||||
def mungeHTML(self, txt: str) -> str:
|
||||
return gui_hooks.editor_will_munge_html(txt, self)
|
||||
|
||||
# Setting/unsetting the current note
|
||||
######################################################################
|
||||
|
||||
def setNote(self, note, hide=True, focusTo=None):
|
||||
def setNote(
|
||||
self, note: Optional[Note], hide: bool = True, focusTo: Optional[int] = None
|
||||
) -> None:
|
||||
"Make NOTE the current note."
|
||||
self.note = note
|
||||
self.currentField = None
|
||||
|
@ -482,10 +486,10 @@ class Editor:
|
|||
if hide:
|
||||
self.widget.hide()
|
||||
|
||||
def loadNoteKeepingFocus(self):
|
||||
def loadNoteKeepingFocus(self) -> None:
|
||||
self.loadNote(self.currentField)
|
||||
|
||||
def loadNote(self, focusTo=None) -> None:
|
||||
def loadNote(self, focusTo: Optional[int] = None) -> None:
|
||||
if not self.note:
|
||||
return
|
||||
|
||||
|
@ -496,7 +500,7 @@ class Editor:
|
|||
self.widget.show()
|
||||
self.updateTags()
|
||||
|
||||
def oncallback(arg):
|
||||
def oncallback(arg: Any) -> None:
|
||||
if not self.note:
|
||||
return
|
||||
self.setupForegroundButton()
|
||||
|
@ -520,7 +524,7 @@ class Editor:
|
|||
for f in self.note.model()["flds"]
|
||||
]
|
||||
|
||||
def saveNow(self, callback, keepFocus=False):
|
||||
def saveNow(self, callback: Callable, keepFocus: bool = False) -> None:
|
||||
"Save unsaved edits then call callback()."
|
||||
if not self.note:
|
||||
# calling code may not expect the callback to fire immediately
|
||||
|
@ -529,7 +533,7 @@ class Editor:
|
|||
self.saveTags()
|
||||
self.web.evalWithCallback("saveNow(%d)" % keepFocus, lambda res: callback())
|
||||
|
||||
def checkValid(self):
|
||||
def checkValid(self) -> None:
|
||||
cols = [""] * len(self.note.fields)
|
||||
err = self.note.dupeOrEmpty()
|
||||
if err == 2:
|
||||
|
@ -537,7 +541,7 @@ class Editor:
|
|||
|
||||
self.web.eval("setBackgrounds(%s);" % json.dumps(cols))
|
||||
|
||||
def showDupes(self):
|
||||
def showDupes(self) -> None:
|
||||
aqt.dialogs.open(
|
||||
"Browser",
|
||||
self.mw,
|
||||
|
@ -551,7 +555,7 @@ class Editor:
|
|||
),
|
||||
)
|
||||
|
||||
def fieldsAreBlank(self, previousNote=None):
|
||||
def fieldsAreBlank(self, previousNote: Optional[Note] = None) -> bool:
|
||||
if not self.note:
|
||||
return True
|
||||
m = self.note.model()
|
||||
|
@ -564,7 +568,7 @@ class Editor:
|
|||
return False
|
||||
return True
|
||||
|
||||
def cleanup(self):
|
||||
def cleanup(self) -> None:
|
||||
self.setNote(None)
|
||||
# prevent any remaining evalWithCallback() events from firing after C++ object deleted
|
||||
self.web = None
|
||||
|
@ -572,11 +576,11 @@ class Editor:
|
|||
# HTML editing
|
||||
######################################################################
|
||||
|
||||
def onHtmlEdit(self):
|
||||
def onHtmlEdit(self) -> None:
|
||||
field = self.currentField
|
||||
self.saveNow(lambda: self._onHtmlEdit(field))
|
||||
|
||||
def _onHtmlEdit(self, field):
|
||||
def _onHtmlEdit(self, field: int) -> None:
|
||||
d = QDialog(self.widget, Qt.Window)
|
||||
form = aqt.forms.edithtml.Ui_Dialog()
|
||||
form.setupUi(d)
|
||||
|
@ -609,7 +613,7 @@ class Editor:
|
|||
# Tag handling
|
||||
######################################################################
|
||||
|
||||
def setupTags(self):
|
||||
def setupTags(self) -> None:
|
||||
import aqt.tagedit
|
||||
|
||||
g = QGroupBox(self.widget)
|
||||
|
@ -631,7 +635,7 @@ class Editor:
|
|||
g.setLayout(tb)
|
||||
self.outerLayout.addWidget(g)
|
||||
|
||||
def updateTags(self):
|
||||
def updateTags(self) -> None:
|
||||
if self.tags.col != self.mw.col:
|
||||
self.tags.setCol(self.mw.col)
|
||||
if not self.tags.text() or not self.addMode:
|
||||
|
@ -645,44 +649,44 @@ class Editor:
|
|||
self.note.flush()
|
||||
gui_hooks.editor_did_update_tags(self.note)
|
||||
|
||||
def saveAddModeVars(self):
|
||||
def saveAddModeVars(self) -> None:
|
||||
if self.addMode:
|
||||
# save tags to model
|
||||
m = self.note.model()
|
||||
m["tags"] = self.note.tags
|
||||
self.mw.col.models.save(m, updateReqs=False)
|
||||
|
||||
def hideCompleters(self):
|
||||
def hideCompleters(self) -> None:
|
||||
self.tags.hideCompleter()
|
||||
|
||||
def onFocusTags(self):
|
||||
def onFocusTags(self) -> None:
|
||||
self.tags.setFocus()
|
||||
|
||||
# Format buttons
|
||||
######################################################################
|
||||
|
||||
def toggleBold(self):
|
||||
def toggleBold(self) -> None:
|
||||
self.web.eval("setFormat('bold');")
|
||||
|
||||
def toggleItalic(self):
|
||||
def toggleItalic(self) -> None:
|
||||
self.web.eval("setFormat('italic');")
|
||||
|
||||
def toggleUnderline(self):
|
||||
def toggleUnderline(self) -> None:
|
||||
self.web.eval("setFormat('underline');")
|
||||
|
||||
def toggleSuper(self):
|
||||
def toggleSuper(self) -> None:
|
||||
self.web.eval("setFormat('superscript');")
|
||||
|
||||
def toggleSub(self):
|
||||
def toggleSub(self) -> None:
|
||||
self.web.eval("setFormat('subscript');")
|
||||
|
||||
def removeFormat(self):
|
||||
def removeFormat(self) -> None:
|
||||
self.web.eval("setFormat('removeFormat');")
|
||||
|
||||
def onCloze(self):
|
||||
def onCloze(self) -> None:
|
||||
self.saveNow(self._onCloze, keepFocus=True)
|
||||
|
||||
def _onCloze(self):
|
||||
def _onCloze(self) -> None:
|
||||
# check that the model is set up for cloze deletion
|
||||
if self.note.model()["type"] != MODEL_CLOZE:
|
||||
if self.addMode:
|
||||
|
@ -706,16 +710,16 @@ class Editor:
|
|||
# Foreground colour
|
||||
######################################################################
|
||||
|
||||
def setupForegroundButton(self):
|
||||
def setupForegroundButton(self) -> None:
|
||||
self.fcolour = self.mw.pm.profile.get("lastColour", "#00f")
|
||||
self.onColourChanged()
|
||||
|
||||
# use last colour
|
||||
def onForeground(self):
|
||||
def onForeground(self) -> None:
|
||||
self._wrapWithColour(self.fcolour)
|
||||
|
||||
# choose new colour
|
||||
def onChangeCol(self):
|
||||
def onChangeCol(self) -> None:
|
||||
if isLin:
|
||||
new = QColorDialog.getColor(
|
||||
QColor(self.fcolour), None, None, QColorDialog.DontUseNativeDialog
|
||||
|
@ -729,32 +733,38 @@ class Editor:
|
|||
self.onColourChanged()
|
||||
self._wrapWithColour(self.fcolour)
|
||||
|
||||
def _updateForegroundButton(self):
|
||||
def _updateForegroundButton(self) -> None:
|
||||
self.web.eval("setFGButton('%s')" % self.fcolour)
|
||||
|
||||
def onColourChanged(self):
|
||||
def onColourChanged(self) -> None:
|
||||
self._updateForegroundButton()
|
||||
self.mw.pm.profile["lastColour"] = self.fcolour
|
||||
|
||||
def _wrapWithColour(self, colour):
|
||||
def _wrapWithColour(self, colour: str) -> None:
|
||||
self.web.eval("setFormat('forecolor', '%s')" % colour)
|
||||
|
||||
# Audio/video/images
|
||||
######################################################################
|
||||
|
||||
def onAddMedia(self):
|
||||
def onAddMedia(self) -> None:
|
||||
extension_filter = " ".join(
|
||||
"*." + extension for extension in sorted(itertools.chain(pics, audio))
|
||||
)
|
||||
key = tr(TR.EDITING_MEDIA) + " (" + extension_filter + ")"
|
||||
|
||||
def accept(file):
|
||||
def accept(file: str) -> None:
|
||||
self.addMedia(file, canDelete=True)
|
||||
|
||||
file = getFile(self.widget, tr(TR.EDITING_ADD_MEDIA), accept, key, key="media")
|
||||
file = getFile(
|
||||
self.widget,
|
||||
tr(TR.EDITING_ADD_MEDIA),
|
||||
cast(Callable[[Any], None], accept),
|
||||
key,
|
||||
key="media",
|
||||
)
|
||||
self.parentWindow.activateWindow()
|
||||
|
||||
def addMedia(self, path, canDelete=False):
|
||||
def addMedia(self, path: str, canDelete: bool = False) -> None:
|
||||
try:
|
||||
html = self._addMedia(path, canDelete)
|
||||
except Exception as e:
|
||||
|
@ -762,7 +772,7 @@ class Editor:
|
|||
return
|
||||
self.web.eval("setFormat('inserthtml', %s);" % json.dumps(html))
|
||||
|
||||
def _addMedia(self, path, canDelete=False):
|
||||
def _addMedia(self, path: str, canDelete: bool = False) -> str:
|
||||
"Add to media folder and return local img or sound tag."
|
||||
# copy to media folder
|
||||
fname = self.mw.col.media.addFile(path)
|
||||
|
@ -779,7 +789,7 @@ class Editor:
|
|||
def _addMediaFromData(self, fname: str, data: bytes) -> str:
|
||||
return self.mw.col.media.writeData(fname, data)
|
||||
|
||||
def onRecSound(self):
|
||||
def onRecSound(self) -> None:
|
||||
aqt.sound.record_audio(
|
||||
self.parentWindow,
|
||||
self.mw,
|
||||
|
@ -813,7 +823,7 @@ class Editor:
|
|||
# not a supported type
|
||||
return None
|
||||
|
||||
def isURL(self, s):
|
||||
def isURL(self, s: str) -> bool:
|
||||
s = s.lower()
|
||||
return (
|
||||
s.startswith("http://")
|
||||
|
@ -962,23 +972,23 @@ class Editor:
|
|||
)
|
||||
|
||||
def doDrop(self, html: str, internal: bool, extended: bool = False) -> None:
|
||||
def pasteIfField(ret):
|
||||
def pasteIfField(ret: bool) -> None:
|
||||
if ret:
|
||||
self.doPaste(html, internal, extended)
|
||||
|
||||
p = self.web.mapFromGlobal(QCursor.pos())
|
||||
self.web.evalWithCallback(f"focusIfField({p.x()}, {p.y()});", pasteIfField)
|
||||
|
||||
def onPaste(self):
|
||||
def onPaste(self) -> None:
|
||||
self.web.onPaste()
|
||||
|
||||
def onCutOrCopy(self):
|
||||
def onCutOrCopy(self) -> None:
|
||||
self.web.flagAnkiText()
|
||||
|
||||
# Advanced menu
|
||||
######################################################################
|
||||
|
||||
def onAdvanced(self):
|
||||
def onAdvanced(self) -> None:
|
||||
m = QMenu(self.mw)
|
||||
|
||||
for text, handler, shortcut in (
|
||||
|
@ -1005,28 +1015,28 @@ class Editor:
|
|||
# LaTeX
|
||||
######################################################################
|
||||
|
||||
def insertLatex(self):
|
||||
def insertLatex(self) -> None:
|
||||
self.web.eval("wrap('[latex]', '[/latex]');")
|
||||
|
||||
def insertLatexEqn(self):
|
||||
def insertLatexEqn(self) -> None:
|
||||
self.web.eval("wrap('[$]', '[/$]');")
|
||||
|
||||
def insertLatexMathEnv(self):
|
||||
def insertLatexMathEnv(self) -> None:
|
||||
self.web.eval("wrap('[$$]', '[/$$]');")
|
||||
|
||||
def insertMathjaxInline(self):
|
||||
def insertMathjaxInline(self) -> None:
|
||||
self.web.eval("wrap('\\\\(', '\\\\)');")
|
||||
|
||||
def insertMathjaxBlock(self):
|
||||
def insertMathjaxBlock(self) -> None:
|
||||
self.web.eval("wrap('\\\\[', '\\\\]');")
|
||||
|
||||
def insertMathjaxChemistry(self):
|
||||
def insertMathjaxChemistry(self) -> None:
|
||||
self.web.eval("wrap('\\\\(\\\\ce{', '}\\\\)');")
|
||||
|
||||
# Links from HTML
|
||||
######################################################################
|
||||
|
||||
_links = dict(
|
||||
_links: Dict[str, Callable] = dict(
|
||||
fields=onFields,
|
||||
cards=onCardLayout,
|
||||
bold=toggleBold,
|
||||
|
@ -1052,7 +1062,7 @@ class Editor:
|
|||
|
||||
|
||||
class EditorWebView(AnkiWebView):
|
||||
def __init__(self, parent, editor):
|
||||
def __init__(self, parent: QWidget, editor: Editor) -> None:
|
||||
AnkiWebView.__init__(self, title="editor")
|
||||
self.editor = editor
|
||||
self.strip = self.editor.mw.pm.profile["stripHTML"]
|
||||
|
@ -1062,15 +1072,15 @@ class EditorWebView(AnkiWebView):
|
|||
qconnect(clip.dataChanged, self._onClipboardChange)
|
||||
gui_hooks.editor_web_view_did_init(self)
|
||||
|
||||
def _onClipboardChange(self):
|
||||
def _onClipboardChange(self) -> None:
|
||||
if self._markInternal:
|
||||
self._markInternal = False
|
||||
self._flagAnkiText()
|
||||
|
||||
def onCut(self):
|
||||
def onCut(self) -> None:
|
||||
self.triggerPageAction(QWebEnginePage.Cut)
|
||||
|
||||
def onCopy(self):
|
||||
def onCopy(self) -> None:
|
||||
self.triggerPageAction(QWebEnginePage.Copy)
|
||||
|
||||
def _wantsExtendedPaste(self) -> bool:
|
||||
|
@ -1093,10 +1103,10 @@ class EditorWebView(AnkiWebView):
|
|||
def onMiddleClickPaste(self) -> None:
|
||||
self._onPaste(QClipboard.Selection)
|
||||
|
||||
def dragEnterEvent(self, evt):
|
||||
def dragEnterEvent(self, evt: QDragEnterEvent) -> None:
|
||||
evt.accept()
|
||||
|
||||
def dropEvent(self, evt):
|
||||
def dropEvent(self, evt: QDropEvent) -> None:
|
||||
extended = self._wantsExtendedPaste()
|
||||
mime = evt.mimeData()
|
||||
|
||||
|
@ -1177,7 +1187,7 @@ class EditorWebView(AnkiWebView):
|
|||
token = html.escape(token).replace("\t", " " * 4)
|
||||
# if there's more than one consecutive space,
|
||||
# use non-breaking spaces for the second one on
|
||||
def repl(match):
|
||||
def repl(match: Match) -> None:
|
||||
return match.group(1).replace(" ", " ") + " "
|
||||
|
||||
token = re.sub(" ( +)", repl, token)
|
||||
|
@ -1223,11 +1233,11 @@ class EditorWebView(AnkiWebView):
|
|||
return self.editor.fnameToLink(fname)
|
||||
return None
|
||||
|
||||
def flagAnkiText(self):
|
||||
def flagAnkiText(self) -> None:
|
||||
# be ready to adjust when clipboard event fires
|
||||
self._markInternal = True
|
||||
|
||||
def _flagAnkiText(self):
|
||||
def _flagAnkiText(self) -> None:
|
||||
# add a comment in the clipboard html so we can tell text is copied
|
||||
# from us and doesn't need to be stripped
|
||||
clip = self.editor.mw.app.clipboard()
|
||||
|
@ -1255,20 +1265,20 @@ class EditorWebView(AnkiWebView):
|
|||
# QFont returns "Kozuka Gothic Pro L" but WebEngine expects "Kozuka Gothic Pro Light"
|
||||
# - there may be other cases like a trailing 'Bold' that need fixing, but will
|
||||
# wait for further reports first.
|
||||
def fontMungeHack(font):
|
||||
def fontMungeHack(font: str) -> str:
|
||||
return re.sub(" L$", " Light", font)
|
||||
|
||||
|
||||
def munge_html(txt, editor):
|
||||
def munge_html(txt: str, editor: Editor) -> str:
|
||||
return "" if txt in ("<br>", "<div><br></div>") else txt
|
||||
|
||||
|
||||
def remove_null_bytes(txt, editor):
|
||||
def remove_null_bytes(txt: str, editor: Editor) -> str:
|
||||
# misbehaving apps may include a null byte in the text
|
||||
return txt.replace("\x00", "")
|
||||
|
||||
|
||||
def reverse_url_quoting(txt, editor):
|
||||
def reverse_url_quoting(txt: str, editor: Editor) -> str:
|
||||
# reverse the url quoting we added to get images to display
|
||||
return editor.mw.col.media.escape_media_filenames(txt, unescape=True)
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ from aqt.utils import TR, disable_help_button, restoreGeom, saveGeom, tooltip, t
|
|||
def show_empty_cards(mw: aqt.main.AnkiQt) -> None:
|
||||
mw.progress.start()
|
||||
|
||||
def on_done(fut):
|
||||
def on_done(fut) -> None:
|
||||
mw.progress.finish()
|
||||
report: EmptyCardsReport = fut.result()
|
||||
if not report.notes:
|
||||
|
@ -54,7 +54,7 @@ class EmptyCardsDialog(QDialog):
|
|||
style = "<style>.allempty { color: red; }</style>"
|
||||
self.form.webview.stdHtml(style + html, context=self)
|
||||
|
||||
def on_finished(code):
|
||||
def on_finished(code) -> None:
|
||||
saveGeom(self, "emptycards")
|
||||
|
||||
qconnect(self.finished, on_finished)
|
||||
|
@ -65,16 +65,16 @@ class EmptyCardsDialog(QDialog):
|
|||
self._delete_button.setAutoDefault(False)
|
||||
self._delete_button.clicked.connect(self._on_delete)
|
||||
|
||||
def _on_note_link_clicked(self, link):
|
||||
def _on_note_link_clicked(self, link) -> None:
|
||||
aqt.dialogs.open("Browser", self.mw, search=(link,))
|
||||
|
||||
def _on_delete(self):
|
||||
def _on_delete(self) -> None:
|
||||
self.mw.progress.start()
|
||||
|
||||
def delete():
|
||||
def delete() -> int:
|
||||
return self._delete_cards(self.form.keep_notes.isChecked())
|
||||
|
||||
def on_done(fut):
|
||||
def on_done(fut) -> None:
|
||||
self.mw.progress.finish()
|
||||
try:
|
||||
count = fut.result()
|
||||
|
|
|
@ -5,16 +5,18 @@ import html
|
|||
import re
|
||||
import sys
|
||||
import traceback
|
||||
from typing import Optional
|
||||
|
||||
from markdown import markdown
|
||||
|
||||
from aqt import mw
|
||||
from aqt.main import AnkiQt
|
||||
from aqt.qt import *
|
||||
from aqt.utils import TR, showText, showWarning, supportText, tr
|
||||
|
||||
if not os.environ.get("DEBUG"):
|
||||
|
||||
def excepthook(etype, val, tb):
|
||||
def excepthook(etype, val, tb) -> None:
|
||||
sys.stderr.write(
|
||||
"Caught exception:\n%s\n"
|
||||
% ("".join(traceback.format_exception(etype, val, tb)))
|
||||
|
@ -29,20 +31,20 @@ class ErrorHandler(QObject):
|
|||
|
||||
errorTimer = pyqtSignal()
|
||||
|
||||
def __init__(self, mw):
|
||||
def __init__(self, mw: AnkiQt) -> None:
|
||||
QObject.__init__(self, mw)
|
||||
self.mw = mw
|
||||
self.timer = None
|
||||
self.timer: Optional[QTimer] = None
|
||||
qconnect(self.errorTimer, self._setTimer)
|
||||
self.pool = ""
|
||||
self._oldstderr = sys.stderr
|
||||
sys.stderr = self
|
||||
|
||||
def unload(self):
|
||||
def unload(self) -> None:
|
||||
sys.stderr = self._oldstderr
|
||||
sys.excepthook = None
|
||||
|
||||
def write(self, data):
|
||||
def write(self, data: str) -> None:
|
||||
# dump to stdout
|
||||
sys.stdout.write(data)
|
||||
# save in buffer
|
||||
|
@ -50,12 +52,12 @@ class ErrorHandler(QObject):
|
|||
# and update timer
|
||||
self.setTimer()
|
||||
|
||||
def setTimer(self):
|
||||
def setTimer(self) -> None:
|
||||
# we can't create a timer from a different thread, so we post a
|
||||
# message to the object on the main thread
|
||||
self.errorTimer.emit() # type: ignore
|
||||
|
||||
def _setTimer(self):
|
||||
def _setTimer(self) -> None:
|
||||
if not self.timer:
|
||||
self.timer = QTimer(self.mw)
|
||||
qconnect(self.timer.timeout, self.onTimeout)
|
||||
|
@ -63,10 +65,10 @@ class ErrorHandler(QObject):
|
|||
self.timer.setSingleShot(True)
|
||||
self.timer.start()
|
||||
|
||||
def tempFolderMsg(self):
|
||||
def tempFolderMsg(self) -> str:
|
||||
return tr(TR.QT_MISC_UNABLE_TO_ACCESS_ANKI_MEDIA_FOLDER)
|
||||
|
||||
def onTimeout(self):
|
||||
def onTimeout(self) -> None:
|
||||
error = html.escape(self.pool)
|
||||
self.pool = ""
|
||||
self.mw.progress.clear()
|
||||
|
@ -75,15 +77,19 @@ class ErrorHandler(QObject):
|
|||
if "DeprecationWarning" in error:
|
||||
return
|
||||
if "10013" in error:
|
||||
return showWarning(tr(TR.QT_MISC_YOUR_FIREWALL_OR_ANTIVIRUS_PROGRAM_IS))
|
||||
showWarning(tr(TR.QT_MISC_YOUR_FIREWALL_OR_ANTIVIRUS_PROGRAM_IS))
|
||||
return
|
||||
if "no default input" in error.lower():
|
||||
return showWarning(tr(TR.QT_MISC_PLEASE_CONNECT_A_MICROPHONE_AND_ENSURE))
|
||||
showWarning(tr(TR.QT_MISC_PLEASE_CONNECT_A_MICROPHONE_AND_ENSURE))
|
||||
return
|
||||
if "invalidTempFolder" in error:
|
||||
return showWarning(self.tempFolderMsg())
|
||||
showWarning(self.tempFolderMsg())
|
||||
return
|
||||
if "Beautiful Soup is not an HTTP client" in error:
|
||||
return
|
||||
if "database or disk is full" in error or "Errno 28" in error:
|
||||
return showWarning(tr(TR.QT_MISC_YOUR_COMPUTERS_STORAGE_MAY_BE_FULL))
|
||||
showWarning(tr(TR.QT_MISC_YOUR_COMPUTERS_STORAGE_MAY_BE_FULL))
|
||||
return
|
||||
if "disk I/O error" in error:
|
||||
showWarning(markdown(tr(TR.ERRORS_ACCESSING_DB)))
|
||||
return
|
||||
|
@ -99,7 +105,7 @@ class ErrorHandler(QObject):
|
|||
txt = txt + "<div style='white-space: pre-wrap'>" + error + "</div>"
|
||||
showText(txt, type="html", copyBtn=True)
|
||||
|
||||
def _addonText(self, error):
|
||||
def _addonText(self, error: str) -> str:
|
||||
matches = re.findall(r"addons21/(.*?)/", error)
|
||||
if not matches:
|
||||
return ""
|
||||
|
|
|
@ -42,7 +42,7 @@ class ExportDialog(QDialog):
|
|||
self.setup(did)
|
||||
self.exec_()
|
||||
|
||||
def setup(self, did: Optional[int]):
|
||||
def setup(self, did: Optional[int]) -> None:
|
||||
self.exporters = exporters(self.col)
|
||||
# if a deck specified, start with .apkg type selected
|
||||
idx = 0
|
||||
|
@ -71,7 +71,7 @@ class ExportDialog(QDialog):
|
|||
index = self.frm.deck.findText(name)
|
||||
self.frm.deck.setCurrentIndex(index)
|
||||
|
||||
def exporterChanged(self, idx):
|
||||
def exporterChanged(self, idx: int) -> None:
|
||||
self.exporter = self.exporters[idx][1](self.col)
|
||||
self.isApkg = self.exporter.ext == ".apkg"
|
||||
self.isVerbatim = getattr(self.exporter, "verbatim", False)
|
||||
|
@ -94,7 +94,7 @@ class ExportDialog(QDialog):
|
|||
# show deck list?
|
||||
self.frm.deck.setVisible(not self.isVerbatim)
|
||||
|
||||
def accept(self):
|
||||
def accept(self) -> None:
|
||||
self.exporter.includeSched = self.frm.includeSched.isChecked()
|
||||
self.exporter.includeMedia = self.frm.includeMedia.isChecked()
|
||||
self.exporter.includeTags = self.frm.includeTags.isChecked()
|
||||
|
@ -155,17 +155,17 @@ class ExportDialog(QDialog):
|
|||
os.unlink(file)
|
||||
|
||||
# progress handler
|
||||
def exported_media(cnt):
|
||||
def exported_media(cnt) -> None:
|
||||
self.mw.taskman.run_on_main(
|
||||
lambda: self.mw.progress.update(
|
||||
label=tr(TR.EXPORTING_EXPORTED_MEDIA_FILE, count=cnt)
|
||||
)
|
||||
)
|
||||
|
||||
def do_export():
|
||||
def do_export() -> None:
|
||||
self.exporter.exportInto(file)
|
||||
|
||||
def on_done(future: Future):
|
||||
def on_done(future: Future) -> None:
|
||||
self.mw.progress.finish()
|
||||
hooks.media_files_did_export.remove(exported_media)
|
||||
# raises if exporter failed
|
||||
|
@ -177,7 +177,7 @@ class ExportDialog(QDialog):
|
|||
|
||||
self.mw.taskman.run_in_background(do_export, on_done)
|
||||
|
||||
def on_export_finished(self):
|
||||
def on_export_finished(self) -> None:
|
||||
if self.isVerbatim:
|
||||
msg = tr(TR.EXPORTING_COLLECTION_EXPORTED)
|
||||
self.mw.reopen()
|
||||
|
|
|
@ -23,7 +23,7 @@ from aqt.utils import (
|
|||
|
||||
|
||||
class FieldDialog(QDialog):
|
||||
def __init__(self, mw: AnkiQt, nt: NoteType, parent=None):
|
||||
def __init__(self, mw: AnkiQt, nt: NoteType, parent=None) -> None:
|
||||
QDialog.__init__(self, parent or mw)
|
||||
self.mw = mw
|
||||
self.col = self.mw.col
|
||||
|
@ -41,7 +41,7 @@ class FieldDialog(QDialog):
|
|||
self.form.buttonBox.button(QDialogButtonBox.Help).setAutoDefault(False)
|
||||
self.form.buttonBox.button(QDialogButtonBox.Cancel).setAutoDefault(False)
|
||||
self.form.buttonBox.button(QDialogButtonBox.Save).setAutoDefault(False)
|
||||
self.currentIdx = None
|
||||
self.currentIdx: Optional[int] = None
|
||||
self.oldSortField = self.model["sortf"]
|
||||
self.fillFields()
|
||||
self.setupSignals()
|
||||
|
@ -52,13 +52,13 @@ class FieldDialog(QDialog):
|
|||
|
||||
##########################################################################
|
||||
|
||||
def fillFields(self):
|
||||
def fillFields(self) -> None:
|
||||
self.currentIdx = None
|
||||
self.form.fieldList.clear()
|
||||
for c, f in enumerate(self.model["flds"]):
|
||||
self.form.fieldList.addItem("{}: {}".format(c + 1, f["name"]))
|
||||
|
||||
def setupSignals(self):
|
||||
def setupSignals(self) -> None:
|
||||
f = self.form
|
||||
qconnect(f.fieldList.currentRowChanged, self.onRowChange)
|
||||
qconnect(f.fieldAdd.clicked, self.onAdd)
|
||||
|
@ -68,7 +68,7 @@ class FieldDialog(QDialog):
|
|||
qconnect(f.sortField.clicked, self.onSortField)
|
||||
qconnect(f.buttonBox.helpRequested, self.onHelp)
|
||||
|
||||
def onDrop(self, ev):
|
||||
def onDrop(self, ev) -> None:
|
||||
fieldList = self.form.fieldList
|
||||
indicatorPos = fieldList.dropIndicatorPosition()
|
||||
dropPos = fieldList.indexAt(ev.pos()).row()
|
||||
|
@ -86,32 +86,34 @@ class FieldDialog(QDialog):
|
|||
movePos -= 1
|
||||
self.moveField(movePos + 1) # convert to 1 based.
|
||||
|
||||
def onRowChange(self, idx):
|
||||
def onRowChange(self, idx: int) -> None:
|
||||
if idx == -1:
|
||||
return
|
||||
self.saveField()
|
||||
self.loadField(idx)
|
||||
|
||||
def _uniqueName(self, prompt, ignoreOrd=None, old=""):
|
||||
def _uniqueName(
|
||||
self, prompt: str, ignoreOrd: Optional[int] = None, old: str = ""
|
||||
) -> Optional[str]:
|
||||
txt = getOnlyText(prompt, default=old).replace('"', "").strip()
|
||||
if not txt:
|
||||
return
|
||||
return None
|
||||
if txt[0] in "#^/":
|
||||
showWarning(tr(TR.FIELDS_NAME_FIRST_LETTER_NOT_VALID))
|
||||
return
|
||||
return None
|
||||
for letter in """:{"}""":
|
||||
if letter in txt:
|
||||
showWarning(tr(TR.FIELDS_NAME_INVALID_LETTER))
|
||||
return
|
||||
return None
|
||||
for f in self.model["flds"]:
|
||||
if ignoreOrd is not None and f["ord"] == ignoreOrd:
|
||||
continue
|
||||
if f["name"] == txt:
|
||||
showWarning(tr(TR.FIELDS_THAT_FIELD_NAME_IS_ALREADY_USED))
|
||||
return
|
||||
return None
|
||||
return txt
|
||||
|
||||
def onRename(self):
|
||||
def onRename(self) -> None:
|
||||
idx = self.currentIdx
|
||||
f = self.model["flds"][idx]
|
||||
name = self._uniqueName(tr(TR.ACTIONS_NEW_NAME), self.currentIdx, f["name"])
|
||||
|
@ -127,7 +129,7 @@ class FieldDialog(QDialog):
|
|||
self.fillFields()
|
||||
self.form.fieldList.setCurrentRow(idx)
|
||||
|
||||
def onAdd(self):
|
||||
def onAdd(self) -> None:
|
||||
name = self._uniqueName(tr(TR.FIELDS_FIELD_NAME))
|
||||
if not name:
|
||||
return
|
||||
|
@ -139,9 +141,10 @@ class FieldDialog(QDialog):
|
|||
self.fillFields()
|
||||
self.form.fieldList.setCurrentRow(len(self.model["flds"]) - 1)
|
||||
|
||||
def onDelete(self):
|
||||
def onDelete(self) -> None:
|
||||
if len(self.model["flds"]) < 2:
|
||||
return showWarning(tr(TR.FIELDS_NOTES_REQUIRE_AT_LEAST_ONE_FIELD))
|
||||
showWarning(tr(TR.FIELDS_NOTES_REQUIRE_AT_LEAST_ONE_FIELD))
|
||||
return
|
||||
count = self.mm.useCount(self.model)
|
||||
c = tr(TR.BROWSING_NOTE_COUNT, count=count)
|
||||
if not askUser(tr(TR.FIELDS_DELETE_FIELD_FROM, val=c)):
|
||||
|
@ -155,7 +158,7 @@ class FieldDialog(QDialog):
|
|||
self.fillFields()
|
||||
self.form.fieldList.setCurrentRow(0)
|
||||
|
||||
def onPosition(self, delta=-1):
|
||||
def onPosition(self, delta=-1) -> None:
|
||||
idx = self.currentIdx
|
||||
l = len(self.model["flds"])
|
||||
txt = getOnlyText(tr(TR.FIELDS_NEW_POSITION_1, val=l), default=str(idx + 1))
|
||||
|
@ -169,23 +172,23 @@ class FieldDialog(QDialog):
|
|||
return
|
||||
self.moveField(pos)
|
||||
|
||||
def onSortField(self):
|
||||
def onSortField(self) -> None:
|
||||
if not self.change_tracker.mark_schema():
|
||||
return False
|
||||
return
|
||||
# don't allow user to disable; it makes no sense
|
||||
self.form.sortField.setChecked(True)
|
||||
self.mm.set_sort_index(self.model, self.form.fieldList.currentRow())
|
||||
|
||||
def moveField(self, pos):
|
||||
def moveField(self, pos) -> None:
|
||||
if not self.change_tracker.mark_schema():
|
||||
return False
|
||||
return
|
||||
self.saveField()
|
||||
f = self.model["flds"][self.currentIdx]
|
||||
self.mm.reposition_field(self.model, f, pos - 1)
|
||||
self.fillFields()
|
||||
self.form.fieldList.setCurrentRow(pos - 1)
|
||||
|
||||
def loadField(self, idx):
|
||||
def loadField(self, idx: int) -> None:
|
||||
self.currentIdx = idx
|
||||
fld = self.model["flds"][idx]
|
||||
f = self.form
|
||||
|
@ -195,7 +198,7 @@ class FieldDialog(QDialog):
|
|||
f.sortField.setChecked(self.model["sortf"] == fld["ord"])
|
||||
f.rtl.setChecked(fld["rtl"])
|
||||
|
||||
def saveField(self):
|
||||
def saveField(self) -> None:
|
||||
# not initialized yet?
|
||||
if self.currentIdx is None:
|
||||
return
|
||||
|
@ -219,20 +222,20 @@ class FieldDialog(QDialog):
|
|||
fld["rtl"] = rtl
|
||||
self.change_tracker.mark_basic()
|
||||
|
||||
def reject(self):
|
||||
def reject(self) -> None:
|
||||
if self.change_tracker.changed():
|
||||
if not askUser("Discard changes?"):
|
||||
return
|
||||
|
||||
QDialog.reject(self)
|
||||
|
||||
def accept(self):
|
||||
def accept(self) -> None:
|
||||
self.saveField()
|
||||
|
||||
def save():
|
||||
def save() -> None:
|
||||
self.mm.save(self.model)
|
||||
|
||||
def on_done(fut):
|
||||
def on_done(fut) -> None:
|
||||
try:
|
||||
fut.result()
|
||||
except TemplateError as e:
|
||||
|
@ -245,5 +248,5 @@ class FieldDialog(QDialog):
|
|||
|
||||
self.mw.taskman.with_progress(save, on_done, self)
|
||||
|
||||
def onHelp(self):
|
||||
def onHelp(self) -> None:
|
||||
openHelp(HelpPage.CUSTOMIZING_FIELDS)
|
||||
|
|
|
@ -9,12 +9,13 @@ import traceback
|
|||
import unicodedata
|
||||
import zipfile
|
||||
from concurrent.futures import Future
|
||||
from typing import Optional
|
||||
from typing import Any, Optional
|
||||
|
||||
import anki.importing as importing
|
||||
import aqt.deckchooser
|
||||
import aqt.forms
|
||||
import aqt.modelchooser
|
||||
from anki.importing.apkg import AnkiPackageImporter
|
||||
from aqt import AnkiQt, gui_hooks
|
||||
from aqt.qt import *
|
||||
from aqt.utils import (
|
||||
|
@ -34,7 +35,7 @@ from aqt.utils import (
|
|||
|
||||
|
||||
class ChangeMap(QDialog):
|
||||
def __init__(self, mw: AnkiQt, model, current):
|
||||
def __init__(self, mw: AnkiQt, model, current) -> None:
|
||||
QDialog.__init__(self, mw, Qt.Window)
|
||||
self.mw = mw
|
||||
self.model = model
|
||||
|
@ -59,11 +60,11 @@ class ChangeMap(QDialog):
|
|||
self.frm.fields.setCurrentRow(n + 1)
|
||||
self.field: Optional[str] = None
|
||||
|
||||
def getField(self):
|
||||
def getField(self) -> str:
|
||||
self.exec_()
|
||||
return self.field
|
||||
|
||||
def accept(self):
|
||||
def accept(self) -> None:
|
||||
row = self.frm.fields.currentRow()
|
||||
if row < len(self.model["flds"]):
|
||||
self.field = self.model["flds"][row]["name"]
|
||||
|
@ -73,7 +74,7 @@ class ChangeMap(QDialog):
|
|||
self.field = None
|
||||
QDialog.accept(self)
|
||||
|
||||
def reject(self):
|
||||
def reject(self) -> None:
|
||||
self.accept()
|
||||
|
||||
|
||||
|
@ -106,19 +107,19 @@ class ImportDialog(QDialog):
|
|||
self.frm.buttonBox.addButton(b, QDialogButtonBox.AcceptRole)
|
||||
self.exec_()
|
||||
|
||||
def setupOptions(self):
|
||||
def setupOptions(self) -> None:
|
||||
self.model = self.mw.col.models.current()
|
||||
self.modelChooser = aqt.modelchooser.ModelChooser(
|
||||
self.mw, self.frm.modelArea, label=False
|
||||
)
|
||||
self.deck = aqt.deckchooser.DeckChooser(self.mw, self.frm.deckArea, label=False)
|
||||
|
||||
def modelChanged(self, unused=None):
|
||||
def modelChanged(self, unused: Any = None) -> None:
|
||||
self.importer.model = self.mw.col.models.current()
|
||||
self.importer.initMapping()
|
||||
self.showMapping()
|
||||
|
||||
def onDelimiter(self):
|
||||
def onDelimiter(self) -> None:
|
||||
str = (
|
||||
getOnlyText(
|
||||
tr(TR.IMPORTING_BY_DEFAULT_ANKI_WILL_DETECT_THE),
|
||||
|
@ -135,14 +136,14 @@ class ImportDialog(QDialog):
|
|||
return
|
||||
self.hideMapping()
|
||||
|
||||
def updateDelim():
|
||||
def updateDelim() -> None:
|
||||
self.importer.delimiter = str
|
||||
self.importer.updateDelimiter()
|
||||
|
||||
self.showMapping(hook=updateDelim)
|
||||
self.updateDelimiterButtonText()
|
||||
|
||||
def updateDelimiterButtonText(self):
|
||||
def updateDelimiterButtonText(self) -> None:
|
||||
if not self.importer.needDelimiter:
|
||||
return
|
||||
if self.importer.delimiter:
|
||||
|
@ -164,7 +165,7 @@ class ImportDialog(QDialog):
|
|||
txt = tr(TR.IMPORTING_FIELDS_SEPARATED_BY, val=d)
|
||||
self.frm.autoDetect.setText(txt)
|
||||
|
||||
def accept(self):
|
||||
def accept(self) -> None:
|
||||
self.importer.mapping = self.mapping
|
||||
if not self.importer.mappingOk():
|
||||
showWarning(tr(TR.IMPORTING_THE_FIRST_FIELD_OF_THE_NOTE))
|
||||
|
@ -182,7 +183,7 @@ class ImportDialog(QDialog):
|
|||
self.mw.progress.start()
|
||||
self.mw.checkpoint(tr(TR.ACTIONS_IMPORT))
|
||||
|
||||
def on_done(future: Future):
|
||||
def on_done(future: Future) -> None:
|
||||
self.mw.progress.finish()
|
||||
|
||||
try:
|
||||
|
@ -211,19 +212,21 @@ class ImportDialog(QDialog):
|
|||
|
||||
self.mw.taskman.run_in_background(self.importer.run, on_done)
|
||||
|
||||
def setupMappingFrame(self):
|
||||
def setupMappingFrame(self) -> None:
|
||||
# qt seems to have a bug with adding/removing from a grid, so we add
|
||||
# to a separate object and add/remove that instead
|
||||
self.frame = QFrame(self.frm.mappingArea)
|
||||
self.frm.mappingArea.setWidget(self.frame)
|
||||
self.mapbox = QVBoxLayout(self.frame)
|
||||
self.mapbox.setContentsMargins(0, 0, 0, 0)
|
||||
self.mapwidget = None
|
||||
self.mapwidget: Optional[QWidget] = None
|
||||
|
||||
def hideMapping(self):
|
||||
def hideMapping(self) -> None:
|
||||
self.frm.mappingGroup.hide()
|
||||
|
||||
def showMapping(self, keepMapping=False, hook=None):
|
||||
def showMapping(
|
||||
self, keepMapping: bool = False, hook: Optional[Callable] = None
|
||||
) -> None:
|
||||
if hook:
|
||||
hook()
|
||||
if not keepMapping:
|
||||
|
@ -255,7 +258,7 @@ class ImportDialog(QDialog):
|
|||
self.grid.addWidget(button, num, 2)
|
||||
qconnect(button.clicked, lambda _, s=self, n=num: s.changeMappingNum(n))
|
||||
|
||||
def changeMappingNum(self, n):
|
||||
def changeMappingNum(self, n) -> None:
|
||||
f = ChangeMap(self.mw, self.importer.model, self.mapping[n]).getField()
|
||||
try:
|
||||
# make sure we don't have it twice
|
||||
|
@ -267,7 +270,7 @@ class ImportDialog(QDialog):
|
|||
if getattr(self.importer, "delimiter", False):
|
||||
self.savedDelimiter = self.importer.delimiter
|
||||
|
||||
def updateDelim():
|
||||
def updateDelim() -> None:
|
||||
self.importer.delimiter = self.savedDelimiter
|
||||
|
||||
self.showMapping(hook=updateDelim, keepMapping=True)
|
||||
|
@ -280,22 +283,22 @@ class ImportDialog(QDialog):
|
|||
gui_hooks.current_note_type_did_change.remove(self.modelChanged)
|
||||
QDialog.reject(self)
|
||||
|
||||
def helpRequested(self):
|
||||
def helpRequested(self) -> None:
|
||||
openHelp(HelpPage.IMPORTING)
|
||||
|
||||
def importModeChanged(self, newImportMode):
|
||||
def importModeChanged(self, newImportMode) -> None:
|
||||
if newImportMode == 0:
|
||||
self.frm.tagModified.setEnabled(True)
|
||||
else:
|
||||
self.frm.tagModified.setEnabled(False)
|
||||
|
||||
|
||||
def showUnicodeWarning():
|
||||
def showUnicodeWarning() -> None:
|
||||
"""Shorthand to show a standard warning."""
|
||||
showWarning(tr(TR.IMPORTING_SELECTED_FILE_WAS_NOT_IN_UTF8))
|
||||
|
||||
|
||||
def onImport(mw):
|
||||
def onImport(mw: AnkiQt) -> None:
|
||||
filt = ";;".join([x[0] for x in importing.Importers])
|
||||
file = getFile(mw, tr(TR.ACTIONS_IMPORT), None, key="import", filter=filt)
|
||||
if not file:
|
||||
|
@ -314,7 +317,7 @@ def onImport(mw):
|
|||
importFile(mw, file)
|
||||
|
||||
|
||||
def importFile(mw, file):
|
||||
def importFile(mw: AnkiQt, file: str) -> None:
|
||||
importerClass = None
|
||||
done = False
|
||||
for i in importing.Importers:
|
||||
|
@ -371,7 +374,7 @@ def importFile(mw, file):
|
|||
# importing non-colpkg files
|
||||
mw.progress.start(immediate=True)
|
||||
|
||||
def on_done(future: Future):
|
||||
def on_done(future: Future) -> None:
|
||||
mw.progress.finish()
|
||||
try:
|
||||
future.result()
|
||||
|
@ -402,11 +405,11 @@ def importFile(mw, file):
|
|||
mw.taskman.run_in_background(importer.run, on_done)
|
||||
|
||||
|
||||
def invalidZipMsg():
|
||||
def invalidZipMsg() -> str:
|
||||
return tr(TR.IMPORTING_THIS_FILE_DOES_NOT_APPEAR_TO)
|
||||
|
||||
|
||||
def setupApkgImport(mw, importer):
|
||||
def setupApkgImport(mw: AnkiQt, importer: AnkiPackageImporter) -> bool:
|
||||
base = os.path.basename(importer.file).lower()
|
||||
full = (
|
||||
(base == "collection.apkg")
|
||||
|
@ -424,16 +427,17 @@ def setupApkgImport(mw, importer):
|
|||
return False
|
||||
|
||||
replaceWithApkg(mw, importer.file, mw.restoringBackup)
|
||||
return False
|
||||
|
||||
|
||||
def replaceWithApkg(mw, file, backup):
|
||||
def replaceWithApkg(mw, file, backup) -> None:
|
||||
mw.unloadCollection(lambda: _replaceWithApkg(mw, file, backup))
|
||||
|
||||
|
||||
def _replaceWithApkg(mw, filename, backup):
|
||||
def _replaceWithApkg(mw, filename, backup) -> None:
|
||||
mw.progress.start(immediate=True)
|
||||
|
||||
def do_import():
|
||||
def do_import() -> None:
|
||||
z = zipfile.ZipFile(filename)
|
||||
|
||||
# v2 scheduler?
|
||||
|
@ -468,7 +472,7 @@ def _replaceWithApkg(mw, filename, backup):
|
|||
|
||||
z.close()
|
||||
|
||||
def on_done(future: Future):
|
||||
def on_done(future: Future) -> None:
|
||||
mw.progress.finish()
|
||||
|
||||
try:
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
Legacy support
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
from typing import Any, List
|
||||
|
||||
import anki
|
||||
import aqt
|
||||
|
@ -31,7 +31,14 @@ def stripSounds(text) -> str:
|
|||
return aqt.mw.col.media.strip_av_tags(text)
|
||||
|
||||
|
||||
def fmtTimeSpan(time, pad=0, point=0, short=False, inTime=False, unit=99):
|
||||
def fmtTimeSpan(
|
||||
time: Any,
|
||||
pad: Any = 0,
|
||||
point: Any = 0,
|
||||
short: Any = False,
|
||||
inTime: Any = False,
|
||||
unit: Any = 99,
|
||||
) -> Any:
|
||||
print("fmtTimeSpan() has become col.format_timespan()")
|
||||
return aqt.mw.col.format_timespan(time)
|
||||
|
||||
|
|
160
qt/aqt/main.py
160
qt/aqt/main.py
|
@ -14,7 +14,7 @@ import weakref
|
|||
import zipfile
|
||||
from argparse import Namespace
|
||||
from threading import Thread
|
||||
from typing import Any, Callable, List, Optional, Sequence, TextIO, Tuple, cast
|
||||
from typing import Any, Callable, List, Optional, Sequence, TextIO, Tuple, Union, cast
|
||||
|
||||
import anki
|
||||
import aqt
|
||||
|
@ -70,6 +70,7 @@ install_pylib_legacy()
|
|||
|
||||
|
||||
class ResetReason(enum.Enum):
|
||||
Unknown = "unknown"
|
||||
AddCardsAddNote = "addCardsAddNote"
|
||||
EditCurrentInit = "editCurrentInit"
|
||||
EditorBridgeCmd = "editorBridgeCmd"
|
||||
|
@ -85,7 +86,7 @@ class ResetReason(enum.Enum):
|
|||
|
||||
|
||||
class ResetRequired:
|
||||
def __init__(self, mw: AnkiQt):
|
||||
def __init__(self, mw: AnkiQt) -> None:
|
||||
self.mw = mw
|
||||
|
||||
|
||||
|
@ -137,7 +138,7 @@ class AnkiQt(QMainWindow):
|
|||
else:
|
||||
fn = self.setupProfile
|
||||
|
||||
def on_window_init():
|
||||
def on_window_init() -> None:
|
||||
fn()
|
||||
gui_hooks.main_window_did_init()
|
||||
|
||||
|
@ -173,7 +174,7 @@ class AnkiQt(QMainWindow):
|
|||
"Actions that are deferred until after add-on loading."
|
||||
self.toolbar.draw()
|
||||
|
||||
def setupProfileAfterWebviewsLoaded(self):
|
||||
def setupProfileAfterWebviewsLoaded(self) -> None:
|
||||
for w in (self.web, self.bottomWeb):
|
||||
if not w._domDone:
|
||||
self.progress.timer(
|
||||
|
@ -204,7 +205,7 @@ class AnkiQt(QMainWindow):
|
|||
self.onClose.emit() # type: ignore
|
||||
evt.accept()
|
||||
|
||||
def closeWithoutQuitting(self):
|
||||
def closeWithoutQuitting(self) -> None:
|
||||
self.closeFires = False
|
||||
self.close()
|
||||
self.closeFires = True
|
||||
|
@ -273,9 +274,10 @@ class AnkiQt(QMainWindow):
|
|||
name = self.pm.profiles()[n]
|
||||
self.pm.load(name)
|
||||
|
||||
def openProfile(self):
|
||||
def openProfile(self) -> None:
|
||||
name = self.pm.profiles()[self.profileForm.profiles.currentRow()]
|
||||
return self.pm.load(name)
|
||||
self.pm.load(name)
|
||||
return
|
||||
|
||||
def onOpenProfile(self) -> None:
|
||||
self.profileDiag.hide()
|
||||
|
@ -286,34 +288,37 @@ class AnkiQt(QMainWindow):
|
|||
def profileNameOk(self, name: str) -> bool:
|
||||
return not checkInvalidFilename(name) and name != "addons21"
|
||||
|
||||
def onAddProfile(self):
|
||||
def onAddProfile(self) -> None:
|
||||
name = getOnlyText(tr(TR.ACTIONS_NAME)).strip()
|
||||
if name:
|
||||
if name in self.pm.profiles():
|
||||
return showWarning(tr(TR.QT_MISC_NAME_EXISTS))
|
||||
showWarning(tr(TR.QT_MISC_NAME_EXISTS))
|
||||
return
|
||||
if not self.profileNameOk(name):
|
||||
return
|
||||
self.pm.create(name)
|
||||
self.pm.name = name
|
||||
self.refreshProfilesList()
|
||||
|
||||
def onRenameProfile(self):
|
||||
def onRenameProfile(self) -> None:
|
||||
name = getOnlyText(tr(TR.ACTIONS_NEW_NAME), default=self.pm.name).strip()
|
||||
if not name:
|
||||
return
|
||||
if name == self.pm.name:
|
||||
return
|
||||
if name in self.pm.profiles():
|
||||
return showWarning(tr(TR.QT_MISC_NAME_EXISTS))
|
||||
showWarning(tr(TR.QT_MISC_NAME_EXISTS))
|
||||
return
|
||||
if not self.profileNameOk(name):
|
||||
return
|
||||
self.pm.rename(name)
|
||||
self.refreshProfilesList()
|
||||
|
||||
def onRemProfile(self):
|
||||
def onRemProfile(self) -> None:
|
||||
profs = self.pm.profiles()
|
||||
if len(profs) < 2:
|
||||
return showWarning(tr(TR.QT_MISC_THERE_MUST_BE_AT_LEAST_ONE))
|
||||
showWarning(tr(TR.QT_MISC_THERE_MUST_BE_AT_LEAST_ONE))
|
||||
return
|
||||
# sure?
|
||||
if not askUser(
|
||||
tr(TR.QT_MISC_ALL_CARDS_NOTES_AND_MEDIA_FOR),
|
||||
|
@ -324,7 +329,7 @@ class AnkiQt(QMainWindow):
|
|||
self.pm.remove(self.pm.name)
|
||||
self.refreshProfilesList()
|
||||
|
||||
def onOpenBackup(self):
|
||||
def onOpenBackup(self) -> None:
|
||||
if not askUser(
|
||||
tr(TR.QT_MISC_REPLACE_YOUR_COLLECTION_WITH_AN_EARLIER),
|
||||
msgfunc=QMessageBox.warning,
|
||||
|
@ -332,7 +337,7 @@ class AnkiQt(QMainWindow):
|
|||
):
|
||||
return
|
||||
|
||||
def doOpen(path):
|
||||
def doOpen(path) -> None:
|
||||
self._openBackup(path)
|
||||
|
||||
getFile(
|
||||
|
@ -343,7 +348,7 @@ class AnkiQt(QMainWindow):
|
|||
dir=self.pm.backupFolder(),
|
||||
)
|
||||
|
||||
def _openBackup(self, path):
|
||||
def _openBackup(self, path) -> None:
|
||||
try:
|
||||
# move the existing collection to the trash, as it may not open
|
||||
self.pm.trashCollection()
|
||||
|
@ -358,14 +363,14 @@ class AnkiQt(QMainWindow):
|
|||
|
||||
self.onOpenProfile()
|
||||
|
||||
def _on_downgrade(self):
|
||||
def _on_downgrade(self) -> None:
|
||||
self.progress.start()
|
||||
profiles = self.pm.profiles()
|
||||
|
||||
def downgrade():
|
||||
def downgrade() -> List[str]:
|
||||
return self.pm.downgrade(profiles)
|
||||
|
||||
def on_done(future):
|
||||
def on_done(future) -> None:
|
||||
self.progress.finish()
|
||||
problems = future.result()
|
||||
if not problems:
|
||||
|
@ -407,7 +412,7 @@ class AnkiQt(QMainWindow):
|
|||
self.pendingImport = None
|
||||
gui_hooks.profile_did_open()
|
||||
|
||||
def _onsuccess():
|
||||
def _onsuccess() -> None:
|
||||
self._refresh_after_sync()
|
||||
if onsuccess:
|
||||
onsuccess()
|
||||
|
@ -415,7 +420,7 @@ class AnkiQt(QMainWindow):
|
|||
self.maybe_auto_sync_on_open_close(_onsuccess)
|
||||
|
||||
def unloadProfile(self, onsuccess: Callable) -> None:
|
||||
def callback():
|
||||
def callback() -> None:
|
||||
self._unloadProfile()
|
||||
onsuccess()
|
||||
|
||||
|
@ -445,7 +450,7 @@ class AnkiQt(QMainWindow):
|
|||
def unloadProfileAndExit(self) -> None:
|
||||
self.unloadProfile(self.cleanupAndExit)
|
||||
|
||||
def unloadProfileAndShowProfileManager(self):
|
||||
def unloadProfileAndShowProfileManager(self) -> None:
|
||||
self.unloadProfile(self.showProfileManager)
|
||||
|
||||
def cleanupAndExit(self) -> None:
|
||||
|
@ -509,23 +514,23 @@ class AnkiQt(QMainWindow):
|
|||
|
||||
return True
|
||||
|
||||
def _loadCollection(self):
|
||||
def _loadCollection(self) -> None:
|
||||
cpath = self.pm.collectionPath()
|
||||
self.col = Collection(cpath, backend=self.backend, log=True)
|
||||
self.setEnabled(True)
|
||||
|
||||
def reopen(self):
|
||||
def reopen(self) -> None:
|
||||
self.col.reopen()
|
||||
|
||||
def unloadCollection(self, onsuccess: Callable) -> None:
|
||||
def after_media_sync():
|
||||
def after_media_sync() -> None:
|
||||
self._unloadCollection()
|
||||
onsuccess()
|
||||
|
||||
def after_sync():
|
||||
def after_sync() -> None:
|
||||
self.media_syncer.show_diag_until_finished(after_media_sync)
|
||||
|
||||
def before_sync():
|
||||
def before_sync() -> None:
|
||||
self.setEnabled(False)
|
||||
self.maybe_auto_sync_on_open_close(after_sync)
|
||||
|
||||
|
@ -563,7 +568,7 @@ class AnkiQt(QMainWindow):
|
|||
##########################################################################
|
||||
|
||||
class BackupThread(Thread):
|
||||
def __init__(self, path, data):
|
||||
def __init__(self, path, data) -> None:
|
||||
Thread.__init__(self)
|
||||
self.path = path
|
||||
self.data = data
|
||||
|
@ -572,7 +577,7 @@ class AnkiQt(QMainWindow):
|
|||
with open(self.path, "wb") as file:
|
||||
pass
|
||||
|
||||
def run(self):
|
||||
def run(self) -> None:
|
||||
z = zipfile.ZipFile(self.path, "w", zipfile.ZIP_DEFLATED)
|
||||
z.writestr("collection.anki2", self.data)
|
||||
z.writestr("media", "{}")
|
||||
|
@ -655,10 +660,10 @@ class AnkiQt(QMainWindow):
|
|||
return self.moveToState("deckBrowser")
|
||||
self.overview.show()
|
||||
|
||||
def _reviewState(self, oldState):
|
||||
def _reviewState(self, oldState: str) -> None:
|
||||
self.reviewer.show()
|
||||
|
||||
def _reviewCleanup(self, newState):
|
||||
def _reviewCleanup(self, newState: str) -> None:
|
||||
if newState != "resetRequired" and newState != "review":
|
||||
self.reviewer.cleanup()
|
||||
|
||||
|
@ -674,7 +679,12 @@ class AnkiQt(QMainWindow):
|
|||
self.maybeEnableUndo()
|
||||
self.moveToState(self.state)
|
||||
|
||||
def requireReset(self, modal=False, reason="unknown", context=None):
|
||||
def requireReset(
|
||||
self,
|
||||
modal: bool = False,
|
||||
reason: ResetReason = ResetReason.Unknown,
|
||||
context: Any = None,
|
||||
) -> None:
|
||||
"Signal queue needs to be rebuilt when edits are finished or by user."
|
||||
self.autosave()
|
||||
self.resetModal = modal
|
||||
|
@ -683,7 +693,7 @@ class AnkiQt(QMainWindow):
|
|||
):
|
||||
self.moveToState("resetRequired")
|
||||
|
||||
def interactiveState(self):
|
||||
def interactiveState(self) -> bool:
|
||||
"True if not in profile manager, syncing, etc."
|
||||
return self.state in ("overview", "review", "deckBrowser")
|
||||
|
||||
|
@ -693,7 +703,7 @@ class AnkiQt(QMainWindow):
|
|||
self.state = self.returnState
|
||||
self.reset()
|
||||
|
||||
def delayedMaybeReset(self):
|
||||
def delayedMaybeReset(self) -> None:
|
||||
# if we redraw the page in a button click event it will often crash on
|
||||
# windows
|
||||
self.progress.timer(100, self.maybeReset, False)
|
||||
|
@ -794,9 +804,9 @@ title="%s" %s>%s</button>""" % (
|
|||
signal.signal(signal.SIGINT, self.onUnixSignal)
|
||||
signal.signal(signal.SIGTERM, self.onUnixSignal)
|
||||
|
||||
def onUnixSignal(self, signum, frame):
|
||||
def onUnixSignal(self, signum, frame) -> None:
|
||||
# schedule a rollback & quit
|
||||
def quit():
|
||||
def quit() -> None:
|
||||
self.col.db.rollback()
|
||||
self.close()
|
||||
|
||||
|
@ -822,7 +832,7 @@ title="%s" %s>%s</button>""" % (
|
|||
self.addonManager.loadAddons()
|
||||
self.maybe_check_for_addon_updates()
|
||||
|
||||
def maybe_check_for_addon_updates(self):
|
||||
def maybe_check_for_addon_updates(self) -> None:
|
||||
last_check = self.pm.last_addon_update_check()
|
||||
elap = intTime() - last_check
|
||||
|
||||
|
@ -865,7 +875,7 @@ title="%s" %s>%s</button>""" % (
|
|||
# Syncing
|
||||
##########################################################################
|
||||
|
||||
def on_sync_button_clicked(self):
|
||||
def on_sync_button_clicked(self) -> None:
|
||||
if self.media_syncer.is_syncing():
|
||||
self.media_syncer.show_sync_log()
|
||||
else:
|
||||
|
@ -878,16 +888,16 @@ title="%s" %s>%s</button>""" % (
|
|||
else:
|
||||
self._sync_collection_and_media(self._refresh_after_sync)
|
||||
|
||||
def _refresh_after_sync(self):
|
||||
def _refresh_after_sync(self) -> None:
|
||||
self.toolbar.redraw()
|
||||
|
||||
def _sync_collection_and_media(self, after_sync: Callable[[], None]):
|
||||
def _sync_collection_and_media(self, after_sync: Callable[[], None]) -> None:
|
||||
"Caller should ensure auth available."
|
||||
# start media sync if not already running
|
||||
if not self.media_syncer.is_syncing():
|
||||
self.media_syncer.start()
|
||||
|
||||
def on_collection_sync_finished():
|
||||
def on_collection_sync_finished() -> None:
|
||||
self.col.clearUndo()
|
||||
self.col.models._clear_cache()
|
||||
gui_hooks.sync_did_finish()
|
||||
|
@ -920,7 +930,7 @@ title="%s" %s>%s</button>""" % (
|
|||
)
|
||||
|
||||
# legacy
|
||||
def _sync(self):
|
||||
def _sync(self) -> None:
|
||||
pass
|
||||
|
||||
onSync = on_sync_button_clicked
|
||||
|
@ -1027,7 +1037,7 @@ title="%s" %s>%s</button>""" % (
|
|||
self.form.actionUndo.setEnabled(False)
|
||||
gui_hooks.undo_state_did_change(False)
|
||||
|
||||
def checkpoint(self, name):
|
||||
def checkpoint(self, name: str) -> None:
|
||||
self.col.save(name)
|
||||
self.maybeEnableUndo()
|
||||
|
||||
|
@ -1046,10 +1056,10 @@ title="%s" %s>%s</button>""" % (
|
|||
def onBrowse(self) -> None:
|
||||
aqt.dialogs.open("Browser", self, card=self.reviewer.card)
|
||||
|
||||
def onEditCurrent(self):
|
||||
def onEditCurrent(self) -> None:
|
||||
aqt.dialogs.open("EditCurrent", self)
|
||||
|
||||
def onDeckConf(self, deck=None):
|
||||
def onDeckConf(self, deck: Optional[Deck] = None) -> None:
|
||||
import aqt.deckconf
|
||||
|
||||
if not deck:
|
||||
|
@ -1059,11 +1069,11 @@ title="%s" %s>%s</button>""" % (
|
|||
else:
|
||||
aqt.deckconf.DeckConf(self, deck)
|
||||
|
||||
def onOverview(self):
|
||||
def onOverview(self) -> None:
|
||||
self.col.reset()
|
||||
self.moveToState("overview")
|
||||
|
||||
def onStats(self):
|
||||
def onStats(self) -> None:
|
||||
deck = self._selectedDeck()
|
||||
if not deck:
|
||||
return
|
||||
|
@ -1073,21 +1083,21 @@ title="%s" %s>%s</button>""" % (
|
|||
else:
|
||||
aqt.dialogs.open("NewDeckStats", self)
|
||||
|
||||
def onPrefs(self):
|
||||
def onPrefs(self) -> None:
|
||||
aqt.dialogs.open("Preferences", self)
|
||||
|
||||
def onNoteTypes(self):
|
||||
def onNoteTypes(self) -> None:
|
||||
import aqt.models
|
||||
|
||||
aqt.models.Models(self, self, fromMain=True)
|
||||
|
||||
def onAbout(self):
|
||||
def onAbout(self) -> None:
|
||||
aqt.dialogs.open("About", self)
|
||||
|
||||
def onDonate(self):
|
||||
def onDonate(self) -> None:
|
||||
openLink(aqt.appDonate)
|
||||
|
||||
def onDocumentation(self):
|
||||
def onDocumentation(self) -> None:
|
||||
openHelp(HelpPage.INDEX)
|
||||
|
||||
# Importing & exporting
|
||||
|
@ -1103,12 +1113,12 @@ title="%s" %s>%s</button>""" % (
|
|||
aqt.importing.importFile(self, path)
|
||||
return None
|
||||
|
||||
def onImport(self):
|
||||
def onImport(self) -> None:
|
||||
import aqt.importing
|
||||
|
||||
aqt.importing.onImport(self)
|
||||
|
||||
def onExport(self, did=None):
|
||||
def onExport(self, did: Optional[int] = None) -> None:
|
||||
import aqt.exporting
|
||||
|
||||
aqt.exporting.ExportDialog(self, did=did)
|
||||
|
@ -1116,7 +1126,7 @@ title="%s" %s>%s</button>""" % (
|
|||
# Installing add-ons from CLI / mimetype handler
|
||||
##########################################################################
|
||||
|
||||
def installAddon(self, path: str, startup: bool = False):
|
||||
def installAddon(self, path: str, startup: bool = False) -> None:
|
||||
from aqt.addons import installAddonPackages
|
||||
|
||||
installAddonPackages(
|
||||
|
@ -1131,7 +1141,7 @@ title="%s" %s>%s</button>""" % (
|
|||
# Cramming
|
||||
##########################################################################
|
||||
|
||||
def onCram(self):
|
||||
def onCram(self) -> None:
|
||||
aqt.dialogs.open("DynDeckConfDialog", self)
|
||||
|
||||
# Menu, title bar & status
|
||||
|
@ -1174,14 +1184,14 @@ title="%s" %s>%s</button>""" % (
|
|||
qconnect(self.autoUpdate.clockIsOff, self.clockIsOff)
|
||||
self.autoUpdate.start()
|
||||
|
||||
def newVerAvail(self, ver):
|
||||
def newVerAvail(self, ver) -> None:
|
||||
if self.pm.meta.get("suppressUpdate", None) != ver:
|
||||
aqt.update.askAndUpdate(self, ver)
|
||||
|
||||
def newMsg(self, data):
|
||||
def newMsg(self, data) -> None:
|
||||
aqt.update.showMessages(self, data)
|
||||
|
||||
def clockIsOff(self, diff):
|
||||
def clockIsOff(self, diff) -> None:
|
||||
if devMode:
|
||||
print("clock is off; ignoring")
|
||||
return
|
||||
|
@ -1202,13 +1212,13 @@ title="%s" %s>%s</button>""" % (
|
|||
# SIGINT/SIGTERM is processed without a long delay
|
||||
self.progress.timer(1000, lambda: None, True, False)
|
||||
|
||||
def onRefreshTimer(self):
|
||||
def onRefreshTimer(self) -> None:
|
||||
if self.state == "deckBrowser":
|
||||
self.deckBrowser.refresh()
|
||||
elif self.state == "overview":
|
||||
self.overview.refresh()
|
||||
|
||||
def on_autosync_timer(self):
|
||||
def on_autosync_timer(self) -> None:
|
||||
elap = self.media_syncer.seconds_since_last_sync()
|
||||
minutes = self.pm.auto_sync_media_minutes()
|
||||
if not minutes:
|
||||
|
@ -1229,7 +1239,7 @@ title="%s" %s>%s</button>""" % (
|
|||
|
||||
self._activeWindowOnPlay: Optional[QWidget] = None
|
||||
|
||||
def onOdueInvalid(self):
|
||||
def onOdueInvalid(self) -> None:
|
||||
showWarning(tr(TR.QT_MISC_INVALID_PROPERTY_FOUND_ON_CARD_PLEASE))
|
||||
|
||||
def _isVideo(self, tag: AVTag) -> bool:
|
||||
|
@ -1274,7 +1284,7 @@ title="%s" %s>%s</button>""" % (
|
|||
##########################################################################
|
||||
|
||||
# this will gradually be phased out
|
||||
def onSchemaMod(self, arg):
|
||||
def onSchemaMod(self, arg: bool) -> bool:
|
||||
assert self.inMainThread()
|
||||
progress_shown = self.progress.busy()
|
||||
if progress_shown:
|
||||
|
@ -1295,14 +1305,14 @@ title="%s" %s>%s</button>""" % (
|
|||
# Advanced features
|
||||
##########################################################################
|
||||
|
||||
def onCheckDB(self):
|
||||
def onCheckDB(self) -> None:
|
||||
check_db(self)
|
||||
|
||||
def on_check_media_db(self) -> None:
|
||||
gui_hooks.media_check_will_start()
|
||||
check_media_db(self)
|
||||
|
||||
def onStudyDeck(self):
|
||||
def onStudyDeck(self) -> None:
|
||||
from aqt.studydeck import StudyDeck
|
||||
|
||||
ret = StudyDeck(self, dyn=True, current=self.col.decks.current()["name"])
|
||||
|
@ -1316,11 +1326,11 @@ title="%s" %s>%s</button>""" % (
|
|||
# Debugging
|
||||
######################################################################
|
||||
|
||||
def onDebug(self):
|
||||
def onDebug(self) -> None:
|
||||
frm = self.debug_diag_form = aqt.forms.debug.Ui_Dialog()
|
||||
|
||||
class DebugDialog(QDialog):
|
||||
def reject(self):
|
||||
def reject(self) -> None:
|
||||
super().reject()
|
||||
saveSplitter(frm.splitter, "DebugConsoleWindow")
|
||||
saveGeom(self, "DebugConsoleWindow")
|
||||
|
@ -1356,7 +1366,7 @@ title="%s" %s>%s</button>""" % (
|
|||
a = menu.addAction("Clear Code")
|
||||
a.setShortcuts(QKeySequence("ctrl+shift+l"))
|
||||
qconnect(a.triggered, frm.text.clear)
|
||||
menu.exec(QCursor.pos())
|
||||
menu.exec_(QCursor.pos())
|
||||
|
||||
frm.log.contextMenuEvent = lambda ev: addContextMenu(ev, "log")
|
||||
frm.text.contextMenuEvent = lambda ev: addContextMenu(ev, "text")
|
||||
|
@ -1367,7 +1377,7 @@ title="%s" %s>%s</button>""" % (
|
|||
mw = self
|
||||
|
||||
class Stream:
|
||||
def write(self, data):
|
||||
def write(self, data) -> None:
|
||||
mw._output += data
|
||||
|
||||
if on:
|
||||
|
@ -1418,7 +1428,7 @@ title="%s" %s>%s</button>""" % (
|
|||
self._card_repr(card)
|
||||
return card
|
||||
|
||||
def onDebugPrint(self, frm):
|
||||
def onDebugPrint(self, frm) -> None:
|
||||
cursor = frm.text.textCursor()
|
||||
position = cursor.position()
|
||||
cursor.select(QTextCursor.LineUnderCursor)
|
||||
|
@ -1431,7 +1441,7 @@ title="%s" %s>%s</button>""" % (
|
|||
frm.text.setTextCursor(cursor)
|
||||
self.onDebugRet(frm)
|
||||
|
||||
def onDebugRet(self, frm):
|
||||
def onDebugRet(self, frm) -> None:
|
||||
import pprint
|
||||
import traceback
|
||||
|
||||
|
@ -1509,14 +1519,15 @@ title="%s" %s>%s</button>""" % (
|
|||
def setupAppMsg(self) -> None:
|
||||
qconnect(self.app.appMsg, self.onAppMsg)
|
||||
|
||||
def onAppMsg(self, buf: str) -> Optional[QTimer]:
|
||||
def onAppMsg(self, buf: str) -> None:
|
||||
is_addon = self._isAddon(buf)
|
||||
|
||||
if self.state == "startup":
|
||||
# try again in a second
|
||||
return self.progress.timer(
|
||||
self.progress.timer(
|
||||
1000, lambda: self.onAppMsg(buf), False, requiresCollection=False
|
||||
)
|
||||
return
|
||||
elif self.state == "profileManager":
|
||||
# can't raise window while in profile manager
|
||||
if buf == "raise":
|
||||
|
@ -1526,7 +1537,8 @@ title="%s" %s>%s</button>""" % (
|
|||
msg = tr(TR.QT_MISC_ADDON_WILL_BE_INSTALLED_WHEN_A)
|
||||
else:
|
||||
msg = tr(TR.QT_MISC_DECK_WILL_BE_IMPORTED_WHEN_A)
|
||||
return tooltip(msg)
|
||||
tooltip(msg)
|
||||
return
|
||||
if not self.interactiveState() or self.progress.busy():
|
||||
# we can't raise the main window while in profile dialog, syncing, etc
|
||||
if buf != "raise":
|
||||
|
|
|
@ -136,7 +136,7 @@ class MediaChecker:
|
|||
diag.exec_()
|
||||
saveGeom(diag, "checkmediadb")
|
||||
|
||||
def _on_render_latex(self):
|
||||
def _on_render_latex(self) -> None:
|
||||
self.progress_dialog = self.mw.progress.start()
|
||||
try:
|
||||
out = self.mw.col.media.render_all_latex(self._on_render_latex_progress)
|
||||
|
@ -160,7 +160,7 @@ class MediaChecker:
|
|||
self.mw.progress.update(tr(TR.MEDIA_CHECK_CHECKED, count=count))
|
||||
return True
|
||||
|
||||
def _on_trash_files(self, fnames: Sequence[str]):
|
||||
def _on_trash_files(self, fnames: Sequence[str]) -> None:
|
||||
if not askUser(tr(TR.MEDIA_CHECK_DELETE_UNUSED_CONFIRM)):
|
||||
return
|
||||
|
||||
|
@ -183,14 +183,14 @@ class MediaChecker:
|
|||
|
||||
tooltip(tr(TR.MEDIA_CHECK_DELETE_UNUSED_COMPLETE, count=total))
|
||||
|
||||
def _on_empty_trash(self):
|
||||
def _on_empty_trash(self) -> None:
|
||||
self.progress_dialog = self.mw.progress.start()
|
||||
self._set_progress_enabled(True)
|
||||
|
||||
def empty_trash():
|
||||
def empty_trash() -> None:
|
||||
self.mw.col.media.empty_trash()
|
||||
|
||||
def on_done(fut: Future):
|
||||
def on_done(fut: Future) -> None:
|
||||
self.mw.progress.finish()
|
||||
self._set_progress_enabled(False)
|
||||
# check for errors
|
||||
|
@ -200,14 +200,14 @@ class MediaChecker:
|
|||
|
||||
self.mw.taskman.run_in_background(empty_trash, on_done)
|
||||
|
||||
def _on_restore_trash(self):
|
||||
def _on_restore_trash(self) -> None:
|
||||
self.progress_dialog = self.mw.progress.start()
|
||||
self._set_progress_enabled(True)
|
||||
|
||||
def restore_trash():
|
||||
def restore_trash() -> None:
|
||||
self.mw.col.media.restore_trash()
|
||||
|
||||
def on_done(fut: Future):
|
||||
def on_done(fut: Future) -> None:
|
||||
self.mw.progress.finish()
|
||||
self._set_progress_enabled(False)
|
||||
# check for errors
|
||||
|
|
|
@ -12,6 +12,7 @@ import threading
|
|||
import time
|
||||
import traceback
|
||||
from http import HTTPStatus
|
||||
from typing import Tuple
|
||||
|
||||
import flask
|
||||
import flask_cors # type: ignore
|
||||
|
@ -26,7 +27,7 @@ from aqt.qt import *
|
|||
from aqt.utils import aqt_data_folder
|
||||
|
||||
|
||||
def _getExportFolder():
|
||||
def _getExportFolder() -> str:
|
||||
data_folder = aqt_data_folder()
|
||||
webInSrcFolder = os.path.abspath(os.path.join(data_folder, "web"))
|
||||
if os.path.exists(webInSrcFolder):
|
||||
|
@ -52,11 +53,11 @@ class MediaServer(threading.Thread):
|
|||
_ready = threading.Event()
|
||||
daemon = True
|
||||
|
||||
def __init__(self, mw: aqt.main.AnkiQt, *args, **kwargs):
|
||||
def __init__(self, mw: aqt.main.AnkiQt, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.is_shutdown = False
|
||||
|
||||
def run(self):
|
||||
def run(self) -> None:
|
||||
try:
|
||||
if devMode:
|
||||
# idempotent if logging has already been set up
|
||||
|
@ -83,7 +84,7 @@ class MediaServer(threading.Thread):
|
|||
if not self.is_shutdown:
|
||||
raise
|
||||
|
||||
def shutdown(self):
|
||||
def shutdown(self) -> None:
|
||||
self.is_shutdown = True
|
||||
sockets = list(self.server._map.values()) # type: ignore
|
||||
for socket in sockets:
|
||||
|
@ -91,13 +92,13 @@ class MediaServer(threading.Thread):
|
|||
# https://github.com/Pylons/webtest/blob/4b8a3ebf984185ff4fefb31b4d0cf82682e1fcf7/webtest/http.py#L93-L104
|
||||
self.server.task_dispatcher.shutdown()
|
||||
|
||||
def getPort(self):
|
||||
def getPort(self) -> int:
|
||||
self._ready.wait()
|
||||
return int(self.server.effective_port) # type: ignore
|
||||
|
||||
|
||||
@app.route("/<path:pathin>", methods=["GET", "POST"])
|
||||
def allroutes(pathin):
|
||||
def allroutes(pathin) -> Response:
|
||||
try:
|
||||
directory, path = _redirectWebExports(pathin)
|
||||
except TypeError:
|
||||
|
@ -171,7 +172,7 @@ def allroutes(pathin):
|
|||
)
|
||||
|
||||
|
||||
def _redirectWebExports(path):
|
||||
def _redirectWebExports(path) -> Tuple[str, str]:
|
||||
# catch /_anki references and rewrite them to web export folder
|
||||
targetPath = "_anki/"
|
||||
if path.startswith(targetPath):
|
||||
|
|
|
@ -28,14 +28,14 @@ class LogEntryWithTime:
|
|||
|
||||
|
||||
class MediaSyncer:
|
||||
def __init__(self, mw: aqt.main.AnkiQt):
|
||||
def __init__(self, mw: aqt.main.AnkiQt) -> None:
|
||||
self.mw = mw
|
||||
self._syncing: bool = False
|
||||
self._log: List[LogEntryWithTime] = []
|
||||
self._progress_timer: Optional[QTimer] = None
|
||||
gui_hooks.media_sync_did_start_or_stop.append(self._on_start_stop)
|
||||
|
||||
def _on_progress(self):
|
||||
def _on_progress(self) -> None:
|
||||
progress = self.mw.col.latest_progress()
|
||||
if progress.kind != ProgressKind.MediaSync:
|
||||
return
|
||||
|
@ -88,7 +88,7 @@ class MediaSyncer:
|
|||
else:
|
||||
self._log_and_notify(tr(TR.SYNC_MEDIA_COMPLETE))
|
||||
|
||||
def _handle_sync_error(self, exc: BaseException):
|
||||
def _handle_sync_error(self, exc: BaseException) -> None:
|
||||
if isinstance(exc, Interrupted):
|
||||
self._log_and_notify(tr(TR.SYNC_MEDIA_ABORTED))
|
||||
return
|
||||
|
@ -116,10 +116,10 @@ class MediaSyncer:
|
|||
def _on_start_stop(self, running: bool) -> None:
|
||||
self.mw.toolbar.set_sync_active(running)
|
||||
|
||||
def show_sync_log(self):
|
||||
def show_sync_log(self) -> None:
|
||||
aqt.dialogs.open("sync_log", self.mw, self)
|
||||
|
||||
def show_diag_until_finished(self, on_finished: Callable[[], None]):
|
||||
def show_diag_until_finished(self, on_finished: Callable[[], None]) -> None:
|
||||
# nothing to do if not syncing
|
||||
if not self.is_syncing():
|
||||
return on_finished()
|
||||
|
@ -129,7 +129,7 @@ class MediaSyncer:
|
|||
|
||||
timer: Optional[QTimer] = None
|
||||
|
||||
def check_finished():
|
||||
def check_finished() -> None:
|
||||
if not self.is_syncing():
|
||||
timer.stop()
|
||||
on_finished()
|
||||
|
@ -197,7 +197,7 @@ class MediaSyncDialog(QDialog):
|
|||
asctime = time.asctime(time.localtime(stamp))
|
||||
return f"{asctime}: {text}"
|
||||
|
||||
def _entry_to_text(self, entry: LogEntryWithTime):
|
||||
def _entry_to_text(self, entry: LogEntryWithTime) -> str:
|
||||
if isinstance(entry.entry, str):
|
||||
txt = entry.entry
|
||||
elif isinstance(entry.entry, MediaSyncProgress):
|
||||
|
@ -209,7 +209,7 @@ class MediaSyncDialog(QDialog):
|
|||
def _logentry_to_text(self, e: MediaSyncProgress) -> str:
|
||||
return f"{e.added}, {e.removed}, {e.checked}"
|
||||
|
||||
def _on_log_entry(self, entry: LogEntryWithTime):
|
||||
def _on_log_entry(self, entry: LogEntryWithTime) -> None:
|
||||
self.form.plainTextEdit.appendPlainText(self._entry_to_text(entry))
|
||||
if not self._syncer.is_syncing():
|
||||
self.abort_button.setHidden(True)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
from typing import Optional
|
||||
from typing import List, Optional
|
||||
|
||||
from aqt import AnkiQt, gui_hooks
|
||||
from aqt.qt import *
|
||||
|
@ -74,7 +74,7 @@ class ModelChooser(QHBoxLayout):
|
|||
# edit button
|
||||
edit = QPushButton(tr(TR.QT_MISC_MANAGE), clicked=self.onEdit) # type: ignore
|
||||
|
||||
def nameFunc():
|
||||
def nameFunc() -> List[str]:
|
||||
return sorted(self.deck.models.allNames())
|
||||
|
||||
ret = StudyDeck(
|
||||
|
|
|
@ -57,7 +57,7 @@ class Models(QDialog):
|
|||
# Models
|
||||
##########################################################################
|
||||
|
||||
def maybe_select_provided_notetype(self):
|
||||
def maybe_select_provided_notetype(self) -> None:
|
||||
if not self.selected_notetype_id:
|
||||
self.form.modelsList.setCurrentRow(0)
|
||||
return
|
||||
|
@ -219,7 +219,7 @@ class Models(QDialog):
|
|||
class AddModel(QDialog):
|
||||
model: Optional[NoteType]
|
||||
|
||||
def __init__(self, mw: AnkiQt, parent: Optional[QWidget] = None):
|
||||
def __init__(self, mw: AnkiQt, parent: Optional[QWidget] = None) -> None:
|
||||
self.parent_ = parent or mw
|
||||
self.mw = mw
|
||||
self.col = mw.col
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||
|
||||
import aqt
|
||||
from aqt import gui_hooks
|
||||
|
@ -14,7 +14,7 @@ from aqt.utils import TR, askUserDialog, openLink, shortcut, tooltip, tr
|
|||
|
||||
|
||||
class OverviewBottomBar:
|
||||
def __init__(self, overview: Overview):
|
||||
def __init__(self, overview: Overview) -> None:
|
||||
self.overview = overview
|
||||
|
||||
|
||||
|
@ -44,13 +44,13 @@ class Overview:
|
|||
self.web = mw.web
|
||||
self.bottom = BottomBar(mw, mw.bottomWeb)
|
||||
|
||||
def show(self):
|
||||
def show(self) -> None:
|
||||
av_player.stop_and_clear_queue()
|
||||
self.web.set_bridge_command(self._linkHandler, self)
|
||||
self.mw.setStateShortcuts(self._shortcutKeys())
|
||||
self.refresh()
|
||||
|
||||
def refresh(self):
|
||||
def refresh(self) -> None:
|
||||
self.mw.col.reset()
|
||||
self._renderPage()
|
||||
self._renderBottom()
|
||||
|
@ -60,7 +60,7 @@ class Overview:
|
|||
# Handlers
|
||||
############################################################
|
||||
|
||||
def _linkHandler(self, url):
|
||||
def _linkHandler(self, url: str) -> bool:
|
||||
if url == "study":
|
||||
self.mw.col.startTimebox()
|
||||
self.mw.moveToState("review")
|
||||
|
@ -90,7 +90,7 @@ class Overview:
|
|||
openLink(url)
|
||||
return False
|
||||
|
||||
def _shortcutKeys(self):
|
||||
def _shortcutKeys(self) -> List[Tuple[str, Callable]]:
|
||||
return [
|
||||
("o", self.mw.onDeckConf),
|
||||
("r", self.onRebuildKey),
|
||||
|
@ -99,24 +99,24 @@ class Overview:
|
|||
("u", self.onUnbury),
|
||||
]
|
||||
|
||||
def _filteredDeck(self):
|
||||
def _filteredDeck(self) -> int:
|
||||
return self.mw.col.decks.current()["dyn"]
|
||||
|
||||
def onRebuildKey(self):
|
||||
def onRebuildKey(self) -> None:
|
||||
if self._filteredDeck():
|
||||
self.mw.col.sched.rebuild_filtered_deck(self.mw.col.decks.selected())
|
||||
self.mw.reset()
|
||||
|
||||
def onEmptyKey(self):
|
||||
def onEmptyKey(self) -> None:
|
||||
if self._filteredDeck():
|
||||
self.mw.col.sched.empty_filtered_deck(self.mw.col.decks.selected())
|
||||
self.mw.reset()
|
||||
|
||||
def onCustomStudyKey(self):
|
||||
def onCustomStudyKey(self) -> None:
|
||||
if not self._filteredDeck():
|
||||
self.onStudyMore()
|
||||
|
||||
def onUnbury(self):
|
||||
def onUnbury(self) -> None:
|
||||
if self.mw.col.schedVer() == 1:
|
||||
self.mw.col.sched.unburyCardsForDeck()
|
||||
self.mw.reset()
|
||||
|
@ -148,7 +148,7 @@ class Overview:
|
|||
# HTML
|
||||
############################################################
|
||||
|
||||
def _renderPage(self):
|
||||
def _renderPage(self) -> None:
|
||||
but = self.mw.button
|
||||
deck = self.mw.col.decks.current()
|
||||
self.sid = deck.get("sharedFrom")
|
||||
|
@ -175,10 +175,10 @@ class Overview:
|
|||
context=self,
|
||||
)
|
||||
|
||||
def _show_finished_screen(self):
|
||||
def _show_finished_screen(self) -> None:
|
||||
self.web.load_ts_page("congrats")
|
||||
|
||||
def _desc(self, deck):
|
||||
def _desc(self, deck: Dict[str, Any]) -> str:
|
||||
if deck["dyn"]:
|
||||
desc = tr(TR.STUDYING_THIS_IS_A_SPECIAL_DECK_FOR)
|
||||
desc += " " + tr(TR.STUDYING_CARDS_WILL_BE_AUTOMATICALLY_RETURNED_TO)
|
||||
|
@ -227,7 +227,7 @@ class Overview:
|
|||
# Bottom area
|
||||
######################################################################
|
||||
|
||||
def _renderBottom(self):
|
||||
def _renderBottom(self) -> None:
|
||||
links = [
|
||||
["O", "opts", tr(TR.ACTIONS_OPTIONS)],
|
||||
]
|
||||
|
@ -254,7 +254,7 @@ class Overview:
|
|||
# Studying more
|
||||
######################################################################
|
||||
|
||||
def onStudyMore(self):
|
||||
def onStudyMore(self) -> None:
|
||||
import aqt.customstudy
|
||||
|
||||
aqt.customstudy.CustomStudy(self.mw)
|
||||
|
|
|
@ -34,7 +34,7 @@ def video_driver_name_for_platform(driver: VideoDriver) -> str:
|
|||
|
||||
|
||||
class Preferences(QDialog):
|
||||
def __init__(self, mw: AnkiQt):
|
||||
def __init__(self, mw: AnkiQt) -> None:
|
||||
QDialog.__init__(self, mw, Qt.Window)
|
||||
self.mw = mw
|
||||
self.prof = self.mw.pm.profile
|
||||
|
@ -55,7 +55,7 @@ class Preferences(QDialog):
|
|||
self.setupOptions()
|
||||
self.show()
|
||||
|
||||
def accept(self):
|
||||
def accept(self) -> None:
|
||||
# avoid exception if main window is already closed
|
||||
if not self.mw.col:
|
||||
return
|
||||
|
@ -68,19 +68,19 @@ class Preferences(QDialog):
|
|||
self.done(0)
|
||||
aqt.dialogs.markClosed("Preferences")
|
||||
|
||||
def reject(self):
|
||||
def reject(self) -> None:
|
||||
self.accept()
|
||||
|
||||
# Language
|
||||
######################################################################
|
||||
|
||||
def setupLang(self):
|
||||
def setupLang(self) -> None:
|
||||
f = self.form
|
||||
f.lang.addItems([x[0] for x in anki.lang.langs])
|
||||
f.lang.setCurrentIndex(self.langIdx())
|
||||
qconnect(f.lang.currentIndexChanged, self.onLangIdxChanged)
|
||||
|
||||
def langIdx(self):
|
||||
def langIdx(self) -> int:
|
||||
codes = [x[1] for x in anki.lang.langs]
|
||||
lang = anki.lang.currentLang
|
||||
if lang in anki.lang.compatMap:
|
||||
|
@ -92,7 +92,7 @@ class Preferences(QDialog):
|
|||
except:
|
||||
return codes.index("en_US")
|
||||
|
||||
def onLangIdxChanged(self, idx):
|
||||
def onLangIdxChanged(self, idx) -> None:
|
||||
code = anki.lang.langs[idx][1]
|
||||
self.mw.pm.setLang(code)
|
||||
showInfo(
|
||||
|
@ -102,7 +102,7 @@ class Preferences(QDialog):
|
|||
# Collection options
|
||||
######################################################################
|
||||
|
||||
def setupCollection(self):
|
||||
def setupCollection(self) -> None:
|
||||
import anki.consts as c
|
||||
|
||||
f = self.form
|
||||
|
@ -130,7 +130,7 @@ class Preferences(QDialog):
|
|||
f.newSched.setChecked(True)
|
||||
f.new_timezone.setChecked(s.new_timezone)
|
||||
|
||||
def setup_video_driver(self):
|
||||
def setup_video_driver(self) -> None:
|
||||
self.video_drivers = VideoDriver.all_for_platform()
|
||||
names = [
|
||||
tr(TR.PREFERENCES_VIDEO_DRIVER, driver=video_driver_name_for_platform(d))
|
||||
|
@ -141,13 +141,13 @@ class Preferences(QDialog):
|
|||
self.video_drivers.index(self.mw.pm.video_driver())
|
||||
)
|
||||
|
||||
def update_video_driver(self):
|
||||
def update_video_driver(self) -> None:
|
||||
new_driver = self.video_drivers[self.form.video_driver.currentIndex()]
|
||||
if new_driver != self.mw.pm.video_driver():
|
||||
self.mw.pm.set_video_driver(new_driver)
|
||||
showInfo(tr(TR.PREFERENCES_CHANGES_WILL_TAKE_EFFECT_WHEN_YOU))
|
||||
|
||||
def updateCollection(self):
|
||||
def updateCollection(self) -> None:
|
||||
f = self.form
|
||||
d = self.mw.col
|
||||
|
||||
|
@ -176,7 +176,7 @@ class Preferences(QDialog):
|
|||
# Scheduler version
|
||||
######################################################################
|
||||
|
||||
def _updateSchedVer(self, wantNew):
|
||||
def _updateSchedVer(self, wantNew) -> None:
|
||||
haveNew = self.mw.col.schedVer() == 2
|
||||
|
||||
# nothing to do?
|
||||
|
@ -194,7 +194,7 @@ class Preferences(QDialog):
|
|||
# Network
|
||||
######################################################################
|
||||
|
||||
def setupNetwork(self):
|
||||
def setupNetwork(self) -> None:
|
||||
self.form.media_log.setText(tr(TR.SYNC_MEDIA_LOG_BUTTON))
|
||||
qconnect(self.form.media_log.clicked, self.on_media_log)
|
||||
self.form.syncOnProgramOpen.setChecked(self.prof["autoSync"])
|
||||
|
@ -207,10 +207,10 @@ class Preferences(QDialog):
|
|||
qconnect(self.form.syncDeauth.clicked, self.onSyncDeauth)
|
||||
self.form.syncDeauth.setText(tr(TR.SYNC_LOG_OUT_BUTTON))
|
||||
|
||||
def on_media_log(self):
|
||||
def on_media_log(self) -> None:
|
||||
self.mw.media_syncer.show_sync_log()
|
||||
|
||||
def _hideAuth(self):
|
||||
def _hideAuth(self) -> None:
|
||||
self.form.syncDeauth.setVisible(False)
|
||||
self.form.syncUser.setText("")
|
||||
self.form.syncLabel.setText(
|
||||
|
@ -225,7 +225,7 @@ class Preferences(QDialog):
|
|||
self.mw.col.media.force_resync()
|
||||
self._hideAuth()
|
||||
|
||||
def updateNetwork(self):
|
||||
def updateNetwork(self) -> None:
|
||||
self.prof["autoSync"] = self.form.syncOnProgramOpen.isChecked()
|
||||
self.prof["syncMedia"] = self.form.syncMedia.isChecked()
|
||||
self.mw.pm.set_auto_sync_media_minutes(
|
||||
|
@ -238,16 +238,16 @@ class Preferences(QDialog):
|
|||
# Backup
|
||||
######################################################################
|
||||
|
||||
def setupBackup(self):
|
||||
def setupBackup(self) -> None:
|
||||
self.form.numBackups.setValue(self.prof["numBackups"])
|
||||
|
||||
def updateBackup(self):
|
||||
def updateBackup(self) -> None:
|
||||
self.prof["numBackups"] = self.form.numBackups.value()
|
||||
|
||||
# Basic & Advanced Options
|
||||
######################################################################
|
||||
|
||||
def setupOptions(self):
|
||||
def setupOptions(self) -> None:
|
||||
self.form.pastePNG.setChecked(self.prof.get("pastePNG", False))
|
||||
self.form.uiScale.setValue(int(self.mw.pm.uiScale() * 100))
|
||||
self.form.pasteInvert.setChecked(self.prof.get("pasteInvert", False))
|
||||
|
@ -270,7 +270,7 @@ class Preferences(QDialog):
|
|||
self._recording_drivers.index(self.mw.pm.recording_driver())
|
||||
)
|
||||
|
||||
def updateOptions(self):
|
||||
def updateOptions(self) -> None:
|
||||
restart_required = False
|
||||
|
||||
self.prof["pastePNG"] = self.form.pastePNG.isChecked()
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
import json
|
||||
import re
|
||||
import time
|
||||
from typing import Any, Callable, Optional, Union
|
||||
from typing import Any, Callable, Optional, Tuple, Union
|
||||
|
||||
from anki.cards import Card
|
||||
from anki.collection import ConfigBoolKey
|
||||
|
@ -19,6 +19,7 @@ from aqt.qt import (
|
|||
QPixmap,
|
||||
QShortcut,
|
||||
Qt,
|
||||
QTimer,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
qconnect,
|
||||
|
@ -29,15 +30,19 @@ from aqt.theme import theme_manager
|
|||
from aqt.utils import TR, disable_help_button, restoreGeom, saveGeom, tr
|
||||
from aqt.webview import AnkiWebView
|
||||
|
||||
LastStateAndMod = Tuple[str, int, int]
|
||||
|
||||
|
||||
class Previewer(QDialog):
|
||||
_last_state = None
|
||||
_last_state: Optional[LastStateAndMod] = None
|
||||
_card_changed = False
|
||||
_last_render: Union[int, float] = 0
|
||||
_timer = None
|
||||
_timer: Optional[QTimer] = None
|
||||
_show_both_sides = False
|
||||
|
||||
def __init__(self, parent: QWidget, mw: AnkiQt, on_close: Callable[[], None]):
|
||||
def __init__(
|
||||
self, parent: QWidget, mw: AnkiQt, on_close: Callable[[], None]
|
||||
) -> None:
|
||||
super().__init__(None, Qt.Window)
|
||||
self._open = True
|
||||
self._parent = parent
|
||||
|
@ -54,7 +59,7 @@ class Previewer(QDialog):
|
|||
def card_changed(self) -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
def open(self):
|
||||
def open(self) -> None:
|
||||
self._state = "question"
|
||||
self._last_state = None
|
||||
self._create_gui()
|
||||
|
@ -62,7 +67,7 @@ class Previewer(QDialog):
|
|||
self.render_card()
|
||||
self.show()
|
||||
|
||||
def _create_gui(self):
|
||||
def _create_gui(self) -> None:
|
||||
self.setWindowTitle(tr(TR.ACTIONS_PREVIEW))
|
||||
|
||||
self.close_shortcut = QShortcut(QKeySequence("Ctrl+Shift+P"), self)
|
||||
|
@ -98,25 +103,25 @@ class Previewer(QDialog):
|
|||
self.setLayout(self.vbox)
|
||||
restoreGeom(self, "preview")
|
||||
|
||||
def _on_finished(self, ok):
|
||||
def _on_finished(self, ok: int) -> None:
|
||||
saveGeom(self, "preview")
|
||||
self.mw.progress.timer(100, self._on_close, False)
|
||||
|
||||
def _on_replay_audio(self):
|
||||
def _on_replay_audio(self) -> None:
|
||||
if self._state == "question":
|
||||
replay_audio(self.card(), True)
|
||||
elif self._state == "answer":
|
||||
replay_audio(self.card(), False)
|
||||
|
||||
def close(self):
|
||||
def close(self) -> None:
|
||||
self._on_close()
|
||||
super().close()
|
||||
|
||||
def _on_close(self):
|
||||
def _on_close(self) -> None:
|
||||
self._open = False
|
||||
self._close_callback()
|
||||
|
||||
def _setup_web_view(self):
|
||||
def _setup_web_view(self) -> None:
|
||||
jsinc = [
|
||||
"js/vendor/jquery.min.js",
|
||||
"js/vendor/css_browser_selector.min.js",
|
||||
|
@ -136,7 +141,7 @@ class Previewer(QDialog):
|
|||
if cmd.startswith("play:"):
|
||||
play_clicked_audio(cmd, self.card())
|
||||
|
||||
def render_card(self):
|
||||
def render_card(self) -> None:
|
||||
self.cancel_timer()
|
||||
# Keep track of whether render() has ever been called
|
||||
# with cardChanged=True since the last successful render
|
||||
|
@ -151,7 +156,7 @@ class Previewer(QDialog):
|
|||
else:
|
||||
self._render_scheduled()
|
||||
|
||||
def cancel_timer(self):
|
||||
def cancel_timer(self) -> None:
|
||||
if self._timer:
|
||||
self._timer.stop()
|
||||
self._timer = None
|
||||
|
@ -214,7 +219,7 @@ class Previewer(QDialog):
|
|||
self._web.eval("{}({},'{}');".format(func, json.dumps(txt), bodyclass))
|
||||
self._card_changed = False
|
||||
|
||||
def _on_show_both_sides(self, toggle):
|
||||
def _on_show_both_sides(self, toggle: bool) -> None:
|
||||
self._show_both_sides = toggle
|
||||
self.mw.col.set_config_bool(ConfigBoolKey.PREVIEW_BOTH_SIDES, toggle)
|
||||
self.mw.col.setMod()
|
||||
|
@ -222,7 +227,7 @@ class Previewer(QDialog):
|
|||
self._state = "question"
|
||||
self.render_card()
|
||||
|
||||
def _state_and_mod(self):
|
||||
def _state_and_mod(self) -> Tuple[str, int, int]:
|
||||
c = self.card()
|
||||
n = c.note()
|
||||
n.load()
|
||||
|
@ -241,7 +246,7 @@ class MultiCardPreviewer(Previewer):
|
|||
# need to state explicitly it's not implement to avoid W0223
|
||||
raise NotImplementedError
|
||||
|
||||
def _create_gui(self):
|
||||
def _create_gui(self) -> None:
|
||||
super()._create_gui()
|
||||
self._prev = self.bbox.addButton("<", QDialogButtonBox.ActionRole)
|
||||
self._prev.setAutoDefault(False)
|
||||
|
@ -256,39 +261,39 @@ class MultiCardPreviewer(Previewer):
|
|||
qconnect(self._prev.clicked, self._on_prev)
|
||||
qconnect(self._next.clicked, self._on_next)
|
||||
|
||||
def _on_prev(self):
|
||||
def _on_prev(self) -> None:
|
||||
if self._state == "answer" and not self._show_both_sides:
|
||||
self._state = "question"
|
||||
self.render_card()
|
||||
else:
|
||||
self._on_prev_card()
|
||||
|
||||
def _on_prev_card(self):
|
||||
def _on_prev_card(self) -> None:
|
||||
pass
|
||||
|
||||
def _on_next(self):
|
||||
def _on_next(self) -> None:
|
||||
if self._state == "question":
|
||||
self._state = "answer"
|
||||
self.render_card()
|
||||
else:
|
||||
self._on_next_card()
|
||||
|
||||
def _on_next_card(self):
|
||||
def _on_next_card(self) -> None:
|
||||
pass
|
||||
|
||||
def _updateButtons(self):
|
||||
def _updateButtons(self) -> None:
|
||||
if not self._open:
|
||||
return
|
||||
self._prev.setEnabled(self._should_enable_prev())
|
||||
self._next.setEnabled(self._should_enable_next())
|
||||
|
||||
def _should_enable_prev(self):
|
||||
def _should_enable_prev(self) -> bool:
|
||||
return self._state == "answer" and not self._show_both_sides
|
||||
|
||||
def _should_enable_next(self):
|
||||
def _should_enable_next(self) -> bool:
|
||||
return self._state == "question"
|
||||
|
||||
def _on_close(self):
|
||||
def _on_close(self) -> None:
|
||||
super()._on_close()
|
||||
self._prev = None
|
||||
self._next = None
|
||||
|
@ -312,20 +317,20 @@ class BrowserPreviewer(MultiCardPreviewer):
|
|||
self._last_card_id = c.id
|
||||
return changed
|
||||
|
||||
def _on_prev_card(self):
|
||||
def _on_prev_card(self) -> None:
|
||||
self._parent.editor.saveNow(
|
||||
lambda: self._parent._moveCur(QAbstractItemView.MoveUp)
|
||||
)
|
||||
|
||||
def _on_next_card(self):
|
||||
def _on_next_card(self) -> None:
|
||||
self._parent.editor.saveNow(
|
||||
lambda: self._parent._moveCur(QAbstractItemView.MoveDown)
|
||||
)
|
||||
|
||||
def _should_enable_prev(self):
|
||||
def _should_enable_prev(self) -> bool:
|
||||
return super()._should_enable_prev() or self._parent.currentRow() > 0
|
||||
|
||||
def _should_enable_next(self):
|
||||
def _should_enable_next(self) -> bool:
|
||||
return (
|
||||
super()._should_enable_next()
|
||||
or self._parent.currentRow() < self._parent.model.rowCount(None) - 1
|
||||
|
|
|
@ -113,17 +113,17 @@ class LoadMetaResult:
|
|||
|
||||
|
||||
class AnkiRestart(SystemExit):
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
self.exitcode = kwargs.pop("exitcode", 0)
|
||||
super().__init__(*args, **kwargs) # type: ignore
|
||||
|
||||
|
||||
class ProfileManager:
|
||||
def __init__(self, base=None):
|
||||
def __init__(self, base: Optional[str] = None) -> None: #
|
||||
## Settings which should be forgotten each Anki restart
|
||||
self.session = {}
|
||||
self.name = None
|
||||
self.db = None
|
||||
self.session: Dict[str, Any] = {}
|
||||
self.name: Optional[str] = None
|
||||
self.db: Optional[DB] = None
|
||||
self.profile: Optional[Dict] = None
|
||||
# instantiate base folder
|
||||
self.base: str
|
||||
|
@ -170,7 +170,7 @@ class ProfileManager:
|
|||
return p
|
||||
return os.path.expanduser("~/Documents/Anki")
|
||||
|
||||
def maybeMigrateFolder(self):
|
||||
def maybeMigrateFolder(self) -> None:
|
||||
newBase = self.base
|
||||
oldBase = self._oldFolderLocation()
|
||||
|
||||
|
@ -185,7 +185,7 @@ class ProfileManager:
|
|||
self.base = newBase
|
||||
shutil.move(oldBase, self.base)
|
||||
|
||||
def _tryToMigrateFolder(self, oldBase):
|
||||
def _tryToMigrateFolder(self, oldBase) -> None:
|
||||
from PyQt5 import QtGui, QtWidgets
|
||||
|
||||
app = QtWidgets.QApplication([])
|
||||
|
@ -206,7 +206,7 @@ class ProfileManager:
|
|||
confirmation.setText(
|
||||
"Anki needs to move its data folder from Documents/Anki to a new location. Proceed?"
|
||||
)
|
||||
retval = confirmation.exec()
|
||||
retval = confirmation.exec_()
|
||||
|
||||
if retval == QMessageBox.Ok:
|
||||
progress = QMessageBox()
|
||||
|
@ -228,7 +228,7 @@ class ProfileManager:
|
|||
completion.setWindowTitle(window_title)
|
||||
completion.setText("Migration complete. Please start Anki again.")
|
||||
completion.show()
|
||||
completion.exec()
|
||||
completion.exec_()
|
||||
else:
|
||||
diag = QMessageBox()
|
||||
diag.setIcon(QMessageBox.Warning)
|
||||
|
@ -239,7 +239,7 @@ class ProfileManager:
|
|||
"Migration aborted. If you would like to keep the old folder location, please "
|
||||
"see the Startup Options section of the manual. Anki will now quit."
|
||||
)
|
||||
diag.exec()
|
||||
diag.exec_()
|
||||
|
||||
raise AnkiRestart(exitcode=0)
|
||||
|
||||
|
@ -269,7 +269,7 @@ class ProfileManager:
|
|||
fn = super().find_class(module, name)
|
||||
if module == "sip" and name == "_unpickle_type":
|
||||
|
||||
def wrapper(mod, obj, args):
|
||||
def wrapper(mod, obj, args) -> Any:
|
||||
if mod.startswith("PyQt4") and obj == "QByteArray":
|
||||
# can't trust str objects from python 2
|
||||
return QByteArray()
|
||||
|
@ -424,7 +424,7 @@ class ProfileManager:
|
|||
os.makedirs(path)
|
||||
return path
|
||||
|
||||
def _setBaseFolder(self, cmdlineBase: None) -> None:
|
||||
def _setBaseFolder(self, cmdlineBase: Optional[str]) -> None:
|
||||
if cmdlineBase:
|
||||
self.base = os.path.abspath(cmdlineBase)
|
||||
elif os.environ.get("ANKI_BASE"):
|
||||
|
@ -534,7 +534,7 @@ create table if not exists profiles
|
|||
def setDefaultLang(self, idx: int) -> None:
|
||||
# create dialog
|
||||
class NoCloseDiag(QDialog):
|
||||
def reject(self):
|
||||
def reject(self) -> None:
|
||||
pass
|
||||
|
||||
d = self.langDiag = NoCloseDiag()
|
||||
|
@ -665,7 +665,7 @@ create table if not exists profiles
|
|||
pass
|
||||
return RecordingDriver.QtAudioInput
|
||||
|
||||
def set_recording_driver(self, driver: RecordingDriver):
|
||||
def set_recording_driver(self, driver: RecordingDriver) -> None:
|
||||
self.profile["recordingDriver"] = driver.value
|
||||
|
||||
######################################################################
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import Optional
|
||||
from typing import Callable, Optional
|
||||
|
||||
import aqt.forms
|
||||
from aqt.qt import *
|
||||
|
@ -15,13 +15,13 @@ from aqt.utils import TR, disable_help_button, tr
|
|||
|
||||
|
||||
class ProgressManager:
|
||||
def __init__(self, mw):
|
||||
def __init__(self, mw: aqt.AnkiQt) -> None:
|
||||
self.mw = mw
|
||||
self.app = QApplication.instance()
|
||||
self.inDB = False
|
||||
self.blockUpdates = False
|
||||
self._show_timer: Optional[QTimer] = None
|
||||
self._win = None
|
||||
self._win: Optional[ProgressDialog] = None
|
||||
self._levels = 0
|
||||
|
||||
# Safer timers
|
||||
|
@ -29,7 +29,9 @@ class ProgressManager:
|
|||
# A custom timer which avoids firing while a progress dialog is active
|
||||
# (likely due to some long-running DB operation)
|
||||
|
||||
def timer(self, ms, func, repeat, requiresCollection=True):
|
||||
def timer(
|
||||
self, ms: int, func: Callable, repeat: bool, requiresCollection: bool = True
|
||||
) -> QTimer:
|
||||
"""Create and start a standard Anki timer.
|
||||
|
||||
If the timer fires while a progress window is shown:
|
||||
|
@ -41,7 +43,7 @@ class ProgressManager:
|
|||
timer to fire even when there is no collection, but will still
|
||||
only fire when there is no current progress dialog."""
|
||||
|
||||
def handler():
|
||||
def handler() -> None:
|
||||
if requiresCollection and not self.mw.col:
|
||||
# no current collection; timer is no longer valid
|
||||
print("Ignored progress func as collection unloaded: %s" % repr(func))
|
||||
|
@ -136,7 +138,7 @@ class ProgressManager:
|
|||
self._updating = False
|
||||
self._lastUpdate = time.time()
|
||||
|
||||
def finish(self):
|
||||
def finish(self) -> None:
|
||||
self._levels -= 1
|
||||
self._levels = max(0, self._levels)
|
||||
if self._levels == 0:
|
||||
|
@ -147,13 +149,13 @@ class ProgressManager:
|
|||
self._show_timer.stop()
|
||||
self._show_timer = None
|
||||
|
||||
def clear(self):
|
||||
def clear(self) -> None:
|
||||
"Restore the interface after an error."
|
||||
if self._levels:
|
||||
self._levels = 1
|
||||
self.finish()
|
||||
|
||||
def _maybeShow(self):
|
||||
def _maybeShow(self) -> None:
|
||||
if not self._levels:
|
||||
return
|
||||
if self._shown:
|
||||
|
@ -181,17 +183,17 @@ class ProgressManager:
|
|||
self._win = None
|
||||
self._shown = 0
|
||||
|
||||
def _setBusy(self):
|
||||
def _setBusy(self) -> None:
|
||||
self.mw.app.setOverrideCursor(QCursor(Qt.WaitCursor))
|
||||
|
||||
def _unsetBusy(self):
|
||||
def _unsetBusy(self) -> None:
|
||||
self.app.restoreOverrideCursor()
|
||||
|
||||
def busy(self):
|
||||
def busy(self) -> int:
|
||||
"True if processing."
|
||||
return self._levels
|
||||
|
||||
def _on_show_timer(self):
|
||||
def _on_show_timer(self) -> None:
|
||||
self._show_timer = None
|
||||
self._showWin()
|
||||
|
||||
|
@ -209,7 +211,7 @@ class ProgressManager:
|
|||
|
||||
|
||||
class ProgressDialog(QDialog):
|
||||
def __init__(self, parent):
|
||||
def __init__(self, parent: QWidget) -> None:
|
||||
QDialog.__init__(self, parent)
|
||||
disable_help_button(self)
|
||||
self.form = aqt.forms.progress.Ui_Dialog()
|
||||
|
@ -219,18 +221,18 @@ class ProgressDialog(QDialog):
|
|||
# required for smooth progress bars
|
||||
self.form.progressBar.setStyleSheet("QProgressBar::chunk { width: 1px; }")
|
||||
|
||||
def cancel(self):
|
||||
def cancel(self) -> None:
|
||||
self._closingDown = True
|
||||
self.hide()
|
||||
|
||||
def closeEvent(self, evt):
|
||||
def closeEvent(self, evt) -> None:
|
||||
if self._closingDown:
|
||||
evt.accept()
|
||||
else:
|
||||
self.wantCancel = True
|
||||
evt.ignore()
|
||||
|
||||
def keyPressEvent(self, evt):
|
||||
def keyPressEvent(self, evt) -> None:
|
||||
if evt.key() == Qt.Key_Escape:
|
||||
evt.ignore()
|
||||
self.wantCancel = True
|
||||
|
|
|
@ -30,7 +30,7 @@ except ImportError:
|
|||
import sip # type: ignore
|
||||
|
||||
|
||||
def debug():
|
||||
def debug() -> None:
|
||||
from pdb import set_trace
|
||||
|
||||
pyqtRemoveInputHook()
|
||||
|
@ -39,7 +39,7 @@ def debug():
|
|||
|
||||
if os.environ.get("DEBUG"):
|
||||
|
||||
def info(type, value, tb):
|
||||
def info(type, value, tb) -> None:
|
||||
for line in traceback.format_exception(type, value, tb):
|
||||
sys.stdout.write(line)
|
||||
pyqtRemoveInputHook()
|
||||
|
|
|
@ -8,7 +8,7 @@ import html
|
|||
import json
|
||||
import re
|
||||
import unicodedata as ucd
|
||||
from typing import Callable, List, Optional, Tuple, Union
|
||||
from typing import Any, Callable, List, Match, Optional, Tuple, Union
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
|
@ -426,7 +426,7 @@ class Reviewer:
|
|||
# compare with typed answer
|
||||
res = self.correct(given, cor, showBad=False)
|
||||
# and update the type answer area
|
||||
def repl(match):
|
||||
def repl(match: Match) -> str:
|
||||
# can't pass a string in directly, and can't use re.escape as it
|
||||
# escapes too much
|
||||
s = """
|
||||
|
@ -448,7 +448,7 @@ class Reviewer:
|
|||
if not matches:
|
||||
return None
|
||||
|
||||
def noHint(txt):
|
||||
def noHint(txt) -> str:
|
||||
if "::" in txt:
|
||||
return txt.split("::")[0]
|
||||
return txt
|
||||
|
@ -652,7 +652,7 @@ time = %(time)d;
|
|||
def _answerButtons(self) -> str:
|
||||
default = self._defaultEase()
|
||||
|
||||
def but(i, label):
|
||||
def but(i, label) -> str:
|
||||
if i == default:
|
||||
extra = """id="defease" class="focus" """
|
||||
else:
|
||||
|
@ -697,7 +697,7 @@ time = %(time)d;
|
|||
##########################################################################
|
||||
|
||||
# note the shortcuts listed here also need to be defined above
|
||||
def _contextMenu(self):
|
||||
def _contextMenu(self) -> List[Any]:
|
||||
currentFlag = self.card and self.card.userFlag()
|
||||
opts = [
|
||||
[
|
||||
|
@ -834,7 +834,7 @@ time = %(time)d;
|
|||
tooltip(tr(TR.STUDYING_NOTE_BURIED))
|
||||
|
||||
def onRecordVoice(self) -> None:
|
||||
def after_record(path: str):
|
||||
def after_record(path: str) -> None:
|
||||
self._recordedAudio = path
|
||||
self.onReplayRecorded()
|
||||
|
||||
|
|
|
@ -17,10 +17,10 @@ class Change(enum.Enum):
|
|||
class ChangeTracker:
|
||||
_changed = Change.NO_CHANGE
|
||||
|
||||
def __init__(self, mw: AnkiQt):
|
||||
def __init__(self, mw: AnkiQt) -> None:
|
||||
self.mw = mw
|
||||
|
||||
def mark_basic(self):
|
||||
def mark_basic(self) -> None:
|
||||
if self._changed == Change.NO_CHANGE:
|
||||
self._changed = Change.BASIC_CHANGE
|
||||
|
||||
|
|
|
@ -6,7 +6,17 @@ from __future__ import annotations
|
|||
|
||||
from concurrent.futures import Future
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Sequence, Tuple, cast
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Dict,
|
||||
Iterable,
|
||||
List,
|
||||
Optional,
|
||||
Sequence,
|
||||
Tuple,
|
||||
cast,
|
||||
)
|
||||
|
||||
import aqt
|
||||
from anki.collection import ConfigBoolKey, SearchTerm
|
||||
|
@ -101,7 +111,7 @@ class SidebarModel(QAbstractItemModel):
|
|||
self.root = root
|
||||
self._cache_rows(root)
|
||||
|
||||
def _cache_rows(self, node: SidebarItem):
|
||||
def _cache_rows(self, node: SidebarItem) -> None:
|
||||
"Cache index of children in parent."
|
||||
for row, item in enumerate(node.children):
|
||||
item.row_in_parent = row
|
||||
|
@ -168,12 +178,12 @@ class SidebarModel(QAbstractItemModel):
|
|||
else:
|
||||
return QVariant(theme_manager.icon_from_resources(item.icon))
|
||||
|
||||
def supportedDropActions(self):
|
||||
return Qt.MoveAction
|
||||
def supportedDropActions(self) -> Qt.DropActions:
|
||||
return cast(Qt.DropActions, Qt.MoveAction)
|
||||
|
||||
def flags(self, index: QModelIndex):
|
||||
def flags(self, index: QModelIndex) -> Qt.ItemFlags:
|
||||
if not index.isValid():
|
||||
return Qt.ItemIsEnabled
|
||||
return cast(Qt.ItemFlags, Qt.ItemIsEnabled)
|
||||
flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
|
||||
|
||||
item: SidebarItem = index.internalPointer()
|
||||
|
@ -183,7 +193,7 @@ class SidebarModel(QAbstractItemModel):
|
|||
):
|
||||
flags |= Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled
|
||||
|
||||
return flags
|
||||
return cast(Qt.ItemFlags, flags)
|
||||
|
||||
# Helpers
|
||||
######################################################################
|
||||
|
@ -193,7 +203,9 @@ class SidebarModel(QAbstractItemModel):
|
|||
return theme_manager.icon_from_resources(iconRef)
|
||||
|
||||
|
||||
def expand_where_necessary(model: SidebarModel, tree: QTreeView, parent=None) -> None:
|
||||
def expand_where_necessary(
|
||||
model: SidebarModel, tree: QTreeView, parent: Optional[QModelIndex] = None
|
||||
) -> None:
|
||||
parent = parent or QModelIndex()
|
||||
for row in range(model.rowCount(parent)):
|
||||
idx = model.index(row, 0, parent)
|
||||
|
@ -213,7 +225,7 @@ class FilterModel(QSortFilterProxyModel):
|
|||
|
||||
|
||||
class SidebarSearchBar(QLineEdit):
|
||||
def __init__(self, sidebar: SidebarTreeView):
|
||||
def __init__(self, sidebar: SidebarTreeView) -> None:
|
||||
QLineEdit.__init__(self, sidebar)
|
||||
self.setPlaceholderText(sidebar.col.tr(TR.BROWSING_SIDEBAR_FILTER))
|
||||
self.sidebar = sidebar
|
||||
|
@ -223,14 +235,14 @@ class SidebarSearchBar(QLineEdit):
|
|||
qconnect(self.timer.timeout, self.onSearch)
|
||||
qconnect(self.textChanged, self.onTextChanged)
|
||||
|
||||
def onTextChanged(self, text: str):
|
||||
def onTextChanged(self, text: str) -> None:
|
||||
if not self.timer.isActive():
|
||||
self.timer.start()
|
||||
|
||||
def onSearch(self):
|
||||
def onSearch(self) -> None:
|
||||
self.sidebar.search_for(self.text())
|
||||
|
||||
def keyPressEvent(self, evt):
|
||||
def keyPressEvent(self, evt: QKeyEvent) -> None:
|
||||
if evt.key() in (Qt.Key_Up, Qt.Key_Down):
|
||||
self.sidebar.setFocus()
|
||||
elif evt.key() in (Qt.Key_Enter, Qt.Key_Return):
|
||||
|
@ -293,7 +305,7 @@ class SidebarTreeView(QTreeView):
|
|||
if not self.isVisible():
|
||||
return
|
||||
|
||||
def on_done(fut: Future):
|
||||
def on_done(fut: Future) -> None:
|
||||
root = fut.result()
|
||||
model = SidebarModel(root)
|
||||
|
||||
|
@ -308,7 +320,7 @@ class SidebarTreeView(QTreeView):
|
|||
|
||||
self.mw.taskman.run_in_background(self._root_tree, on_done)
|
||||
|
||||
def search_for(self, text: str):
|
||||
def search_for(self, text: str) -> None:
|
||||
if not text.strip():
|
||||
self.current_search = None
|
||||
self.refresh()
|
||||
|
@ -331,7 +343,7 @@ class SidebarTreeView(QTreeView):
|
|||
|
||||
def drawRow(
|
||||
self, painter: QPainter, options: QStyleOptionViewItem, idx: QModelIndex
|
||||
):
|
||||
) -> None:
|
||||
if self.current_search is None:
|
||||
return super().drawRow(painter, options, idx)
|
||||
if not (item := self.model().item_for_index(idx)):
|
||||
|
@ -364,7 +376,7 @@ class SidebarTreeView(QTreeView):
|
|||
if not source_ids:
|
||||
return False
|
||||
|
||||
def on_done(fut):
|
||||
def on_done(fut: Future) -> None:
|
||||
fut.result()
|
||||
self.refresh()
|
||||
|
||||
|
@ -444,7 +456,7 @@ class SidebarTreeView(QTreeView):
|
|||
collapse_key: ConfigBoolKeyValue,
|
||||
type: Optional[SidebarItemType] = None,
|
||||
) -> SidebarItem:
|
||||
def update(expanded: bool):
|
||||
def update(expanded: bool) -> None:
|
||||
self.col.set_config_bool(collapse_key, not expanded)
|
||||
|
||||
top = SidebarItem(
|
||||
|
@ -486,7 +498,7 @@ class SidebarTreeView(QTreeView):
|
|||
type=SidebarItemType.SAVED_SEARCH_ROOT,
|
||||
)
|
||||
|
||||
def on_click():
|
||||
def on_click() -> None:
|
||||
self.show_context_menu(root, None)
|
||||
|
||||
root.onClick = on_click
|
||||
|
@ -503,10 +515,12 @@ class SidebarTreeView(QTreeView):
|
|||
def _tag_tree(self, root: SidebarItem) -> None:
|
||||
icon = ":/icons/tag.svg"
|
||||
|
||||
def render(root: SidebarItem, nodes: Iterable[TagTreeNode], head="") -> None:
|
||||
def render(
|
||||
root: SidebarItem, nodes: Iterable[TagTreeNode], head: str = ""
|
||||
) -> None:
|
||||
for node in nodes:
|
||||
|
||||
def toggle_expand():
|
||||
def toggle_expand() -> Callable[[bool], None]:
|
||||
full_name = head + node.name # pylint: disable=cell-var-from-loop
|
||||
return lambda expanded: self.mw.col.tags.set_collapsed(
|
||||
full_name, not expanded
|
||||
|
@ -537,10 +551,12 @@ class SidebarTreeView(QTreeView):
|
|||
def _deck_tree(self, root: SidebarItem) -> None:
|
||||
icon = ":/icons/deck.svg"
|
||||
|
||||
def render(root, nodes: Iterable[DeckTreeNode], head="") -> None:
|
||||
def render(
|
||||
root: SidebarItem, nodes: Iterable[DeckTreeNode], head: str = ""
|
||||
) -> None:
|
||||
for node in nodes:
|
||||
|
||||
def toggle_expand():
|
||||
def toggle_expand() -> Callable[[bool], None]:
|
||||
did = node.deck_id # pylint: disable=cell-var-from-loop
|
||||
return lambda _: self.mw.col.decks.collapseBrowser(did)
|
||||
|
||||
|
@ -613,7 +629,7 @@ class SidebarTreeView(QTreeView):
|
|||
return
|
||||
self.show_context_menu(item, idx)
|
||||
|
||||
def show_context_menu(self, item: SidebarItem, idx: Optional[QModelIndex]):
|
||||
def show_context_menu(self, item: SidebarItem, idx: Optional[QModelIndex]) -> None:
|
||||
m = QMenu()
|
||||
|
||||
if item.item_type in self.context_menus:
|
||||
|
@ -680,7 +696,8 @@ class SidebarTreeView(QTreeView):
|
|||
try:
|
||||
self.mw.col.decks.rename(deck, new_name)
|
||||
except DeckRenameError as e:
|
||||
return showWarning(e.description)
|
||||
showWarning(e.description)
|
||||
return
|
||||
self.refresh()
|
||||
self.mw.deckBrowser.refresh()
|
||||
|
||||
|
@ -690,11 +707,11 @@ class SidebarTreeView(QTreeView):
|
|||
def _remove_tag(self, item: "aqt.browser.SidebarItem") -> None:
|
||||
old_name = item.full_name
|
||||
|
||||
def do_remove():
|
||||
def do_remove() -> None:
|
||||
self.mw.col.tags.remove(old_name)
|
||||
self.col.tags.rename(old_name, "")
|
||||
|
||||
def on_done(fut: Future):
|
||||
def on_done(fut: Future) -> None:
|
||||
self.mw.requireReset(reason=ResetReason.BrowserRemoveTags, context=self)
|
||||
self.browser.model.endReset()
|
||||
fut.result()
|
||||
|
@ -713,11 +730,11 @@ class SidebarTreeView(QTreeView):
|
|||
if new_name == old_name or not new_name:
|
||||
return
|
||||
|
||||
def do_rename():
|
||||
def do_rename() -> int:
|
||||
self.mw.col.tags.remove(old_name)
|
||||
return self.col.tags.rename(old_name, new_name)
|
||||
|
||||
def on_done(fut: Future):
|
||||
def on_done(fut: Future) -> None:
|
||||
self.mw.requireReset(reason=ResetReason.BrowserAddTags, context=self)
|
||||
self.browser.model.endReset()
|
||||
|
||||
|
@ -739,10 +756,10 @@ class SidebarTreeView(QTreeView):
|
|||
did = item.id
|
||||
if self.mw.deckBrowser.ask_delete_deck(did):
|
||||
|
||||
def do_delete():
|
||||
def do_delete() -> None:
|
||||
return self.mw.col.decks.rem(did, True)
|
||||
|
||||
def on_done(fut: Future):
|
||||
def on_done(fut: Future) -> None:
|
||||
self.mw.requireReset(reason=ResetReason.BrowserDeleteDeck, context=self)
|
||||
self.browser.search()
|
||||
self.browser.model.endReset()
|
||||
|
@ -777,7 +794,7 @@ class SidebarTreeView(QTreeView):
|
|||
self.col.set_config("savedFilters", conf)
|
||||
self.refresh()
|
||||
|
||||
def save_current_search(self, _item=None) -> None:
|
||||
def save_current_search(self, _item: Any = None) -> None:
|
||||
try:
|
||||
filt = self.col.build_search_string(
|
||||
self.browser.form.searchEdit.lineEdit().text()
|
||||
|
|
|
@ -438,7 +438,7 @@ class MpvManager(MPV, SoundOrVideoPlayer):
|
|||
|
||||
|
||||
class SimpleMplayerSlaveModePlayer(SimpleMplayerPlayer):
|
||||
def __init__(self, taskman: TaskManager):
|
||||
def __init__(self, taskman: TaskManager) -> None:
|
||||
super().__init__(taskman)
|
||||
self.args.append("-slave")
|
||||
|
||||
|
@ -494,7 +494,7 @@ def encode_mp3(mw: aqt.AnkiQt, src_wav: str, on_done: Callable[[str], None]) ->
|
|||
"Encode the provided wav file to .mp3, and call on_done() with the path."
|
||||
dst_mp3 = src_wav.replace(".wav", "%d.mp3" % time.time())
|
||||
|
||||
def _on_done(fut: Future):
|
||||
def _on_done(fut: Future) -> None:
|
||||
fut.result()
|
||||
on_done(dst_mp3)
|
||||
|
||||
|
@ -509,7 +509,7 @@ class Recorder(ABC):
|
|||
# seconds to wait before recording
|
||||
STARTUP_DELAY = 0.3
|
||||
|
||||
def __init__(self, output_path: str):
|
||||
def __init__(self, output_path: str) -> None:
|
||||
self.output_path = output_path
|
||||
|
||||
def start(self, on_done: Callable[[], None]) -> None:
|
||||
|
@ -517,7 +517,7 @@ class Recorder(ABC):
|
|||
self._started_at = time.time()
|
||||
on_done()
|
||||
|
||||
def stop(self, on_done: Callable[[str], None]):
|
||||
def stop(self, on_done: Callable[[str], None]) -> None:
|
||||
"Stop recording, then call on_done() when finished."
|
||||
on_done(self.output_path)
|
||||
|
||||
|
@ -525,7 +525,7 @@ class Recorder(ABC):
|
|||
"Seconds since recording started."
|
||||
return time.time() - self._started_at
|
||||
|
||||
def on_timer(self):
|
||||
def on_timer(self) -> None:
|
||||
"Will be called periodically."
|
||||
|
||||
|
||||
|
@ -534,7 +534,7 @@ class Recorder(ABC):
|
|||
|
||||
|
||||
class QtAudioInputRecorder(Recorder):
|
||||
def __init__(self, output_path: str, mw: aqt.AnkiQt, parent: QWidget):
|
||||
def __init__(self, output_path: str, mw: aqt.AnkiQt, parent: QWidget) -> None:
|
||||
super().__init__(output_path)
|
||||
|
||||
self.mw = mw
|
||||
|
@ -567,11 +567,11 @@ class QtAudioInputRecorder(Recorder):
|
|||
self._iodevice.readyRead.connect(self._on_read_ready) # type: ignore
|
||||
super().start(on_done)
|
||||
|
||||
def _on_read_ready(self):
|
||||
def _on_read_ready(self) -> None:
|
||||
self._buffer += self._iodevice.readAll()
|
||||
|
||||
def stop(self, on_done: Callable[[str], None]):
|
||||
def on_stop_timer():
|
||||
def stop(self, on_done: Callable[[str], None]) -> None:
|
||||
def on_stop_timer() -> None:
|
||||
# read anything remaining in buffer & stop
|
||||
self._on_read_ready()
|
||||
self._audio_input.stop()
|
||||
|
@ -580,7 +580,7 @@ class QtAudioInputRecorder(Recorder):
|
|||
showWarning(f"recording failed: {err}")
|
||||
return
|
||||
|
||||
def write_file():
|
||||
def write_file() -> None:
|
||||
# swallow the first 300ms to allow audio device to quiesce
|
||||
wait = int(44100 * self.STARTUP_DELAY)
|
||||
if len(self._buffer) <= wait:
|
||||
|
@ -595,7 +595,7 @@ class QtAudioInputRecorder(Recorder):
|
|||
wf.writeframes(self._buffer)
|
||||
wf.close()
|
||||
|
||||
def and_then(fut):
|
||||
def and_then(fut) -> None:
|
||||
fut.result()
|
||||
Recorder.stop(self, on_done)
|
||||
|
||||
|
@ -672,7 +672,7 @@ class PyAudioThreadedRecorder(threading.Thread):
|
|||
|
||||
|
||||
class PyAudioRecorder(Recorder):
|
||||
def __init__(self, mw: aqt.AnkiQt, output_path: str):
|
||||
def __init__(self, mw: aqt.AnkiQt, output_path: str) -> None:
|
||||
super().__init__(output_path)
|
||||
self.mw = mw
|
||||
|
||||
|
@ -686,7 +686,7 @@ class PyAudioRecorder(Recorder):
|
|||
while self.duration() < 1:
|
||||
time.sleep(0.1)
|
||||
|
||||
def func(fut):
|
||||
def func(fut) -> None:
|
||||
Recorder.stop(self, on_done)
|
||||
|
||||
self.thread.finish = True
|
||||
|
@ -715,7 +715,7 @@ class RecordDialog(QDialog):
|
|||
self._start_recording()
|
||||
self._setup_dialog()
|
||||
|
||||
def _setup_dialog(self):
|
||||
def _setup_dialog(self) -> None:
|
||||
self.setWindowTitle("Anki")
|
||||
icon = QLabel()
|
||||
icon.setPixmap(QPixmap(":/icons/media-record.png"))
|
||||
|
@ -740,10 +740,10 @@ class RecordDialog(QDialog):
|
|||
restoreGeom(self, "audioRecorder2")
|
||||
self.show()
|
||||
|
||||
def _save_diag(self):
|
||||
def _save_diag(self) -> None:
|
||||
saveGeom(self, "audioRecorder2")
|
||||
|
||||
def _start_recording(self):
|
||||
def _start_recording(self) -> None:
|
||||
driver = self.mw.pm.recording_driver()
|
||||
if driver is RecordingDriver.PyAudio:
|
||||
self._recorder = PyAudioRecorder(self.mw, namedtmp("rec.wav"))
|
||||
|
@ -755,18 +755,18 @@ class RecordDialog(QDialog):
|
|||
assert_exhaustive(driver)
|
||||
self._recorder.start(self._start_timer)
|
||||
|
||||
def _start_timer(self):
|
||||
def _start_timer(self) -> None:
|
||||
self._timer = t = QTimer(self._parent)
|
||||
t.timeout.connect(self._on_timer) # type: ignore
|
||||
t.setSingleShot(False)
|
||||
t.start(100)
|
||||
|
||||
def _on_timer(self):
|
||||
def _on_timer(self) -> None:
|
||||
self._recorder.on_timer()
|
||||
duration = self._recorder.duration()
|
||||
self.label.setText(tr(TR.MEDIA_RECORDINGTIME, secs="%0.1f" % duration))
|
||||
|
||||
def accept(self):
|
||||
def accept(self) -> None:
|
||||
self._timer.stop()
|
||||
|
||||
try:
|
||||
|
@ -775,10 +775,10 @@ class RecordDialog(QDialog):
|
|||
finally:
|
||||
QDialog.accept(self)
|
||||
|
||||
def reject(self):
|
||||
def reject(self) -> None:
|
||||
self._timer.stop()
|
||||
|
||||
def cleanup(out: str):
|
||||
def cleanup(out: str) -> None:
|
||||
os.unlink(out)
|
||||
|
||||
try:
|
||||
|
@ -790,7 +790,7 @@ class RecordDialog(QDialog):
|
|||
def record_audio(
|
||||
parent: QWidget, mw: aqt.AnkiQt, encode: bool, on_done: Callable[[str], None]
|
||||
):
|
||||
def after_record(path: str):
|
||||
def after_record(path: str) -> None:
|
||||
if not encode:
|
||||
on_done(path)
|
||||
else:
|
||||
|
|
|
@ -25,7 +25,7 @@ from aqt.utils import (
|
|||
class NewDeckStats(QDialog):
|
||||
"""New deck stats."""
|
||||
|
||||
def __init__(self, mw: aqt.main.AnkiQt):
|
||||
def __init__(self, mw: aqt.main.AnkiQt) -> None:
|
||||
QDialog.__init__(self, mw, Qt.Window)
|
||||
mw.setupDialogGC(self)
|
||||
self.mw = mw
|
||||
|
@ -54,17 +54,17 @@ class NewDeckStats(QDialog):
|
|||
self.form.web.set_bridge_command(self._on_bridge_cmd, self)
|
||||
self.activateWindow()
|
||||
|
||||
def reject(self):
|
||||
def reject(self) -> None:
|
||||
self.form.web = None
|
||||
saveGeom(self, self.name)
|
||||
aqt.dialogs.markClosed("NewDeckStats")
|
||||
QDialog.reject(self)
|
||||
|
||||
def closeWithCallback(self, callback):
|
||||
def closeWithCallback(self, callback) -> None:
|
||||
self.reject()
|
||||
callback()
|
||||
|
||||
def _imagePath(self):
|
||||
def _imagePath(self) -> str:
|
||||
name = time.strftime("-%Y-%m-%d@%H-%M-%S.pdf", time.localtime(time.time()))
|
||||
name = "anki-" + tr(TR.STATISTICS_STATS) + name
|
||||
file = getSaveFile(
|
||||
|
@ -77,17 +77,17 @@ class NewDeckStats(QDialog):
|
|||
)
|
||||
return file
|
||||
|
||||
def saveImage(self):
|
||||
def saveImage(self) -> None:
|
||||
path = self._imagePath()
|
||||
if not path:
|
||||
return
|
||||
self.form.web.page().printToPdf(path)
|
||||
tooltip(tr(TR.STATISTICS_SAVED))
|
||||
|
||||
def changePeriod(self, n):
|
||||
def changePeriod(self, n) -> None:
|
||||
pass
|
||||
|
||||
def changeScope(self, type):
|
||||
def changeScope(self, type) -> None:
|
||||
pass
|
||||
|
||||
def _on_bridge_cmd(self, cmd: str) -> bool:
|
||||
|
@ -98,14 +98,14 @@ class NewDeckStats(QDialog):
|
|||
|
||||
return False
|
||||
|
||||
def refresh(self):
|
||||
def refresh(self) -> None:
|
||||
self.form.web.load_ts_page("graphs")
|
||||
|
||||
|
||||
class DeckStats(QDialog):
|
||||
"""Legacy deck stats, used by some add-ons."""
|
||||
|
||||
def __init__(self, mw):
|
||||
def __init__(self, mw: aqt.main.AnkiQt) -> None:
|
||||
QDialog.__init__(self, mw, Qt.Window)
|
||||
mw.setupDialogGC(self)
|
||||
self.mw = mw
|
||||
|
@ -143,17 +143,17 @@ class DeckStats(QDialog):
|
|||
self.refresh()
|
||||
self.activateWindow()
|
||||
|
||||
def reject(self):
|
||||
def reject(self) -> None:
|
||||
self.form.web = None
|
||||
saveGeom(self, self.name)
|
||||
aqt.dialogs.markClosed("DeckStats")
|
||||
QDialog.reject(self)
|
||||
|
||||
def closeWithCallback(self, callback):
|
||||
def closeWithCallback(self, callback) -> None:
|
||||
self.reject()
|
||||
callback()
|
||||
|
||||
def _imagePath(self):
|
||||
def _imagePath(self) -> str:
|
||||
name = time.strftime("-%Y-%m-%d@%H-%M-%S.pdf", time.localtime(time.time()))
|
||||
name = "anki-" + tr(TR.STATISTICS_STATS) + name
|
||||
file = getSaveFile(
|
||||
|
@ -166,22 +166,22 @@ class DeckStats(QDialog):
|
|||
)
|
||||
return file
|
||||
|
||||
def saveImage(self):
|
||||
def saveImage(self) -> None:
|
||||
path = self._imagePath()
|
||||
if not path:
|
||||
return
|
||||
self.form.web.page().printToPdf(path)
|
||||
tooltip(tr(TR.STATISTICS_SAVED))
|
||||
|
||||
def changePeriod(self, n):
|
||||
def changePeriod(self, n: int) -> None:
|
||||
self.period = n
|
||||
self.refresh()
|
||||
|
||||
def changeScope(self, type):
|
||||
def changeScope(self, type: str) -> None:
|
||||
self.wholeCollection = type == "collection"
|
||||
self.refresh()
|
||||
|
||||
def refresh(self):
|
||||
def refresh(self) -> None:
|
||||
self.mw.progress.start(parent=self)
|
||||
stats = self.mw.col.stats()
|
||||
stats.wholeCollection = self.wholeCollection
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import aqt
|
||||
from aqt import gui_hooks
|
||||
from aqt.qt import *
|
||||
|
@ -109,7 +111,7 @@ class StudyDeck(QDialog):
|
|||
return True
|
||||
return False
|
||||
|
||||
def redraw(self, filt, focus=None):
|
||||
def redraw(self, filt: str, focus: Optional[str] = None) -> None:
|
||||
self.filt = filt
|
||||
self.focus = focus
|
||||
self.names = [n for n in self.origNames if self._matches(n, filt)]
|
||||
|
@ -123,7 +125,7 @@ class StudyDeck(QDialog):
|
|||
l.setCurrentRow(idx)
|
||||
l.scrollToItem(l.item(idx), QAbstractItemView.PositionAtCenter)
|
||||
|
||||
def _matches(self, name, filt):
|
||||
def _matches(self, name: str, filt: str) -> bool:
|
||||
name = name.lower()
|
||||
filt = filt.lower()
|
||||
if not filt:
|
||||
|
@ -133,7 +135,7 @@ class StudyDeck(QDialog):
|
|||
return False
|
||||
return True
|
||||
|
||||
def onReset(self):
|
||||
def onReset(self) -> None:
|
||||
# model updated?
|
||||
if self.nameFunc:
|
||||
self.origNames = self.nameFunc()
|
||||
|
|
|
@ -40,12 +40,14 @@ class FullSyncChoice(enum.Enum):
|
|||
DOWNLOAD = 2
|
||||
|
||||
|
||||
def get_sync_status(mw: aqt.main.AnkiQt, callback: Callable[[SyncStatus], None]):
|
||||
def get_sync_status(
|
||||
mw: aqt.main.AnkiQt, callback: Callable[[SyncStatus], None]
|
||||
) -> None:
|
||||
auth = mw.pm.sync_auth()
|
||||
if not auth:
|
||||
return SyncStatus(required=SyncStatus.NO_CHANGES) # pylint:disable=no-member
|
||||
callback(SyncStatus(required=SyncStatus.NO_CHANGES)) # pylint:disable=no-member
|
||||
|
||||
def on_future_done(fut):
|
||||
def on_future_done(fut) -> None:
|
||||
try:
|
||||
out = fut.result()
|
||||
except Exception as e:
|
||||
|
@ -57,7 +59,7 @@ def get_sync_status(mw: aqt.main.AnkiQt, callback: Callable[[SyncStatus], None])
|
|||
mw.taskman.run_in_background(lambda: mw.col.sync_status(auth), on_future_done)
|
||||
|
||||
|
||||
def handle_sync_error(mw: aqt.main.AnkiQt, err: Exception):
|
||||
def handle_sync_error(mw: aqt.main.AnkiQt, err: Exception) -> None:
|
||||
if isinstance(err, SyncError):
|
||||
if err.is_auth_error():
|
||||
mw.pm.clear_sync_auth()
|
||||
|
@ -87,14 +89,14 @@ def sync_collection(mw: aqt.main.AnkiQt, on_done: Callable[[], None]) -> None:
|
|||
auth = mw.pm.sync_auth()
|
||||
assert auth
|
||||
|
||||
def on_timer():
|
||||
def on_timer() -> None:
|
||||
on_normal_sync_timer(mw)
|
||||
|
||||
timer = QTimer(mw)
|
||||
qconnect(timer.timeout, on_timer)
|
||||
timer.start(150)
|
||||
|
||||
def on_future_done(fut):
|
||||
def on_future_done(fut) -> None:
|
||||
mw.col.db.begin()
|
||||
timer.stop()
|
||||
try:
|
||||
|
@ -171,14 +173,14 @@ def on_full_sync_timer(mw: aqt.main.AnkiQt) -> None:
|
|||
def full_download(mw: aqt.main.AnkiQt, on_done: Callable[[], None]) -> None:
|
||||
mw.col.close_for_full_sync()
|
||||
|
||||
def on_timer():
|
||||
def on_timer() -> None:
|
||||
on_full_sync_timer(mw)
|
||||
|
||||
timer = QTimer(mw)
|
||||
qconnect(timer.timeout, on_timer)
|
||||
timer.start(150)
|
||||
|
||||
def on_future_done(fut):
|
||||
def on_future_done(fut) -> None:
|
||||
timer.stop()
|
||||
mw.col.reopen(after_full_sync=True)
|
||||
mw.reset()
|
||||
|
@ -199,14 +201,14 @@ def full_download(mw: aqt.main.AnkiQt, on_done: Callable[[], None]) -> None:
|
|||
def full_upload(mw: aqt.main.AnkiQt, on_done: Callable[[], None]) -> None:
|
||||
mw.col.close_for_full_sync()
|
||||
|
||||
def on_timer():
|
||||
def on_timer() -> None:
|
||||
on_full_sync_timer(mw)
|
||||
|
||||
timer = QTimer(mw)
|
||||
qconnect(timer.timeout, on_timer)
|
||||
timer.start(150)
|
||||
|
||||
def on_future_done(fut):
|
||||
def on_future_done(fut) -> None:
|
||||
timer.stop()
|
||||
mw.col.reopen(after_full_sync=True)
|
||||
mw.reset()
|
||||
|
@ -235,7 +237,7 @@ def sync_login(
|
|||
if username and password:
|
||||
break
|
||||
|
||||
def on_future_done(fut):
|
||||
def on_future_done(fut) -> None:
|
||||
try:
|
||||
auth = fut.result()
|
||||
except SyncError as e:
|
||||
|
|
|
@ -4,7 +4,9 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Iterable, List, Optional, Union
|
||||
|
||||
from anki.collection import Collection
|
||||
from aqt import gui_hooks
|
||||
from aqt.qt import *
|
||||
|
||||
|
@ -15,9 +17,9 @@ class TagEdit(QLineEdit):
|
|||
lostFocus = pyqtSignal()
|
||||
|
||||
# 0 = tags, 1 = decks
|
||||
def __init__(self, parent, type=0):
|
||||
def __init__(self, parent: QDialog, type: int = 0) -> None:
|
||||
QLineEdit.__init__(self, parent)
|
||||
self.col = None
|
||||
self.col: Optional[Collection] = None
|
||||
self.model = QStringListModel()
|
||||
self.type = type
|
||||
if type == 0:
|
||||
|
@ -28,19 +30,20 @@ class TagEdit(QLineEdit):
|
|||
self.completer.setCaseSensitivity(Qt.CaseInsensitive)
|
||||
self.setCompleter(self.completer)
|
||||
|
||||
def setCol(self, col):
|
||||
def setCol(self, col: Collection) -> None:
|
||||
"Set the current col, updating list of available tags."
|
||||
self.col = col
|
||||
l: Iterable[str]
|
||||
if self.type == 0:
|
||||
l = self.col.tags.all()
|
||||
else:
|
||||
l = (d.name for d in self.col.decks.all_names_and_ids())
|
||||
self.model.setStringList(l)
|
||||
|
||||
def focusInEvent(self, evt):
|
||||
def focusInEvent(self, evt: QFocusEvent) -> None:
|
||||
QLineEdit.focusInEvent(self, evt)
|
||||
|
||||
def keyPressEvent(self, evt):
|
||||
def keyPressEvent(self, evt: QKeyEvent) -> None:
|
||||
if evt.key() in (Qt.Key_Up, Qt.Key_Down):
|
||||
# show completer on arrow key up/down
|
||||
if not self.completer.popup().isVisible():
|
||||
|
@ -85,7 +88,7 @@ class TagEdit(QLineEdit):
|
|||
self.showCompleter()
|
||||
gui_hooks.tag_editor_did_process_key(self, evt)
|
||||
|
||||
def showCompleter(self):
|
||||
def showCompleter(self) -> None:
|
||||
self.completer.setCompletionPrefix(self.text())
|
||||
self.completer.complete()
|
||||
|
||||
|
@ -94,20 +97,26 @@ class TagEdit(QLineEdit):
|
|||
self.lostFocus.emit() # type: ignore
|
||||
self.completer.popup().hide()
|
||||
|
||||
def hideCompleter(self):
|
||||
def hideCompleter(self) -> None:
|
||||
if sip.isdeleted(self.completer):
|
||||
return
|
||||
self.completer.popup().hide()
|
||||
|
||||
|
||||
class TagCompleter(QCompleter):
|
||||
def __init__(self, model, parent, edit, *args):
|
||||
def __init__(
|
||||
self,
|
||||
model: QStringListModel,
|
||||
parent: QWidget,
|
||||
edit: TagEdit,
|
||||
*args,
|
||||
) -> None:
|
||||
QCompleter.__init__(self, model, parent)
|
||||
self.tags = []
|
||||
self.tags: List[str] = []
|
||||
self.edit = edit
|
||||
self.cursor = None
|
||||
self.cursor: Optional[int] = None
|
||||
|
||||
def splitPath(self, tags):
|
||||
def splitPath(self, tags: str) -> List[str]:
|
||||
stripped_tags = tags.strip()
|
||||
stripped_tags = re.sub(" +", " ", stripped_tags)
|
||||
self.tags = self.edit.col.tags.split(stripped_tags)
|
||||
|
@ -119,7 +128,7 @@ class TagCompleter(QCompleter):
|
|||
self.cursor = stripped_tags.count(" ", 0, p)
|
||||
return [self.tags[self.cursor]]
|
||||
|
||||
def pathFromIndex(self, idx):
|
||||
def pathFromIndex(self, idx: QModelIndex) -> str:
|
||||
if self.cursor is None:
|
||||
return self.edit.text()
|
||||
ret = QCompleter.pathFromIndex(self, idx)
|
||||
|
|
|
@ -3,14 +3,17 @@
|
|||
from typing import List, Optional
|
||||
|
||||
import aqt
|
||||
from aqt.customstudy import CustomStudy
|
||||
from aqt.main import AnkiQt
|
||||
from aqt.qt import *
|
||||
from aqt.utils import disable_help_button, restoreGeom, saveGeom
|
||||
|
||||
|
||||
class TagLimit(QDialog):
|
||||
def __init__(self, mw, parent):
|
||||
def __init__(self, mw: AnkiQt, parent: CustomStudy) -> None:
|
||||
QDialog.__init__(self, parent, Qt.Window)
|
||||
self.tags: Union[str, List] = ""
|
||||
self.tags: str = ""
|
||||
self.tags_list: List[str] = []
|
||||
self.mw = mw
|
||||
self.parent: Optional[QWidget] = parent
|
||||
self.deck = self.parent.deck
|
||||
|
@ -29,7 +32,7 @@ class TagLimit(QDialog):
|
|||
restoreGeom(self, "tagLimit")
|
||||
self.exec_()
|
||||
|
||||
def rebuildTagList(self):
|
||||
def rebuildTagList(self) -> None:
|
||||
usertags = self.mw.col.tags.byDeck(self.deck["id"], True)
|
||||
yes = self.deck.get("activeTags", [])
|
||||
no = self.deck.get("inactiveTags", [])
|
||||
|
@ -42,10 +45,10 @@ class TagLimit(QDialog):
|
|||
groupedTags = []
|
||||
usertags.sort()
|
||||
groupedTags.append(usertags)
|
||||
self.tags = []
|
||||
self.tags_list = []
|
||||
for tags in groupedTags:
|
||||
for t in tags:
|
||||
self.tags.append(t)
|
||||
self.tags_list.append(t)
|
||||
item = QListWidgetItem(t.replace("_", " "))
|
||||
self.dialog.activeList.addItem(item)
|
||||
if t in yesHash:
|
||||
|
@ -65,11 +68,11 @@ class TagLimit(QDialog):
|
|||
idx = self.dialog.inactiveList.indexFromItem(item)
|
||||
self.dialog.inactiveList.selectionModel().select(idx, mode)
|
||||
|
||||
def reject(self):
|
||||
def reject(self) -> None:
|
||||
self.tags = ""
|
||||
QDialog.reject(self)
|
||||
|
||||
def accept(self):
|
||||
def accept(self) -> None:
|
||||
self.hide()
|
||||
# gather yes/no tags
|
||||
yes = []
|
||||
|
@ -80,12 +83,12 @@ class TagLimit(QDialog):
|
|||
item = self.dialog.activeList.item(c)
|
||||
idx = self.dialog.activeList.indexFromItem(item)
|
||||
if self.dialog.activeList.selectionModel().isSelected(idx):
|
||||
yes.append(self.tags[c])
|
||||
yes.append(self.tags_list[c])
|
||||
# inactive
|
||||
item = self.dialog.inactiveList.item(c)
|
||||
idx = self.dialog.inactiveList.indexFromItem(item)
|
||||
if self.dialog.inactiveList.selectionModel().isSelected(idx):
|
||||
no.append(self.tags[c])
|
||||
no.append(self.tags_list[c])
|
||||
# save in the deck for future invocations
|
||||
self.deck["activeTags"] = yes
|
||||
self.deck["inactiveTags"] = no
|
||||
|
|
|
@ -31,7 +31,7 @@ class TaskManager(QObject):
|
|||
self._closures_lock = Lock()
|
||||
qconnect(self._closures_pending, self._on_closures_pending)
|
||||
|
||||
def run_on_main(self, closure: Closure):
|
||||
def run_on_main(self, closure: Closure) -> None:
|
||||
"Run the provided closure on the main thread."
|
||||
with self._closures_lock:
|
||||
self._closures.append(closure)
|
||||
|
@ -71,14 +71,14 @@ class TaskManager(QObject):
|
|||
):
|
||||
self.mw.progress.start(parent=parent, label=label, immediate=immediate)
|
||||
|
||||
def wrapped_done(fut):
|
||||
def wrapped_done(fut) -> None:
|
||||
self.mw.progress.finish()
|
||||
if on_done:
|
||||
on_done(fut)
|
||||
|
||||
self.run_in_background(task, wrapped_done)
|
||||
|
||||
def _on_closures_pending(self):
|
||||
def _on_closures_pending(self) -> None:
|
||||
"""Run any pending closures. This runs in the main thread."""
|
||||
with self._closures_lock:
|
||||
closures = self._closures
|
||||
|
|
|
@ -481,7 +481,7 @@ if isWin:
|
|||
return []
|
||||
return list(map(self._voice_to_object, self.speaker.GetVoices()))
|
||||
|
||||
def _voice_to_object(self, voice: Any):
|
||||
def _voice_to_object(self, voice: Any) -> WindowsVoice:
|
||||
lang = voice.GetAttribute("language")
|
||||
lang = lcid_hex_str_to_lang_code(lang)
|
||||
name = self._tidy_name(voice.GetAttribute("name"))
|
||||
|
@ -525,7 +525,7 @@ if isWin:
|
|||
id: Any
|
||||
|
||||
class WindowsRTTTSFilePlayer(TTSProcessPlayer):
|
||||
voice_list = None
|
||||
voice_list: List[Any] = []
|
||||
tmppath = os.path.join(tmpdir(), "tts.wav")
|
||||
|
||||
def import_voices(self) -> None:
|
||||
|
@ -561,7 +561,7 @@ if isWin:
|
|||
)
|
||||
asyncio.run(self.speakText(tag, voice.id))
|
||||
|
||||
def _on_done(self, ret: Future, cb: OnDoneCallback):
|
||||
def _on_done(self, ret: Future, cb: OnDoneCallback) -> None:
|
||||
ret.result()
|
||||
|
||||
# inject file into the top of the audio queue
|
||||
|
@ -572,7 +572,7 @@ if isWin:
|
|||
# then tell player to advance, which will cause the file to be played
|
||||
cb()
|
||||
|
||||
async def speakText(self, tag: TTSTag, voice_id):
|
||||
async def speakText(self, tag: TTSTag, voice_id) -> None:
|
||||
import winrt.windows.media.speechsynthesis as speechsynthesis # type: ignore
|
||||
import winrt.windows.storage.streams as streams # type: ignore
|
||||
|
||||
|
|
|
@ -2,11 +2,13 @@
|
|||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import time
|
||||
from typing import Any, Dict
|
||||
|
||||
import requests
|
||||
|
||||
import aqt
|
||||
from anki.utils import platDesc, versionWithBuild
|
||||
from aqt.main import AnkiQt
|
||||
from aqt.qt import *
|
||||
from aqt.utils import TR, openLink, showText, tr
|
||||
|
||||
|
@ -17,12 +19,12 @@ class LatestVersionFinder(QThread):
|
|||
newMsg = pyqtSignal(dict)
|
||||
clockIsOff = pyqtSignal(float)
|
||||
|
||||
def __init__(self, main):
|
||||
def __init__(self, main: AnkiQt) -> None:
|
||||
QThread.__init__(self)
|
||||
self.main = main
|
||||
self.config = main.pm.meta
|
||||
|
||||
def _data(self):
|
||||
def _data(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"ver": versionWithBuild(),
|
||||
"os": platDesc(),
|
||||
|
@ -31,7 +33,7 @@ class LatestVersionFinder(QThread):
|
|||
"crt": self.config["created"],
|
||||
}
|
||||
|
||||
def run(self):
|
||||
def run(self) -> None:
|
||||
if not self.config["updates"]:
|
||||
return
|
||||
d = self._data()
|
||||
|
@ -54,7 +56,7 @@ class LatestVersionFinder(QThread):
|
|||
self.clockIsOff.emit(diff) # type: ignore
|
||||
|
||||
|
||||
def askAndUpdate(mw, ver):
|
||||
def askAndUpdate(mw, ver) -> None:
|
||||
baseStr = tr(TR.QT_MISC_ANKI_UPDATEDANKI_HAS_BEEN_RELEASED, val=ver)
|
||||
msg = QMessageBox(mw)
|
||||
msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No) # type: ignore
|
||||
|
@ -71,6 +73,6 @@ def askAndUpdate(mw, ver):
|
|||
openLink(aqt.appWebsite)
|
||||
|
||||
|
||||
def showMessages(mw, data):
|
||||
def showMessages(mw, data) -> None:
|
||||
showText(data["msg"], parent=mw, type="html")
|
||||
mw.pm.meta["lastMsg"] = data["msgId"]
|
||||
|
|
296
qt/aqt/utils.py
296
qt/aqt/utils.py
|
@ -8,12 +8,35 @@ import re
|
|||
import subprocess
|
||||
import sys
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Any, List, Literal, Optional, Union, cast
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
List,
|
||||
Literal,
|
||||
Optional,
|
||||
Sequence,
|
||||
Tuple,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
|
||||
from markdown import markdown
|
||||
from PyQt5.QtWidgets import (
|
||||
QAction,
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
QFileDialog,
|
||||
QHeaderView,
|
||||
QMenu,
|
||||
QPushButton,
|
||||
QSplitter,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
import anki
|
||||
import aqt
|
||||
from anki import Collection
|
||||
from anki.errors import InvalidInput
|
||||
from anki.lang import TR # pylint: disable=unused-import
|
||||
from anki.utils import invalidFilename, isMac, isWin, noBundledLibs, versionWithBuild
|
||||
|
@ -77,7 +100,7 @@ argument. However, add-on may use string, and we want to accept this.
|
|||
"""
|
||||
|
||||
|
||||
def openHelp(section: HelpPageArgument):
|
||||
def openHelp(section: HelpPageArgument) -> None:
|
||||
link = aqt.appHelpSite
|
||||
if section:
|
||||
if isinstance(section, HelpPage):
|
||||
|
@ -87,27 +110,35 @@ def openHelp(section: HelpPageArgument):
|
|||
openLink(link)
|
||||
|
||||
|
||||
def openLink(link):
|
||||
def openLink(link: str) -> None:
|
||||
tooltip(tr(TR.QT_MISC_LOADING), period=1000)
|
||||
with noBundledLibs():
|
||||
QDesktopServices.openUrl(QUrl(link))
|
||||
|
||||
|
||||
def showWarning(
|
||||
text, parent=None, help="", title="Anki", textFormat: Optional[TextFormat] = None
|
||||
):
|
||||
text: str,
|
||||
parent: Optional[QDialog] = None,
|
||||
help: HelpPageArgument = "",
|
||||
title: str = "Anki",
|
||||
textFormat: Optional[TextFormat] = None,
|
||||
) -> int:
|
||||
"Show a small warning with an OK button."
|
||||
return showInfo(text, parent, help, "warning", title=title, textFormat=textFormat)
|
||||
|
||||
|
||||
def showCritical(
|
||||
text, parent=None, help="", title="Anki", textFormat: Optional[TextFormat] = None
|
||||
):
|
||||
text: str,
|
||||
parent: Optional[QDialog] = None,
|
||||
help: str = "",
|
||||
title: str = "Anki",
|
||||
textFormat: Optional[TextFormat] = None,
|
||||
) -> int:
|
||||
"Show a small critical error with an OK button."
|
||||
return showInfo(text, parent, help, "critical", title=title, textFormat=textFormat)
|
||||
|
||||
|
||||
def show_invalid_search_error(err: Exception):
|
||||
def show_invalid_search_error(err: Exception) -> None:
|
||||
"Render search errors in markdown, then display a warning."
|
||||
text = str(err)
|
||||
if isinstance(err, InvalidInput):
|
||||
|
@ -116,24 +147,27 @@ def show_invalid_search_error(err: Exception):
|
|||
|
||||
|
||||
def showInfo(
|
||||
text,
|
||||
parent=False,
|
||||
help="",
|
||||
type="info",
|
||||
title="Anki",
|
||||
text: str,
|
||||
parent: Union[Literal[False], QDialog] = False,
|
||||
help: HelpPageArgument = "",
|
||||
type: str = "info",
|
||||
title: str = "Anki",
|
||||
textFormat: Optional[TextFormat] = None,
|
||||
customBtns=None,
|
||||
customBtns: Optional[List[QMessageBox.StandardButton]] = None,
|
||||
) -> int:
|
||||
"Show a small info window with an OK button."
|
||||
parent_widget: QWidget
|
||||
if parent is False:
|
||||
parent = aqt.mw.app.activeWindow() or aqt.mw
|
||||
parent_widget = aqt.mw.app.activeWindow() or aqt.mw
|
||||
else:
|
||||
parent_widget = parent
|
||||
if type == "warning":
|
||||
icon = QMessageBox.Warning
|
||||
elif type == "critical":
|
||||
icon = QMessageBox.Critical
|
||||
else:
|
||||
icon = QMessageBox.Information
|
||||
mb = QMessageBox(parent)
|
||||
mb = QMessageBox(parent_widget) #
|
||||
if textFormat == "plain":
|
||||
mb.setTextFormat(Qt.PlainText)
|
||||
elif textFormat == "rich":
|
||||
|
@ -161,16 +195,16 @@ def showInfo(
|
|||
|
||||
|
||||
def showText(
|
||||
txt,
|
||||
parent=None,
|
||||
type="text",
|
||||
run=True,
|
||||
geomKey=None,
|
||||
minWidth=500,
|
||||
minHeight=400,
|
||||
title="Anki",
|
||||
copyBtn=False,
|
||||
):
|
||||
txt: str,
|
||||
parent: Optional[QWidget] = None,
|
||||
type: str = "text",
|
||||
run: bool = True,
|
||||
geomKey: Optional[str] = None,
|
||||
minWidth: int = 500,
|
||||
minHeight: int = 400,
|
||||
title: str = "Anki",
|
||||
copyBtn: bool = False,
|
||||
) -> Optional[Tuple[QDialog, QDialogButtonBox]]:
|
||||
if not parent:
|
||||
parent = aqt.mw.app.activeWindow() or aqt.mw
|
||||
diag = QDialog(parent)
|
||||
|
@ -189,21 +223,21 @@ def showText(
|
|||
layout.addWidget(box)
|
||||
if copyBtn:
|
||||
|
||||
def onCopy():
|
||||
def onCopy() -> None:
|
||||
QApplication.clipboard().setText(text.toPlainText())
|
||||
|
||||
btn = QPushButton(tr(TR.QT_MISC_COPY_TO_CLIPBOARD))
|
||||
qconnect(btn.clicked, onCopy)
|
||||
box.addButton(btn, QDialogButtonBox.ActionRole)
|
||||
|
||||
def onReject():
|
||||
def onReject() -> None:
|
||||
if geomKey:
|
||||
saveGeom(diag, geomKey)
|
||||
QDialog.reject(diag)
|
||||
|
||||
qconnect(box.rejected, onReject)
|
||||
|
||||
def onFinish():
|
||||
def onFinish() -> None:
|
||||
if geomKey:
|
||||
saveGeom(diag, geomKey)
|
||||
|
||||
|
@ -214,18 +248,19 @@ def showText(
|
|||
restoreGeom(diag, geomKey)
|
||||
if run:
|
||||
diag.exec_()
|
||||
return None
|
||||
else:
|
||||
return diag, box
|
||||
|
||||
|
||||
def askUser(
|
||||
text,
|
||||
parent=None,
|
||||
text: str,
|
||||
parent: QDialog = None,
|
||||
help: HelpPageArgument = None,
|
||||
defaultno=False,
|
||||
msgfunc=None,
|
||||
title="Anki",
|
||||
):
|
||||
defaultno: bool = False,
|
||||
msgfunc: Optional[Callable] = None,
|
||||
title: str = "Anki",
|
||||
) -> bool:
|
||||
"Show a yes/no question. Return true if yes."
|
||||
if not parent:
|
||||
parent = aqt.mw.app.activeWindow()
|
||||
|
@ -239,7 +274,7 @@ def askUser(
|
|||
default = QMessageBox.No
|
||||
else:
|
||||
default = QMessageBox.Yes
|
||||
r = msgfunc(parent, title, text, sb, default)
|
||||
r = msgfunc(parent, title, text, cast(QMessageBox.StandardButtons, sb), default)
|
||||
if r == QMessageBox.Help:
|
||||
|
||||
openHelp(help)
|
||||
|
@ -250,10 +285,15 @@ def askUser(
|
|||
|
||||
class ButtonedDialog(QMessageBox):
|
||||
def __init__(
|
||||
self, text, buttons, parent=None, help: HelpPageArgument = None, title="Anki"
|
||||
self,
|
||||
text: str,
|
||||
buttons: List[str],
|
||||
parent: Optional[QDialog] = None,
|
||||
help: HelpPageArgument = None,
|
||||
title: str = "Anki",
|
||||
):
|
||||
QMessageBox.__init__(self, parent)
|
||||
self._buttons = []
|
||||
self._buttons: List[QPushButton] = []
|
||||
self.setWindowTitle(title)
|
||||
self.help = help
|
||||
self.setIcon(QMessageBox.Warning)
|
||||
|
@ -264,7 +304,7 @@ class ButtonedDialog(QMessageBox):
|
|||
self.addButton(tr(TR.ACTIONS_HELP), QMessageBox.HelpRole)
|
||||
buttons.append(tr(TR.ACTIONS_HELP))
|
||||
|
||||
def run(self):
|
||||
def run(self) -> str:
|
||||
self.exec_()
|
||||
but = self.clickedButton().text()
|
||||
if but == "Help":
|
||||
|
@ -274,13 +314,17 @@ class ButtonedDialog(QMessageBox):
|
|||
# work around KDE 'helpfully' adding accelerators to button text of Qt apps
|
||||
return txt.replace("&", "")
|
||||
|
||||
def setDefault(self, idx):
|
||||
def setDefault(self, idx: int) -> None:
|
||||
self.setDefaultButton(self._buttons[idx])
|
||||
|
||||
|
||||
def askUserDialog(
|
||||
text, buttons, parent=None, help: HelpPageArgument = None, title="Anki"
|
||||
):
|
||||
text: str,
|
||||
buttons: List[str],
|
||||
parent: Optional[QDialog] = None,
|
||||
help: HelpPageArgument = None,
|
||||
title: str = "Anki",
|
||||
) -> ButtonedDialog:
|
||||
if not parent:
|
||||
parent = aqt.mw
|
||||
diag = ButtonedDialog(text, buttons, parent, help, title=title)
|
||||
|
@ -290,14 +334,14 @@ def askUserDialog(
|
|||
class GetTextDialog(QDialog):
|
||||
def __init__(
|
||||
self,
|
||||
parent,
|
||||
question,
|
||||
parent: Optional[QDialog],
|
||||
question: str,
|
||||
help: HelpPageArgument = None,
|
||||
edit=None,
|
||||
default="",
|
||||
title="Anki",
|
||||
minWidth=400,
|
||||
):
|
||||
edit: Optional[QLineEdit] = None,
|
||||
default: str = "",
|
||||
title: str = "Anki",
|
||||
minWidth: int = 400,
|
||||
) -> None:
|
||||
QDialog.__init__(self, parent)
|
||||
self.setWindowTitle(title)
|
||||
disable_help_button(self)
|
||||
|
@ -325,26 +369,26 @@ class GetTextDialog(QDialog):
|
|||
if help:
|
||||
qconnect(b.button(QDialogButtonBox.Help).clicked, self.helpRequested)
|
||||
|
||||
def accept(self):
|
||||
def accept(self) -> None:
|
||||
return QDialog.accept(self)
|
||||
|
||||
def reject(self):
|
||||
def reject(self) -> None:
|
||||
return QDialog.reject(self)
|
||||
|
||||
def helpRequested(self):
|
||||
def helpRequested(self) -> None:
|
||||
openHelp(self.help)
|
||||
|
||||
|
||||
def getText(
|
||||
prompt,
|
||||
parent=None,
|
||||
prompt: str,
|
||||
parent: Optional[QDialog] = None,
|
||||
help: HelpPageArgument = None,
|
||||
edit=None,
|
||||
default="",
|
||||
title="Anki",
|
||||
geomKey=None,
|
||||
**kwargs,
|
||||
):
|
||||
edit: Optional[QLineEdit] = None,
|
||||
default: str = "",
|
||||
title: str = "Anki",
|
||||
geomKey: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> Tuple[str, int]:
|
||||
if not parent:
|
||||
parent = aqt.mw.app.activeWindow() or aqt.mw
|
||||
d = GetTextDialog(
|
||||
|
@ -359,7 +403,7 @@ def getText(
|
|||
return (str(d.l.text()), ret)
|
||||
|
||||
|
||||
def getOnlyText(*args, **kwargs):
|
||||
def getOnlyText(*args: Any, **kwargs: Any) -> str:
|
||||
(s, r) = getText(*args, **kwargs)
|
||||
if r:
|
||||
return s
|
||||
|
@ -368,7 +412,10 @@ def getOnlyText(*args, **kwargs):
|
|||
|
||||
|
||||
# fixme: these utilities could be combined into a single base class
|
||||
def chooseList(prompt, choices, startrow=0, parent=None):
|
||||
# unused by Anki, but used by add-ons
|
||||
def chooseList(
|
||||
prompt: str, choices: List[str], startrow: int = 0, parent: Any = None
|
||||
) -> int:
|
||||
if not parent:
|
||||
parent = aqt.mw.app.activeWindow()
|
||||
d = QDialog(parent)
|
||||
|
@ -389,7 +436,9 @@ def chooseList(prompt, choices, startrow=0, parent=None):
|
|||
return c.currentRow()
|
||||
|
||||
|
||||
def getTag(parent, deck, question, tags="user", **kwargs):
|
||||
def getTag(
|
||||
parent: QDialog, deck: Collection, question: str, **kwargs: Any
|
||||
) -> Tuple[str, int]:
|
||||
from aqt.tagedit import TagEdit
|
||||
|
||||
te = TagEdit(parent)
|
||||
|
@ -409,7 +458,15 @@ def disable_help_button(widget: QWidget) -> None:
|
|||
######################################################################
|
||||
|
||||
|
||||
def getFile(parent, title, cb, filter="*.*", dir=None, key=None, multi=False):
|
||||
def getFile(
|
||||
parent: QDialog,
|
||||
title: str,
|
||||
cb: Optional[Callable[[Union[str, Sequence[str]]], None]],
|
||||
filter: str = "*.*",
|
||||
dir: Optional[str] = None,
|
||||
key: Optional[str] = None,
|
||||
multi: bool = False, # controls whether a single or multiple files is returned
|
||||
) -> Optional[Union[Sequence[str], str]]:
|
||||
"Ask the user for a file."
|
||||
assert not dir or not key
|
||||
if not dir:
|
||||
|
@ -426,7 +483,7 @@ def getFile(parent, title, cb, filter="*.*", dir=None, key=None, multi=False):
|
|||
d.setNameFilter(filter)
|
||||
ret = []
|
||||
|
||||
def accept():
|
||||
def accept() -> None:
|
||||
files = list(d.selectedFiles())
|
||||
if dirkey:
|
||||
dir = os.path.dirname(files[0])
|
||||
|
@ -442,10 +499,17 @@ def getFile(parent, title, cb, filter="*.*", dir=None, key=None, multi=False):
|
|||
d.exec_()
|
||||
if key:
|
||||
saveState(d, key)
|
||||
return ret and ret[0]
|
||||
return ret[0] if ret else None
|
||||
|
||||
|
||||
def getSaveFile(parent, title, dir_description, key, ext, fname=None):
|
||||
def getSaveFile(
|
||||
parent: QDialog,
|
||||
title: str,
|
||||
dir_description: str,
|
||||
key: str,
|
||||
ext: str,
|
||||
fname: Optional[str] = None,
|
||||
) -> str:
|
||||
"""Ask the user for a file to save. Use DIR_DESCRIPTION as config
|
||||
variable. The file dialog will default to open with FNAME."""
|
||||
config_key = dir_description + "Directory"
|
||||
|
@ -474,7 +538,7 @@ def getSaveFile(parent, title, dir_description, key, ext, fname=None):
|
|||
return file
|
||||
|
||||
|
||||
def saveGeom(widget, key: str):
|
||||
def saveGeom(widget: QDialog, key: str) -> None:
|
||||
key += "Geom"
|
||||
if isMac and widget.windowState() & Qt.WindowFullScreen:
|
||||
geom = None
|
||||
|
@ -483,7 +547,9 @@ def saveGeom(widget, key: str):
|
|||
aqt.mw.pm.profile[key] = geom
|
||||
|
||||
|
||||
def restoreGeom(widget, key: str, offset=None, adjustSize=False):
|
||||
def restoreGeom(
|
||||
widget: QWidget, key: str, offset: Optional[int] = None, adjustSize: bool = False
|
||||
) -> None:
|
||||
key += "Geom"
|
||||
if aqt.mw.pm.profile.get(key):
|
||||
widget.restoreGeometry(aqt.mw.pm.profile[key])
|
||||
|
@ -498,7 +564,7 @@ def restoreGeom(widget, key: str, offset=None, adjustSize=False):
|
|||
widget.adjustSize()
|
||||
|
||||
|
||||
def ensureWidgetInScreenBoundaries(widget):
|
||||
def ensureWidgetInScreenBoundaries(widget: QWidget) -> None:
|
||||
handle = widget.window().windowHandle()
|
||||
if not handle:
|
||||
# window has not yet been shown, retry later
|
||||
|
@ -524,58 +590,60 @@ def ensureWidgetInScreenBoundaries(widget):
|
|||
widget.move(x, y)
|
||||
|
||||
|
||||
def saveState(widget, key: str):
|
||||
def saveState(widget: QFileDialog, key: str) -> None:
|
||||
key += "State"
|
||||
aqt.mw.pm.profile[key] = widget.saveState()
|
||||
|
||||
|
||||
def restoreState(widget, key: str):
|
||||
def restoreState(widget: Union[aqt.AnkiQt, QFileDialog], key: str) -> None:
|
||||
key += "State"
|
||||
if aqt.mw.pm.profile.get(key):
|
||||
widget.restoreState(aqt.mw.pm.profile[key])
|
||||
|
||||
|
||||
def saveSplitter(widget, key):
|
||||
def saveSplitter(widget: QSplitter, key: str) -> None:
|
||||
key += "Splitter"
|
||||
aqt.mw.pm.profile[key] = widget.saveState()
|
||||
|
||||
|
||||
def restoreSplitter(widget, key):
|
||||
def restoreSplitter(widget: QSplitter, key: str) -> None:
|
||||
key += "Splitter"
|
||||
if aqt.mw.pm.profile.get(key):
|
||||
widget.restoreState(aqt.mw.pm.profile[key])
|
||||
|
||||
|
||||
def saveHeader(widget, key):
|
||||
def saveHeader(widget: QHeaderView, key: str) -> None:
|
||||
key += "Header"
|
||||
aqt.mw.pm.profile[key] = widget.saveState()
|
||||
|
||||
|
||||
def restoreHeader(widget, key):
|
||||
def restoreHeader(widget: QHeaderView, key: str) -> None:
|
||||
key += "Header"
|
||||
if aqt.mw.pm.profile.get(key):
|
||||
widget.restoreState(aqt.mw.pm.profile[key])
|
||||
|
||||
|
||||
def save_is_checked(widget, key: str):
|
||||
def save_is_checked(widget: QWidget, key: str) -> None:
|
||||
key += "IsChecked"
|
||||
aqt.mw.pm.profile[key] = widget.isChecked()
|
||||
|
||||
|
||||
def restore_is_checked(widget, key: str):
|
||||
def restore_is_checked(widget: QWidget, key: str) -> None:
|
||||
key += "IsChecked"
|
||||
if aqt.mw.pm.profile.get(key) is not None:
|
||||
widget.setChecked(aqt.mw.pm.profile[key])
|
||||
|
||||
|
||||
def save_combo_index_for_session(widget: QComboBox, key: str):
|
||||
def save_combo_index_for_session(widget: QComboBox, key: str) -> None:
|
||||
textKey = key + "ComboActiveText"
|
||||
indexKey = key + "ComboActiveIndex"
|
||||
aqt.mw.pm.session[textKey] = widget.currentText()
|
||||
aqt.mw.pm.session[indexKey] = widget.currentIndex()
|
||||
|
||||
|
||||
def restore_combo_index_for_session(widget: QComboBox, history: List[str], key: str):
|
||||
def restore_combo_index_for_session(
|
||||
widget: QComboBox, history: List[str], key: str
|
||||
) -> None:
|
||||
textKey = key + "ComboActiveText"
|
||||
indexKey = key + "ComboActiveIndex"
|
||||
text = aqt.mw.pm.session.get(textKey)
|
||||
|
@ -585,7 +653,7 @@ def restore_combo_index_for_session(widget: QComboBox, history: List[str], key:
|
|||
widget.setCurrentIndex(index)
|
||||
|
||||
|
||||
def save_combo_history(comboBox: QComboBox, history: List[str], name: str):
|
||||
def save_combo_history(comboBox: QComboBox, history: List[str], name: str) -> str:
|
||||
name += "BoxHistory"
|
||||
text_input = comboBox.lineEdit().text()
|
||||
if text_input in history:
|
||||
|
@ -599,7 +667,7 @@ def save_combo_history(comboBox: QComboBox, history: List[str], name: str):
|
|||
return text_input
|
||||
|
||||
|
||||
def restore_combo_history(comboBox: QComboBox, name: str):
|
||||
def restore_combo_history(comboBox: QComboBox, name: str) -> List[str]:
|
||||
name += "BoxHistory"
|
||||
history = aqt.mw.pm.profile.get(name, [])
|
||||
comboBox.addItems([""] + history)
|
||||
|
@ -611,13 +679,13 @@ def restore_combo_history(comboBox: QComboBox, name: str):
|
|||
return history
|
||||
|
||||
|
||||
def mungeQA(col, txt):
|
||||
def mungeQA(col: Collection, txt: str) -> str:
|
||||
print("mungeQA() deprecated; use mw.prepare_card_text_for_display()")
|
||||
txt = col.media.escape_media_filenames(txt)
|
||||
return txt
|
||||
|
||||
|
||||
def openFolder(path):
|
||||
def openFolder(path: str) -> None:
|
||||
if isWin:
|
||||
subprocess.Popen(["explorer", "file://" + path])
|
||||
else:
|
||||
|
@ -625,27 +693,27 @@ def openFolder(path):
|
|||
QDesktopServices.openUrl(QUrl("file://" + path))
|
||||
|
||||
|
||||
def shortcut(key):
|
||||
def shortcut(key: str) -> str:
|
||||
if isMac:
|
||||
return re.sub("(?i)ctrl", "Command", key)
|
||||
return key
|
||||
|
||||
|
||||
def maybeHideClose(bbox):
|
||||
def maybeHideClose(bbox: QDialogButtonBox) -> None:
|
||||
if isMac:
|
||||
b = bbox.button(QDialogButtonBox.Close)
|
||||
if b:
|
||||
bbox.removeButton(b)
|
||||
|
||||
|
||||
def addCloseShortcut(widg):
|
||||
def addCloseShortcut(widg: QDialog) -> None:
|
||||
if not isMac:
|
||||
return
|
||||
widg._closeShortcut = QShortcut(QKeySequence("Ctrl+W"), widg)
|
||||
qconnect(widg._closeShortcut.activated, widg.reject)
|
||||
|
||||
|
||||
def downArrow():
|
||||
def downArrow() -> str:
|
||||
if isWin:
|
||||
return "▼"
|
||||
# windows 10 is lacking the smaller arrow on English installs
|
||||
|
@ -659,13 +727,19 @@ _tooltipTimer: Optional[QTimer] = None
|
|||
_tooltipLabel: Optional[QLabel] = None
|
||||
|
||||
|
||||
def tooltip(msg, period=3000, parent=None, x_offset=0, y_offset=100):
|
||||
def tooltip(
|
||||
msg: str,
|
||||
period: int = 3000,
|
||||
parent: Optional[aqt.AnkiQt] = None,
|
||||
x_offset: int = 0,
|
||||
y_offset: int = 100,
|
||||
) -> None:
|
||||
global _tooltipTimer, _tooltipLabel
|
||||
|
||||
class CustomLabel(QLabel):
|
||||
silentlyClose = True
|
||||
|
||||
def mousePressEvent(self, evt):
|
||||
def mousePressEvent(self, evt: QMouseEvent) -> None:
|
||||
evt.accept()
|
||||
self.hide()
|
||||
|
||||
|
@ -697,7 +771,7 @@ def tooltip(msg, period=3000, parent=None, x_offset=0, y_offset=100):
|
|||
_tooltipLabel = lab
|
||||
|
||||
|
||||
def closeTooltip():
|
||||
def closeTooltip() -> None:
|
||||
global _tooltipLabel, _tooltipTimer
|
||||
if _tooltipLabel:
|
||||
try:
|
||||
|
@ -712,7 +786,7 @@ def closeTooltip():
|
|||
|
||||
|
||||
# true if invalid; print warning
|
||||
def checkInvalidFilename(str, dirsep=True):
|
||||
def checkInvalidFilename(str: str, dirsep: bool = True) -> bool:
|
||||
bad = invalidFilename(str, dirsep)
|
||||
if bad:
|
||||
showWarning(tr(TR.QT_MISC_THE_FOLLOWING_CHARACTER_CAN_NOT_BE, val=bad))
|
||||
|
@ -723,28 +797,30 @@ def checkInvalidFilename(str, dirsep=True):
|
|||
# Menus
|
||||
######################################################################
|
||||
|
||||
MenuListChild = Union["SubMenu", QAction, "MenuItem", "MenuList"]
|
||||
|
||||
|
||||
class MenuList:
|
||||
def __init__(self):
|
||||
self.children = []
|
||||
def __init__(self) -> None:
|
||||
self.children: List[MenuListChild] = []
|
||||
|
||||
def addItem(self, title, func):
|
||||
def addItem(self, title: str, func: Callable) -> MenuItem:
|
||||
item = MenuItem(title, func)
|
||||
self.children.append(item)
|
||||
return item
|
||||
|
||||
def addSeparator(self):
|
||||
def addSeparator(self) -> None:
|
||||
self.children.append(None)
|
||||
|
||||
def addMenu(self, title):
|
||||
def addMenu(self, title: str) -> SubMenu:
|
||||
submenu = SubMenu(title)
|
||||
self.children.append(submenu)
|
||||
return submenu
|
||||
|
||||
def addChild(self, child):
|
||||
def addChild(self, child: Union[SubMenu, QAction, MenuList]) -> None:
|
||||
self.children.append(child)
|
||||
|
||||
def renderTo(self, qmenu):
|
||||
def renderTo(self, qmenu: QMenu) -> None:
|
||||
for child in self.children:
|
||||
if child is None:
|
||||
qmenu.addSeparator()
|
||||
|
@ -753,33 +829,33 @@ class MenuList:
|
|||
else:
|
||||
child.renderTo(qmenu)
|
||||
|
||||
def popupOver(self, widget):
|
||||
def popupOver(self, widget: QPushButton) -> None:
|
||||
qmenu = QMenu()
|
||||
self.renderTo(qmenu)
|
||||
qmenu.exec_(widget.mapToGlobal(QPoint(0, 0)))
|
||||
|
||||
|
||||
class SubMenu(MenuList):
|
||||
def __init__(self, title):
|
||||
def __init__(self, title: str) -> None:
|
||||
super().__init__()
|
||||
self.title = title
|
||||
|
||||
def renderTo(self, menu):
|
||||
def renderTo(self, menu: QMenu) -> None:
|
||||
submenu = menu.addMenu(self.title)
|
||||
super().renderTo(submenu)
|
||||
|
||||
|
||||
class MenuItem:
|
||||
def __init__(self, title, func):
|
||||
def __init__(self, title: str, func: Callable) -> None:
|
||||
self.title = title
|
||||
self.func = func
|
||||
|
||||
def renderTo(self, qmenu):
|
||||
def renderTo(self, qmenu: QMenu) -> None:
|
||||
a = qmenu.addAction(self.title)
|
||||
qconnect(a.triggered, self.func)
|
||||
|
||||
|
||||
def qtMenuShortcutWorkaround(qmenu):
|
||||
def qtMenuShortcutWorkaround(qmenu: QMenu) -> None:
|
||||
if qtminor < 10:
|
||||
return
|
||||
for act in qmenu.actions():
|
||||
|
@ -789,7 +865,7 @@ def qtMenuShortcutWorkaround(qmenu):
|
|||
######################################################################
|
||||
|
||||
|
||||
def supportText():
|
||||
def supportText() -> str:
|
||||
import platform
|
||||
import time
|
||||
|
||||
|
@ -802,9 +878,9 @@ def supportText():
|
|||
else:
|
||||
platname = "Linux"
|
||||
|
||||
def schedVer():
|
||||
def schedVer() -> str:
|
||||
try:
|
||||
return mw.col.schedVer()
|
||||
return str(mw.col.schedVer())
|
||||
except:
|
||||
return "?"
|
||||
|
||||
|
@ -832,7 +908,7 @@ Add-ons, last update check: {}
|
|||
######################################################################
|
||||
|
||||
# adapted from version detection in qutebrowser
|
||||
def opengl_vendor():
|
||||
def opengl_vendor() -> Optional[str]:
|
||||
old_context = QOpenGLContext.currentContext()
|
||||
old_surface = None if old_context is None else old_context.surface()
|
||||
|
||||
|
@ -871,7 +947,7 @@ def opengl_vendor():
|
|||
old_context.makeCurrent(old_surface)
|
||||
|
||||
|
||||
def gfxDriverIsBroken():
|
||||
def gfxDriverIsBroken() -> bool:
|
||||
driver = opengl_vendor()
|
||||
return driver == "nouveau"
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ serverbaseurl = re.compile(r"^.+:\/\/[^\/]+")
|
|||
|
||||
|
||||
class AnkiWebPage(QWebEnginePage):
|
||||
def __init__(self, onBridgeCmd):
|
||||
def __init__(self, onBridgeCmd) -> None:
|
||||
QWebEnginePage.__init__(self)
|
||||
self._onBridgeCmd = onBridgeCmd
|
||||
self._setupBridge()
|
||||
|
@ -31,7 +31,7 @@ class AnkiWebPage(QWebEnginePage):
|
|||
def _setupBridge(self) -> None:
|
||||
class Bridge(QObject):
|
||||
@pyqtSlot(str, result=str) # type: ignore
|
||||
def cmd(self, str):
|
||||
def cmd(self, str) -> Any:
|
||||
return json.dumps(self.onCmd(str))
|
||||
|
||||
self._bridge = Bridge()
|
||||
|
@ -74,7 +74,7 @@ class AnkiWebPage(QWebEnginePage):
|
|||
script.setRunsOnSubFrames(False)
|
||||
self.profile().scripts().insert(script)
|
||||
|
||||
def javaScriptConsoleMessage(self, level, msg, line, srcID):
|
||||
def javaScriptConsoleMessage(self, level, msg, line, srcID) -> None:
|
||||
# not translated because console usually not visible,
|
||||
# and may only accept ascii text
|
||||
if srcID.startswith("data"):
|
||||
|
@ -101,7 +101,7 @@ class AnkiWebPage(QWebEnginePage):
|
|||
# https://github.com/ankitects/anki/pull/560
|
||||
sys.stdout.write(buf)
|
||||
|
||||
def acceptNavigationRequest(self, url, navType, isMainFrame):
|
||||
def acceptNavigationRequest(self, url, navType, isMainFrame) -> bool:
|
||||
if not self.open_links_externally:
|
||||
return super().acceptNavigationRequest(url, navType, isMainFrame)
|
||||
|
||||
|
@ -120,10 +120,10 @@ class AnkiWebPage(QWebEnginePage):
|
|||
openLink(url)
|
||||
return False
|
||||
|
||||
def _onCmd(self, str):
|
||||
def _onCmd(self, str) -> None:
|
||||
return self._onBridgeCmd(str)
|
||||
|
||||
def javaScriptAlert(self, url: QUrl, text: str):
|
||||
def javaScriptAlert(self, url: QUrl, text: str) -> None:
|
||||
showInfo(text)
|
||||
|
||||
|
||||
|
@ -150,7 +150,7 @@ class WebContent:
|
|||
You should avoid overwriting or interfering with existing data as much
|
||||
as possible, instead opting to append your own changes, e.g.:
|
||||
|
||||
def on_webview_will_set_content(web_content: WebContent, context):
|
||||
def on_webview_will_set_content(web_content: WebContent, context) -> None:
|
||||
web_content.body += "<my_html>"
|
||||
web_content.head += "<my_head>"
|
||||
|
||||
|
@ -173,7 +173,7 @@ class WebContent:
|
|||
Then append the subpaths to the corresponding web_content fields
|
||||
within a function subscribing to gui_hooks.webview_will_set_content:
|
||||
|
||||
def on_webview_will_set_content(web_content: WebContent, context):
|
||||
def on_webview_will_set_content(web_content: WebContent, context) -> None:
|
||||
addon_package = mw.addonManager.addonFromModule(__name__)
|
||||
web_content.css.append(
|
||||
f"/_addons/{addon_package}/web/my-addon.css")
|
||||
|
@ -251,7 +251,7 @@ class AnkiWebView(QWebEngineView):
|
|||
def set_open_links_externally(self, enable: bool) -> None:
|
||||
self._page.open_links_externally = enable
|
||||
|
||||
def onEsc(self):
|
||||
def onEsc(self) -> None:
|
||||
w = self.parent()
|
||||
while w:
|
||||
if isinstance(w, QDialog) or isinstance(w, QMainWindow):
|
||||
|
@ -266,7 +266,7 @@ class AnkiWebView(QWebEngineView):
|
|||
break
|
||||
w = w.parent()
|
||||
|
||||
def onCopy(self):
|
||||
def onCopy(self) -> None:
|
||||
if not self.selectedText():
|
||||
ctx = self._page.contextMenuData()
|
||||
if ctx and ctx.mediaType() == QWebEngineContextMenuData.MediaTypeImage:
|
||||
|
@ -274,16 +274,16 @@ class AnkiWebView(QWebEngineView):
|
|||
else:
|
||||
self.triggerPageAction(QWebEnginePage.Copy)
|
||||
|
||||
def onCut(self):
|
||||
def onCut(self) -> None:
|
||||
self.triggerPageAction(QWebEnginePage.Cut)
|
||||
|
||||
def onPaste(self):
|
||||
def onPaste(self) -> None:
|
||||
self.triggerPageAction(QWebEnginePage.Paste)
|
||||
|
||||
def onMiddleClickPaste(self):
|
||||
def onMiddleClickPaste(self) -> None:
|
||||
self.triggerPageAction(QWebEnginePage.Paste)
|
||||
|
||||
def onSelectAll(self):
|
||||
def onSelectAll(self) -> None:
|
||||
self.triggerPageAction(QWebEnginePage.SelectAll)
|
||||
|
||||
def contextMenuEvent(self, evt: QContextMenuEvent) -> None:
|
||||
|
@ -293,7 +293,7 @@ class AnkiWebView(QWebEngineView):
|
|||
gui_hooks.webview_will_show_context_menu(self, m)
|
||||
m.popup(QCursor.pos())
|
||||
|
||||
def dropEvent(self, evt):
|
||||
def dropEvent(self, evt) -> None:
|
||||
pass
|
||||
|
||||
def setHtml(self, html: str) -> None: # type: ignore
|
||||
|
@ -312,7 +312,7 @@ class AnkiWebView(QWebEngineView):
|
|||
if oldFocus:
|
||||
oldFocus.setFocus()
|
||||
|
||||
def load(self, url: QUrl):
|
||||
def load(self, url: QUrl) -> None:
|
||||
# allow queuing actions when loading url directly
|
||||
self._domDone = False
|
||||
super().load(url)
|
||||
|
@ -364,7 +364,7 @@ class AnkiWebView(QWebEngineView):
|
|||
else:
|
||||
return 3
|
||||
|
||||
def _getWindowColor(self):
|
||||
def _getWindowColor(self) -> QColor:
|
||||
if theme_manager.night_mode:
|
||||
return theme_manager.qcolor("window-bg")
|
||||
if isMac:
|
||||
|
@ -508,7 +508,7 @@ body {{ zoom: {zoom}; background: {background}; direction: {lang_dir}; {font} }}
|
|||
def _evalWithCallback(self, js: str, cb: Callable[[Any], Any]) -> None:
|
||||
if cb:
|
||||
|
||||
def handler(val):
|
||||
def handler(val) -> None:
|
||||
if self._shouldIgnoreWebEvent():
|
||||
print("ignored late js callback", cb)
|
||||
return
|
||||
|
@ -597,18 +597,18 @@ body {{ zoom: {zoom}; background: {background}; direction: {lang_dir}; {font} }}
|
|||
self.onBridgeCmd = func
|
||||
self._bridge_context = context
|
||||
|
||||
def hide_while_preserving_layout(self):
|
||||
def hide_while_preserving_layout(self) -> None:
|
||||
"Hide but keep existing size."
|
||||
sp = self.sizePolicy()
|
||||
sp.setRetainSizeWhenHidden(True)
|
||||
self.setSizePolicy(sp)
|
||||
self.hide()
|
||||
|
||||
def inject_dynamic_style_and_show(self):
|
||||
def inject_dynamic_style_and_show(self) -> None:
|
||||
"Add dynamic styling, and reveal."
|
||||
css = self.standard_css()
|
||||
|
||||
def after_style(arg):
|
||||
def after_style(arg) -> None:
|
||||
gui_hooks.webview_did_inject_style_into_page(self)
|
||||
self.show()
|
||||
|
||||
|
|
13
qt/dmypy-watch.sh
Executable file
13
qt/dmypy-watch.sh
Executable file
|
@ -0,0 +1,13 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# semi-working support for mypy daemon
|
||||
# - install fs_watch
|
||||
# - build anki/aqt wheels first
|
||||
# - create a new venv and activate it
|
||||
# - install the wheels
|
||||
# - then run this script from this folder
|
||||
|
||||
(sleep 1 && touch aqt)
|
||||
. ~/pyenv/bin/activate
|
||||
fswatch -o aqt | xargs -n1 -I{} sh -c 'printf \\033c\\n; dmypy run aqt'
|
||||
|
11
qt/mypy.ini
11
qt/mypy.ini
|
@ -1,6 +1,5 @@
|
|||
[mypy]
|
||||
python_version = 3.8
|
||||
pretty = true
|
||||
no_strict_optional = true
|
||||
show_error_codes = true
|
||||
disallow_untyped_decorators = True
|
||||
|
@ -9,6 +8,16 @@ warn_unused_configs = True
|
|||
check_untyped_defs = true
|
||||
strict_equality = true
|
||||
|
||||
[mypy-aqt.browser]
|
||||
disallow_untyped_defs=true
|
||||
[mypy-aqt.sidebar]
|
||||
disallow_untyped_defs=true
|
||||
[mypy-aqt.editor]
|
||||
disallow_untyped_defs=true
|
||||
[mypy-aqt.utils]
|
||||
disallow_untyped_defs=true
|
||||
|
||||
|
||||
[mypy-aqt.mpv]
|
||||
ignore_errors=true
|
||||
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
load("@rules_proto//proto:defs.bzl", "proto_library")
|
||||
load("@io_bazel_rules_rust//rust:rust.bzl", "rust_binary", "rust_library", "rust_test")
|
||||
load("@io_bazel_rules_rust//cargo:cargo_build_script.bzl", "cargo_build_script")
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
syntax = "proto3";
|
||||
|
||||
package BackendProto;
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
pub mod mergeftl;
|
||||
pub mod protobuf;
|
||||
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use fluent_syntax::ast::Entry;
|
||||
use fluent_syntax::parser::Parser;
|
||||
use std::path::Path;
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::{env, fmt::Write};
|
||||
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
include!("mergeftl.rs");
|
||||
|
||||
fn main() {
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
"""
|
||||
@generated
|
||||
cargo-raze generated Bazel file.
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import sys, subprocess, os, difflib
|
||||
|
||||
clang_format = sys.argv[1]
|
||||
|
@ -33,4 +36,4 @@ for path in sys.argv[2:]:
|
|||
found_bad = True
|
||||
|
||||
if found_bad:
|
||||
sys.exit(1)
|
||||
sys.exit(1)
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
# Copyright: Ankitects Pty Ltd and contributors
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
def _rustfmt_impl(ctx):
|
||||
toolchain = ctx.toolchains["@io_bazel_rules_rust//rust:toolchain"]
|
||||
script_name = ctx.label.name + "_script"
|
||||
|
|
|
@ -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"));
|
||||
|
|
|
@ -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"));
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
mod cards;
|
||||
mod notes;
|
||||
mod parser;
|
||||
|
|
|
@ -18,9 +18,9 @@ sql_format_setup()
|
|||
|
||||
exports_files([
|
||||
"tsconfig.json",
|
||||
"d3_missing.d.ts",
|
||||
".prettierrc",
|
||||
"rollup.config.js",
|
||||
"rollup.aqt.config.js",
|
||||
".eslintrc.js",
|
||||
"licenses.json",
|
||||
"sql_format.ts",
|
||||
|
|
62
ts/editor/BUILD.bazel
Normal file
62
ts/editor/BUILD.bazel
Normal file
|
@ -0,0 +1,62 @@
|
|||
load("@npm//@bazel/typescript:index.bzl", "ts_library")
|
||||
load("@npm//@bazel/rollup:index.bzl", "rollup_bundle")
|
||||
load("//ts:prettier.bzl", "prettier_test")
|
||||
load("//ts:eslint.bzl", "eslint_test")
|
||||
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary")
|
||||
|
||||
sass_binary(
|
||||
name = "editor_css",
|
||||
src = "editor.scss",
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
sass_binary(
|
||||
name = "editable_css",
|
||||
src = "editable.scss",
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "editor_ts",
|
||||
srcs = glob(["*.ts"]),
|
||||
tsconfig = "//qt/aqt/data/web/js:tsconfig.json",
|
||||
deps = [
|
||||
"@npm//@types/jquery",
|
||||
],
|
||||
)
|
||||
|
||||
rollup_bundle(
|
||||
name = "editor",
|
||||
config_file = "//ts:rollup.aqt.config.js",
|
||||
entry_point = "index.ts",
|
||||
format = "iife",
|
||||
link_workspace_root = True,
|
||||
silent = True,
|
||||
sourcemap = "false",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"editor_ts",
|
||||
"@npm//@rollup/plugin-commonjs",
|
||||
"@npm//@rollup/plugin-node-resolve",
|
||||
"@npm//rollup-plugin-terser",
|
||||
],
|
||||
)
|
||||
|
||||
# Tests
|
||||
################
|
||||
|
||||
prettier_test(
|
||||
name = "format_check",
|
||||
srcs = glob([
|
||||
"*.ts",
|
||||
]),
|
||||
)
|
||||
|
||||
# eslint_test(
|
||||
# name = "eslint",
|
||||
# srcs = glob(
|
||||
# [
|
||||
# "*.ts",
|
||||
# ],
|
||||
# ),
|
||||
# )
|
170
ts/editor/filterHtml.ts
Normal file
170
ts/editor/filterHtml.ts
Normal file
|
@ -0,0 +1,170 @@
|
|||
import { nodeIsElement } from "./helpers";
|
||||
|
||||
export let filterHTML = function (
|
||||
html: string,
|
||||
internal: boolean,
|
||||
extendedMode: boolean
|
||||
): string {
|
||||
// wrap it in <top> as we aren't allowed to change top level elements
|
||||
const top = document.createElement("ankitop");
|
||||
top.innerHTML = html;
|
||||
|
||||
if (internal) {
|
||||
filterInternalNode(top);
|
||||
} else {
|
||||
filterNode(top, extendedMode);
|
||||
}
|
||||
let outHtml = top.innerHTML;
|
||||
if (!extendedMode && !internal) {
|
||||
// collapse whitespace
|
||||
outHtml = outHtml.replace(/[\n\t ]+/g, " ");
|
||||
}
|
||||
outHtml = outHtml.trim();
|
||||
return outHtml;
|
||||
};
|
||||
|
||||
let allowedTagsBasic = {};
|
||||
let allowedTagsExtended = {};
|
||||
|
||||
let TAGS_WITHOUT_ATTRS = ["P", "DIV", "BR", "SUB", "SUP"];
|
||||
for (const tag of TAGS_WITHOUT_ATTRS) {
|
||||
allowedTagsBasic[tag] = { attrs: [] };
|
||||
}
|
||||
|
||||
TAGS_WITHOUT_ATTRS = [
|
||||
"B",
|
||||
"BLOCKQUOTE",
|
||||
"CODE",
|
||||
"DD",
|
||||
"DL",
|
||||
"DT",
|
||||
"EM",
|
||||
"H1",
|
||||
"H2",
|
||||
"H3",
|
||||
"I",
|
||||
"LI",
|
||||
"OL",
|
||||
"PRE",
|
||||
"RP",
|
||||
"RT",
|
||||
"RUBY",
|
||||
"STRONG",
|
||||
"TABLE",
|
||||
"U",
|
||||
"UL",
|
||||
];
|
||||
for (const tag of TAGS_WITHOUT_ATTRS) {
|
||||
allowedTagsExtended[tag] = { attrs: [] };
|
||||
}
|
||||
|
||||
allowedTagsBasic["IMG"] = { attrs: ["SRC"] };
|
||||
|
||||
allowedTagsExtended["A"] = { attrs: ["HREF"] };
|
||||
allowedTagsExtended["TR"] = { attrs: ["ROWSPAN"] };
|
||||
allowedTagsExtended["TD"] = { attrs: ["COLSPAN", "ROWSPAN"] };
|
||||
allowedTagsExtended["TH"] = { attrs: ["COLSPAN", "ROWSPAN"] };
|
||||
allowedTagsExtended["FONT"] = { attrs: ["COLOR"] };
|
||||
|
||||
const allowedStyling = {
|
||||
color: true,
|
||||
"background-color": true,
|
||||
"font-weight": true,
|
||||
"font-style": true,
|
||||
"text-decoration-line": true,
|
||||
};
|
||||
|
||||
let isNightMode = function (): boolean {
|
||||
return document.body.classList.contains("nightMode");
|
||||
};
|
||||
|
||||
let filterExternalSpan = function (elem: HTMLElement) {
|
||||
// filter out attributes
|
||||
for (const attr of [...elem.attributes]) {
|
||||
const attrName = attr.name.toUpperCase();
|
||||
|
||||
if (attrName !== "STYLE") {
|
||||
elem.removeAttributeNode(attr);
|
||||
}
|
||||
}
|
||||
|
||||
// filter styling
|
||||
for (const name of [...elem.style]) {
|
||||
const value = elem.style.getPropertyValue(name);
|
||||
|
||||
if (
|
||||
!allowedStyling.hasOwnProperty(name) ||
|
||||
// google docs adds this unnecessarily
|
||||
(name === "background-color" && value === "transparent") ||
|
||||
// ignore coloured text in night mode for now
|
||||
(isNightMode() && (name === "background-color" || name === "color"))
|
||||
) {
|
||||
elem.style.removeProperty(name);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
allowedTagsExtended["SPAN"] = filterExternalSpan;
|
||||
|
||||
// add basic tags to extended
|
||||
Object.assign(allowedTagsExtended, allowedTagsBasic);
|
||||
|
||||
function isHTMLElement(elem: Element): elem is HTMLElement {
|
||||
return elem instanceof HTMLElement;
|
||||
}
|
||||
|
||||
// filtering from another field
|
||||
let filterInternalNode = function (elem: Element) {
|
||||
if (isHTMLElement(elem)) {
|
||||
elem.style.removeProperty("background-color");
|
||||
elem.style.removeProperty("font-size");
|
||||
elem.style.removeProperty("font-family");
|
||||
}
|
||||
// recurse
|
||||
for (let i = 0; i < elem.children.length; i++) {
|
||||
const child = elem.children[i];
|
||||
filterInternalNode(child);
|
||||
}
|
||||
};
|
||||
|
||||
// filtering from external sources
|
||||
let filterNode = function (node: Node, extendedMode: boolean): void {
|
||||
if (!nodeIsElement(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// descend first, and take a copy of the child nodes as the loop will skip
|
||||
// elements due to node modifications otherwise
|
||||
for (const child of [...node.children]) {
|
||||
filterNode(child, extendedMode);
|
||||
}
|
||||
|
||||
if (node.tagName === "ANKITOP") {
|
||||
return;
|
||||
}
|
||||
|
||||
const tag = extendedMode
|
||||
? allowedTagsExtended[node.tagName]
|
||||
: allowedTagsBasic[node.tagName];
|
||||
|
||||
if (!tag) {
|
||||
if (!node.innerHTML || node.tagName === "TITLE") {
|
||||
node.parentNode.removeChild(node);
|
||||
} else {
|
||||
node.outerHTML = node.innerHTML;
|
||||
}
|
||||
} else {
|
||||
if (typeof tag === "function") {
|
||||
// filtering function provided
|
||||
tag(node);
|
||||
} else {
|
||||
// allowed, filter out attributes
|
||||
for (const attr of [...node.attributes]) {
|
||||
const attrName = attr.name.toUpperCase();
|
||||
if (tag.attrs.indexOf(attrName) === -1) {
|
||||
node.removeAttributeNode(attr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
65
ts/editor/helpers.ts
Normal file
65
ts/editor/helpers.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
export function nodeIsElement(node: Node): node is Element {
|
||||
return node.nodeType === Node.ELEMENT_NODE;
|
||||
}
|
||||
|
||||
const INLINE_TAGS = [
|
||||
"A",
|
||||
"ABBR",
|
||||
"ACRONYM",
|
||||
"AUDIO",
|
||||
"B",
|
||||
"BDI",
|
||||
"BDO",
|
||||
"BIG",
|
||||
"BR",
|
||||
"BUTTON",
|
||||
"CANVAS",
|
||||
"CITE",
|
||||
"CODE",
|
||||
"DATA",
|
||||
"DATALIST",
|
||||
"DEL",
|
||||
"DFN",
|
||||
"EM",
|
||||
"EMBED",
|
||||
"I",
|
||||
"IFRAME",
|
||||
"IMG",
|
||||
"INPUT",
|
||||
"INS",
|
||||
"KBD",
|
||||
"LABEL",
|
||||
"MAP",
|
||||
"MARK",
|
||||
"METER",
|
||||
"NOSCRIPT",
|
||||
"OBJECT",
|
||||
"OUTPUT",
|
||||
"PICTURE",
|
||||
"PROGRESS",
|
||||
"Q",
|
||||
"RUBY",
|
||||
"S",
|
||||
"SAMP",
|
||||
"SCRIPT",
|
||||
"SELECT",
|
||||
"SLOT",
|
||||
"SMALL",
|
||||
"SPAN",
|
||||
"STRONG",
|
||||
"SUB",
|
||||
"SUP",
|
||||
"SVG",
|
||||
"TEMPLATE",
|
||||
"TEXTAREA",
|
||||
"TIME",
|
||||
"U",
|
||||
"TT",
|
||||
"VAR",
|
||||
"VIDEO",
|
||||
"WBR",
|
||||
];
|
||||
|
||||
export function nodeIsInline(node: Node): boolean {
|
||||
return !nodeIsElement(node) || INLINE_TAGS.includes(node.tagName);
|
||||
}
|
|
@ -1,12 +1,25 @@
|
|||
/* Copyright: Ankitects Pty Ltd and contributors
|
||||
* License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */
|
||||
|
||||
import { filterHTML } from "./filterHtml";
|
||||
import { nodeIsElement, nodeIsInline } from "./helpers";
|
||||
import { bridgeCommand } from "./lib";
|
||||
|
||||
let currentField: EditingArea | null = null;
|
||||
let changeTimer: number | null = null;
|
||||
let currentNoteId: number | null = null;
|
||||
|
||||
declare interface String {
|
||||
format(...args: string[]): string;
|
||||
declare global {
|
||||
interface String {
|
||||
format(...args: string[]): string;
|
||||
}
|
||||
|
||||
interface Selection {
|
||||
modify(s: string, t: string, u: string): void;
|
||||
addRange(r: Range): void;
|
||||
removeAllRanges(): void;
|
||||
getRangeAt(n: number): Range;
|
||||
}
|
||||
}
|
||||
|
||||
/* kept for compatibility with add-ons */
|
||||
|
@ -18,11 +31,11 @@ String.prototype.format = function (...args: string[]): string {
|
|||
});
|
||||
};
|
||||
|
||||
function setFGButton(col: string): void {
|
||||
export function setFGButton(col: string): void {
|
||||
document.getElementById("forecolor").style.backgroundColor = col;
|
||||
}
|
||||
|
||||
function saveNow(keepFocus: boolean): void {
|
||||
export function saveNow(keepFocus: boolean): void {
|
||||
if (!currentField) {
|
||||
return;
|
||||
}
|
||||
|
@ -45,10 +58,6 @@ function triggerKeyTimer(): void {
|
|||
}, 600);
|
||||
}
|
||||
|
||||
interface Selection {
|
||||
modify(s: string, t: string, u: string): void;
|
||||
}
|
||||
|
||||
function onKey(evt: KeyboardEvent): void {
|
||||
// esc clears focus, allowing dialog to close
|
||||
if (evt.code === "Escape") {
|
||||
|
@ -100,72 +109,6 @@ function onKeyUp(evt: KeyboardEvent): void {
|
|||
}
|
||||
}
|
||||
|
||||
function nodeIsElement(node: Node): node is Element {
|
||||
return node.nodeType === Node.ELEMENT_NODE;
|
||||
}
|
||||
|
||||
const INLINE_TAGS = [
|
||||
"A",
|
||||
"ABBR",
|
||||
"ACRONYM",
|
||||
"AUDIO",
|
||||
"B",
|
||||
"BDI",
|
||||
"BDO",
|
||||
"BIG",
|
||||
"BR",
|
||||
"BUTTON",
|
||||
"CANVAS",
|
||||
"CITE",
|
||||
"CODE",
|
||||
"DATA",
|
||||
"DATALIST",
|
||||
"DEL",
|
||||
"DFN",
|
||||
"EM",
|
||||
"EMBED",
|
||||
"I",
|
||||
"IFRAME",
|
||||
"IMG",
|
||||
"INPUT",
|
||||
"INS",
|
||||
"KBD",
|
||||
"LABEL",
|
||||
"MAP",
|
||||
"MARK",
|
||||
"METER",
|
||||
"NOSCRIPT",
|
||||
"OBJECT",
|
||||
"OUTPUT",
|
||||
"PICTURE",
|
||||
"PROGRESS",
|
||||
"Q",
|
||||
"RUBY",
|
||||
"S",
|
||||
"SAMP",
|
||||
"SCRIPT",
|
||||
"SELECT",
|
||||
"SLOT",
|
||||
"SMALL",
|
||||
"SPAN",
|
||||
"STRONG",
|
||||
"SUB",
|
||||
"SUP",
|
||||
"SVG",
|
||||
"TEMPLATE",
|
||||
"TEXTAREA",
|
||||
"TIME",
|
||||
"U",
|
||||
"TT",
|
||||
"VAR",
|
||||
"VIDEO",
|
||||
"WBR",
|
||||
];
|
||||
|
||||
function nodeIsInline(node: Node): boolean {
|
||||
return !nodeIsElement(node) || INLINE_TAGS.includes(node.tagName);
|
||||
}
|
||||
|
||||
function inListItem(): boolean {
|
||||
const anchor = currentField.getSelection().anchorNode;
|
||||
|
||||
|
@ -179,7 +122,7 @@ function inListItem(): boolean {
|
|||
return inList;
|
||||
}
|
||||
|
||||
function insertNewline(): void {
|
||||
export function insertNewline(): void {
|
||||
if (!inPreEnvironment()) {
|
||||
setFormat("insertText", "\n");
|
||||
return;
|
||||
|
@ -228,12 +171,12 @@ function updateButtonState(): void {
|
|||
// 'col': document.queryCommandValue("forecolor")
|
||||
}
|
||||
|
||||
function toggleEditorButton(buttonid: string): void {
|
||||
export function toggleEditorButton(buttonid: string): void {
|
||||
const button = $(buttonid)[0];
|
||||
button.classList.toggle("highlighted");
|
||||
}
|
||||
|
||||
function setFormat(cmd: string, arg?: any, nosave: boolean = false): void {
|
||||
export function setFormat(cmd: string, arg?: any, nosave: boolean = false): void {
|
||||
document.execCommand(cmd, false, arg);
|
||||
if (!nosave) {
|
||||
saveField("key");
|
||||
|
@ -256,7 +199,7 @@ function onFocus(evt: FocusEvent): void {
|
|||
}
|
||||
elem.focusEditable();
|
||||
currentField = elem;
|
||||
pycmd(`focus:${currentField.ord}`);
|
||||
bridgeCommand(`focus:${currentField.ord}`);
|
||||
enableButtons();
|
||||
// do this twice so that there's no flicker on newer versions
|
||||
caretToEnd();
|
||||
|
@ -279,7 +222,7 @@ function onFocus(evt: FocusEvent): void {
|
|||
}
|
||||
}
|
||||
|
||||
function focusField(n: number): void {
|
||||
export function focusField(n: number): void {
|
||||
const field = getEditorField(n);
|
||||
|
||||
if (field) {
|
||||
|
@ -287,7 +230,7 @@ function focusField(n: number): void {
|
|||
}
|
||||
}
|
||||
|
||||
function focusIfField(x: number, y: number): boolean {
|
||||
export function focusIfField(x: number, y: number): boolean {
|
||||
const elements = document.elementsFromPoint(x, y);
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
let elem = elements[i] as EditingArea;
|
||||
|
@ -303,7 +246,7 @@ function focusIfField(x: number, y: number): boolean {
|
|||
}
|
||||
|
||||
function onPaste(): void {
|
||||
pycmd("paste");
|
||||
bridgeCommand("paste");
|
||||
window.event.preventDefault();
|
||||
}
|
||||
|
||||
|
@ -353,7 +296,9 @@ function saveField(type: "blur" | "key"): void {
|
|||
return;
|
||||
}
|
||||
|
||||
pycmd(`${type}:${currentField.ord}:${currentNoteId}:${currentField.fieldHTML}`);
|
||||
bridgeCommand(
|
||||
`${type}:${currentField.ord}:${currentNoteId}:${currentField.fieldHTML}`
|
||||
);
|
||||
}
|
||||
|
||||
function wrappedExceptForWhitespace(text: string, front: string, back: string): string {
|
||||
|
@ -361,7 +306,7 @@ function wrappedExceptForWhitespace(text: string, front: string, back: string):
|
|||
return match[1] + front + match[2] + back + match[3];
|
||||
}
|
||||
|
||||
function preventButtonFocus(): void {
|
||||
export function preventButtonFocus(): void {
|
||||
for (const element of document.querySelectorAll("button.linkb")) {
|
||||
element.addEventListener("mousedown", (evt: Event) => {
|
||||
evt.preventDefault();
|
||||
|
@ -386,7 +331,7 @@ function maybeDisableButtons(): void {
|
|||
}
|
||||
}
|
||||
|
||||
function wrap(front: string, back: string): void {
|
||||
export function wrap(front: string, back: string): void {
|
||||
wrapInternal(front, back, false);
|
||||
}
|
||||
|
||||
|
@ -419,7 +364,7 @@ function wrapInternal(front: string, back: string, plainText: boolean): void {
|
|||
}
|
||||
|
||||
function onCutOrCopy(): boolean {
|
||||
pycmd("cutOrCopy");
|
||||
bridgeCommand("cutOrCopy");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -606,12 +551,12 @@ function adjustFieldAmount(amount: number): void {
|
|||
}
|
||||
}
|
||||
|
||||
function getEditorField(n: number): EditorField | null {
|
||||
export function getEditorField(n: number): EditorField | null {
|
||||
const fields = document.getElementById("fields").children;
|
||||
return (fields[n] as EditorField) ?? null;
|
||||
}
|
||||
|
||||
function forEditorField<T>(
|
||||
export function forEditorField<T>(
|
||||
values: T[],
|
||||
func: (field: EditorField, value: T) => void
|
||||
): void {
|
||||
|
@ -622,7 +567,7 @@ function forEditorField<T>(
|
|||
}
|
||||
}
|
||||
|
||||
function setFields(fields: [string, string][]): void {
|
||||
export function setFields(fields: [string, string][]): void {
|
||||
// webengine will include the variable after enter+backspace
|
||||
// if we don't convert it to a literal colour
|
||||
const color = window
|
||||
|
@ -637,7 +582,7 @@ function setFields(fields: [string, string][]): void {
|
|||
maybeDisableButtons();
|
||||
}
|
||||
|
||||
function setBackgrounds(cols: ("dupe" | "")[]) {
|
||||
export function setBackgrounds(cols: ("dupe" | "")[]) {
|
||||
forEditorField(cols, (field, value) =>
|
||||
field.editingArea.classList.toggle("dupe", value === "dupe")
|
||||
);
|
||||
|
@ -646,17 +591,17 @@ function setBackgrounds(cols: ("dupe" | "")[]) {
|
|||
.classList.toggle("is-inactive", !cols.includes("dupe"));
|
||||
}
|
||||
|
||||
function setFonts(fonts: [string, number, boolean][]): void {
|
||||
export function setFonts(fonts: [string, number, boolean][]): void {
|
||||
forEditorField(fonts, (field, [fontFamily, fontSize, isRtl]) => {
|
||||
field.setBaseStyling(fontFamily, `${fontSize}px`, isRtl ? "rtl" : "ltr");
|
||||
});
|
||||
}
|
||||
|
||||
function setNoteId(id: number): void {
|
||||
export function setNoteId(id: number): void {
|
||||
currentNoteId = id;
|
||||
}
|
||||
|
||||
let pasteHTML = function (
|
||||
export let pasteHTML = function (
|
||||
html: string,
|
||||
internal: boolean,
|
||||
extendedMode: boolean
|
||||
|
@ -667,172 +612,3 @@ let pasteHTML = function (
|
|||
setFormat("inserthtml", html);
|
||||
}
|
||||
};
|
||||
|
||||
let filterHTML = function (
|
||||
html: string,
|
||||
internal: boolean,
|
||||
extendedMode: boolean
|
||||
): string {
|
||||
// wrap it in <top> as we aren't allowed to change top level elements
|
||||
const top = document.createElement("ankitop");
|
||||
top.innerHTML = html;
|
||||
|
||||
if (internal) {
|
||||
filterInternalNode(top);
|
||||
} else {
|
||||
filterNode(top, extendedMode);
|
||||
}
|
||||
let outHtml = top.innerHTML;
|
||||
if (!extendedMode && !internal) {
|
||||
// collapse whitespace
|
||||
outHtml = outHtml.replace(/[\n\t ]+/g, " ");
|
||||
}
|
||||
outHtml = outHtml.trim();
|
||||
return outHtml;
|
||||
};
|
||||
|
||||
let allowedTagsBasic = {};
|
||||
let allowedTagsExtended = {};
|
||||
|
||||
let TAGS_WITHOUT_ATTRS = ["P", "DIV", "BR", "SUB", "SUP"];
|
||||
for (const tag of TAGS_WITHOUT_ATTRS) {
|
||||
allowedTagsBasic[tag] = { attrs: [] };
|
||||
}
|
||||
|
||||
TAGS_WITHOUT_ATTRS = [
|
||||
"B",
|
||||
"BLOCKQUOTE",
|
||||
"CODE",
|
||||
"DD",
|
||||
"DL",
|
||||
"DT",
|
||||
"EM",
|
||||
"H1",
|
||||
"H2",
|
||||
"H3",
|
||||
"I",
|
||||
"LI",
|
||||
"OL",
|
||||
"PRE",
|
||||
"RP",
|
||||
"RT",
|
||||
"RUBY",
|
||||
"STRONG",
|
||||
"TABLE",
|
||||
"U",
|
||||
"UL",
|
||||
];
|
||||
for (const tag of TAGS_WITHOUT_ATTRS) {
|
||||
allowedTagsExtended[tag] = { attrs: [] };
|
||||
}
|
||||
|
||||
allowedTagsBasic["IMG"] = { attrs: ["SRC"] };
|
||||
|
||||
allowedTagsExtended["A"] = { attrs: ["HREF"] };
|
||||
allowedTagsExtended["TR"] = { attrs: ["ROWSPAN"] };
|
||||
allowedTagsExtended["TD"] = { attrs: ["COLSPAN", "ROWSPAN"] };
|
||||
allowedTagsExtended["TH"] = { attrs: ["COLSPAN", "ROWSPAN"] };
|
||||
allowedTagsExtended["FONT"] = { attrs: ["COLOR"] };
|
||||
|
||||
const allowedStyling = {
|
||||
color: true,
|
||||
"background-color": true,
|
||||
"font-weight": true,
|
||||
"font-style": true,
|
||||
"text-decoration-line": true,
|
||||
};
|
||||
|
||||
let isNightMode = function (): boolean {
|
||||
return document.body.classList.contains("nightMode");
|
||||
};
|
||||
|
||||
let filterExternalSpan = function (elem: HTMLElement) {
|
||||
// filter out attributes
|
||||
for (const attr of [...elem.attributes]) {
|
||||
const attrName = attr.name.toUpperCase();
|
||||
|
||||
if (attrName !== "STYLE") {
|
||||
elem.removeAttributeNode(attr);
|
||||
}
|
||||
}
|
||||
|
||||
// filter styling
|
||||
for (const name of [...elem.style]) {
|
||||
const value = elem.style.getPropertyValue(name);
|
||||
|
||||
if (
|
||||
!allowedStyling.hasOwnProperty(name) ||
|
||||
// google docs adds this unnecessarily
|
||||
(name === "background-color" && value === "transparent") ||
|
||||
// ignore coloured text in night mode for now
|
||||
(isNightMode() && (name === "background-color" || name === "color"))
|
||||
) {
|
||||
elem.style.removeProperty(name);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
allowedTagsExtended["SPAN"] = filterExternalSpan;
|
||||
|
||||
// add basic tags to extended
|
||||
Object.assign(allowedTagsExtended, allowedTagsBasic);
|
||||
|
||||
function isHTMLElement(elem: Element): elem is HTMLElement {
|
||||
return elem instanceof HTMLElement;
|
||||
}
|
||||
|
||||
// filtering from another field
|
||||
let filterInternalNode = function (elem: Element) {
|
||||
if (isHTMLElement(elem)) {
|
||||
elem.style.removeProperty("background-color");
|
||||
elem.style.removeProperty("font-size");
|
||||
elem.style.removeProperty("font-family");
|
||||
}
|
||||
// recurse
|
||||
for (let i = 0; i < elem.children.length; i++) {
|
||||
const child = elem.children[i];
|
||||
filterInternalNode(child);
|
||||
}
|
||||
};
|
||||
|
||||
// filtering from external sources
|
||||
let filterNode = function (node: Node, extendedMode: boolean): void {
|
||||
if (!nodeIsElement(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// descend first, and take a copy of the child nodes as the loop will skip
|
||||
// elements due to node modifications otherwise
|
||||
for (const child of [...node.children]) {
|
||||
filterNode(child, extendedMode);
|
||||
}
|
||||
|
||||
if (node.tagName === "ANKITOP") {
|
||||
return;
|
||||
}
|
||||
|
||||
const tag = extendedMode
|
||||
? allowedTagsExtended[node.tagName]
|
||||
: allowedTagsBasic[node.tagName];
|
||||
|
||||
if (!tag) {
|
||||
if (!node.innerHTML || node.tagName === "TITLE") {
|
||||
node.parentNode.removeChild(node);
|
||||
} else {
|
||||
node.outerHTML = node.innerHTML;
|
||||
}
|
||||
} else {
|
||||
if (typeof tag === "function") {
|
||||
// filtering function provided
|
||||
tag(node);
|
||||
} else {
|
||||
// allowed, filter out attributes
|
||||
for (const attr of [...node.attributes]) {
|
||||
const attrName = attr.name.toUpperCase();
|
||||
if (tag.attrs.indexOf(attrName) === -1) {
|
||||
node.removeAttributeNode(attr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
9
ts/editor/lib.ts
Normal file
9
ts/editor/lib.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
declare global {
|
||||
interface Window {
|
||||
bridgeCommand<T>(command: string, callback?: (value: T) => void): void;
|
||||
}
|
||||
}
|
||||
|
||||
export function bridgeCommand<T>(command: string, callback?: (value: T) => void): void {
|
||||
window.bridgeCommand<T>(command, callback);
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue