From c07f6db193a513a88268535b026f1ff8630d7c60 Mon Sep 17 00:00:00 2001 From: mjuhanne Date: Fri, 4 Aug 2023 12:49:18 +0300 Subject: [PATCH 1/9] Do not use deprecated Anki commands (byName, getCard etc..) but their new counterparts --- addon/card_type.py | 2 +- addon/convert_notes_dialog.py | 4 ++-- addon/kanji.py | 34 +++++++++++++++++----------------- addon/note_type_selector.py | 4 ++-- addon/stats_window.py | 2 +- addon/text_parser.py | 6 +++--- 6 files changed, 26 insertions(+), 26 deletions(-) diff --git a/addon/card_type.py b/addon/card_type.py index 52c35fc..265c42d 100644 --- a/addon/card_type.py +++ b/addon/card_type.py @@ -78,7 +78,7 @@ def find_card_ids(self): def upsert_model(self): # Get or create model - model = aqt.mw.col.models.byName(self.model_name) + model = aqt.mw.col.models.by_name(self.model_name) if model is None: model = aqt.mw.col.models.new(self.model_name) diff --git a/addon/convert_notes_dialog.py b/addon/convert_notes_dialog.py index 004b572..0d9a420 100644 --- a/addon/convert_notes_dialog.py +++ b/addon/convert_notes_dialog.py @@ -158,7 +158,7 @@ def convert(self): if box.currentIndex() < 1: continue deck_name = ct.deck_name - deck = aqt.mw.col.decks.byName(deck_name) + deck = aqt.mw.col.decks.by_name(deck_name) if deck is None: util.error_msg( self, @@ -275,7 +275,7 @@ def show_modal(cls, note_ids, parent=None): for ct in CardType: ct_model_name = ct.model_name - ct_model = aqt.mw.col.models.byName(ct_model_name) + ct_model = aqt.mw.col.models.by_name(ct_model_name) if ct_model["id"] == model_id: util.error_msg( parent, diff --git a/addon/kanji.py b/addon/kanji.py index 052d89b..c8c8397 100644 --- a/addon/kanji.py +++ b/addon/kanji.py @@ -266,7 +266,7 @@ def recalc_user_cards(self, card_type): card_ids = aqt.mw.col.find_cards(" AND ".join(find_filter)) for card_id in card_ids: - card = aqt.mw.col.getCard(card_id) + card = aqt.mw.col.get_card(card_id) note = card.note() character = clean_character_field(note[entry_field]) character_card_ids[character] = card_id @@ -308,7 +308,7 @@ def recalc_user_words(self): if not check_new: note_ids_not_new.add(note_id) - note = aqt.mw.col.getNote(note_id) + note = aqt.mw.col.get_note(note_id) field_value = note[entry_field] words = text_parser.get_cjk_words(field_value, reading=True) @@ -330,7 +330,7 @@ def recalc_user_words(self): def on_note_update(self, note_id, deck_id, is_new=False): try: - note = aqt.mw.col.getNote(note_id) + note = aqt.mw.col.get_note(note_id) except Exception: # TODO: properly check if this is related to card import/export instead of this mess. return @@ -350,14 +350,14 @@ def on_note_update(self, note_id, deck_id, is_new=False): wr_deck_name = wr["deck"] wr_field = wr["field"] - wr_model = aqt.mw.col.models.byName(wr_note) + wr_model = aqt.mw.col.models.by_name(wr_note) if wr_model is None: continue if note.mid != wr_model["id"]: continue if wr_deck_name != "All": - wr_deck = aqt.mw.col.decks.byName(wr_deck_name) + wr_deck = aqt.mw.col.decks.by_name(wr_deck_name) if wr_deck is None: continue if deck_id != wr_deck["id"]: @@ -391,7 +391,7 @@ def on_note_update(self, note_id, deck_id, is_new=False): if r: cid = r[0] try: - card = aqt.mw.col.getCard(cid) + card = aqt.mw.col.get_card(cid) except Exception: # anki.errors.NotFoundError for newer versions continue if card: @@ -428,7 +428,7 @@ def refresh_learn_ahead(self): deck_name = e["deck"] max_num = e["num"] - deck = aqt.mw.col.decks.byName(deck_name) + deck = aqt.mw.col.decks.by_name(deck_name) if deck is None: continue deck_id = deck["id"] @@ -454,21 +454,21 @@ def new_learn_ahead_kanji(self, card_type, deck_id, max_cards): kanji = [] # to preserve order for [nid] in nids: - note = aqt.mw.col.getNote(nid) + note = aqt.mw.col.get_note(nid) for wr in config.get("word_recognized", []): wr_note = wr["note"] wr_deck_name = wr["deck"] wr_field = wr["field"] - wr_model = aqt.mw.col.models.byName(wr_note) + wr_model = aqt.mw.col.models.by_name(wr_note) if wr_model is None: continue if note.mid != wr_model["id"]: continue if wr_deck_name != "All": - wr_deck = aqt.mw.col.decks.byName(wr_deck_name) + wr_deck = aqt.mw.col.decks.by_name(wr_deck_name) if wr_deck is None: continue if deck_id != wr_deck["id"]: @@ -571,7 +571,7 @@ def refresh_notes_for_character(self, character): ) for note_id in note_ids: - note = aqt.mw.col.getNote(note_id) + note = aqt.mw.col.get_note(note_id) self.refresh_note(note, do_flush=True) def make_card_unsafe(self, card_type, character): @@ -580,12 +580,12 @@ def make_card_unsafe(self, card_type, character): deck_name = card_type.deck_name model_name = card_type.model_name - deck = aqt.mw.col.decks.byName(deck_name) + deck = aqt.mw.col.decks.by_name(deck_name) if deck is None: raise InvalidDeckError(card_type) deck_id = deck["id"] - model = aqt.mw.col.models.byName(model_name) + model = aqt.mw.col.models.by_name(model_name) note = anki.notes.Note(aqt.mw.col, model) note["Character"] = character @@ -605,7 +605,7 @@ def make_cards_from_characters(self, card_type, new_characters, checkpoint=None) deck_name = card_type.deck_name model_name = card_type.model_name - deck = aqt.mw.col.decks.byName(deck_name) + deck = aqt.mw.col.decks.by_name(deck_name) if deck is None: raise InvalidDeckError(card_type) @@ -613,7 +613,7 @@ def make_cards_from_characters(self, card_type, new_characters, checkpoint=None) aqt.mw.checkpoint(checkpoint) deck_id = deck["id"] - model = aqt.mw.col.models.byName(model_name) + model = aqt.mw.col.models.by_name(model_name) for c in characters: note = anki.notes.Note(aqt.mw.col, model) @@ -704,7 +704,7 @@ def recalc_all(self, callback=None): num_notes = len(note_ids) for i, note_id in enumerate(note_ids): - note = aqt.mw.col.getNote(note_id) + note = aqt.mw.col.get_note(note_id) self.refresh_note(note, do_flush=True) if callback and ((i + 1) % 25) == 0: @@ -828,7 +828,7 @@ def get_kanji_result_data( ct_user_data = "" if ct_card_id: try: - ct_card = aqt.mw.col.getCard(ct_card_id) + ct_card = aqt.mw.col.get_card(ct_card_id) ct_user_data = ct_card.note()["UserData"] except: pass diff --git a/addon/note_type_selector.py b/addon/note_type_selector.py index 43bfc4a..be8ff92 100644 --- a/addon/note_type_selector.py +++ b/addon/note_type_selector.py @@ -137,11 +137,11 @@ def on_note_change(self): note_name = note_box.currentText() - cards = [f["name"] for f in aqt.mw.col.models.byName(note_name)["tmpls"]] + cards = [f["name"] for f in aqt.mw.col.models.by_name(note_name)["tmpls"]] card_box.clear() card_box.addItems(cards) - fields = [f["name"] for f in aqt.mw.col.models.byName(note_name)["flds"]] + fields = [f["name"] for f in aqt.mw.col.models.by_name(note_name)["flds"]] field_box.clear() field_box.addItems(fields) diff --git a/addon/stats_window.py b/addon/stats_window.py index 78ad293..c6a7ff3 100644 --- a/addon/stats_window.py +++ b/addon/stats_window.py @@ -370,7 +370,7 @@ def refresh(self): marked_known = True else: try: - card = aqt.mw.col.getCard(card_id) + card = aqt.mw.col.get_card(card_id) ivl_days = card_ival(card) except ( Exception diff --git a/addon/text_parser.py b/addon/text_parser.py index 470e0d5..720d785 100644 --- a/addon/text_parser.py +++ b/addon/text_parser.py @@ -25,9 +25,9 @@ def __init__(self): self.mecab_extra_args = {} - if anki.utils.isLin: + if anki.utils.is_lin: self.mecab_bin += "-linux" - elif anki.utils.isMac: + elif anki.utils.is_mac: self.mecab_bin += "-macos" elif anki.utils.isWin: self.mecab_bin += "-windows.exe" @@ -49,7 +49,7 @@ def __init__(self): def start(self): self.stop() - if anki.utils.isLin or anki.utils.isMac: + if anki.utils.is_lin or anki.utils.is_mac: os.chmod(self.mecab_bin, 0o755) if self.mecab_process is None: From 09b077adf511b39fab8557d5f1d1c25cd3c62a7b Mon Sep 17 00:00:00 2001 From: mjuhanne Date: Fri, 18 Aug 2023 21:44:11 +0300 Subject: [PATCH 2/9] A new search engine for kanjis and primitives. A comma-separated list of search terms can be used and best matching results are returned. Database is searched for following fields: Keywords, primitive names, meanings, Heisig stories/comments and Koohi stories, radicals and kanji readings (in this order). Exact matches are prioritized for partials. User can also search for multiple occurrences of a search term (e.g. '2*mouth' or 'mouth*2') A new search bar is added into Lookup window, giving real-time search results. Click on the result bubble to view the corresponding item or press Enter to immediately select the first item. --- addon/kanji.py | 4 +- addon/lookup_window.py | 4 + addon/power_search_bar.py | 123 ++++++++++++ addon/search_engine.py | 396 ++++++++++++++++++++++++++++++++++++++ addon/web/lookup.html | 10 + 5 files changed, 536 insertions(+), 1 deletion(-) create mode 100644 addon/power_search_bar.py create mode 100644 addon/search_engine.py diff --git a/addon/kanji.py b/addon/kanji.py index c8c8397..9b62103 100644 --- a/addon/kanji.py +++ b/addon/kanji.py @@ -14,7 +14,7 @@ from . import config from . import text_parser from .kanji_confirm_dialog import KanjiConfirmDialog - +from .search_engine import SearchEngine kanji_db_path = addon_path("kanji.db") user_db_path = user_path("user.db") @@ -100,6 +100,8 @@ def initialize(self): except sqlite3.OperationalError: pass + self.search_engine = SearchEngine(self.crs) + # Close db def shutdown(self): self.crs.close() diff --git a/addon/lookup_window.py b/addon/lookup_window.py index 21af3b6..5219543 100644 --- a/addon/lookup_window.py +++ b/addon/lookup_window.py @@ -10,6 +10,7 @@ from . import fonts from . import config +from .power_search_bar import PowerSearchBar key_sequence = QKeySequence("Ctrl+Shift+K") key_sequence_txt = key_sequence.toString(QKeySequence.SequenceFormat.NativeText) @@ -51,6 +52,8 @@ def __init__(self, parent=None): search_btn.clicked.connect(self.on_search_submit) search_lyt.addWidget(search_btn) + self.power_search_bar = PowerSearchBar(search_lyt, lyt, 20, search_btn.sizeHint().height()*1.5, self.search) + self.keep_tab_on_search_box = QCheckBox("Keep tabs open") self.keep_tab_on_search_box.setChecked(False) self.keep_tab_on_search_box.setFocusPolicy(Qt.FocusPolicy.NoFocus) @@ -168,6 +171,7 @@ def on_bridge_cmd(self, cmd): if not handle_bridge_action(cmd, lookup_window=self): print("Unhandled bridge command:", cmd) + def on_search_submit(self): text = self.search_bar.text() self.search(text) diff --git a/addon/power_search_bar.py b/addon/power_search_bar.py new file mode 100644 index 0000000..ea85455 --- /dev/null +++ b/addon/power_search_bar.py @@ -0,0 +1,123 @@ +from . import util +import aqt +from aqt.qt import * + + +class ResultsBar(): + + def __init__(self, layout, max_results, result_button_size, on_select_result_function, hide_empty_buttons=True): + self.hide_empty_buttons = hide_empty_buttons + self.on_select_result_function = on_select_result_function + self.max_results = max_results + self.button_size = result_button_size + + # search result buttons + self.buttons_lyt = QHBoxLayout() + + layout.addLayout(self.buttons_lyt) + self.buttons_lyt.setSpacing(3) + self.buttons_lyt.setAlignment(Qt.AlignmentFlag.AlignLeft) + self.buttons = [] + + if aqt.theme.theme_manager.night_mode: + btn_style_sheet = \ + "color: #e9e9e9;" \ + "background-color: #454545;" + else: + btn_style_sheet = \ + "color: #202020;" \ + "background-color: #e9e9e9;" + + btn_style_sheet += \ + "font-size: 20px;" \ + "border-style: ridge;" \ + "border-width: 2px;" \ + "border-radius: 6px;" \ + "padding: 2px;" + + for i in range(max_results): + result_btn = QPushButton(' ', objectName='result_btn_' + str(i+1)) + result_btn.setStyleSheet(btn_style_sheet) + result_btn.setFixedWidth(result_button_size) + result_btn.setFixedHeight(result_button_size) + result_btn.setFocusPolicy(Qt.FocusPolicy.NoFocus) + result_btn.clicked.connect(lambda state, x=result_btn: self.on_button_click(x)) + result_btn.character = None + if self.hide_empty_buttons: + result_btn.setVisible(False) + else: + result_btn.setText('') + self.buttons_lyt.addWidget(result_btn) + self.buttons.append(result_btn) + + + def set_contents(self, contents): + idx = 0 + for r in contents: + if idx < self.max_results: + btn = self.buttons[idx] + btn.setVisible(True) + if r[0] == '[': + # [primitive] tag -> convert to image + img = r[1:-1] + path = util.addon_path('primitives','%s.svg' % img) + btn.setText('') + icon_size = int(self.button_size*0.8) + icon = QIcon(path) + btn.setIcon(icon) + btn.setIconSize(QSize(icon_size,icon_size)) + else: + # normal unicode kanji character + btn.setText(r) + btn.setIcon(QIcon()) + btn.character = r + idx += 1 + for i in range(idx,self.max_results): + # clear/hide rest of the buttons + btn = self.buttons[i] + btn.character = None + if self.hide_empty_buttons: + btn.setVisible(False) + else: + btn.setText('') + btn.setIcon(QIcon()) + + + def on_button_click(self, button): + text = button.text() + if button.character is not None: + self.on_select_result_function(button.character) + + + +class PowerSearchBar(): + + def __init__(self, search_bar_layout, search_results_layout, max_results, result_button_width, on_select_result_function, hide_empty_result_buttons=True): + + self.on_select_result_function = on_select_result_function + self.max_results = max_results + + # search bar + self.input_bar = QLineEdit() + self.input_bar.setPlaceholderText("") + self.input_bar.returnPressed.connect(self.on_power_search_submit) + self.input_bar.textChanged.connect(self.on_power_search_changed) + search_bar_layout.addWidget(self.input_bar) + + self.results_bar = ResultsBar(search_results_layout, max_results, result_button_width, on_select_result_function, hide_empty_result_buttons) + + + def on_power_search_changed(self): + text = self.input_bar.text() + result = aqt.mw.migaku_kanji_db.search_engine.search(text, self.max_results) + self.results_bar.set_contents(result) + + def on_power_search_submit(self): + # do not do any additional searches but just select the first match + btn = self.results_bar.buttons[0] + if btn.character is not None: + self.on_select_result_function(btn.character) + + def clear(self): + self.input_bar.setText("") + self.results_bar.set_contents([]) \ No newline at end of file diff --git a/addon/search_engine.py b/addon/search_engine.py new file mode 100644 index 0000000..14eb12b --- /dev/null +++ b/addon/search_engine.py @@ -0,0 +1,396 @@ +import json +from .util import custom_list + +def j2c(d): + if d is not None: + item = json.loads(d) + return ", ".join(item) + else: + return "" + +def csv_to_list(csv): + value_list = csv.split(',') + return [value.strip() for value in value_list] + +# (field_name, load_function, column) +_ = lambda x: x +requested_fields = [ + ("character", _, "characters.character"), + #("stroke_count", _, None), + ("onyomi", j2c, None), + ("kunyomi", j2c, None), + ("nanori", j2c, None), + ("meanings", j2c, None), + #("frequency_rank", _, None), + #("grade", _, None), + #("jlpt", _, None), + #("kanken", _, None), + ("primitives", _, None), + ("primitive_of", _, None), + ("primitive_keywords", j2c, None), + ("primitive_alternatives", _, None), + #("heisig_id5", _, None), + #("heisig_id6", _, None), + ("heisig_keyword5", _, None), + ("heisig_keyword6", _, None), + ("heisig_story", _, None), + ("heisig_comment", _, None), + ("radicals", _, None), + #("words_default", _, None), + ("koohi_stories", j2c, None), + #("wk", _, None), + ("usr_keyword", _, "usr.keywords.usr_keyword"), + ("usr_primitive_keyword", _, "usr.keywords.usr_primitive_keyword"), + ("usr_story", _, "usr.stories.usr_story"), +] + +sql_fields = ",".join((rf[2] if rf[2] else rf[0]) for rf in requested_fields) + +joins = [ + f"LEFT OUTER JOIN usr.keywords ON characters.character == usr.keywords.character ", + f"LEFT OUTER JOIN usr.stories ON characters.character == usr.stories.character ", +] +sql_joins_txt = "".join(joins) + + +# we ignore these radicals for now because they don't have a keyword containing +# kanji counterpart in our database +ignore_radicals = ['亅','屮','禸','爻','尢','无','彑','黽','鬲','鬥','齊','龠','黹','黹','鬯'] + +# this table links radicals to their Heisig primitive counterpart to allow for +# even better search capability +# (some of them have identical visual look but differ only in Unicode) +unicode_conversion_table = { + 'ノ' : '丿', + '|' : '丨', + '⺅' : '亻', + '⺾' : '艹', + '爿' : '丬', + '辶' : '辶', +} + +# When two characters reference each other as alternatives (for example 艹 -> 艸 and 艸 -> 艹 ) +# then we want to link to the character which is the primary primitive +primary_primitives = ['艹','扌','⻖','⻏','川','罒','冫','月'] + +class SearchEngine: + + def __init__(self, db_cursor): + self.crs = db_cursor + self.keyword_set_cache = dict() + self.keyword_cache = dict() + self.radical_set_cache = dict() + self.radical_name_set_cache = dict() + self.radical_name_cache = dict() + self.primitive_list_cache = dict() + self.rec_primitive_list_cache = dict() + self.rec_primitive_name_list_cache = dict() + self.rec_primitive_name_cache = dict() + self.reading_set_cache = dict() + self.reading_cache = dict() + self.meaning_set_cache = dict() + self.meaning_cache = dict() + self.stories_cache = dict() + self.primitive_alternative_cache = dict() + + self.init_cache() + + def radical_to_primitive(self,r): + # First do unicode conversion because some radicals in the list might use slightly + # # different (albeit visually indistinguishable) unicode character. + if r in unicode_conversion_table: + r = unicode_conversion_table[r] + # .. then reference the main primitive instead if this is an alternative primitive + if r not in primary_primitives and r in self.primitive_alternative_cache: + r = self.primitive_alternative_cache[r] + return r + + def recursively_find_all_primitives(self, character): + if character not in self.primitive_list_cache: + return [character] + primitives = self.primitive_list_cache[character] or list() + if len(primitives) == 1 and character in primitives: + # skip all the primitives that have only themselves listed as primitives + return [character] + found_primitives = primitives.copy() + # recursively find all primitives + for p in primitives: + if p != character: + new_primitives = self.recursively_find_all_primitives(p) + for np in new_primitives: + if np not in found_primitives: + found_primitives.append(np) + return found_primitives + + def update_recursive_primitive_cache(self,character): + rec_primitive_list = self.recursively_find_all_primitives(character) + if len(rec_primitive_list) > 0: + rec_primitive_names_list = list() + for p in rec_primitive_list: + if p in self.keyword_set_cache: + kw_set = self.keyword_set_cache[p] + for kw in kw_set: + #if kw not in rec_primitive_names_list: + rec_primitive_names_list.append(kw) + else: + print("Note! Kanji %s references primitive %s without a keyword" % (character,p)) + + rec_primitive_names = ','.join(rec_primitive_names_list) + self.rec_primitive_list_cache[character] = rec_primitive_list + self.rec_primitive_name_list_cache[character] = rec_primitive_names_list + self.rec_primitive_name_cache[character] = rec_primitive_names + + def init_cache(self): + self.update_cache() + + # By iterating through a radicals list of each kanji, + # create a cache set of radical names and also a free text cache for partial matching + for c,radical_set in self.radical_set_cache.items(): + if len(radical_set) > 0: + radical_names_set = set() + for r in radical_set: + # We want to get keywords from the associated primitive + r = self.radical_to_primitive(r) + if r in self.keyword_set_cache: + radical_names_set.update(self.keyword_set_cache[r]) + else: + if r not in ignore_radicals: + print("Note! Kanji %s has unknown radical %s" % (c,r)) + radical_names = ','.join(radical_names_set) + self.radical_name_set_cache[c] = radical_names_set + self.radical_name_cache[c] = radical_names + + print("Search engine cache initialization complete!") + + # Update cache for a character. If character is None, then update cache for all characters + def update_cache(self, character=None): + + if character: + self.crs.execute( + f"SELECT {sql_fields} FROM characters {sql_joins_txt} WHERE characters.character=?", + (character,), + ) + else: + self.crs.execute( + f"SELECT {sql_fields} FROM characters {sql_joins_txt} " + ) + + raw_data = self.crs.fetchall() + + if raw_data: + for raw_row in raw_data: + + # convert json escaping to comma separated value lists + d = {} + for data, (name, load_func, _) in zip(raw_row, requested_fields): + d[name] = load_func(data) or '' + + c = d['character'] + + # create a bunch of caches: sets of values for exact matching and then string + # representation for partial matching + + # Keywords.. + kw_set= set() + kw_set.add(d['heisig_keyword5']) + kw_set.add(d['heisig_keyword6']) + kw_set.add(d['usr_keyword'].lower()) + kw_set.update(csv_to_list(d['primitive_keywords'].lower())) + kw_set.update(csv_to_list(d['usr_primitive_keyword'].lower())) + if '' in kw_set: + kw_set.remove('') + if len(kw_set)>0: + keywords = ','.join(list(kw_set)) + self.keyword_cache[c] = keywords + self.keyword_set_cache[c] = kw_set + + # Primitives.. + if len(d['primitives'])>0: + self.primitive_list_cache[c] = custom_list(d['primitives']) + + # Radicals.. + if len(d['radicals'])>0: + self.radical_set_cache[c] = set(custom_list(d['radicals'])) + + # Readings.. + reading_set= set() + reading_set.update(csv_to_list(d['onyomi'])) + reading_set.update(csv_to_list(d['kunyomi'])) + reading_set.update(csv_to_list(d['nanori'])) + if '' in reading_set: + reading_set.remove('') + if len(reading_set)>0: + readings = ','.join(list(reading_set)) + self.reading_cache[c] = readings + self.reading_set_cache[c] = reading_set + + # Meanings.. + meaning_set= set() + meaning_set.update(csv_to_list(d['meanings'])) + if '' in meaning_set: + meaning_set.remove('') + if len(meaning_set)>0: + self.meaning_cache[c] = d['meanings'] + self.meaning_set_cache[c] = meaning_set + + # Stories.. + st = d['usr_story'].lower() + d['koohi_stories'].lower() + st += d['heisig_story'].lower() + st += d['heisig_comment'].lower() + + self.stories_cache[c] = st + + # create a reverse lookup table for primitive alternatives + if len(d['primitive_alternatives']) > 0: + prim_alt_list = custom_list(d['primitive_alternatives']) + for p in prim_alt_list: + self.primitive_alternative_cache[p] = c + + # Recursively iterate through a primitives list of each kanji + # (i.e. create a list of primitives the kanji uses, down to the basic building blocks) + # With this list create a cache set of primitives, their names and also a free text cache for partial matching + if not character: + for c in self.primitive_list_cache.keys(): + self.update_recursive_primitive_cache(c) + else: + if character in self.primitive_list_cache.keys(): + self.update_recursive_primitive_cache(c) + + + + def get_matching_characters(self, search_terms, pool, is_set, results, max_results): + for search_term,required_count in search_terms.items(): + if is_set and required_count>1: + # we want more than 1 occurence but this is a set -> not found + return results + + for character, data in pool.items(): + found = True + for search_term,required_count in search_terms.items(): + if required_count>1: + if data.count(search_term) < required_count: + found = False + else: + if search_term not in data and character != search_term: + found = False + if found: + if character not in results: + results.append(character) + if len(results)>=max_results: + return results + return results + + + def get_matching_characters_with_scoring(self, search_terms, pool, is_set, pool_priority, kanji_scores, kanji_matches): + for search_term,required_count in search_terms.items(): + if is_set and required_count>1: + # we want more than 1 occurence but this is a set -> not found + return + + for character, data in pool.items(): + for search_term, required_count in search_terms.items(): + found = False + if required_count>1: + if data.count(search_term) >= required_count: + found = True + else: + if search_term in data or character == search_term: + found = True + + if found: + if character in kanji_scores: + kanji_scores[character] += pool_priority + kanji_matches[character].add(search_term) + else: + kanji_scores[character] = pool_priority + kanji_matches[character] = {search_term} + + + def get_matching_characters_from_list_of_pools(self, search_terms, pool_list, max_results): + + if len(search_terms) == 1: + # In the case of only one search term its a simple exhaustive search until enough matches are found. + # Search goes through all search pools (keywords, primitive names, free text search) starting + # from the most prioritized one + results = [] + for pool_priority, pool, is_set in pool_list: + self.get_matching_characters(search_terms, pool, is_set, results, max_results) + if len(results)>=max_results: + return results + return results + + else: + # In the case of many search terms it's a bit trickier. We want to give each kanji + # points - the higher points the more matched search terms in high priority pools + kanji_scores = dict() # score for each kanji + kanji_matches = dict() # how many search terms were matched + + for pool_priority, pool, is_set in pool_list: + self.get_matching_characters_with_scoring(search_terms, pool, is_set, pool_priority, kanji_scores, kanji_matches) + + # remove those kanjis that didn't match all the search terms + for kanji, matched_search_terms in kanji_matches.items(): + if len(matched_search_terms) < len(search_terms): + kanji_scores.pop(kanji) + + # return only the matching kanjis with highest scoring + sorted_kanji_scores = sorted(kanji_scores.items(), key=lambda x:x[1], reverse=True) + sorted_kanji_scores = list(dict(sorted_kanji_scores).keys()) + if len(sorted_kanji_scores) > max_results: + return sorted_kanji_scores[:max_results] + + return sorted_kanji_scores + + + def search(self, search_str, max_results=15): + + if search_str == '': + return [] + + # clean up search terms + search_terms_list = search_str.split(',') + if '' in search_terms_list: + search_terms_list.remove('') + search_terms_list = [x.strip() for x in search_terms_list] + + # check if a required occurrence multiplier for the search term is given.. + search_terms_dict = dict() + for term in search_terms_list: + if '*' in term: + elements = term.split('*') + if len(elements)==2: + try: + elements = [x.strip() for x in elements] + if elements[0].isdigit(): + search_terms_dict[elements[1]] = int(elements[0]) + elif elements[1].isdigit(): + search_terms_dict[elements[0]] = int(elements[1]) + except: + # incomplete/invalid search term + pass + else: + if term in search_terms_dict: + search_terms_dict[term] += 1 + else: + search_terms_dict[term] = 1 + + # A list of search pools, each having a distinct priority + priority_list = [ + [30,self.keyword_set_cache,True], + [26,self.keyword_cache,False], + [20,self.rec_primitive_list_cache,False], + [18,self.rec_primitive_name_list_cache,False], + [16,self.rec_primitive_name_cache,False], + [14,self.meaning_set_cache,True], + [12,self.meaning_cache,False], + [10,self.stories_cache,False], + [8,self.radical_set_cache,True], + [7,self.radical_name_set_cache,True], + [6,self.radical_name_cache,False], + [5,self.reading_set_cache,True], + [4,self.reading_cache,False], + ] + + results = self.get_matching_characters_from_list_of_pools(search_terms_dict, priority_list, max_results) + + return list(results) \ No newline at end of file diff --git a/addon/web/lookup.html b/addon/web/lookup.html index c801446..66989e7 100644 --- a/addon/web/lookup.html +++ b/addon/web/lookup.html @@ -16,6 +16,16 @@

