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: