add separate file for gui hooks

This commit is contained in:
Damien Elmes 2020-01-13 14:38:05 +10:00
parent d92e27ab50
commit 4bb3d7a958
8 changed files with 222 additions and 136 deletions

View file

@ -5,7 +5,7 @@ MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules MAKEFLAGS += --no-builtin-rules
RUNARGS := RUNARGS :=
.SUFFIXES: .SUFFIXES:
BLACKARGS := -t py36 anki tests setup.py --exclude='backend_pb2|buildinfo' BLACKARGS := -t py36 anki tests setup.py tools/*.py --exclude='backend_pb2|buildinfo'
ISORTARGS := anki tests setup.py ISORTARGS := anki tests setup.py
$(shell mkdir -p .build ../dist) $(shell mkdir -p .build ../dist)

129
pylib/anki/hooks_gen.py Normal file
View file

@ -0,0 +1,129 @@
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
"""
Code for generating parts of hooks.py
"""
import re
from dataclasses import dataclass
from operator import attrgetter
from typing import List, Optional
@dataclass
class Hook:
# the name of the hook. _filter or _hook is appending automatically.
name: str
# string of the typed arguments passed to the callback, eg
# "kind: str, val: int"
cb_args: str = ""
# string of the return type. if set, hook is a filter.
return_type: Optional[str] = None
# if add-ons may be relying on the legacy hook name, add it here
legacy_hook: Optional[str] = None
def callable(self) -> str:
"Convert args into a Callable."
types = []
for arg in self.cb_args.split(","):
if not arg:
continue
(name, type) = arg.split(":")
types.append(type.strip())
types_str = ", ".join(types)
return f"Callable[[{types_str}], {self.return_type or 'None'}]"
def arg_names(self) -> List[str]:
names = []
for arg in self.cb_args.split(","):
if not arg:
continue
(name, type) = arg.split(":")
names.append(name.strip())
return names
def full_name(self) -> str:
return f"{self.name}_{self.kind()}"
def kind(self) -> str:
if self.return_type is not None:
return "filter"
else:
return "hook"
def list_code(self) -> str:
return f"""\
{self.full_name()}: List[{self.callable()}] = []
"""
def fire_code(self) -> str:
if self.return_type is not None:
# filter
return self.filter_fire_code()
else:
# hook
return self.hook_fire_code()
def hook_fire_code(self) -> str:
arg_names = self.arg_names()
out = f"""\
def run_{self.full_name()}({self.cb_args}) -> None:
for hook in {self.full_name()}:
try:
hook({", ".join(arg_names)})
except:
# if the hook fails, remove it
{self.full_name()}.remove(hook)
raise
"""
if self.legacy_hook:
args = ", ".join([f'"{self.legacy_hook}"'] + arg_names)
out += f"""\
# legacy support
runHook({args})
"""
return out + "\n\n"
def filter_fire_code(self) -> str:
arg_names = self.arg_names()
out = f"""\
def run_{self.full_name()}({self.cb_args}) -> {self.return_type}:
for filter in {self.full_name()}:
try:
{arg_names[0]} = filter({", ".join(arg_names)})
except:
# if the hook fails, remove it
{self.full_name()}.remove(filter)
raise
"""
if self.legacy_hook:
args = ", ".join([f'"{self.legacy_hook}"'] + arg_names)
out += f"""\
# legacy support
runFilter({args})
"""
out += f"""\
return {arg_names[0]}
"""
return out + "\n\n"
def update_file(path: str, hooks: List[Hook]):
hooks.sort(key=attrgetter("name"))
code = ""
for hook in hooks:
code += hook.list_code()
code += "\n\n"
for hook in hooks:
code += hook.fire_code()
orig = open(path).read()
new = re.sub(
"(?s)# @@AUTOGEN@@.*?# @@AUTOGEN@@\n",
f"# @@AUTOGEN@@\n\n{code}# @@AUTOGEN@@\n",
orig,
)
open(path, "wb").write(new.encode("utf8"))

View file

@ -11,109 +11,7 @@ To add a new hook:
""" """
import os import os
import re from anki.hooks_gen import Hook, update_file
from dataclasses import dataclass
from operator import attrgetter
from typing import Optional, List
@dataclass
class Hook:
# the name of the hook. _filter or _hook is appending automatically.
name: str
# string of the typed arguments passed to the callback, eg
# "kind: str, val: int"
cb_args: str = ""
# string of the return type. if set, hook is a filter.
return_type: Optional[str] = None
# if add-ons may be relying on the legacy hook name, add it here
legacy_hook: Optional[str] = None
def callable(self) -> str:
"Convert args into a Callable."
types = []
for arg in self.cb_args.split(","):
if not arg:
continue
(name, type) = arg.split(":")
types.append(type.strip())
types_str = ", ".join(types)
return f"Callable[[{types_str}], {self.return_type or 'None'}]"
def arg_names(self) -> List[str]:
names = []
for arg in self.cb_args.split(","):
if not arg:
continue
(name, type) = arg.split(":")
names.append(name.strip())
return names
def full_name(self) -> str:
return f"{self.name}_{self.kind()}"
def kind(self) -> str:
if self.return_type is not None:
return "filter"
else:
return "hook"
def list_code(self) -> str:
return f"""\
{self.full_name()}: List[{self.callable()}] = []
"""
def fire_code(self) -> str:
if self.return_type is not None:
# filter
return self.filter_fire_code()
else:
# hook
return self.hook_fire_code()
def hook_fire_code(self) -> str:
arg_names = self.arg_names()
out = f"""\
def run_{self.full_name()}({self.cb_args}) -> None:
for hook in {self.full_name()}:
try:
hook({", ".join(arg_names)})
except:
# if the hook fails, remove it
{self.full_name()}.remove(hook)
raise
"""
if self.legacy_hook:
args = ", ".join([f'"{self.legacy_hook}"'] + arg_names)
out += f"""\
# legacy support
runHook({args})
"""
return out + "\n\n"
def filter_fire_code(self) -> str:
arg_names = self.arg_names()
out = f"""\
def run_{self.full_name()}({self.cb_args}) -> {self.return_type}:
for filter in {self.full_name()}:
try:
{arg_names[0]} = filter({", ".join(arg_names)})
except:
# if the hook fails, remove it
{self.full_name()}.remove(filter)
raise
"""
if self.legacy_hook:
args = ", ".join([f'"{self.legacy_hook}"'] + arg_names)
out += f"""\
# legacy support
runFilter({args})
"""
out += f"""\
return {arg_names[0]}
"""
return out + "\n\n"
# Hook list # Hook list
###################################################################### ######################################################################
@ -121,26 +19,9 @@ def run_{self.full_name()}({self.cb_args}) -> {self.return_type}:
hooks = [ hooks = [
Hook(name="leech", cb_args="card: Card", legacy_hook="leech"), Hook(name="leech", cb_args="card: Card", legacy_hook="leech"),
Hook(name="odue_invalid"), Hook(name="odue_invalid"),
Hook(name="mod_schema", cb_args="proceed: bool", return_type="bool") Hook(name="mod_schema", cb_args="proceed: bool", return_type="bool"),
] ]
hooks.sort(key=attrgetter("name")) if __name__ == "__main__":
path = os.path.join(os.path.dirname(__file__), "..", "anki", "hooks.py")
###################################################################### update_file(path, hooks)
tools_dir = os.path.dirname(__file__)
hooks_py = os.path.join(tools_dir, "..", "anki", "hooks.py")
code = ""
for hook in hooks:
code += hook.list_code()
code += "\n\n"
for hook in hooks:
code += hook.fire_code()
orig = open(hooks_py).read()
new = re.sub("(?s)# @@AUTOGEN@@.*?# @@AUTOGEN@@\n", f"# @@AUTOGEN@@\n\n{code}# @@AUTOGEN@@\n", orig)
open(hooks_py, "wb").write(new.encode("utf8"))
print("Updated hooks.py")

View file

@ -5,7 +5,7 @@ MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules MAKEFLAGS += --no-builtin-rules
.SUFFIXES: .SUFFIXES:
BLACKARGS := -t py36 aqt tests setup.py --exclude='aqt/forms|buildinfo' BLACKARGS := -t py36 aqt tests setup.py tools/*.py --exclude='aqt/forms|buildinfo'
ISORTARGS := aqt tests setup.py ISORTARGS := aqt tests setup.py
$(shell mkdir -p .build ../dist) $(shell mkdir -p .build ../dist)
@ -35,7 +35,11 @@ TSDEPS := $(wildcard ts/src/*.ts)
(cd ts && npm i && npm run build) (cd ts && npm i && npm run build)
@touch $@ @touch $@
BUILD_STEPS := .build/run-deps .build/dev-deps .build/js .build/ui .build/i18n aqt/buildinfo.py .build/hooks: tools/genhooks.py
python tools/genhooks.py
@touch $@
BUILD_STEPS := .build/run-deps .build/dev-deps .build/js .build/ui .build/hooks .build/i18n aqt/buildinfo.py
# Checking # Checking
###################### ######################

47
qt/aqt/gui_hooks.py Normal file
View file

@ -0,0 +1,47 @@
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
"""
See pylib/anki/hooks.py
"""
from __future__ import annotations
from typing import Any, Callable, Dict, List # pylint: disable=unused-import
from anki.hooks import runFilter, runHook # pylint: disable=unused-import
# New hook/filter handling
##############################################################################
# The code in this section is automatically generated - any edits you make
# will be lost. To add new hooks, see ../tools/genhooks.py
#
# @@AUTOGEN@@
mpv_idle_hook: List[Callable[[], None]] = []
mpv_will_play_hook: List[Callable[[str], None]] = []
def run_mpv_idle_hook() -> None:
for hook in mpv_idle_hook:
try:
hook()
except:
# if the hook fails, remove it
mpv_idle_hook.remove(hook)
raise
def run_mpv_will_play_hook(file: str) -> None:
for hook in mpv_will_play_hook:
try:
hook(file)
except:
# if the hook fails, remove it
mpv_will_play_hook.remove(hook)
raise
# legacy support
runHook("mpvWillPlay", file)
# @@AUTOGEN@@

View file

@ -30,6 +30,7 @@ from anki.hooks import addHook, runFilter, runHook
from anki.lang import _, ngettext from anki.lang import _, ngettext
from anki.storage import Collection from anki.storage import Collection
from anki.utils import devMode, ids2str, intTime, isMac, isWin, splitFields from anki.utils import devMode, ids2str, intTime, isMac, isWin, splitFields
from aqt import gui_hooks
from aqt.profiles import ProfileManager as ProfileManagerType from aqt.profiles import ProfileManager as ProfileManagerType
from aqt.qt import * from aqt.qt import *
from aqt.qt import sip from aqt.qt import sip
@ -1158,9 +1159,10 @@ Difference to correct time: %s."""
addHook("remNotes", self.onRemNotes) addHook("remNotes", self.onRemNotes)
hooks.odue_invalid_hook.append(self.onOdueInvalid) hooks.odue_invalid_hook.append(self.onOdueInvalid)
addHook("mpvWillPlay", self.onMpvWillPlay) gui_hooks.mpv_will_play_hook.append(self.on_mpv_will_play)
addHook("mpvIdleHook", self.onMpvIdle) gui_hooks.mpv_idle_hook.append(self.on_mpv_idle)
self._activeWindowOnPlay = None
self._activeWindowOnPlay: Optional[QWidget] = None
def onOdueInvalid(self): def onOdueInvalid(self):
showWarning( showWarning(
@ -1175,13 +1177,13 @@ and if the problem comes up again, please ask on the support site."""
head, ext = os.path.splitext(file.lower()) head, ext = os.path.splitext(file.lower())
return ext in (".mp4", ".mov", ".mpg", ".mpeg", ".mkv", ".avi") return ext in (".mp4", ".mov", ".mpg", ".mpeg", ".mkv", ".avi")
def onMpvWillPlay(self, file): def on_mpv_will_play(self, file: str) -> None:
if not self._isVideo(file): if not self._isVideo(file):
return return
self._activeWindowOnPlay = self.app.activeWindow() or self._activeWindowOnPlay self._activeWindowOnPlay = self.app.activeWindow() or self._activeWindowOnPlay
def onMpvIdle(self): def on_mpv_idle(self) -> None:
w = self._activeWindowOnPlay w = self._activeWindowOnPlay
if not self.app.activeWindow() and w and not sip.isdeleted(w) and w.isVisible(): if not self.app.activeWindow() and w and not sip.isdeleted(w) and w.isVisible():
w.activateWindow() w.activateWindow()

View file

@ -11,10 +11,11 @@ import threading
import time import time
from typing import Any, Callable, Dict, List, Optional, Tuple from typing import Any, Callable, Dict, List, Optional, Tuple
from anki.hooks import addHook, runHook from anki.hooks import addHook
from anki.lang import _ from anki.lang import _
from anki.sound import allSounds from anki.sound import allSounds
from anki.utils import isLin, isMac, isWin, tmpdir from anki.utils import isLin, isMac, isWin, tmpdir
from aqt import gui_hooks
from aqt.mpv import MPV, MPVBase from aqt.mpv import MPV, MPVBase
from aqt.qt import * from aqt.qt import *
from aqt.utils import restoreGeom, saveGeom, showWarning from aqt.utils import restoreGeom, saveGeom, showWarning
@ -156,8 +157,8 @@ class MpvManager(MPV):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__(window_id=None, debug=False) super().__init__(window_id=None, debug=False)
def queueFile(self, file) -> None: def queueFile(self, file: str) -> None:
runHook("mpvWillPlay", file) gui_hooks.run_mpv_will_play_hook(file)
path = os.path.join(os.getcwd(), file) path = os.path.join(os.getcwd(), file)
self.command("loadfile", path, "append-play") self.command("loadfile", path, "append-play")
@ -172,7 +173,7 @@ class MpvManager(MPV):
self.command("seek", secs, "relative") self.command("seek", secs, "relative")
def on_idle(self) -> None: def on_idle(self) -> None:
runHook("mpvIdleHook") gui_hooks.run_mpv_idle_hook()
def setMpvConfigBase(base) -> None: def setMpvConfigBase(base) -> None:

22
qt/tools/genhooks.py Normal file
View file

@ -0,0 +1,22 @@
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
"""
See pylib/tools/genhooks.py for more info.
"""
import os
from anki.hooks_gen import Hook, update_file
# Hook list
######################################################################
hooks = [
Hook(name="mpv_idle"),
Hook(name="mpv_will_play", cb_args="file: str", legacy_hook="mpvWillPlay"),
]
if __name__ == "__main__":
path = os.path.join(os.path.dirname(__file__), "..", "aqt", "gui_hooks.py")
update_file(path, hooks)