3
2
Fork
You've already forked SubSearch
0

Optional embedded window #25

Open
opened 2025年10月31日 03:59:00 +01:00 by Samuelxiaozhuofeng · 4 comments

Description of your Idea

I was using your wonderful addon this morning and thinking that would it be possible if there's an option that users can chooes to embed it into ANKI, justi like the ANKI Terminator addon. In this way, not only can users do the quick search, but also they don't have to turn on and off the window for the search. The search window is just there all the time. :)
Please see the picture to get an idea of what I am talking about.

Alternatives

Anki Terminator
embedded window

Additional Context

No response

Checkout

  • I have changed the title
  • I have filled out everything required
  • I have checked for duplicates
### Description of your Idea I was using your wonderful addon this morning and thinking that would it be possible if there's an option that users can chooes to embed it into ANKI, justi like the ANKI Terminator addon. In this way, not only can users do the quick search, but also they don't have to turn on and off the window for the search. The search window is just there all the time. :) Please see the picture to get an idea of what I am talking about. ### Alternatives ![Anki Terminator](/attachments/193ba3c6-c7ef-4c19-bdbf-c683edfee222) ![embedded window](/attachments/cf97e5d1-e206-4fa6-bd47-e3238f187268) ### Additional Context _No response_ ### Checkout - [x] I have changed the title - [x] I have filled out everything required - [x] I have checked for duplicates
301 KiB
983 KiB

recording
If the embedded window is possible, the next option might be to allow the addon to inspect clipboard(if possible) to do automatic search. Just like Yomitan on the right. 👍

![recording](/attachments/0c04097e-8892-4fe6-ac1c-019039f2ddf1) If the embedded window is possible, the next option might be to allow the addon to inspect clipboard(if possible) to do automatic search. Just like Yomitan on the right. 👍
Owner
Copy link

Currently the Window is based on a dialog, so to change this, it would have to be changed to a whole new class.
I'm not sure how far I can go with that but I will look into it. Could need some time though.