WELCOME!

You can also press anywhere in Anki to search for the highlighted characters.

+

+ In the upper right search box you can search kanjis and primitives by using keywords, meanings, readings and radicals. + You can use either a kanji character or its English translation. Also partial search terms can be given. +

+

+ Example: to search for kanjis that contain 'flowers','water' and 'mouth' you can enter: flow,wat,mout +

+

+ Also Heisig stories and comments as well as Koohi stories will be searched. Try for example: tarzan,tree +

From 4066a03073acebdd5226cd30096d9e7195dc1eac Mon Sep 17 00:00:00 2001 From: mjuhanne Date: Sun, 3 Sep 2023 18:24:11 +0300 Subject: [PATCH 3/9] Search engine: Prioritize high frequency (and higher Kanken grade) kanjis. --- addon/search_engine.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/addon/search_engine.py b/addon/search_engine.py index 14eb12b..a64b20a 100644 --- a/addon/search_engine.py +++ b/addon/search_engine.py @@ -21,10 +21,10 @@ def csv_to_list(csv): ("kunyomi", j2c, None), ("nanori", j2c, None), ("meanings", j2c, None), - #("frequency_rank", _, None), + ("frequency_rank", _, None), #("grade", _, None), #("jlpt", _, None), - #("kanken", _, None), + ("kanken", _, None), ("primitives", _, None), ("primitive_of", _, None), ("primitive_keywords", j2c, None), @@ -92,6 +92,7 @@ def __init__(self, db_cursor): self.meaning_cache = dict() self.stories_cache = dict() self.primitive_alternative_cache = dict() + self.frequency_points = dict() self.init_cache() @@ -240,6 +241,17 @@ def update_cache(self, character=None): self.stories_cache[c] = st + points = 0 + if d['frequency_rank'] is not None and d['frequency_rank'] != '': + fr_points = (4000 - d['frequency_rank'])/400 + if fr_points <= 0: + fr_points = 0 + points += fr_points + if d['kanken'] is not None and d['kanken'] != '': + points += 11 - float(d['kanken']) + if points > 0: + self.frequency_points[c] = points + # create a reverse lookup table for primitive alternatives if len(d['primitive_alternatives']) > 0: prim_alt_list = custom_list(d['primitive_alternatives']) @@ -304,6 +316,8 @@ def get_matching_characters_with_scoring(self, search_terms, pool, is_set, pool_ else: kanji_scores[character] = pool_priority kanji_matches[character] = {search_term} + if character in self.frequency_points: + kanji_scores[character] += self.frequency_points[character] def get_matching_characters_from_list_of_pools(self, search_terms, pool_list, max_results): From 82fc11b4761c0393fa130a5b112944f7622a13a6 Mon Sep 17 00:00:00 2001 From: mjuhanne Date: Sun, 12 Nov 2023 13:22:20 +0200 Subject: [PATCH 4/9] Search engine: More commenting --- addon/search_engine.py | 52 ++++++++++++++++++------------------------ 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/addon/search_engine.py b/addon/search_engine.py index a64b20a..81734d0 100644 --- a/addon/search_engine.py +++ b/addon/search_engine.py @@ -52,15 +52,10 @@ def csv_to_list(csv): ] sql_joins_txt = "".join(joins) - -# we ignore these radicals for now because they don't have a keyword containing -# kanji counterpart in our database -ignore_radicals = ['亅','屮','禸','爻','尢','无','彑','黽','鬲','鬥','齊','龠','黹','黹','鬯'] - -# this table links radicals to their Heisig primitive counterpart to allow for -# even better search capability -# (some of them have identical visual look but differ only in Unicode) -unicode_conversion_table = { +# This conversion table links radicals to their Heisig primitive counterpart to allow +# for better search capability. Few of them have identical visual look but differ +# only in Unicode. +radical_conversion_table = { 'ノ' : '丿', '|' : '丨', '⺅' : '亻', @@ -70,7 +65,7 @@ def csv_to_list(csv): } # When two characters reference each other as alternatives (for example 艹 -> 艸 and 艸 -> 艹 ) -# then we want to link to the character which is the primary primitive +# then we want to link to the character which is the primary primitive. primary_primitives = ['艹','扌','⻖','⻏','川','罒','冫','月'] class SearchEngine: @@ -99,8 +94,8 @@ def __init__(self, db_cursor): def radical_to_primitive(self,r): # First do unicode conversion because some radicals in the list might use slightly # # different (albeit visually indistinguishable) unicode character. - if r in unicode_conversion_table: - r = unicode_conversion_table[r] + if r in radical_conversion_table: + r = radical_conversion_table[r] # .. then reference the main primitive instead if this is an alternative primitive if r not in primary_primitives and r in self.primitive_alternative_cache: r = self.primitive_alternative_cache[r] @@ -131,7 +126,6 @@ def update_recursive_primitive_cache(self,character): if p in self.keyword_set_cache: kw_set = self.keyword_set_cache[p] for kw in kw_set: - #if kw not in rec_primitive_names_list: rec_primitive_names_list.append(kw) else: print("Note! Kanji %s references primitive %s without a keyword" % (character,p)) @@ -154,9 +148,6 @@ def init_cache(self): r = self.radical_to_primitive(r) if r in self.keyword_set_cache: radical_names_set.update(self.keyword_set_cache[r]) - else: - if r not in ignore_radicals: - print("Note! Kanji %s has unknown radical %s" % (c,r)) radical_names = ','.join(radical_names_set) self.radical_name_set_cache[c] = radical_names_set self.radical_name_cache[c] = radical_names @@ -209,7 +200,7 @@ def update_cache(self, character=None): if len(d['primitives'])>0: self.primitive_list_cache[c] = custom_list(d['primitives']) - # Radicals.. + # Radicals.. if len(d['radicals'])>0: self.radical_set_cache[c] = set(custom_list(d['radicals'])) @@ -238,9 +229,9 @@ def update_cache(self, character=None): st = d['usr_story'].lower() + d['koohi_stories'].lower() st += d['heisig_story'].lower() st += d['heisig_comment'].lower() - self.stories_cache[c] = st + # Frequency ranking points: Prioritize high frequency (and higher Kanken grade) kanjis. points = 0 if d['frequency_rank'] is not None and d['frequency_rank'] != '': fr_points = (4000 - d['frequency_rank'])/400 @@ -269,10 +260,9 @@ def update_cache(self, character=None): self.update_recursive_primitive_cache(c) - - def get_matching_characters(self, search_terms, pool, is_set, results, max_results): + def get_matching_characters(self, search_terms, pool, is_a_set, results, max_results): for search_term,required_count in search_terms.items(): - if is_set and required_count>1: + if is_a_set and required_count>1: # we want more than 1 occurence but this is a set -> not found return results @@ -293,9 +283,9 @@ def get_matching_characters(self, search_terms, pool, is_set, results, max_resul return results - def get_matching_characters_with_scoring(self, search_terms, pool, is_set, pool_priority, kanji_scores, kanji_matches): + def get_matching_characters_with_scoring(self, search_terms, pool, is_a_set, pool_priority, kanji_scores, kanji_matches): for search_term,required_count in search_terms.items(): - if is_set and required_count>1: + if is_a_set and required_count>1: # we want more than 1 occurence but this is a set -> not found return @@ -323,12 +313,13 @@ def get_matching_characters_with_scoring(self, search_terms, pool, is_set, pool_ def get_matching_characters_from_list_of_pools(self, search_terms, pool_list, max_results): if len(search_terms) == 1: - # In the case of only one search term its a simple exhaustive search until enough matches are found. - # Search goes through all search pools (keywords, primitive names, free text search) starting + # In the case of only one search term its a matter of simple exhaustive search + # until enough matches are found. Search crams through all search pools + # (keywords, primitive names, free text search) starting # from the most prioritized one results = [] - for pool_priority, pool, is_set in pool_list: - self.get_matching_characters(search_terms, pool, is_set, results, max_results) + for pool_priority, pool, is_a_set in pool_list: + self.get_matching_characters(search_terms, pool, is_a_set, results, max_results) if len(results)>=max_results: return results return results @@ -339,8 +330,8 @@ def get_matching_characters_from_list_of_pools(self, search_terms, pool_list, ma kanji_scores = dict() # score for each kanji kanji_matches = dict() # how many search terms were matched - for pool_priority, pool, is_set in pool_list: - self.get_matching_characters_with_scoring(search_terms, pool, is_set, pool_priority, kanji_scores, kanji_matches) + for pool_priority, pool, is_a_set in pool_list: + self.get_matching_characters_with_scoring(search_terms, pool, is_a_set, pool_priority, kanji_scores, kanji_matches) # remove those kanjis that didn't match all the search terms for kanji, matched_search_terms in kanji_matches.items(): @@ -389,7 +380,8 @@ def search(self, search_str, max_results=15): search_terms_dict[term] = 1 # A list of search pools, each having a distinct priority - priority_list = [ + priority_list = [ + # [ priority, pool, the_pool_is_a_set ] [30,self.keyword_set_cache,True], [26,self.keyword_cache,False], [20,self.rec_primitive_list_cache,False], From 44c5650a8f61b44dcea21f1757dc2fdfeeb112f3 Mon Sep 17 00:00:00 2001 From: mjuhanne Date: Mon, 4 Dec 2023 05:13:30 +0200 Subject: [PATCH 5/9] Fix crash when PowerSearchBar result_button_width is initialized with float number --- addon/lookup_window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addon/lookup_window.py b/addon/lookup_window.py index 5219543..6903432 100644 --- a/addon/lookup_window.py +++ b/addon/lookup_window.py @@ -52,7 +52,7 @@ def __init__(self, parent=None): search_btn.clicked.connect(self.on_search_submit) search_lyt.addWidget(search_btn) - self.power_search_bar = PowerSearchBar(search_lyt, lyt, 20, search_btn.sizeHint().height()*1.5, self.search) + self.power_search_bar = PowerSearchBar(search_lyt, lyt, 20, int(search_btn.sizeHint().height()*1.5), self.search) self.keep_tab_on_search_box = QCheckBox("Keep tabs open") self.keep_tab_on_search_box.setChecked(False) From 1a1f90dcc1dae7eb455c3f9bd85ecfc2c81a043a Mon Sep 17 00:00:00 2001 From: mjuhanne Date: Mon, 4 Dec 2023 05:14:30 +0200 Subject: [PATCH 6/9] Add Ctrl+L shortcut key to launch Kanji Lookup tool --- addon/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/addon/__init__.py b/addon/__init__.py index 07f7c94..b8c5ec8 100644 --- a/addon/__init__.py +++ b/addon/__init__.py @@ -29,6 +29,7 @@ def setup_menu(): submenu = QMenu("Kanji", aqt.mw) lookup_action = QAction("Lookup", aqt.mw) + lookup_action.setShortcut(_("Ctrl+L")) lookup_action.triggered.connect(on_loopup) submenu.addAction(lookup_action) From e8b45b35159295e1149faf1045948ad1f1701e02 Mon Sep 17 00:00:00 2001 From: mjuhanne Date: Mon, 4 Dec 2023 14:37:58 +0200 Subject: [PATCH 7/9] Fix non-Unicode primitives incorrect colouring in dark mode in Power Search Bar, Kanji confirm dialog and in cards' Stories section. --- addon/kanji_confirm_dialog.py | 7 ++++++- addon/power_search_bar.py | 20 ++++++++++---------- addon/util.py | 14 ++++++++++++-- addon/web/styles.css | 7 +++++-- 4 files changed, 33 insertions(+), 15 deletions(-) diff --git a/addon/kanji_confirm_dialog.py b/addon/kanji_confirm_dialog.py index d560aad..24773cb 100644 --- a/addon/kanji_confirm_dialog.py +++ b/addon/kanji_confirm_dialog.py @@ -32,6 +32,11 @@ def __init__(self): super().__init__() self.kanji = [] self.states = defaultdict(lambda: 0) + # These are used only by non-Unicode primitive .svg files + if aqt.theme.theme_manager.night_mode: + self.color = QColor( 0xff, 0xff, 0xff ) + else: + self.color = QColor( 0x20, 0x20, 0x20 ) def add(self, kanji_list): new_kanji = [kanji for kanji in kanji_list if kanji not in self.kanji] @@ -59,7 +64,7 @@ def data(self, idx, role): return str(kanji) if role == Qt.ItemDataRole.DecorationRole: if kanji[0] == '[': - return util.get_pixmap_from_tag(kanji, 35) + return util.get_pixmap_from_tag(kanji, 37, self.color) if role == Qt.ItemDataRole.BackgroundRole: state = self.states[kanji] return QBrush(QColor(self.state_colors[state])) diff --git a/addon/power_search_bar.py b/addon/power_search_bar.py index ea85455..edb36f0 100644 --- a/addon/power_search_bar.py +++ b/addon/power_search_bar.py @@ -20,13 +20,15 @@ def __init__(self, layout, max_results, result_button_size, on_select_result_fun self.buttons = [] if aqt.theme.theme_manager.night_mode: - btn_style_sheet = \ - "color: #e9e9e9;" \ - "background-color: #454545;" + self.color = QColor( 0xe9, 0xe9, 0xe9 ) + self.background_color = QColor( 0x45, 0x45, 0x45 ) else: - btn_style_sheet = \ - "color: #202020;" \ - "background-color: #e9e9e9;" + self.color = QColor( 0x20, 0x20, 0x20 ) + self.background_color = QColor( 0xe9, 0xe9, 0xe9 ) + + btn_style_sheet = \ + f"color: {self.color.name()};" \ + f"background-color: {self.background_color.name()};" btn_style_sheet += \ "font-size: 20px;" \ @@ -58,12 +60,10 @@ def set_contents(self, contents): btn = self.buttons[idx] btn.setVisible(True) if r[0] == '[': - # [primitive] tag -> convert to image - img = r[1:-1] - path = util.addon_path('primitives','%s.svg' % img) btn.setText('') icon_size = int(self.button_size*0.8) - icon = QIcon(path) + pixmap = util.get_pixmap_from_tag(r, icon_size*2, self.color) # to avoid smoothing we double the size and then resize below + icon = QIcon(pixmap) btn.setIcon(icon) btn.setIconSize(QSize(icon_size,icon_size)) else: diff --git a/addon/util.py b/addon/util.py index 6080bc4..b4ebbe3 100644 --- a/addon/util.py +++ b/addon/util.py @@ -70,11 +70,21 @@ def unique_characters(string): import aqt from aqt.qt import * +from PyQt6 import QtSvg -def get_pixmap_from_tag(kanji, size): +def get_pixmap_from_tag(kanji, size, color): img = kanji[1:-1] path = addon_path('primitives','%s.svg' % img) - pixmap = QIcon(path).pixmap(QSize(size,size)) + renderer = QtSvg.QSvgRenderer(path) + pixmap = QPixmap(size, size) + pixmap.fill(Qt.GlobalColor.transparent) + painter = QPainter(pixmap) + renderer.render(painter) + painter.setRenderHint(QPainter.RenderHint.Antialiasing, False) + painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform, False) + painter.setCompositionMode( QPainter.CompositionMode.CompositionMode_SourceIn ) + painter.fillRect( pixmap.rect(), color ) + painter.end() return pixmap def log(*args): diff --git a/addon/web/styles.css b/addon/web/styles.css index a58d90e..55130ee 100644 --- a/addon/web/styles.css +++ b/addon/web/styles.css @@ -566,8 +566,11 @@ button.primitive:focus .kanji-addon__hover-tooltip, .ankidroid_dark_mode .title-tooltip__container img, .nightMode .title-tooltip__container img, -.dark .title-tooltip__container img { - filter: invert(1); +.dark .title-tooltip__container img, +.ankidroid_dark_mode .stories__container img, +.nightMode .stories__container img, +.dark .stories__container img { + filter: invert(1); } .title-tooltip__container .separator { From 4030c7af4a6473962a02ec1035ac72846f4eca7c Mon Sep 17 00:00:00 2001 From: mjuhanne Date: Mon, 4 Dec 2023 14:39:54 +0200 Subject: [PATCH 8/9] Combine the two search bars (the old and the Power Search Bar from Search Engine) into single search bar in the Lookup tool. --- addon/lookup_window.py | 16 +++------------- addon/power_search_bar.py | 9 ++++++++- addon/web/lookup.html | 16 ++++++++-------- 3 files changed, 19 insertions(+), 22 deletions(-) diff --git a/addon/lookup_window.py b/addon/lookup_window.py index 6903432..a132e40 100644 --- a/addon/lookup_window.py +++ b/addon/lookup_window.py @@ -41,18 +41,13 @@ def __init__(self, parent=None): search_lyt = QHBoxLayout() lyt.addLayout(search_lyt) - self.search_bar = QLineEdit() - self.search_bar.setPlaceholderText("字") - self.search_bar.returnPressed.connect(self.on_search_submit) - search_lyt.addWidget(self.search_bar) - search_btn = QPushButton("🔍") - search_btn.setFixedWidth(search_btn.sizeHint().height()) + search_btn.setFixedWidth(search_btn.sizeHint().height()*2) search_btn.setFocusPolicy(Qt.FocusPolicy.NoFocus) - search_btn.clicked.connect(self.on_search_submit) - search_lyt.addWidget(search_btn) self.power_search_bar = PowerSearchBar(search_lyt, lyt, 20, int(search_btn.sizeHint().height()*1.5), self.search) + search_btn.clicked.connect(self.power_search_bar.on_power_search_submit) + search_lyt.addWidget(search_btn) self.keep_tab_on_search_box = QCheckBox("Keep tabs open") self.keep_tab_on_search_box.setChecked(False) @@ -171,11 +166,6 @@ def on_bridge_cmd(self, cmd): if not handle_bridge_action(cmd, lookup_window=self): print("Unhandled bridge command:", cmd) - - def on_search_submit(self): - text = self.search_bar.text() - self.search(text) - def search(self, text, internal=False): unique_characters = util.unique_characters(text) diff --git a/addon/power_search_bar.py b/addon/power_search_bar.py index edb36f0..682f369 100644 --- a/addon/power_search_bar.py +++ b/addon/power_search_bar.py @@ -113,10 +113,17 @@ def on_power_search_changed(self): self.results_bar.set_contents(result) def on_power_search_submit(self): - # do not do any additional searches but just select the first match btn = self.results_bar.buttons[0] if btn.character is not None: + # There is a match from search engine --> Do not do any additional searches + # but just select the first match self.on_select_result_function(btn.character) + else: + text = self.input_bar.text() + if len(text) > 0: + # retain the old functionality of the search bar: Open many tabs (one for each character) + self.on_select_result_function(text) + def clear(self): self.input_bar.setText("") diff --git a/addon/web/lookup.html b/addon/web/lookup.html index 66989e7..9438159 100644 --- a/addon/web/lookup.html +++ b/addon/web/lookup.html @@ -9,22 +9,22 @@

