Skip to content

Commit

Permalink
V3 Compatibility, Improved Recalc, Progress Bars Fixed (#296)
Browse files Browse the repository at this point in the history
- Now works on the V3 scheduler (#276)
- Exponentially faster recalc action
- Progress dialogs are now shown when recalcing and anki no longer freezes (background thread is used instead of main thread)
- Renamed some files that were poorly named
- Refactored init file and extracted hidden hooks to make the code execution logic clearer
- Added some type hints for clearer logic
- Refactored some variable names to follow PEP conventions
  • Loading branch information
mortii authored Aug 13, 2023
1 parent 6eabbfd commit e597ae3
Show file tree
Hide file tree
Showing 19 changed files with 861 additions and 770 deletions.
28 changes: 21 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,29 @@ and accompanying [blog post](https://massimmersionapproach.com/table-of-contents
See the [MorphMan wiki](https://github.com/kaegi/MorphMan/wiki) for more information.

# Development
## Linux
- Set up local environment:
- The best is to use a Python virtual environment and install prebuilt Anki wheels:
```
python -m virtualenv pyenv
source pyenv/bin/activate
python -m pip install aqt==2.1.54 anki==2.1.54 pyqt6-webengine pylint
export PYTHONPATH=./
python -m virtualenv venv
source venv/bin/activate
python -m pip install aqt[qt6] anki pylint mypy types-setuptools
```
- Run tests: `python test.py`
- Build Qt Developer UI with `python scripts/build_ui.py`
- Install git commit hook to run tests and pylint
` scripts/setup_dev.sh`
- Run tests: `python test.py`
- If ui files have been changed:
- Build Qt Developer UI with `python scripts/build_ui.py`

## Windows:
- Set up local environment:
```
python -m virtualenv venv
.\venv\Scripts\activate
python -m pip install aqt[qt6] anki pylint mypy types-setuptools
```
- Run type checking: mypy filename.py
- Run lint checking: pylint filename.py
- Run tests: `python test.py`
- If ui files have been changed:
- Build Qt Developer UI with `python scripts/build_ui.py`

228 changes: 154 additions & 74 deletions __init__.py
Original file line number Diff line number Diff line change
@@ -1,84 +1,40 @@
from .morph.util import *
from PyQt6.QtWidgets import *
import anki.stats
from anki.hooks import wrap
from aqt.reviewer import Reviewer
from aqt.utils import tooltip
from aqt import gui_hooks

# TODO: importlib is seemingly used to patch over and disguise veeeeery bad bugs... remove its usages and fix the bugs
import importlib

try:
from anki.lang import _
except:
pass


def onMorphManRecalc():
from .morph import main
importlib.reload(main)
main.main()


def onMorphManManager():
mw.toolbar.draw()
from .morph import manager
importlib.reload(manager)
manager.main()
import anki.stats
from anki import hooks
from anki.collection import Collection
from anki.lang import _ # TODO: deprecated?

from .morph.util import * # TODO: replace this star
from .morph import morph_stats
from .morph import reviewing_utils
from .morph import main as main_module # TODO: change the file name 'main' to something more fitting like 'recalc'
from .morph import manager
from .morph import readability
from .morph import preferencesDialog
from .morph import graphs
from .morph import preferences

def onMorphManReadability():
mw.toolbar.draw()
from .morph import readability
importlib.reload(readability)
readability.main()
morphman_sub_menu = None
morphman_sub_menu_creation_action = None


def onMorphManPreferences():
from .morph import preferencesDialog
importlib.reload(preferencesDialog)
preferencesDialog.main()
def main():
# Support anki version 2.1.50 and above
# Hooks should be in the order they are executed!

def morphGraphsWrapper(*args, **kwargs):
from .morph import graphs
importlib.reload(graphs)
return graphs.morphGraphs(args, kwargs)
gui_hooks.profile_did_open.append(preferences.init_preferences)

# Adds morphman to menu multiples times when profiles are changed
gui_hooks.profile_did_open.append(init_actions_and_submenu)

def main():
# Add MorphMan submenu
morphmanSubMenu = QMenu("MorphMan", mw)
mw.form.menuTools.addMenu(morphmanSubMenu)

# Add recalculate menu button
a = QAction('&Recalc', mw)
a.setStatusTip(_("Recalculate all.db, note fields, and new card ordering"))
a.setShortcut(_("Ctrl+M"))
a.triggered.connect(onMorphManRecalc)
morphmanSubMenu.addAction(a)

# Add gui preferences menu button
a = QAction('&Preferences', mw)
a.setStatusTip(_("Change inspected cards, fields and tags"))
a.setShortcut(_("Ctrl+O"))
a.triggered.connect(onMorphManPreferences)
morphmanSubMenu.addAction(a)

# Add gui manager menu button
a = QAction('&Database Manager', mw)
a.setStatusTip(
_("Open gui manager to inspect, compare, and analyze MorphMan DBs"))
a.setShortcut(_("Ctrl+D"))
a.triggered.connect(onMorphManManager)
morphmanSubMenu.addAction(a)

# Add readability tool menu button
a = QAction('Readability &Analyzer', mw)
a.setStatusTip(_("Check readability and build frequency lists"))
a.setShortcut(_("Ctrl+A"))
a.triggered.connect(onMorphManReadability)
morphmanSubMenu.addAction(a)

# ToDo: remove this pylint disable. These imports are here because they have Anki
# addHooks to initialize the UI. It would be better to initialize all Anki UI
# in one single place with explicit call to reveal true intention.
# TODO: Extract all hooks from the imports below and remove the pylint disable
# pylint: disable=W0611
from .morph.browser import viewMorphemes
from .morph.browser import extractMorphemes
Expand All @@ -88,12 +44,136 @@ def main():
from .morph.browser import boldUnknowns
from .morph.browser import browseMorph
from .morph.browser import alreadyKnownTagger
from .morph import newMorphHelper
from .morph import stats

gui_hooks.collection_did_load.append(replace_reviewer_functions)

# This stores the focus morphs seen today, necessary for the respective skipping option to work
gui_hooks.reviewer_did_answer_card.append(mark_morph_seen)

# Adds the 'K: V:' to the toolbar
gui_hooks.top_toolbar_did_init_links.append(add_morph_stats_to_toolbar)

# See more detailed morph stats by holding 'Shift'-key while pressing 'Stats' in toolbar
# TODO: maybe move it somewhere less hidden if possible? E.g.a separate toolbar button
gui_hooks.stats_dialog_will_show(add_morph_stats_to_ease_graph)

gui_hooks.profile_will_close.append(tear_down_actions_and_submenu)


def init_actions_and_submenu():
global morphman_sub_menu

recalc_action = create_recalc_action()
preferences_action = create_preferences_action()
database_manager_action = create_database_manager_action()
readability_analyzer_action = create_readability_analyzer_action()

morphman_sub_menu = create_morphman_submenu()
morphman_sub_menu.addAction(recalc_action)
morphman_sub_menu.addAction(preferences_action)
morphman_sub_menu.addAction(database_manager_action)
morphman_sub_menu.addAction(readability_analyzer_action)

# test_action = create_test_action()
# morphman_sub_menu.addAction(test_action)


def mark_morph_seen(reviewer: Reviewer, card, ease):
# Hook gives extra input parameters, hence this seemingly redundant function
reviewing_utils.mark_morph_seen(card.note())


def replace_reviewer_functions(collection: Collection) -> None:
# This skips the cards the user specified in preferences GUI
Reviewer.nextCard = hooks.wrap(Reviewer.nextCard, reviewing_utils.my_next_card, "around")

# Automatically highlights morphs on cards if the respective note stylings are present
hooks.field_filter.append(reviewing_utils.highlight)


def add_morph_stats_to_toolbar(links, toolbar):
name, details = morph_stats.get_stats()
links.append(
toolbar.create_link(
"morph", name, morph_stats.on_morph_stats_clicked, tip=details, id="morph"
)
)


def add_morph_stats_to_ease_graph():
anki.stats.CollectionStats.easeGraph = hooks.wrap(anki.stats.CollectionStats.easeGraph, morph_graphs_wrapper,
"around")


def create_morphman_submenu() -> QMenu:
global morphman_sub_menu_creation_action

morphman_sub_menu = QMenu("MorphMan", mw)
morphman_sub_menu_creation_action = mw.form.menuTools.addMenu(morphman_sub_menu)

return morphman_sub_menu


def create_test_action() -> QAction:
action = QAction('&Test', mw)
action.setStatusTip(_("Recalculate all.db, note fields, and new card ordering"))
action.setShortcut(_("Ctrl+T"))
action.triggered.connect(test_function)
return action


def create_recalc_action() -> QAction:
action = QAction('&Recalc', mw)
action.setStatusTip(_("Recalculate all.db, note fields, and new card ordering"))
action.setShortcut(_("Ctrl+M"))
action.triggered.connect(main_module.main)
return action


def create_preferences_action() -> QAction:
action = QAction('&Preferences', mw)
action.setStatusTip(_("Change inspected cards, fields and tags"))
action.setShortcut(_("Ctrl+O"))
action.triggered.connect(preferencesDialog.main)
return action


def create_database_manager_action() -> QAction:
action = QAction('&Database Manager', mw)
action.setStatusTip(
_("Open gui manager to inspect, compare, and analyze MorphMan DBs"))
action.setShortcut(_("Ctrl+D"))
action.triggered.connect(manager.main)
return action


def create_readability_analyzer_action() -> QAction:
action = QAction('Readability &Analyzer', mw)
action.setStatusTip(_("Check readability and build frequency lists"))
action.setShortcut(_("Ctrl+A"))
action.triggered.connect(readability.main)
return action


def morph_graphs_wrapper(*args, **kwargs):
importlib.reload(graphs)
return graphs.morphGraphs(args, kwargs)


def tear_down_actions_and_submenu():
if morphman_sub_menu is not None:
morphman_sub_menu.clear()
mw.form.menuTools.removeAction(morphman_sub_menu_creation_action)


def test_function():
skipped_cards = reviewing_utils.SkippedCards()

skipped_cards.skipped_cards['comprehension'] += 10
skipped_cards.skipped_cards['fresh'] += 1
skipped_cards.skipped_cards['today'] += 1

anki.stats.CollectionStats.easeGraph = \
wrap(anki.stats.CollectionStats.easeGraph, morphGraphsWrapper, pos="")
skipped_cards.show_tooltip_of_skipped_cards()


main()
4 changes: 2 additions & 2 deletions morph/browser/alreadyKnownTagger.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from aqt.utils import tooltip
from anki.hooks import addHook
from ..util import addBrowserNoteSelectionCmd, getFilter, runOnce
from ..util import addBrowserNoteSelectionCmd, get_filter, runOnce
from ..preferences import get_preference
from anki.lang import _

Expand All @@ -12,7 +12,7 @@ def pre(b): # :: Browser -> State


def per(st, n): # :: State -> Note -> State
if getFilter(n) is None:
if get_filter(n) is None:
return st

n.addTag(st['tag'])
Expand Down
8 changes: 4 additions & 4 deletions morph/browser/browseMorph.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from anki.lang import _
from aqt.utils import tooltip

from ..newMorphHelper import focus, focusName, focusQuery
from ..reviewing_utils import try_to_get_focus_morphs, focus_query
from ..util import addBrowserNoteSelectionCmd, runOnce
from ..preferences import get_preference as cfg

Expand All @@ -15,17 +15,17 @@ def per(st, n):
if n is None:
return st

for focusMorph in focus(n):
for focusMorph in try_to_get_focus_morphs(n): # TODO: is this safe??
st['focusMorphs'].add(focusMorph)
return st


def post(st):
search = ''
focusField = focusName()
focusField = cfg('Field_FocusMorph')
focusMorphs = st['focusMorphs']

q = focusQuery(focusField, focusMorphs)
q = focus_query(focusField, focusMorphs)
if q != '':
st['b'].form.searchEdit.lineEdit().setText(q)
st['b'].onSearchActivated()
Expand Down
4 changes: 2 additions & 2 deletions morph/browser/extractMorphemes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from anki.utils import strip_html
from ..morphemes import AnkiDeck, MorphDb, getMorphemes
from ..morphemizer import getMorphemizerByName
from ..util import addBrowserNoteSelectionCmd, mw, getFilter, infoMsg, QFileDialog, runOnce
from ..util import addBrowserNoteSelectionCmd, mw, get_filter, infoMsg, QFileDialog, runOnce
from ..preferences import get_preference as cfg


Expand All @@ -17,7 +17,7 @@ def pre(b):

def per(st, n):
mats = mw.col.db.list('select ivl from cards where nid = :nid', nid=n.id)
note_cfg = getFilter(n)
note_cfg = get_filter(n)
if note_cfg is None:
return st

Expand Down
4 changes: 2 additions & 2 deletions morph/browser/massTagger.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from anki.utils import strip_html
from ..morphemes import getMorphemes, MorphDb
from ..morphemizer import getMorphemizerByName
from ..util import addBrowserNoteSelectionCmd, getFilter, infoMsg, QInputDialog, QFileDialog, QLineEdit, runOnce
from ..util import addBrowserNoteSelectionCmd, get_filter, infoMsg, QInputDialog, QFileDialog, QLineEdit, runOnce
from ..preferences import get_preference as cfg
from anki.lang import _

Expand All @@ -28,7 +28,7 @@ def pre(b): # :: Browser -> State

def per(st, n): # :: State -> Note -> State

note_cfg = getFilter(n)
note_cfg = get_filter(n)
if note_cfg is None:
return st
morphemizer = getMorphemizerByName(note_cfg['Morphemizer'])
Expand Down
4 changes: 2 additions & 2 deletions morph/browser/viewMorphemes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
from anki.utils import strip_html
from ..morphemes import getMorphemes, ms2str
from ..morphemizer import getMorphemizerByName
from ..util import addBrowserNoteSelectionCmd, getFilter, infoMsg, runOnce
from ..util import addBrowserNoteSelectionCmd, get_filter, infoMsg, runOnce
from ..preferences import get_preference as cfg


def pre(b): return {'morphemes': []}


def per(st, n):
notecfg = getFilter(n)
notecfg = get_filter(n)
if notecfg is None:
return st

Expand Down
2 changes: 1 addition & 1 deletion morph/graphs.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ def get_stats(self, db_table, bucket_size_days, day_cutoff_seconds, num_buckets=
if not all_reviews_for_bucket:
return stats_by_name

all_db = util.allDb()
all_db = util.get_all_db()
nid_to_morphs = defaultdict(set)

for m, ls in all_db.db.items():
Expand Down
Loading

0 comments on commit e597ae3

Please sign in to comment.