Regarding the clipboard thing; that would be pretty performance heavy but could be possible.
I will for now move this to a new issue (#26)

Currently the Window is based on a dialog, so to change this, it would have to be changed to a whole new class. I'm not sure how far I can go with that but I will look into it. Could need some time though. Regarding the clipboard thing; that would be pretty performance heavy but could be possible. I will for now move this to a new issue (#26)

@FileX wrote in #25 (comment):

Currently the Window is based on a dialog, so to change this, it would have to be changed to a whole new class. I'm not sure how far I can go with that but I will look into it. Could need some time though.

Regarding the clipboard thing; that would be pretty performance heavy but could be possible. I will for now move this to a new issue (#26)


I have achieved this embedded function with Cursor in less than 5 minutes. But I don't know if it is buggy. Below is the code that it editted, for your reference.

" # TODO: # - Add full support for Hiragana and Romaji conversion # - Add debug log open button # - Replace menu entry with expandable one in tools # menu_for_helper = mw.form.menuTools.addMenu("FSRS4Anki Helper") # menu_for_helper.addAction(menu_auto_reschedule) # menu_for_helper.addAction(menu_auto_reschedule_after_review) # menu_for_helper.addAction(menu_auto_disperse) # menu_for_helper.addAction(menu_load_balance) # menu_for_free_days = menu_for_helper.addMenu( # "No Anki on Free Days (requires Load Balancing)" # ) # menu_for_helper.addSeparator() # - Check for comp of editing the sound:URL tag and the playback of the correct audio # - add Listening part to templ # - play button playing once and then is unresponsive until stopped # - add option to switch all cards between online and offline # - fix linux left pref arrow not correctly displaying # - fix if image really not found, show error in tile and be careful with download of it # - fit default config # - if playing around with config while cached window existing (esp. Displ. Notes), it gets mashed up

import json
from collections import defaultdict
from os import path, mkdir
from math import ceil

from aqt import mw, gui_hooks
from aqt.qt import *
from aqt.utils import showInfo, showWarning, openLink, tooltip, ask_user, show_critical, getOnlyText
from aqt.operations import QueryOp

from .config import config
from .common import ADDON_NAME, LogDebug, add_kakasi, sorted_decks_and_ids, NameId, create_subsearch_template
from .subsearch_ajt.about_menu import menu_root_entry
from .subsearch_ajt.consts import SOURCE_LINK
from .note_importer import import_note, ImportResult
from .widgets import SearchResultLabel, DeckCombo, ComboBox, SpinBox, StatusBar, NoteList, ItemBox, WIDGET_HEIGHT
from .edit_window import AddDialogLauncher
from .settings_dialog import SubSearchSettingsDialog
from . import note_getter

logDebug = LogDebug()

class MainDialogUI(QWidget):
name = "subsearch_dialog"

def __init__(self, *args, **kwargs):
 super().__init__(*args, **kwargs)
 self.status_bar = StatusBar()
 self.search_result_label = SearchResultLabel()
 self.current_profile_deck_combo = DeckCombo()
 self.exact = QCheckBox("Exact Matches")
 self.jlpt_level = [QLabel('JLPT'), ComboBox()]
 self.wanikani_level = [QLabel('WaniKani'), ComboBox()]
 self.category = [QLabel('Category'), ComboBox()]
 self.sort = [QLabel('Sort by'), ComboBox()]
 self.min_length = [QLabel('Minimal Length:'), SpinBox(0, 200, 1, 0)]
 self.max_length = [QLabel('Maximal Length:'), SpinBox(0, 500, 1, 0)]
 self.edit_button = QPushButton('Edit')
 self.import_button = QPushButton('Import')
 self.filter_collapse = QPushButton('🞁')
 self.search_term_edit = QLineEdit()
 self.search_button = QPushButton('Search')
 if config['show_help_buttons']:
 self.help_button = QPushButton('Help')
 self.page_search_left = QPushButton("🔍")
 self.page_search_right = QPushButton("🔍")
 self.page_prev = QPushButton('🞀')
 self.page_first = QPushButton("⏮")
 self.note_list = NoteList()
 self.page_skip = QPushButton('🞂')
 self.page_last = QPushButton("⏭")
 self.note_type_selection_combo = ComboBox()
 self.note_fields = QLabel(wordWrap=True)
 self.init_ui()
def init_ui(self):
 self.search_term_edit.setPlaceholderText('Search Term')
 self.setLayout(self.make_main_layout())
 self.set_default_sizes()
def make_filter_row(self) -> QLayout:
 filter_row = QHBoxLayout()
 self.filter_collapse.setFixedWidth(40)
 self.filter_collapse.setDefault(False)
 filter_row.addWidget(self.filter_collapse)
 filter_row.addWidget(self.search_term_edit)
 self.search_button.setDefault(True)
 filter_row.addWidget(self.search_button)
 if config['show_help_buttons']:
 filter_row.addWidget(self.help_button)
 return filter_row
def make_main_layout(self) -> QLayout:
 main_vbox = QVBoxLayout()
 main_vbox.addLayout(self.make_filter_row())
 main_vbox.addLayout(self.make_preferences_row())
 main_vbox.addLayout(self.make_preferences_row2())
 main_vbox.addWidget(self.search_result_label)
 main_vbox.addLayout(self.make_note_list())
 main_vbox.addLayout(self.status_bar)
 main_vbox.addLayout(self.make_input_row())
 main_vbox.addLayout(self.make_note_field_label())
 return main_vbox
def make_preferences_row(self) -> QLayout:
 pref_row = QHBoxLayout()
 pref_row.addWidget(self.exact)
 pref_row.addWidget(self.jlpt_level[0])
 pref_row.addWidget(self.jlpt_level[1])
 pref_row.addWidget(self.wanikani_level[0])
 pref_row.addWidget(self.wanikani_level[1])
 pref_row.addWidget(self.category[0])
 pref_row.addWidget(self.category[1])
 pref_row.addWidget(self.sort[0])
 pref_row.addWidget(self.sort[1])
 pref_row.setStretchFactor(pref_row, 1)
 return pref_row
def make_preferences_row2(self):
 pref_row2 = QHBoxLayout()
 pref_row2.addWidget(self.min_length[0])
 pref_row2.addWidget(self.min_length[1])
 pref_row2.addWidget(self.max_length[0])
 pref_row2.addWidget(self.max_length[1])
 pref_row2.setStretchFactor(pref_row2, 1)
 return pref_row2
def make_note_list(self):
 note_row = QHBoxLayout()
 left_span = QVBoxLayout()
 right_span = QVBoxLayout()
 self.page_search_left.setFixedWidth(17)
 self.page_search_left.setFixedHeight(45)
 self.page_search_left.setEnabled(False)
 self.page_search_left.setToolTip("Go to page...")
 self.page_prev.setFixedWidth(17)
 self.page_prev.setFixedHeight(45)
 self.page_prev.setEnabled(False)
 self.page_prev.setToolTip("Previous Page")
 self.page_first.setFixedWidth(17)
 self.page_first.setFixedHeight(45)
 self.page_first.setEnabled(False)
 self.page_first.setToolTip("First Page")
 self.page_search_right.setFixedWidth(17)
 self.page_search_right.setFixedHeight(45)
 self.page_search_right.setEnabled(False)
 self.page_search_right.setToolTip("Go to page...")
 self.page_skip.setFixedWidth(17)
 self.page_skip.setFixedHeight(45)
 self.page_skip.setEnabled(False)
 self.page_skip.setToolTip("Next Page")
 self.page_last.setFixedWidth(17)
 self.page_last.setFixedHeight(45)
 self.page_last.setEnabled(False)
 self.page_last.setToolTip("Last Page")
 left_span.addStretch(1)
 left_span.addWidget(self.page_search_left)
 left_span.addWidget(self.page_prev)
 left_span.addWidget(self.page_first)
 left_span.addStretch(1)
 note_row.addLayout(left_span)
 note_row.addWidget(self.note_list)
 right_span.addStretch(1)
 right_span.addWidget(self.page_search_right)
 right_span.addWidget(self.page_skip)
 right_span.addWidget(self.page_last)
 right_span.addStretch(1)
 note_row.addLayout(right_span)
 note_row.setStretch(1, 1)
 return note_row
def set_default_sizes(self):
 combo_min_width = 120
 self.setMinimumSize(680, 500)
 all_combos = [self.current_profile_deck_combo,
 self.note_type_selection_combo,
 self.jlpt_level[1],
 self.sort[1],
 self.wanikani_level[1],
 self.category[1]]
 all_widgets = [self.exact,
 self.edit_button,
 self.import_button,
 self.filter_collapse,
 self.search_button,
 self.search_term_edit]
 if config['show_help_buttons']:
 all_widgets.append(self.help_button)
 for w in all_widgets:
 w.setMinimumHeight(WIDGET_HEIGHT)
 for combo in all_combos:
 combo.setMinimumWidth(combo_min_width)
 combo.setSizePolicy(QSizePolicy.Policy.Expanding,
 QSizePolicy.Policy.Expanding)
def make_input_row(self) -> QLayout:
 import_row = QHBoxLayout()
 import_row.addWidget(QLabel('Into Deck'))
 import_row.addWidget(self.current_profile_deck_combo)
 import_row.addWidget(QLabel('Map to Note Type'))
 import_row.addWidget(self.note_type_selection_combo)
 import_row.addWidget(self.edit_button)
 import_row.addWidget(self.import_button)
 import_row.setStretchFactor(import_row, 1)
 return import_row
def make_note_field_label(self) -> QLayout:
 note_field_box = QHBoxLayout()
 self.note_fields.hide()
 self.note_fields.setStyleSheet('QLabel { color: red; }')
 note_field_box.addWidget(self.note_fields)
 note_field_box.setStretch(0, 1)
 return note_field_box

#############################################################################

UI logic

#############################################################################

class WindowState:
def init(self, window: MainDialogUI):
self._window = window
self._json_filepath = path.join(path.dirname(
file), 'user_files', 'window_state.json')
self._map = {
"to_deck": self._window.current_profile_deck_combo,
"note_type": self._window.note_type_selection_combo,
"exact": self._window.exact,
'jlpt_level': self._window.jlpt_level[1],
'wanikani_level': self._window.wanikani_level[1],
'category': self._window.category[1],
'sort': self._window.sort[1],
'min_length': self._window.min_length[1],
'max_length': self._window.max_length[1]
}

 self._state = defaultdict(dict)
def save(self):
 for key, widget in self._map.items():
 try:
 self._state[mw.pm.name][key] = widget.currentText()
 except AttributeError:
 try:
 self._state[mw.pm.name][key] = widget.value()
 except AttributeError:
 self._state[mw.pm.name][key] = widget.isChecked()
 if not path.exists(self._json_filepath):
 mkdir(path.dirname(self._json_filepath))
 with open(self._json_filepath, 'w', encoding='utf8') as of:
 json.dump(self._state, of, indent=4, ensure_ascii=False)
 # Note: Geometry is managed by QDockWidget, no need to save separately
 logDebug('Saved window state.')
def _load(self) -> bool:
 if self._state:
 return True
 elif path.isfile(self._json_filepath):
 with open(self._json_filepath, encoding='utf8') as f:
 self._state.update(json.load(f))
 return True
 else:
 return False
def restore(self):
 if self._load() and (profile_settings := self._state.get(mw.pm.name)):
 for key, widget in self._map.items():
 if profile_settings.get(key):
 try:
 if (value := profile_settings[key]) in widget.all_items():
 widget.setCurrentText(value)
 except AttributeError:
 try:
 widget.setValue(value)
 except AttributeError:
 widget.setChecked(value)
 # Note: Geometry is managed by QDockWidget, no need to restore separately

class MainDialog(MainDialogUI):
def init(self, *args, **kwargs):
super().init(*args, **kwargs)
self.window_state = WindowState(self)
self._add_window_mgr = AddDialogLauncher(self)
self.search_block = False
self.page = 0
self.results = []
self.connect_elements()

def connect_elements(self):
 qconnect(self.edit_button.clicked, self.new_edit_win)
 qconnect(self.import_button.clicked, self.start_import)
 qconnect(self.search_button.clicked, self.update_notes_list)
 qconnect(self.search_term_edit.editingFinished, self.update_notes_list)
 qconnect(self.filter_collapse.clicked, self.toggle_filter_rows)
 if config['show_help_buttons']:
 qconnect(self.help_button.clicked, lambda: openLink(
 f"{SOURCE_LINK}/src/branch/main/README.md#screenshots---how-to-use"))
 qconnect(self.page_prev.clicked, lambda: self.change_page(self.page - 1))
 qconnect(self.page_skip.clicked, lambda: self.change_page(self.page + 1))
 qconnect(self.page_first.clicked, lambda: self.change_page(1))
 qconnect(self.page_last.clicked, lambda: self.change_page(-1))
 qconnect(self.page_search_left.clicked, self.ask_for_page)
 qconnect(self.page_search_right.clicked, self.ask_for_page)
 qconnect(self.note_type_selection_combo.currentTextChanged,
 self.update_note_fields)
 
 # Registered in settings_dialog and config
 shortcut = QShortcut(QKeySequence(config["toggle_search_focus_shortcut"]), self)
 qconnect(shortcut.activated, lambda: self.search_term_edit.clearFocus() if self.search_term_edit.hasFocus() else self.search_term_edit.setFocus())
 shortcut = QShortcut(QKeySequence(config["toggle_filter_shortcut"]), self)
 qconnect(shortcut.activated, self.toggle_filter_rows)
 shortcut = QShortcut(QKeySequence(config["import_shortcut"]), self)
 qconnect(shortcut.activated, self.start_import)
 shortcut = QShortcut(QKeySequence(config["edit_shortcut"]), self)
 qconnect(shortcut.activated, self.new_edit_win)
 shortcut = QShortcut(QKeySequence("Escape"), self)
 qconnect(shortcut.activated, self.hide_dock)
 shortcut = QShortcut(QKeySequence(config["close_shortcut"]), self)
 qconnect(shortcut.activated, self.hide_dock)
 shortcut = QShortcut(QKeySequence(config["prev_page_shortcut"]), self)
 qconnect(shortcut.activated, lambda: self.change_page(self.page - 1))
 shortcut = QShortcut(QKeySequence(config["next_page_shortcut"]), self)
 qconnect(shortcut.activated, lambda: self.change_page(self.page + 1))
 shortcut = QShortcut(QKeySequence(config["first_page_shortcut"]), self)
 qconnect(shortcut.activated, lambda: self.change_page(1))
 shortcut = QShortcut(QKeySequence(config["last_page_shortcut"]), self)
 qconnect(shortcut.activated, lambda: self.change_page(-1))
 shortcut = QShortcut(QKeySequence(config["jump_page_shortcut"]), self)
 qconnect(shortcut.activated, self.ask_for_page)
 shortcut = QShortcut(QKeySequence(config["settings_shortcut"]), self)
 qconnect(shortcut.activated, lambda: SubSearchSettingsDialog(parent=self).exec())
 # + Ctrl+F for selection in previewer
def show_dock(self):
 """Show the dock widget and populate UI if needed."""
 if config["jlab_format"] and not path.exists(path.join(path.dirname(__file__), 'pykakasi', 'src', '__init__.py')):
 logDebug("Pykakasi not found, asking for add...")
 ask_user("Sub2Srs Search:\n\nFor Jlab Format required pykakasi not found. Do you want to download it now?",
 lambda yes: add_kakasi(note_getter.import_kakasi) if yes else 0)
 
 self.populate_ui()
 self.show()
def hide_dock(self):
 """Hide the dock widget and save state."""
 global dock_widget
 self.window_state.save()
 if dock_widget is not None:
 dock_widget.hide()
def populate_ui(self):
 self.status_bar.hide()
 if not config['show_extended_filters']:
 self.toggle_filter_rows(True)
 self.populate_note_type_selection_combo()
 self.populate_selection_boxes()
 self.populate_current_profile_decks()
 self.window_state.restore()
 self.update_note_fields()
 
 self.sort[1].setEnabled(False) # Currently not respected in requests
 self.sort[1].setToolTip("Currently not yet supported by the API")
def populate_note_type_selection_combo(self):
 self.note_type_selection_combo.clear()
 self.note_type_selection_combo.addItem(*NameId.none_type())
 for note_type in mw.col.models.all_names_and_ids():
 self.note_type_selection_combo.addItem(
 note_type.name, note_type.id)
def populate_selection_boxes(self):
 self.jlpt_level[1].clear()
 self.sort[1].clear()
 self.wanikani_level[1].clear()
 self.category[1].clear()
 self.jlpt_level[1].addItems(["--", 'N5', 'N4', 'N3', 'N2', 'N1'])
 # self.sort[1].addItems(['--', 'Short First', 'Long First'])
 self.sort[1].addItems(['XXXXXX'])
 self.wanikani_level[1].addItem("--")
 self.wanikani_level[1].addItems(
 ['Level '+str(lvl+1) for lvl in range(60)])
 self.category[1].addItems(
 ['--', 'Anime', 'Drama', 'Games', 'Literature'])
def populate_current_profile_decks(self):
 logDebug("Populating current profile decks...")
 self.current_profile_deck_combo.set_decks(sorted_decks_and_ids(mw.col))
def toggle_filter_rows(self, no_config_overwrite=False):
 filter_widgets = (
 self.exact,
 self.jlpt_level[0], self.jlpt_level[1],
 self.wanikani_level[0], self.wanikani_level[1],
 self.category[0], self.category[1],
 self.sort[0], self.sort[1],
 self.min_length[0], self.min_length[1],
 self.max_length[0], self.max_length[1]
 )
 if not no_config_overwrite and config['show_extended_filters'] or no_config_overwrite and not config['show_extended_filters']:
 for widget in filter_widgets:
 widget.hide()
 else:
 for widget in filter_widgets:
 widget.show()
 if not no_config_overwrite:
 config['show_extended_filters'] = not config['show_extended_filters']
 config.write_config()
 self.filter_collapse.setToolTip(
 f'{"Hide" if config["show_extended_filters"] else "Show"} extended filters')
 self.filter_collapse.setText(
 '🞁' if config['show_extended_filters'] else '🞃')
def update_note_fields(self):
 if self.note_type_selection_combo.currentData() != NameId.none_type().id and self.note_type_selection_combo.currentData() is not None:
 fields = mw.col.models.field_names(mw.col.models.get(
 self.note_type_selection_combo.currentData()))
 needed_fields = list((config["jlab_template_fields"] if config["jlab_format"] else config["template_fields"]).values())
 if not config['import_source_info']:
 needed_fields.remove(config["jlab_template_fields"]['source_info'] if config["jlab_format"] else config["template_fields"]['source_info'])
 missing_fields = []
 for field in needed_fields:
 if field not in fields:
 missing_fields.append(field)
 if missing_fields:
 self.note_fields.setText(
 f"Note Type is missing fields:\n{', '.join(missing_fields)}. (Check preview if image is needed)")
 return self.note_fields.show()
 self.note_fields.hide()
def update_notes_list(self):
 self.search_term_edit.setFocus()
 self.search_result_label.hide()
 if not self.search_term_edit.text():
 return
 # measure against double search
 if self.search_block:
 return
 self.search_block = True
 self.search_result_label.set_count(custom_text="Loading...")
 def on_load_finished(notes: list[dict]):
 if isinstance(notes, IOError):
 print(notes)
 self.search_result_label.set_count(
 custom_text="Connection failed.")
 self.search_block = False
 return showInfo("You need an active internet connection. Please try again.\nIf this keeps popping up, try unsetting min. length and max. length.")
 limited_notes = notes[:config['notes_per_page']]
 self.note_list.set_notes(
 limited_notes,
 hide_fields=config['hidden_fields'],
 previewer=config['preview_on_right_side']
 )
 self.search_result_label.set_count(len(notes), config['notes_per_page'], len(limited_notes))
 self.page_prev.setEnabled(False)
 self.page_first.setEnabled(False)
 self.page_search_left.setEnabled(True)
 self.page_search_right.setEnabled(True)
 if len(notes) > config['notes_per_page']:
 self.page_skip.setEnabled(True)
 self.page_last.setEnabled(True)
 self.page = 1
 self.search_block = False
 QueryOp(
 parent=self,
 op=lambda c: note_getter.get_for("https://apiv2.immersionkit.com/search", self.search_term_edit.text(),
 extended_filters=[self.category[1].currentText(), self.sort[1].currentText(),
 [self.min_length[1].value(), self.max_length[1].value(
 )], self.jlpt_level[1].currentText(),
 self.wanikani_level[1].currentText(), self.exact.isChecked()]),
 success=on_load_finished,
 ).with_progress("Searching for cards...").run_in_background()
def change_page(self, page_nr):
 # if the current page is 2 and npp 100, x:y = 100:200. If setting the npp to 50, x:y changes to 50:100.
 # We need the new page now. This however is based on the x:y. So we need to calculate the CURRENT x:y by npp, as easy as recalculating. (Easier said than done)
 # So, now we got the problem: recalculating the page based on x:y which is based on the page is impossible.
 # New example: We got the page 2, npp 100, x:y = 100:200. New setting: 200.
 # We transfer the page to the new setting and need to check we are not above limits.
 # To do this, we take the page, 2, the current x:y based on the new setting, 200:400, and ceil((all := 300)/200) = 2 ~~-> x:y = allp voilà we got the page.~~
 # Wait a sec, we got what we need. Just the check for upwards off-limit is missing.... why am i even doing this?
 # found out I managed to get the if else the exact same without noticing it
 # always the same... just can't get it right
 # page | 1 | 2 | 3 | ...
 # x=100 | 0:100 | 100:200 | 200:300 | ...
 # | [(page-1)*x] : [page*x]
 # x=100 | [(1-1)*100=0] : [1*100=100] | [(2-1)*100=100] : [2*100=200] ✔️ (Now it just gotta work)
 if not isinstance(page_nr, int):
 return showWarning("Given page is no number")
 if page_nr == 0:
 return
 # First setting the page, afterwards it's the same for upward and downward
 cur_notes_len = len(note_getter.cur_note_list)
 if page_nr < 0 and abs(page_nr) < ceil(cur_notes_len / config['notes_per_page']):
 self.page = ceil(cur_notes_len / config['notes_per_page']) + page_nr + 1 # = go from last page forward
 elif cur_notes_len <= page_nr * config['notes_per_page']: # note: before page set
 # set the last page based on the above example
 self.page = ceil(cur_notes_len / config['notes_per_page'])
 elif page_nr * config['notes_per_page'] < 0:
 self.page = 1
 else: 
 self.page = page_nr
 page_start = (self.page-1)*config['notes_per_page']
 page_end = self.page*config['notes_per_page']
 
 logDebug(f"Page switch to {self.page} ({page_nr}) ({page_start}:{page_end} in {cur_notes_len})")
 limited_notes = note_getter.cur_note_list[page_start : page_end]
 self.note_list.set_notes(
 limited_notes,
 hide_fields=config['hidden_fields'],
 previewer=config['preview_on_right_side']
 )
 self.search_result_label.set_count(
 cur_notes_len, config['notes_per_page'], len(limited_notes), self.page)
 self.page_prev.setEnabled(page_start > 0)
 self.page_first.setEnabled(page_start > 0)
 self.page_skip.setEnabled(cur_notes_len-1 > page_end) # note: aims for the next page's start
 self.page_last.setEnabled(cur_notes_len-1 > page_end)
 
 self.note_list.clear_selection() # try to clear, seems not to work though
def ask_for_page(self):
 page = getOnlyText("Go to page ...", self, default=str(self.page), title="Page prompt")
 try:
 page = ceil(int(page))
 except:
 return showWarning("Your input is no number")
 if page == 0:
 return showWarning("0 is no valid page")
 
 self.change_page(page)
def start_import(self):
 def _execute_import(self):
 logDebug('Beginning Import')
 # Get selected notes
 notes = self.note_list.selected_notes()
 logDebug(f'Importing {len(notes)} notes')
 for note in notes:
 result = import_note(
 model_id=self.note_type_selection_combo.currentData(),
 note=note,
 deck_id=self.current_profile_deck_combo.currentData()
 )
 self.results.append(result)
 def _finish_import(self):
 # Clear the selection here to be able to run in background
 self.note_list.clear_selection()
 self.status_bar.set_status(successes=self.results.count(ImportResult.success),
 dupes=self.results.count(ImportResult.dupe), 
 fails=self.results.count(ImportResult.fail))
 mw.reset()
 if len(self.note_list.selected_notes()) < 1:
 return self.status_bar.set_status(custom_text="No notes selected.")
 self.results = []
 QueryOp(
 parent=self,
 op=lambda c: _execute_import(self),
 success=lambda c: QTimer.singleShot(0, lambda: _finish_import(self)),
 ).with_progress("Importing...").run_in_background()
 
def new_edit_win(self):
 if len(selected_notes := self.note_list.selected_notes()) > 0:
 self.status_bar.set_status(custom_text="Loading...")
 self._add_window_mgr.create_window(selected_notes[-1])
 else:
 self.status_bar.set_status(custom_text="No notes selected.")
def hideEvent(self, event):
 """Override hideEvent to save state when dock is hidden."""
 self.window_state.save()
 super().hideEvent(event)

######################################################################

Entry point

######################################################################

Global variables for dock widget management

dock_widget = None
content_widget = None

def create_or_toggle_dock():
"""Create or toggle the dock widget."""
global dock_widget, content_widget

# If dock already exists, toggle visibility
if dock_widget is not None:
 if dock_widget.isVisible():
 dock_widget.hide()
 else:
 dock_widget.show()
 dock_widget.setFocus()
 if content_widget:
 content_widget.show_dock()
 return
# Create content widget (MainDialog)
content_widget = MainDialog(parent=mw)
# Create dock widget
dock_widget = QDockWidget(ADDON_NAME, mw)
# Key settings:
# 1. Remove all dock features (no dragging, no floating)
dock_widget.setFeatures(QDockWidget.DockWidgetFeature.NoDockWidgetFeatures)
# 2. Hide title bar (make it look more embedded)
dock_widget.setTitleBarWidget(QWidget())
# 3. Set object name for later lookup and management
dock_widget.setObjectName("SubSearch_Dock")
# 4. Put content widget into dock widget
dock_widget.setWidget(content_widget)
# 5. Add to main window's right dock area
# Use QTimer.singleShot to ensure main window is fully initialized
QTimer.singleShot(0, lambda: mw.addDockWidget(
 Qt.DockWidgetArea.RightDockWidgetArea,
 dock_widget
))
# Show the dock and populate UI
dock_widget.show()
content_widget.show_dock()

def cleanup_dock():
"""Clean up dock widget resources."""
global dock_widget, content_widget

if dock_widget is not None:
 if content_widget is not None:
 # Save state before cleanup
 content_widget.window_state.save()
 content_widget.close()
 content_widget.deleteLater()
 content_widget = None
 
 dock_widget.close()
 dock_widget.deleteLater()
 dock_widget = None

def init():
# get AJT menu
root_menu = menu_root_entry()
# create a new menu item
action = QAction('Search for Sub2Srs Cards...', root_menu)
# set it to call create_or_toggle_dock function when it's clicked
qconnect(action.triggered, create_or_toggle_dock)
# and add it to the tools menu
root_menu.addActions([action])
# react to anki's state changes
gui_hooks.profile_will_close.append(cleanup_dock)

# check for kakasi
if config["jlab_format"] and not path.exists(path.join(path.dirname(__file__), 'pykakasi', 'src', '__init__.py')):
 logDebug("Pykakasi not found, asking for add...")
 gui_hooks.main_window_did_init.append(lambda: ask_user("Sub2Srs Search:\n\nFor Jlab Format required pykakasi not found. Do you want to download it now?",
 lambda yes: add_kakasi(note_getter.import_kakasi) if yes else 0))
elif config["jlab_format"]:
 note_getter.import_kakasi(1)

"

![image](/attachments/264d2687-02ba-4002-8180-0254d0c5888a)
@FileX wrote in https://codeberg.org/FileX/SubSearch/issues/25#issuecomment-8013797: > Currently the Window is based on a dialog, so to change this, it would have to be changed to a whole new class. I'm not sure how far I can go with that but I will look into it. Could need some time though. > > Regarding the clipboard thing; that would be pretty performance heavy but could be possible. I will for now move this to a new issue (#26) --- I have achieved this embedded function with Cursor in less than 5 minutes. But I don't know if it is buggy. Below is the code that it editted, for your reference. <details> " # TODO: # - Add full support for Hiragana and Romaji conversion # - Add debug log open button # - Replace menu entry with expandable one in tools # menu_for_helper = mw.form.menuTools.addMenu("FSRS4Anki Helper") # menu_for_helper.addAction(menu_auto_reschedule) # menu_for_helper.addAction(menu_auto_reschedule_after_review) # menu_for_helper.addAction(menu_auto_disperse) # menu_for_helper.addAction(menu_load_balance) # menu_for_free_days = menu_for_helper.addMenu( # "No Anki on Free Days (requires Load Balancing)" # ) # menu_for_helper.addSeparator() # - Check for comp of editing the sound:URL tag and the playback of the correct audio # - add Listening part to templ # - play button playing once and then is unresponsive until stopped # - add option to switch all cards between online and offline # - fix linux left pref arrow not correctly displaying # - fix if image really not found, show error in tile and be careful with download of it # - fit default config # - if playing around with config while cached window existing (esp. Displ. Notes), it gets mashed up import json from collections import defaultdict from os import path, mkdir from math import ceil from aqt import mw, gui_hooks from aqt.qt import * from aqt.utils import showInfo, showWarning, openLink, tooltip, ask_user, show_critical, getOnlyText from aqt.operations import QueryOp from .config import config from .common import ADDON_NAME, LogDebug, add_kakasi, sorted_decks_and_ids, NameId, create_subsearch_template from .subsearch_ajt.about_menu import menu_root_entry from .subsearch_ajt.consts import SOURCE_LINK from .note_importer import import_note, ImportResult from .widgets import SearchResultLabel, DeckCombo, ComboBox, SpinBox, StatusBar, NoteList, ItemBox, WIDGET_HEIGHT from .edit_window import AddDialogLauncher from .settings_dialog import SubSearchSettingsDialog from . import note_getter logDebug = LogDebug() class MainDialogUI(QWidget): name = "subsearch_dialog" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.status_bar = StatusBar() self.search_result_label = SearchResultLabel() self.current_profile_deck_combo = DeckCombo() self.exact = QCheckBox("Exact Matches") self.jlpt_level = [QLabel('JLPT'), ComboBox()] self.wanikani_level = [QLabel('WaniKani'), ComboBox()] self.category = [QLabel('Category'), ComboBox()] self.sort = [QLabel('Sort by'), ComboBox()] self.min_length = [QLabel('Minimal Length:'), SpinBox(0, 200, 1, 0)] self.max_length = [QLabel('Maximal Length:'), SpinBox(0, 500, 1, 0)] self.edit_button = QPushButton('Edit') self.import_button = QPushButton('Import') self.filter_collapse = QPushButton('🞁') self.search_term_edit = QLineEdit() self.search_button = QPushButton('Search') if config['show_help_buttons']: self.help_button = QPushButton('Help') self.page_search_left = QPushButton("🔍") self.page_search_right = QPushButton("🔍") self.page_prev = QPushButton('🞀') self.page_first = QPushButton("⏮") self.note_list = NoteList() self.page_skip = QPushButton('🞂') self.page_last = QPushButton("⏭") self.note_type_selection_combo = ComboBox() self.note_fields = QLabel(wordWrap=True) self.init_ui() def init_ui(self): self.search_term_edit.setPlaceholderText('Search Term') self.setLayout(self.make_main_layout()) self.set_default_sizes() def make_filter_row(self) -> QLayout: filter_row = QHBoxLayout() self.filter_collapse.setFixedWidth(40) self.filter_collapse.setDefault(False) filter_row.addWidget(self.filter_collapse) filter_row.addWidget(self.search_term_edit) self.search_button.setDefault(True) filter_row.addWidget(self.search_button) if config['show_help_buttons']: filter_row.addWidget(self.help_button) return filter_row def make_main_layout(self) -> QLayout: main_vbox = QVBoxLayout() main_vbox.addLayout(self.make_filter_row()) main_vbox.addLayout(self.make_preferences_row()) main_vbox.addLayout(self.make_preferences_row2()) main_vbox.addWidget(self.search_result_label) main_vbox.addLayout(self.make_note_list()) main_vbox.addLayout(self.status_bar) main_vbox.addLayout(self.make_input_row()) main_vbox.addLayout(self.make_note_field_label()) return main_vbox def make_preferences_row(self) -> QLayout: pref_row = QHBoxLayout() pref_row.addWidget(self.exact) pref_row.addWidget(self.jlpt_level[0]) pref_row.addWidget(self.jlpt_level[1]) pref_row.addWidget(self.wanikani_level[0]) pref_row.addWidget(self.wanikani_level[1]) pref_row.addWidget(self.category[0]) pref_row.addWidget(self.category[1]) pref_row.addWidget(self.sort[0]) pref_row.addWidget(self.sort[1]) pref_row.setStretchFactor(pref_row, 1) return pref_row def make_preferences_row2(self): pref_row2 = QHBoxLayout() pref_row2.addWidget(self.min_length[0]) pref_row2.addWidget(self.min_length[1]) pref_row2.addWidget(self.max_length[0]) pref_row2.addWidget(self.max_length[1]) pref_row2.setStretchFactor(pref_row2, 1) return pref_row2 def make_note_list(self): note_row = QHBoxLayout() left_span = QVBoxLayout() right_span = QVBoxLayout() self.page_search_left.setFixedWidth(17) self.page_search_left.setFixedHeight(45) self.page_search_left.setEnabled(False) self.page_search_left.setToolTip("Go to page...") self.page_prev.setFixedWidth(17) self.page_prev.setFixedHeight(45) self.page_prev.setEnabled(False) self.page_prev.setToolTip("Previous Page") self.page_first.setFixedWidth(17) self.page_first.setFixedHeight(45) self.page_first.setEnabled(False) self.page_first.setToolTip("First Page") self.page_search_right.setFixedWidth(17) self.page_search_right.setFixedHeight(45) self.page_search_right.setEnabled(False) self.page_search_right.setToolTip("Go to page...") self.page_skip.setFixedWidth(17) self.page_skip.setFixedHeight(45) self.page_skip.setEnabled(False) self.page_skip.setToolTip("Next Page") self.page_last.setFixedWidth(17) self.page_last.setFixedHeight(45) self.page_last.setEnabled(False) self.page_last.setToolTip("Last Page") left_span.addStretch(1) left_span.addWidget(self.page_search_left) left_span.addWidget(self.page_prev) left_span.addWidget(self.page_first) left_span.addStretch(1) note_row.addLayout(left_span) note_row.addWidget(self.note_list) right_span.addStretch(1) right_span.addWidget(self.page_search_right) right_span.addWidget(self.page_skip) right_span.addWidget(self.page_last) right_span.addStretch(1) note_row.addLayout(right_span) note_row.setStretch(1, 1) return note_row def set_default_sizes(self): combo_min_width = 120 self.setMinimumSize(680, 500) all_combos = [self.current_profile_deck_combo, self.note_type_selection_combo, self.jlpt_level[1], self.sort[1], self.wanikani_level[1], self.category[1]] all_widgets = [self.exact, self.edit_button, self.import_button, self.filter_collapse, self.search_button, self.search_term_edit] if config['show_help_buttons']: all_widgets.append(self.help_button) for w in all_widgets: w.setMinimumHeight(WIDGET_HEIGHT) for combo in all_combos: combo.setMinimumWidth(combo_min_width) combo.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) def make_input_row(self) -> QLayout: import_row = QHBoxLayout() import_row.addWidget(QLabel('Into Deck')) import_row.addWidget(self.current_profile_deck_combo) import_row.addWidget(QLabel('Map to Note Type')) import_row.addWidget(self.note_type_selection_combo) import_row.addWidget(self.edit_button) import_row.addWidget(self.import_button) import_row.setStretchFactor(import_row, 1) return import_row def make_note_field_label(self) -> QLayout: note_field_box = QHBoxLayout() self.note_fields.hide() self.note_fields.setStyleSheet('QLabel { color: red; }') note_field_box.addWidget(self.note_fields) note_field_box.setStretch(0, 1) return note_field_box ############################################################################# # UI logic ############################################################################# class WindowState: def __init__(self, window: MainDialogUI): self._window = window self._json_filepath = path.join(path.dirname( __file__), 'user_files', 'window_state.json') self._map = { "to_deck": self._window.current_profile_deck_combo, "note_type": self._window.note_type_selection_combo, "exact": self._window.exact, 'jlpt_level': self._window.jlpt_level[1], 'wanikani_level': self._window.wanikani_level[1], 'category': self._window.category[1], 'sort': self._window.sort[1], 'min_length': self._window.min_length[1], 'max_length': self._window.max_length[1] } self._state = defaultdict(dict) def save(self): for key, widget in self._map.items(): try: self._state[mw.pm.name][key] = widget.currentText() except AttributeError: try: self._state[mw.pm.name][key] = widget.value() except AttributeError: self._state[mw.pm.name][key] = widget.isChecked() if not path.exists(self._json_filepath): mkdir(path.dirname(self._json_filepath)) with open(self._json_filepath, 'w', encoding='utf8') as of: json.dump(self._state, of, indent=4, ensure_ascii=False) # Note: Geometry is managed by QDockWidget, no need to save separately logDebug('Saved window state.') def _load(self) -> bool: if self._state: return True elif path.isfile(self._json_filepath): with open(self._json_filepath, encoding='utf8') as f: self._state.update(json.load(f)) return True else: return False def restore(self): if self._load() and (profile_settings := self._state.get(mw.pm.name)): for key, widget in self._map.items(): if profile_settings.get(key): try: if (value := profile_settings[key]) in widget.all_items(): widget.setCurrentText(value) except AttributeError: try: widget.setValue(value) except AttributeError: widget.setChecked(value) # Note: Geometry is managed by QDockWidget, no need to restore separately class MainDialog(MainDialogUI): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.window_state = WindowState(self) self._add_window_mgr = AddDialogLauncher(self) self.search_block = False self.page = 0 self.results = [] self.connect_elements() def connect_elements(self): qconnect(self.edit_button.clicked, self.new_edit_win) qconnect(self.import_button.clicked, self.start_import) qconnect(self.search_button.clicked, self.update_notes_list) qconnect(self.search_term_edit.editingFinished, self.update_notes_list) qconnect(self.filter_collapse.clicked, self.toggle_filter_rows) if config['show_help_buttons']: qconnect(self.help_button.clicked, lambda: openLink( f"{SOURCE_LINK}/src/branch/main/README.md#screenshots---how-to-use")) qconnect(self.page_prev.clicked, lambda: self.change_page(self.page - 1)) qconnect(self.page_skip.clicked, lambda: self.change_page(self.page + 1)) qconnect(self.page_first.clicked, lambda: self.change_page(1)) qconnect(self.page_last.clicked, lambda: self.change_page(-1)) qconnect(self.page_search_left.clicked, self.ask_for_page) qconnect(self.page_search_right.clicked, self.ask_for_page) qconnect(self.note_type_selection_combo.currentTextChanged, self.update_note_fields) # Registered in settings_dialog and config shortcut = QShortcut(QKeySequence(config["toggle_search_focus_shortcut"]), self) qconnect(shortcut.activated, lambda: self.search_term_edit.clearFocus() if self.search_term_edit.hasFocus() else self.search_term_edit.setFocus()) shortcut = QShortcut(QKeySequence(config["toggle_filter_shortcut"]), self) qconnect(shortcut.activated, self.toggle_filter_rows) shortcut = QShortcut(QKeySequence(config["import_shortcut"]), self) qconnect(shortcut.activated, self.start_import) shortcut = QShortcut(QKeySequence(config["edit_shortcut"]), self) qconnect(shortcut.activated, self.new_edit_win) shortcut = QShortcut(QKeySequence("Escape"), self) qconnect(shortcut.activated, self.hide_dock) shortcut = QShortcut(QKeySequence(config["close_shortcut"]), self) qconnect(shortcut.activated, self.hide_dock) shortcut = QShortcut(QKeySequence(config["prev_page_shortcut"]), self) qconnect(shortcut.activated, lambda: self.change_page(self.page - 1)) shortcut = QShortcut(QKeySequence(config["next_page_shortcut"]), self) qconnect(shortcut.activated, lambda: self.change_page(self.page + 1)) shortcut = QShortcut(QKeySequence(config["first_page_shortcut"]), self) qconnect(shortcut.activated, lambda: self.change_page(1)) shortcut = QShortcut(QKeySequence(config["last_page_shortcut"]), self) qconnect(shortcut.activated, lambda: self.change_page(-1)) shortcut = QShortcut(QKeySequence(config["jump_page_shortcut"]), self) qconnect(shortcut.activated, self.ask_for_page) shortcut = QShortcut(QKeySequence(config["settings_shortcut"]), self) qconnect(shortcut.activated, lambda: SubSearchSettingsDialog(parent=self).exec()) # + Ctrl+F for selection in previewer def show_dock(self): """Show the dock widget and populate UI if needed.""" if config["jlab_format"] and not path.exists(path.join(path.dirname(__file__), 'pykakasi', 'src', '__init__.py')): logDebug("Pykakasi not found, asking for add...") ask_user("Sub2Srs Search:\n\nFor Jlab Format required pykakasi not found. Do you want to download it now?", lambda yes: add_kakasi(note_getter.import_kakasi) if yes else 0) self.populate_ui() self.show() def hide_dock(self): """Hide the dock widget and save state.""" global dock_widget self.window_state.save() if dock_widget is not None: dock_widget.hide() def populate_ui(self): self.status_bar.hide() if not config['show_extended_filters']: self.toggle_filter_rows(True) self.populate_note_type_selection_combo() self.populate_selection_boxes() self.populate_current_profile_decks() self.window_state.restore() self.update_note_fields() self.sort[1].setEnabled(False) # Currently not respected in requests self.sort[1].setToolTip("Currently not yet supported by the API") def populate_note_type_selection_combo(self): self.note_type_selection_combo.clear() self.note_type_selection_combo.addItem(*NameId.none_type()) for note_type in mw.col.models.all_names_and_ids(): self.note_type_selection_combo.addItem( note_type.name, note_type.id) def populate_selection_boxes(self): self.jlpt_level[1].clear() self.sort[1].clear() self.wanikani_level[1].clear() self.category[1].clear() self.jlpt_level[1].addItems(["--", 'N5', 'N4', 'N3', 'N2', 'N1']) # self.sort[1].addItems(['--', 'Short First', 'Long First']) self.sort[1].addItems(['XXXXXX']) self.wanikani_level[1].addItem("--") self.wanikani_level[1].addItems( ['Level '+str(lvl+1) for lvl in range(60)]) self.category[1].addItems( ['--', 'Anime', 'Drama', 'Games', 'Literature']) def populate_current_profile_decks(self): logDebug("Populating current profile decks...") self.current_profile_deck_combo.set_decks(sorted_decks_and_ids(mw.col)) def toggle_filter_rows(self, no_config_overwrite=False): filter_widgets = ( self.exact, self.jlpt_level[0], self.jlpt_level[1], self.wanikani_level[0], self.wanikani_level[1], self.category[0], self.category[1], self.sort[0], self.sort[1], self.min_length[0], self.min_length[1], self.max_length[0], self.max_length[1] ) if not no_config_overwrite and config['show_extended_filters'] or no_config_overwrite and not config['show_extended_filters']: for widget in filter_widgets: widget.hide() else: for widget in filter_widgets: widget.show() if not no_config_overwrite: config['show_extended_filters'] = not config['show_extended_filters'] config.write_config() self.filter_collapse.setToolTip( f'{"Hide" if config["show_extended_filters"] else "Show"} extended filters') self.filter_collapse.setText( '🞁' if config['show_extended_filters'] else '🞃') def update_note_fields(self): if self.note_type_selection_combo.currentData() != NameId.none_type().id and self.note_type_selection_combo.currentData() is not None: fields = mw.col.models.field_names(mw.col.models.get( self.note_type_selection_combo.currentData())) needed_fields = list((config["jlab_template_fields"] if config["jlab_format"] else config["template_fields"]).values()) if not config['import_source_info']: needed_fields.remove(config["jlab_template_fields"]['source_info'] if config["jlab_format"] else config["template_fields"]['source_info']) missing_fields = [] for field in needed_fields: if field not in fields: missing_fields.append(field) if missing_fields: self.note_fields.setText( f"Note Type is missing fields:\n{', '.join(missing_fields)}. (Check preview if image is needed)") return self.note_fields.show() self.note_fields.hide() def update_notes_list(self): self.search_term_edit.setFocus() self.search_result_label.hide() if not self.search_term_edit.text(): return # measure against double search if self.search_block: return self.search_block = True self.search_result_label.set_count(custom_text="Loading...") def on_load_finished(notes: list[dict]): if isinstance(notes, IOError): print(notes) self.search_result_label.set_count( custom_text="Connection failed.") self.search_block = False return showInfo("You need an active internet connection. Please try again.\nIf this keeps popping up, try unsetting min. length and max. length.") limited_notes = notes[:config['notes_per_page']] self.note_list.set_notes( limited_notes, hide_fields=config['hidden_fields'], previewer=config['preview_on_right_side'] ) self.search_result_label.set_count(len(notes), config['notes_per_page'], len(limited_notes)) self.page_prev.setEnabled(False) self.page_first.setEnabled(False) self.page_search_left.setEnabled(True) self.page_search_right.setEnabled(True) if len(notes) > config['notes_per_page']: self.page_skip.setEnabled(True) self.page_last.setEnabled(True) self.page = 1 self.search_block = False QueryOp( parent=self, op=lambda c: note_getter.get_for("https://apiv2.immersionkit.com/search", self.search_term_edit.text(), extended_filters=[self.category[1].currentText(), self.sort[1].currentText(), [self.min_length[1].value(), self.max_length[1].value( )], self.jlpt_level[1].currentText(), self.wanikani_level[1].currentText(), self.exact.isChecked()]), success=on_load_finished, ).with_progress("Searching for cards...").run_in_background() def change_page(self, page_nr): # if the current page is 2 and npp 100, x:y = 100:200. If setting the npp to 50, x:y changes to 50:100. # We need the new page now. This however is based on the x:y. So we need to calculate the CURRENT x:y by npp, as easy as recalculating. (Easier said than done) # So, now we got the problem: recalculating the page based on x:y which is based on the page is impossible. # New example: We got the page 2, npp 100, x:y = 100:200. New setting: 200. # We transfer the page to the new setting and need to check we are not above limits. # To do this, we take the page, 2, the current x:y based on the new setting, 200:400, and ceil((all := 300)/200) = 2 ~~-> x:y = allp voilà we got the page.~~ # Wait a sec, we got what we need. Just the check for upwards off-limit is missing.... why am i even doing this? # found out I managed to get the if else the exact same without noticing it # always the same... just can't get it right # page | 1 | 2 | 3 | ... # x=100 | 0:100 | 100:200 | 200:300 | ... # | [(page-1)*x] : [page*x] # x=100 | [(1-1)*100=0] : [1*100=100] | [(2-1)*100=100] : [2*100=200] ✔️ (Now it just gotta work) if not isinstance(page_nr, int): return showWarning("Given page is no number") if page_nr == 0: return # First setting the page, afterwards it's the same for upward and downward cur_notes_len = len(note_getter.cur_note_list) if page_nr < 0 and abs(page_nr) < ceil(cur_notes_len / config['notes_per_page']): self.page = ceil(cur_notes_len / config['notes_per_page']) + page_nr + 1 # = go from last page forward elif cur_notes_len <= page_nr * config['notes_per_page']: # note: before page set # set the last page based on the above example self.page = ceil(cur_notes_len / config['notes_per_page']) elif page_nr * config['notes_per_page'] < 0: self.page = 1 else: self.page = page_nr page_start = (self.page-1)*config['notes_per_page'] page_end = self.page*config['notes_per_page'] logDebug(f"Page switch to {self.page} ({page_nr}) ({page_start}:{page_end} in {cur_notes_len})") limited_notes = note_getter.cur_note_list[page_start : page_end] self.note_list.set_notes( limited_notes, hide_fields=config['hidden_fields'], previewer=config['preview_on_right_side'] ) self.search_result_label.set_count( cur_notes_len, config['notes_per_page'], len(limited_notes), self.page) self.page_prev.setEnabled(page_start > 0) self.page_first.setEnabled(page_start > 0) self.page_skip.setEnabled(cur_notes_len-1 > page_end) # note: aims for the next page's start self.page_last.setEnabled(cur_notes_len-1 > page_end) self.note_list.clear_selection() # try to clear, seems not to work though def ask_for_page(self): page = getOnlyText("Go to page ...", self, default=str(self.page), title="Page prompt") try: page = ceil(int(page)) except: return showWarning("Your input is no number") if page == 0: return showWarning("0 is no valid page") self.change_page(page) def start_import(self): def _execute_import(self): logDebug('Beginning Import') # Get selected notes notes = self.note_list.selected_notes() logDebug(f'Importing {len(notes)} notes') for note in notes: result = import_note( model_id=self.note_type_selection_combo.currentData(), note=note, deck_id=self.current_profile_deck_combo.currentData() ) self.results.append(result) def _finish_import(self): # Clear the selection here to be able to run in background self.note_list.clear_selection() self.status_bar.set_status(successes=self.results.count(ImportResult.success), dupes=self.results.count(ImportResult.dupe), fails=self.results.count(ImportResult.fail)) mw.reset() if len(self.note_list.selected_notes()) < 1: return self.status_bar.set_status(custom_text="No notes selected.") self.results = [] QueryOp( parent=self, op=lambda c: _execute_import(self), success=lambda c: QTimer.singleShot(0, lambda: _finish_import(self)), ).with_progress("Importing...").run_in_background() def new_edit_win(self): if len(selected_notes := self.note_list.selected_notes()) > 0: self.status_bar.set_status(custom_text="Loading...") self._add_window_mgr.create_window(selected_notes[-1]) else: self.status_bar.set_status(custom_text="No notes selected.") def hideEvent(self, event): """Override hideEvent to save state when dock is hidden.""" self.window_state.save() super().hideEvent(event) ###################################################################### # Entry point ###################################################################### # Global variables for dock widget management dock_widget = None content_widget = None def create_or_toggle_dock(): """Create or toggle the dock widget.""" global dock_widget, content_widget # If dock already exists, toggle visibility if dock_widget is not None: if dock_widget.isVisible(): dock_widget.hide() else: dock_widget.show() dock_widget.setFocus() if content_widget: content_widget.show_dock() return # Create content widget (MainDialog) content_widget = MainDialog(parent=mw) # Create dock widget dock_widget = QDockWidget(ADDON_NAME, mw) # Key settings: # 1. Remove all dock features (no dragging, no floating) dock_widget.setFeatures(QDockWidget.DockWidgetFeature.NoDockWidgetFeatures) # 2. Hide title bar (make it look more embedded) dock_widget.setTitleBarWidget(QWidget()) # 3. Set object name for later lookup and management dock_widget.setObjectName("SubSearch_Dock") # 4. Put content widget into dock widget dock_widget.setWidget(content_widget) # 5. Add to main window's right dock area # Use QTimer.singleShot to ensure main window is fully initialized QTimer.singleShot(0, lambda: mw.addDockWidget( Qt.DockWidgetArea.RightDockWidgetArea, dock_widget )) # Show the dock and populate UI dock_widget.show() content_widget.show_dock() def cleanup_dock(): """Clean up dock widget resources.""" global dock_widget, content_widget if dock_widget is not None: if content_widget is not None: # Save state before cleanup content_widget.window_state.save() content_widget.close() content_widget.deleteLater() content_widget = None dock_widget.close() dock_widget.deleteLater() dock_widget = None def init(): # get AJT menu root_menu = menu_root_entry() # create a new menu item action = QAction('Search for Sub2Srs Cards...', root_menu) # set it to call create_or_toggle_dock function when it's clicked qconnect(action.triggered, create_or_toggle_dock) # and add it to the tools menu root_menu.addActions([action]) # react to anki's state changes gui_hooks.profile_will_close.append(cleanup_dock) # check for kakasi if config["jlab_format"] and not path.exists(path.join(path.dirname(__file__), 'pykakasi', 'src', '__init__.py')): logDebug("Pykakasi not found, asking for add...") gui_hooks.main_window_did_init.append(lambda: ask_user("Sub2Srs Search:\n\nFor Jlab Format required pykakasi not found. Do you want to download it now?", lambda yes: add_kakasi(note_getter.import_kakasi) if yes else 0)) elif config["jlab_format"]: note_getter.import_kakasi(1) " </details> ![image](/attachments/264d2687-02ba-4002-8180-0254d0c5888a)
1.3 MiB
Owner
Copy link

I see...
The main point here is that it would have to be a hybrid. Your code seems to work, though it would have to be rearranged a lot including implementing a lot of if else paths.
Further, I don't think there are many people that would need this in daily use, so I will put this a bit later in my list.

Also, one other thing I want to ask. You aren't seeing any icons/emojis in the interface, right? Is that because of a special font or do you have any clue what is causing that?

I see... The main point here is that it would have to be a hybrid. Your code seems to work, though it would have to be rearranged a lot including implementing a lot of if else paths. Further, I don't think there are many people that would need this in daily use, so I will put this a bit later in my list. Also, one other thing I want to ask. You aren't seeing any icons/emojis in the interface, right? Is that because of a special font or do you have any clue what is causing that?
Sign in to join this conversation.
No Branch/Tag specified
main
v2.1.x
v1.3.4
v1.3.3
v1.3.2
v1.3.1
v1.3.0
v1.2.2
1.2.1
v1.2.0
v1.1.0
v1.0.0
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
FileX/SubSearch#25
Reference in a new issue
FileX/SubSearch
No description provided.
Delete branch "%!s()"

Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?