WELCOME!

- To look up Kanji, enter them into the box above and press Enter or press - the search button. + To look up Kanji or a primitive, enter them into the box above and press Enter or press + the search button.

- You can also press anywhere in Anki to - search for the highlighted characters. + You can also give a comma separated list of full or partial keywords, meanings, readings and radicals + and the search engine will return the best matches. Pressing Enter will then select the first match in the list.

- In the upper right search box you can search kanjis and primitives by using keywords, meanings, readings and radicals. - You can use either a kanji character or its English translation. Also partial search terms can be given. + Example: to search for kanjis that contain primitives flowers, water and mouth you can enter: flow,wat,mout

- Example: to search for kanjis that contain 'flowers','water' and 'mouth' you can enter: flow,wat,mout + Heisig and Koohi stories will also be searched. Try for example: tarzan,tree

- Also Heisig stories and comments as well as Koohi stories will be searched. Try for example: tarzan,tree + You can also press anywhere in Anki to + search for the highlighted characters.

From a5e0605bf008f6e58c8d6b3c3e94b8a8d1af8e13 Mon Sep 17 00:00:00 2001 From: mjuhanne Date: Mon, 4 Dec 2023 15:04:50 +0200 Subject: [PATCH 9/9] Revert "Do not use deprecated Anki commands (byName, getCard etc..) but their new counterparts" This reverts commit c07f6db193a513a88268535b026f1ff8630d7c60. --- addon/card_type.py | 2 +- addon/convert_notes_dialog.py | 4 ++-- addon/kanji.py | 34 +++++++++++++++++----------------- addon/note_type_selector.py | 4 ++-- addon/stats_window.py | 2 +- addon/text_parser.py | 6 +++--- 6 files changed, 26 insertions(+), 26 deletions(-) diff --git a/addon/card_type.py b/addon/card_type.py index 265c42d..52c35fc 100644 --- a/addon/card_type.py +++ b/addon/card_type.py @@ -78,7 +78,7 @@ def find_card_ids(self): def upsert_model(self): # Get or create model - model = aqt.mw.col.models.by_name(self.model_name) + model = aqt.mw.col.models.byName(self.model_name) if model is None: model = aqt.mw.col.models.new(self.model_name) diff --git a/addon/convert_notes_dialog.py b/addon/convert_notes_dialog.py index 0d9a420..004b572 100644 --- a/addon/convert_notes_dialog.py +++ b/addon/convert_notes_dialog.py @@ -158,7 +158,7 @@ def convert(self): if box.currentIndex() < 1: continue deck_name = ct.deck_name - deck = aqt.mw.col.decks.by_name(deck_name) + deck = aqt.mw.col.decks.byName(deck_name) if deck is None: util.error_msg( self, @@ -275,7 +275,7 @@ def show_modal(cls, note_ids, parent=None): for ct in CardType: ct_model_name = ct.model_name - ct_model = aqt.mw.col.models.by_name(ct_model_name) + ct_model = aqt.mw.col.models.byName(ct_model_name) if ct_model["id"] == model_id: util.error_msg( parent, diff --git a/addon/kanji.py b/addon/kanji.py index 9b62103..14440f8 100644 --- a/addon/kanji.py +++ b/addon/kanji.py @@ -268,7 +268,7 @@ def recalc_user_cards(self, card_type): card_ids = aqt.mw.col.find_cards(" AND ".join(find_filter)) for card_id in card_ids: - card = aqt.mw.col.get_card(card_id) + card = aqt.mw.col.getCard(card_id) note = card.note() character = clean_character_field(note[entry_field]) character_card_ids[character] = card_id @@ -310,7 +310,7 @@ def recalc_user_words(self): if not check_new: note_ids_not_new.add(note_id) - note = aqt.mw.col.get_note(note_id) + note = aqt.mw.col.getNote(note_id) field_value = note[entry_field] words = text_parser.get_cjk_words(field_value, reading=True) @@ -332,7 +332,7 @@ def recalc_user_words(self): def on_note_update(self, note_id, deck_id, is_new=False): try: - note = aqt.mw.col.get_note(note_id) + note = aqt.mw.col.getNote(note_id) except Exception: # TODO: properly check if this is related to card import/export instead of this mess. return @@ -352,14 +352,14 @@ def on_note_update(self, note_id, deck_id, is_new=False): wr_deck_name = wr["deck"] wr_field = wr["field"] - wr_model = aqt.mw.col.models.by_name(wr_note) + wr_model = aqt.mw.col.models.byName(wr_note) if wr_model is None: continue if note.mid != wr_model["id"]: continue if wr_deck_name != "All": - wr_deck = aqt.mw.col.decks.by_name(wr_deck_name) + wr_deck = aqt.mw.col.decks.byName(wr_deck_name) if wr_deck is None: continue if deck_id != wr_deck["id"]: @@ -393,7 +393,7 @@ def on_note_update(self, note_id, deck_id, is_new=False): if r: cid = r[0] try: - card = aqt.mw.col.get_card(cid) + card = aqt.mw.col.getCard(cid) except Exception: # anki.errors.NotFoundError for newer versions continue if card: @@ -430,7 +430,7 @@ def refresh_learn_ahead(self): deck_name = e["deck"] max_num = e["num"] - deck = aqt.mw.col.decks.by_name(deck_name) + deck = aqt.mw.col.decks.byName(deck_name) if deck is None: continue deck_id = deck["id"] @@ -456,21 +456,21 @@ def new_learn_ahead_kanji(self, card_type, deck_id, max_cards): kanji = [] # to preserve order for [nid] in nids: - note = aqt.mw.col.get_note(nid) + note = aqt.mw.col.getNote(nid) for wr in config.get("word_recognized", []): wr_note = wr["note"] wr_deck_name = wr["deck"] wr_field = wr["field"] - wr_model = aqt.mw.col.models.by_name(wr_note) + wr_model = aqt.mw.col.models.byName(wr_note) if wr_model is None: continue if note.mid != wr_model["id"]: continue if wr_deck_name != "All": - wr_deck = aqt.mw.col.decks.by_name(wr_deck_name) + wr_deck = aqt.mw.col.decks.byName(wr_deck_name) if wr_deck is None: continue if deck_id != wr_deck["id"]: @@ -573,7 +573,7 @@ def refresh_notes_for_character(self, character): ) for note_id in note_ids: - note = aqt.mw.col.get_note(note_id) + note = aqt.mw.col.getNote(note_id) self.refresh_note(note, do_flush=True) def make_card_unsafe(self, card_type, character): @@ -582,12 +582,12 @@ def make_card_unsafe(self, card_type, character): deck_name = card_type.deck_name model_name = card_type.model_name - deck = aqt.mw.col.decks.by_name(deck_name) + deck = aqt.mw.col.decks.byName(deck_name) if deck is None: raise InvalidDeckError(card_type) deck_id = deck["id"] - model = aqt.mw.col.models.by_name(model_name) + model = aqt.mw.col.models.byName(model_name) note = anki.notes.Note(aqt.mw.col, model) note["Character"] = character @@ -607,7 +607,7 @@ def make_cards_from_characters(self, card_type, new_characters, checkpoint=None) deck_name = card_type.deck_name model_name = card_type.model_name - deck = aqt.mw.col.decks.by_name(deck_name) + deck = aqt.mw.col.decks.byName(deck_name) if deck is None: raise InvalidDeckError(card_type) @@ -615,7 +615,7 @@ def make_cards_from_characters(self, card_type, new_characters, checkpoint=None) aqt.mw.checkpoint(checkpoint) deck_id = deck["id"] - model = aqt.mw.col.models.by_name(model_name) + model = aqt.mw.col.models.byName(model_name) for c in characters: note = anki.notes.Note(aqt.mw.col, model) @@ -706,7 +706,7 @@ def recalc_all(self, callback=None): num_notes = len(note_ids) for i, note_id in enumerate(note_ids): - note = aqt.mw.col.get_note(note_id) + note = aqt.mw.col.getNote(note_id) self.refresh_note(note, do_flush=True) if callback and ((i + 1) % 25) == 0: @@ -830,7 +830,7 @@ def get_kanji_result_data( ct_user_data = "" if ct_card_id: try: - ct_card = aqt.mw.col.get_card(ct_card_id) + ct_card = aqt.mw.col.getCard(ct_card_id) ct_user_data = ct_card.note()["UserData"] except: pass diff --git a/addon/note_type_selector.py b/addon/note_type_selector.py index be8ff92..43bfc4a 100644 --- a/addon/note_type_selector.py +++ b/addon/note_type_selector.py @@ -137,11 +137,11 @@ def on_note_change(self): note_name = note_box.currentText() - cards = [f["name"] for f in aqt.mw.col.models.by_name(note_name)["tmpls"]] + cards = [f["name"] for f in aqt.mw.col.models.byName(note_name)["tmpls"]] card_box.clear() card_box.addItems(cards) - fields = [f["name"] for f in aqt.mw.col.models.by_name(note_name)["flds"]] + fields = [f["name"] for f in aqt.mw.col.models.byName(note_name)["flds"]] field_box.clear() field_box.addItems(fields) diff --git a/addon/stats_window.py b/addon/stats_window.py index c6a7ff3..78ad293 100644 --- a/addon/stats_window.py +++ b/addon/stats_window.py @@ -370,7 +370,7 @@ def refresh(self): marked_known = True else: try: - card = aqt.mw.col.get_card(card_id) + card = aqt.mw.col.getCard(card_id) ivl_days = card_ival(card) except ( Exception diff --git a/addon/text_parser.py b/addon/text_parser.py index 720d785..470e0d5 100644 --- a/addon/text_parser.py +++ b/addon/text_parser.py @@ -25,9 +25,9 @@ def __init__(self): self.mecab_extra_args = {} - if anki.utils.is_lin: + if anki.utils.isLin: self.mecab_bin += "-linux" - elif anki.utils.is_mac: + elif anki.utils.isMac: self.mecab_bin += "-macos" elif anki.utils.isWin: self.mecab_bin += "-windows.exe" @@ -49,7 +49,7 @@ def __init__(self): def start(self): self.stop() - if anki.utils.is_lin or anki.utils.is_mac: + if anki.utils.isLin or anki.utils.isMac: os.chmod(self.mecab_bin, 0o755) if self.mecab_process is None: