def toggle_mark_on_current_note(self) -> None: def redraw_mark(out: OpChangesWithCount) -> None: self.card.load() self._update_mark_icon() note = self.card.note() if note.has_tag(MARKED_TAG): remove_tags_from_notes( parent=self.mw, note_ids=[note.id], space_separated_tags=MARKED_TAG ).success(redraw_mark).run_in_background(initiator=self) else: add_tags_to_notes( parent=self.mw, note_ids=[note.id], space_separated_tags=MARKED_TAG, ).success(redraw_mark).run_in_background(initiator=self)
class Browser(QMainWindow): mw: AnkiQt col: Collection editor: Editor | None table: Table def __init__( self, mw: AnkiQt, card: Card | None = None, search: tuple[str | SearchNode] | None = None, ) -> None: """ card -- try to select the provided card after executing "search" or "deck:current" (if "search" was None) search -- set and perform search; caller must ensure validity """ QMainWindow.__init__(self, None, Qt.WindowType.Window) self.mw = mw self.col = self.mw.col self.lastFilter = "" self.focusTo: int | None = None self._previewer: Previewer | None = None self._card_info = BrowserCardInfo(self.mw) self._closeEventHasCleanedUp = False self.form = aqt.forms.browser.Ui_Dialog() self.form.setupUi(self) self.setupSidebar() restoreGeom(self, "editor", 0) restoreState(self, "editor") restoreSplitter(self.form.splitter, "editor3") self.form.splitter.setChildrenCollapsible(False) # set if exactly 1 row is selected; used by the previewer self.card: Card | None = None self.current_card: Card | None = None self.setup_table() self.setupMenus() self.setupHooks() self.setupEditor() # disable undo/redo self.on_undo_state_change(mw.undo_actions_info()) # legacy alias self.model = MockModel(self) gui_hooks.browser_will_show(self) self.show() self.setupSearch(card, search) def on_operation_did_execute(self, changes: OpChanges, handler: object | None) -> None: focused = current_window() == self self.table.op_executed(changes, handler, focused) self.sidebar.op_executed(changes, handler, focused) if changes.note_text: if handler is not self.editor: # fixme: this will leave the splitter shown, but with no current # note being edited note = self.editor.note if note: try: note.load() except NotFoundError: self.editor.set_note(None) return self.editor.set_note(note) if changes.browser_table and changes.card: self.card = self.table.get_single_selected_card() self.current_card = self.table.get_current_card() self._update_card_info() self._update_current_actions() # changes.card is required for updating flag icon if changes.note_text or changes.card: self._renderPreview() def on_focus_change(self, new: QWidget | None, old: QWidget | None) -> None: if current_window() == self: self.setUpdatesEnabled(True) self.table.redraw_cells() self.sidebar.refresh_if_needed() def setupMenus(self) -> None: # actions f = self.form # edit qconnect(f.actionUndo.triggered, self.undo) qconnect(f.actionRedo.triggered, self.redo) qconnect(f.actionInvertSelection.triggered, self.table.invert_selection) qconnect(f.actionSelectNotes.triggered, self.selectNotes) if not isMac: f.actionClose.setVisible(False) qconnect(f.actionCreateFilteredDeck.triggered, self.createFilteredDeck) f.actionCreateFilteredDeck.setShortcuts(["Ctrl+G", "Ctrl+Alt+G"]) # notes qconnect(f.actionAdd.triggered, self.mw.onAddCard) qconnect(f.actionAdd_Tags.triggered, self.add_tags_to_selected_notes) qconnect(f.actionRemove_Tags.triggered, self.remove_tags_from_selected_notes) qconnect(f.actionClear_Unused_Tags.triggered, self.clear_unused_tags) qconnect(f.actionToggle_Mark.triggered, self.toggle_mark_of_selected_notes) qconnect(f.actionChangeModel.triggered, self.onChangeModel) qconnect(f.actionFindDuplicates.triggered, self.onFindDupes) qconnect(f.actionFindReplace.triggered, self.onFindReplace) qconnect(f.actionManage_Note_Types.triggered, self.mw.onNoteTypes) qconnect(f.actionDelete.triggered, self.delete_selected_notes) # cards qconnect(f.actionChange_Deck.triggered, self.set_deck_of_selected_cards) qconnect(f.action_Info.triggered, self.showCardInfo) qconnect(f.actionReposition.triggered, self.reposition) qconnect(f.action_set_due_date.triggered, self.set_due_date) qconnect(f.action_forget.triggered, self.forget_cards) qconnect(f.actionToggle_Suspend.triggered, self.suspend_selected_cards) def set_flag_func(desired_flag: int) -> Callable: return lambda: self.set_flag_of_selected_cards(desired_flag) for flag in self.mw.flags.all(): qconnect( getattr(self.form, flag.action).triggered, set_flag_func(flag.index)) self._update_flag_labels() qconnect(f.actionExport.triggered, self._on_export_notes) # jumps qconnect(f.actionPreviousCard.triggered, self.onPreviousCard) qconnect(f.actionNextCard.triggered, self.onNextCard) qconnect(f.actionFirstCard.triggered, self.onFirstCard) qconnect(f.actionLastCard.triggered, self.onLastCard) qconnect(f.actionFind.triggered, self.onFind) qconnect(f.actionNote.triggered, self.onNote) qconnect(f.actionSidebar.triggered, self.focusSidebar) qconnect(f.actionCardList.triggered, self.onCardList) # help qconnect(f.actionGuide.triggered, self.onHelp) # keyboard shortcut for shift+home/end self.pgUpCut = QShortcut(QKeySequence("Shift+Home"), self) qconnect(self.pgUpCut.activated, self.onFirstCard) self.pgDownCut = QShortcut(QKeySequence("Shift+End"), self) qconnect(self.pgDownCut.activated, self.onLastCard) # add-on hook gui_hooks.browser_menus_did_init(self) self.mw.maybeHideAccelerators(self) def closeEvent(self, evt: QCloseEvent) -> None: if self._closeEventHasCleanedUp: evt.accept() return self.editor.call_after_note_saved(self._closeWindow) evt.ignore() def _closeWindow(self) -> None: self._cleanup_preview() self._card_info.close() self.editor.cleanup() self.table.cleanup() self.sidebar.cleanup() saveSplitter(self.form.splitter, "editor3") saveGeom(self, "editor") saveState(self, "editor") self.teardownHooks() self.mw.maybeReset() aqt.dialogs.markClosed("Browser") self._closeEventHasCleanedUp = True self.mw.deferred_delete_and_garbage_collect(self) self.close() @ensure_editor_saved def closeWithCallback(self, onsuccess: Callable) -> None: self._closeWindow() onsuccess() def keyPressEvent(self, evt: QKeyEvent) -> None: if evt.key() == Qt.Key.Key_Escape: self.close() else: super().keyPressEvent(evt) def reopen( self, _mw: AnkiQt, card: Card | None = None, search: tuple[str | SearchNode] | None = None, ) -> None: if search is not None: self.search_for_terms(*search) self.form.searchEdit.setFocus() if card is not None: if search is None: # implicitly assume 'card' is in the current deck self._default_search(card) self.form.searchEdit.setFocus() self.table.select_single_card(card.id) # Searching ###################################################################### def setupSearch( self, card: Card | None = None, search: tuple[str | SearchNode] | None = None, ) -> None: qconnect(self.form.searchEdit.lineEdit().returnPressed, self.onSearchActivated) self.form.searchEdit.setCompleter(None) self.form.searchEdit.lineEdit().setPlaceholderText( tr.browsing_search_bar_hint()) self.form.searchEdit.addItems([""] + self.mw.pm.profile["searchHistory"]) if search is not None: self.search_for_terms(*search) else: self._default_search(card) self.form.searchEdit.setFocus() if card: self.table.select_single_card(card.id) # search triggered by user @ensure_editor_saved def onSearchActivated(self) -> None: text = self.current_search() try: normed = self.col.build_search_string(text) except Exception as err: showWarning(str(err)) else: self.search_for(normed) self.update_history() def search_for(self, search: str, prompt: str | None = None) -> None: """Keep track of search string so that we reuse identical search when refreshing, rather than whatever is currently in the search field. Optionally set the search bar to a different text than the actual search. """ self._lastSearchTxt = search prompt = search if prompt == None else prompt self.form.searchEdit.lineEdit().setText(prompt) self.search() def current_search(self) -> str: return self.form.searchEdit.lineEdit().text() def search(self) -> None: """Search triggered programmatically. Caller must have saved note first.""" try: self.table.search(self._lastSearchTxt) except Exception as err: showWarning(str(err)) def update_history(self) -> None: sh = self.mw.pm.profile["searchHistory"] if self._lastSearchTxt in sh: sh.remove(self._lastSearchTxt) sh.insert(0, self._lastSearchTxt) sh = sh[:30] self.form.searchEdit.clear() self.form.searchEdit.addItems(sh) self.mw.pm.profile["searchHistory"] = sh def updateTitle(self) -> None: selected = self.table.len_selection() cur = self.table.len() tr_title = (tr.browsing_window_title_notes if self.table.is_notes_mode() else tr.browsing_window_title) self.setWindowTitle( without_unicode_isolation(tr_title(total=cur, selected=selected))) def search_for_terms(self, *search_terms: str | SearchNode) -> None: search = self.col.build_search_string(*search_terms) self.form.searchEdit.setEditText(search) self.onSearchActivated() def _default_search(self, card: Card | None = None) -> None: default = self.col.get_config_string(Config.String.DEFAULT_SEARCH_TEXT) if default.strip(): search = default prompt = default else: search = self.col.build_search_string(SearchNode(deck="current")) prompt = "" if card is not None: search = gui_hooks.default_search(search, card) self.search_for(search, prompt) def onReset(self) -> None: self.sidebar.refresh() self.begin_reset() self.end_reset() # caller must have called editor.saveNow() before calling this or .reset() def begin_reset(self) -> None: self.editor.set_note(None, hide=False) self.mw.progress.start() self.table.begin_reset() def end_reset(self) -> None: self.table.end_reset() self.mw.progress.finish() # Table & Editor ###################################################################### def setup_table(self) -> None: self.table = Table(self) self.table.set_view(self.form.tableView) switch = Switch(11, tr.browsing_card_initial(), tr.browsing_note_initial()) switch.setChecked(self.table.is_notes_mode()) switch.setToolTip(tr.browsing_toggle_showing_cards_notes()) qconnect(self.form.action_toggle_mode.triggered, switch.toggle) qconnect(switch.toggled, self.on_table_state_changed) self.form.gridLayout.addWidget(switch, 0, 0) def setupEditor(self) -> None: QShortcut(QKeySequence("Ctrl+Shift+P"), self, self.onTogglePreview) def add_preview_button(editor: Editor) -> None: editor._links["preview"] = lambda _editor: self.onTogglePreview() editor.web.eval( "$editorToolbar.then(({ notetypeButtons }) => notetypeButtons.appendButton({ component: editorToolbar.PreviewButton, id: 'preview' }));" ) gui_hooks.editor_did_init.append(add_preview_button) self.editor = aqt.editor.Editor(self.mw, self.form.fieldsArea, self) gui_hooks.editor_did_init.remove(add_preview_button) @ensure_editor_saved def on_all_or_selected_rows_changed(self) -> None: """Called after the selected or all rows (searching, toggling mode) have changed. Update window title, card preview, context actions, and editor. """ if self._closeEventHasCleanedUp: return self.updateTitle() # if there is only one selected card, use it in the editor # it might differ from the current card self.card = self.table.get_single_selected_card() self.singleCard = bool(self.card) self.form.splitter.widget(1).setVisible(self.singleCard) if self.singleCard: self.editor.set_note(self.card.note(), focusTo=self.focusTo) self.focusTo = None self.editor.card = self.card else: self.editor.set_note(None) self._renderPreview() self._update_row_actions() self._update_selection_actions() gui_hooks.browser_did_change_row(self) @deprecated(info="please use on_all_or_selected_rows_changed() instead.") def onRowChanged(self, *args: Any) -> None: self.on_all_or_selected_rows_changed() def on_current_row_changed(self) -> None: """Called after the row of the current element has changed.""" if self._closeEventHasCleanedUp: return self.current_card = self.table.get_current_card() self._update_current_actions() self._update_card_info() def _update_row_actions(self) -> None: has_rows = bool(self.table.len()) self.form.actionSelectAll.setEnabled(has_rows) self.form.actionInvertSelection.setEnabled(has_rows) self.form.actionFirstCard.setEnabled(has_rows) self.form.actionLastCard.setEnabled(has_rows) def _update_selection_actions(self) -> None: has_selection = bool(self.table.len_selection()) self.form.actionSelectNotes.setEnabled(has_selection) self.form.actionExport.setEnabled(has_selection) self.form.actionAdd_Tags.setEnabled(has_selection) self.form.actionRemove_Tags.setEnabled(has_selection) self.form.actionToggle_Mark.setEnabled(has_selection) self.form.actionChangeModel.setEnabled(has_selection) self.form.actionDelete.setEnabled(has_selection) self.form.actionChange_Deck.setEnabled(has_selection) self.form.action_set_due_date.setEnabled(has_selection) self.form.action_forget.setEnabled(has_selection) self.form.actionReposition.setEnabled(has_selection) self.form.actionToggle_Suspend.setEnabled(has_selection) self.form.menuFlag.setEnabled(has_selection) def _update_current_actions(self) -> None: self._update_flags_menu() self._update_toggle_mark_action() self._update_toggle_suspend_action() self.form.action_Info.setEnabled(self.table.has_current()) self.form.actionPreviousCard.setEnabled(self.table.has_previous()) self.form.actionNextCard.setEnabled(self.table.has_next()) @ensure_editor_saved def on_table_state_changed(self, checked: bool) -> None: self.mw.progress.start() self.table.toggle_state(checked, self._lastSearchTxt) self.mw.progress.finish() # Sidebar ###################################################################### def setupSidebar(self) -> None: dw = self.sidebarDockWidget = QDockWidget(tr.browsing_sidebar(), self) dw.setFeatures(QDockWidget.DockWidgetFeature.NoDockWidgetFeatures) dw.setObjectName("Sidebar") dw.setAllowedAreas(Qt.DockWidgetArea.LeftDockWidgetArea) self.sidebar = SidebarTreeView(self) self.sidebarTree = self.sidebar # legacy alias dw.setWidget(self.sidebar) qconnect( self.form.actionSidebarFilter.triggered, self.focusSidebarSearchBar, ) grid = QGridLayout() grid.addWidget(self.sidebar.searchBar, 0, 0) grid.addWidget(self.sidebar.toolbar, 0, 1) grid.addWidget(self.sidebar, 1, 0, 1, 2) grid.setContentsMargins(0, 0, 0, 0) grid.setSpacing(0) w = QWidget() w.setLayout(grid) dw.setWidget(w) self.sidebarDockWidget.setFloating(False) self.sidebarDockWidget.setTitleBarWidget(QWidget()) self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, dw) # schedule sidebar to refresh after browser window has loaded, so the # UI is more responsive self.mw.progress.timer(10, self.sidebar.refresh, False) def showSidebar(self) -> None: self.sidebarDockWidget.setVisible(True) def focusSidebar(self) -> None: self.showSidebar() self.sidebar.setFocus() def focusSidebarSearchBar(self) -> None: self.showSidebar() self.sidebar.searchBar.setFocus() def toggle_sidebar(self) -> None: want_visible = not self.sidebarDockWidget.isVisible() self.sidebarDockWidget.setVisible(want_visible) if want_visible: self.sidebar.refresh() # legacy def setFilter(self, *terms: str) -> None: self.sidebar.update_search(*terms) # Info ###################################################################### def showCardInfo(self) -> None: self._card_info.toggle() def _update_card_info(self) -> None: self._card_info.set_card(self.current_card) # Menu helpers ###################################################################### def selected_cards(self) -> Sequence[CardId]: return self.table.get_selected_card_ids() def selected_notes(self) -> Sequence[NoteId]: return self.table.get_selected_note_ids() def selectedNotesAsCards(self) -> Sequence[CardId]: return self.table.get_card_ids_from_selected_note_ids() def onHelp(self) -> None: openHelp(HelpPage.BROWSING) # legacy selectedCards = selected_cards selectedNotes = selected_notes # Misc menu options ###################################################################### @no_arg_trigger @skip_if_selection_is_empty @ensure_editor_saved def onChangeModel(self) -> None: ids = self.selected_notes() change_notetype_dialog(parent=self, note_ids=ids) def createFilteredDeck(self) -> None: search = self.current_search() if self.mw.col.sched_ver() != 1 and KeyboardModifiersPressed().alt: aqt.dialogs.open("FilteredDeckConfigDialog", self.mw, search_2=search) else: aqt.dialogs.open("FilteredDeckConfigDialog", self.mw, search=search) # Preview ###################################################################### def onTogglePreview(self) -> None: if self._previewer: self._previewer.close() self._on_preview_closed() elif self.editor.note: self._previewer = PreviewDialog(self, self.mw, self._on_preview_closed) self._previewer.open() def _renderPreview(self) -> None: if self._previewer: if self.singleCard: self._previewer.render_card() else: self.onTogglePreview() def _cleanup_preview(self) -> None: if self._previewer: self._previewer.cancel_timer() self._previewer.close() def _on_preview_closed(self) -> None: if self.editor.web: self.editor.web.eval( "document.getElementById('previewButton').classList.remove('highlighted')" ) self._previewer = None # Card deletion ###################################################################### @no_arg_trigger @skip_if_selection_is_empty def delete_selected_notes(self) -> None: # ensure deletion is not accidentally triggered when the user is focused # in the editing screen or search bar focus = self.focusWidget() if focus != self.form.tableView: return nids = self.table.to_row_of_unselected_note() remove_notes(parent=self, note_ids=nids).run_in_background() # legacy deleteNotes = delete_selected_notes # Deck change ###################################################################### @no_arg_trigger @skip_if_selection_is_empty @ensure_editor_saved def set_deck_of_selected_cards(self) -> None: from aqt.studydeck import StudyDeck cids = self.table.get_selected_card_ids() did = self.mw.col.db.scalar("select did from cards where id = ?", cids[0]) current = self.mw.col.decks.get(did)["name"] ret = StudyDeck( self.mw, current=current, accept=tr.browsing_move_cards(), title=tr.browsing_change_deck(), help=HelpPage.BROWSING, parent=self, ) if not ret.name: return did = self.col.decks.id(ret.name) set_card_deck(parent=self, card_ids=cids, deck_id=did).run_in_background() # legacy setDeck = set_deck_of_selected_cards # Tags ###################################################################### @no_arg_trigger @skip_if_selection_is_empty @ensure_editor_saved def add_tags_to_selected_notes( self, tags: str | None = None, ) -> None: "Shows prompt if tags not provided." if not (tags := tags or self._prompt_for_tags(tr.browsing_enter_tags_to_add())): return add_tags_to_notes( parent=self, note_ids=self.selected_notes(), space_separated_tags=tags).run_in_background(initiator=self)
class Browser(QMainWindow): mw: AnkiQt col: Collection editor: Optional[Editor] table: Table def __init__( self, mw: AnkiQt, card: Optional[Card] = None, search: Optional[Tuple[Union[str, SearchNode]]] = None, ) -> None: """ card : try to search for its note and select it search: set and perform search; caller must ensure validity """ QMainWindow.__init__(self, None, Qt.Window) self.mw = mw self.col = self.mw.col self.lastFilter = "" self.focusTo: Optional[int] = None self._previewer: Optional[Previewer] = None self._closeEventHasCleanedUp = False self.form = aqt.forms.browser.Ui_Dialog() self.form.setupUi(self) self.setupSidebar() restoreGeom(self, "editor", 0) restoreState(self, "editor") restoreSplitter(self.form.splitter, "editor3") self.form.splitter.setChildrenCollapsible(False) self.card: Optional[Card] = None self.setup_table() self.setupMenus() self.setupHooks() self.setupEditor() self.onUndoState(self.mw.form.actionUndo.isEnabled()) self.setupSearch(card, search) gui_hooks.browser_will_show(self) self.show() def on_operation_did_execute(self, changes: OpChanges, handler: Optional[object]) -> None: focused = current_top_level_widget() == self self.table.op_executed(changes, handler, focused) self.sidebar.op_executed(changes, handler, focused) if changes.note or changes.notetype: if handler is not self.editor: # fixme: this will leave the splitter shown, but with no current # note being edited note = self.editor.note if note: try: note.load() except NotFoundError: self.editor.set_note(None) return self.editor.set_note(note) self._renderPreview() def on_focus_change(self, new: Optional[QWidget], old: Optional[QWidget]) -> None: if current_top_level_widget() == self: self.setUpdatesEnabled(True) self.table.redraw_cells() self.sidebar.refresh_if_needed() def setupMenus(self) -> None: # pylint: disable=unnecessary-lambda # actions f = self.form # edit qconnect(f.actionUndo.triggered, self.undo) qconnect(f.actionInvertSelection.triggered, self.table.invert_selection) qconnect(f.actionSelectNotes.triggered, self.selectNotes) if not isMac: f.actionClose.setVisible(False) qconnect(f.actionCreateFilteredDeck.triggered, self.createFilteredDeck) f.actionCreateFilteredDeck.setShortcuts(["Ctrl+G", "Ctrl+Alt+G"]) # notes qconnect(f.actionAdd.triggered, self.mw.onAddCard) qconnect(f.actionAdd_Tags.triggered, lambda: self.add_tags_to_selected_notes()) qconnect( f.actionRemove_Tags.triggered, lambda: self.remove_tags_from_selected_notes(), ) qconnect(f.actionClear_Unused_Tags.triggered, self.clear_unused_tags) qconnect(f.actionToggle_Mark.triggered, self.toggle_mark_of_selected_notes) qconnect(f.actionChangeModel.triggered, self.onChangeModel) qconnect(f.actionFindDuplicates.triggered, self.onFindDupes) qconnect(f.actionFindReplace.triggered, self.onFindReplace) qconnect(f.actionManage_Note_Types.triggered, self.mw.onNoteTypes) qconnect(f.actionDelete.triggered, self.delete_selected_notes) # cards qconnect(f.actionChange_Deck.triggered, self.set_deck_of_selected_cards) qconnect(f.action_Info.triggered, self.showCardInfo) qconnect(f.actionReposition.triggered, self.reposition) qconnect(f.action_set_due_date.triggered, self.set_due_date) qconnect(f.action_forget.triggered, self.forget_cards) qconnect(f.actionToggle_Suspend.triggered, self.suspend_selected_cards) qconnect(f.actionRed_Flag.triggered, lambda: self.set_flag_of_selected_cards(1)) qconnect(f.actionOrange_Flag.triggered, lambda: self.set_flag_of_selected_cards(2)) qconnect(f.actionGreen_Flag.triggered, lambda: self.set_flag_of_selected_cards(3)) qconnect(f.actionBlue_Flag.triggered, lambda: self.set_flag_of_selected_cards(4)) qconnect(f.actionExport.triggered, lambda: self._on_export_notes()) # jumps qconnect(f.actionPreviousCard.triggered, self.onPreviousCard) qconnect(f.actionNextCard.triggered, self.onNextCard) qconnect(f.actionFirstCard.triggered, self.onFirstCard) qconnect(f.actionLastCard.triggered, self.onLastCard) qconnect(f.actionFind.triggered, self.onFind) qconnect(f.actionNote.triggered, self.onNote) qconnect(f.actionSidebar.triggered, self.focusSidebar) qconnect(f.actionCardList.triggered, self.onCardList) # help qconnect(f.actionGuide.triggered, self.onHelp) # keyboard shortcut for shift+home/end self.pgUpCut = QShortcut(QKeySequence("Shift+Home"), self) qconnect(self.pgUpCut.activated, self.onFirstCard) self.pgDownCut = QShortcut(QKeySequence("Shift+End"), self) qconnect(self.pgDownCut.activated, self.onLastCard) # add-on hook gui_hooks.browser_menus_did_init(self) self.mw.maybeHideAccelerators(self) def closeEvent(self, evt: QCloseEvent) -> None: if self._closeEventHasCleanedUp: evt.accept() return self.editor.call_after_note_saved(self._closeWindow) evt.ignore() def _closeWindow(self) -> None: self._cleanup_preview() self.editor.cleanup() saveSplitter(self.form.splitter, "editor3") saveGeom(self, "editor") saveState(self, "editor") saveHeader(self.form.tableView.horizontalHeader(), "editor") self.teardownHooks() self.mw.maybeReset() aqt.dialogs.markClosed("Browser") self._closeEventHasCleanedUp = True self.mw.deferred_delete_and_garbage_collect(self) self.close() @ensure_editor_saved def closeWithCallback(self, onsuccess: Callable) -> None: self._closeWindow() onsuccess() def keyPressEvent(self, evt: QKeyEvent) -> None: if evt.key() == Qt.Key_Escape: self.close() else: super().keyPressEvent(evt) def reopen( self, _mw: AnkiQt, card: Optional[Card] = None, search: Optional[Tuple[Union[str, SearchNode]]] = None, ) -> None: if search is not None: self.search_for_terms(*search) self.form.searchEdit.setFocus() elif card: self.show_single_card(card) self.form.searchEdit.setFocus() # Searching ###################################################################### def setupSearch( self, card: Optional[Card] = None, search: Optional[Tuple[Union[str, SearchNode]]] = None, ) -> None: qconnect(self.form.searchEdit.lineEdit().returnPressed, self.onSearchActivated) self.form.searchEdit.setCompleter(None) self.form.searchEdit.lineEdit().setPlaceholderText( tr.browsing_search_bar_hint()) self.form.searchEdit.addItems(self.mw.pm.profile["searchHistory"]) if search is not None: self.search_for_terms(*search) elif card: self.show_single_card(card) else: self.search_for( self.col.build_search_string(SearchNode(deck="current")), "") self.form.searchEdit.setFocus() # search triggered by user @ensure_editor_saved def onSearchActivated(self) -> None: text = self.form.searchEdit.lineEdit().text() try: normed = self.col.build_search_string(text) except Exception as err: showWarning(str(err)) else: self.search_for(normed) self.update_history() def search_for(self, search: str, prompt: Optional[str] = None) -> None: """Keep track of search string so that we reuse identical search when refreshing, rather than whatever is currently in the search field. Optionally set the search bar to a different text than the actual search. """ self._lastSearchTxt = search prompt = search if prompt == None else prompt self.form.searchEdit.lineEdit().setText(prompt) self.search() def current_search(self) -> str: return self.form.searchEdit.lineEdit().text() def search(self) -> None: """Search triggered programmatically. Caller must have saved note first.""" try: self.table.search(self._lastSearchTxt) except Exception as err: showWarning(str(err)) def update_history(self) -> None: sh = self.mw.pm.profile["searchHistory"] if self._lastSearchTxt in sh: sh.remove(self._lastSearchTxt) sh.insert(0, self._lastSearchTxt) sh = sh[:30] self.form.searchEdit.clear() self.form.searchEdit.addItems(sh) self.mw.pm.profile["searchHistory"] = sh def updateTitle(self) -> None: selected = self.table.len_selection() cur = self.table.len() tr_title = (tr.browsing_window_title_notes if self.table.is_notes_mode() else tr.browsing_window_title) self.setWindowTitle( without_unicode_isolation(tr_title(total=cur, selected=selected))) def search_for_terms(self, *search_terms: Union[str, SearchNode]) -> None: search = self.col.build_search_string(*search_terms) self.form.searchEdit.setEditText(search) self.onSearchActivated() def show_single_card(self, card: Card) -> None: if card.nid: def on_show_single_card() -> None: self.card = card search = self.col.build_search_string(SearchNode(nid=card.nid)) search = gui_hooks.default_search(search, card) self.search_for(search, "") self.table.select_single_card(card.id) self.editor.call_after_note_saved(on_show_single_card) def onReset(self) -> None: self.sidebar.refresh() self.begin_reset() self.end_reset() # caller must have called editor.saveNow() before calling this or .reset() def begin_reset(self) -> None: self.editor.set_note(None, hide=False) self.mw.progress.start() self.table.begin_reset() def end_reset(self) -> None: self.table.end_reset() self.mw.progress.finish() # Table & Editor ###################################################################### def setup_table(self) -> None: self.table = Table(self) self.table.set_view(self.form.tableView) switch = Switch(11, tr.browsing_card_initial(), tr.browsing_note_initial()) switch.setChecked(self.table.is_notes_mode()) switch.setToolTip(tr.browsing_toggle_cards_notes_mode()) qconnect(self.form.action_toggle_mode.triggered, switch.toggle) qconnect(switch.toggled, self.on_table_state_changed) self.form.gridLayout.addWidget(switch, 0, 0) def setupEditor(self) -> None: def add_preview_button(leftbuttons: List[str], editor: Editor) -> None: preview_shortcut = "Ctrl+Shift+P" leftbuttons.insert( 0, editor.addButton( None, "preview", lambda _editor: self.onTogglePreview(), tr.browsing_preview_selected_card( val=shortcut(preview_shortcut), ), tr.actions_preview(), id="previewButton", keys=preview_shortcut, disables=False, rightside=False, toggleable=True, ), ) gui_hooks.editor_did_init_left_buttons.append(add_preview_button) self.editor = aqt.editor.Editor(self.mw, self.form.fieldsArea, self) gui_hooks.editor_did_init_left_buttons.remove(add_preview_button) @ensure_editor_saved def onRowChanged(self, _current: Optional[QItemSelection], _previous: Optional[QItemSelection]) -> None: """Update current note and hide/show editor. """ if self._closeEventHasCleanedUp: return self.updateTitle() # the current card is used for context actions self.card = self.table.get_current_card() # if there is only one selected card, use it in the editor # it might differ from the current card card = self.table.get_single_selected_card() self.singleCard = bool(card) self.form.splitter.widget(1).setVisible(self.singleCard) if self.singleCard: self.editor.set_note(card.note(), focusTo=self.focusTo) self.focusTo = None self.editor.card = card else: self.editor.set_note(None) self._renderPreview() self._update_context_actions() gui_hooks.browser_did_change_row(self) def _update_context_actions(self) -> None: self._update_flags_menu() self._update_toggle_mark_action() self._update_toggle_suspend_action() @ensure_editor_saved def on_table_state_changed(self, checked: bool) -> None: self.mw.progress.start() self.table.toggle_state(checked, self._lastSearchTxt) self.mw.progress.finish() # Sidebar ###################################################################### def setupSidebar(self) -> None: dw = self.sidebarDockWidget = QDockWidget(tr.browsing_sidebar(), self) dw.setFeatures(QDockWidget.DockWidgetClosable) dw.setObjectName("Sidebar") dw.setAllowedAreas(Qt.LeftDockWidgetArea) self.sidebar = SidebarTreeView(self) self.sidebarTree = self.sidebar # legacy alias dw.setWidget(self.sidebar) qconnect( self.form.actionSidebarFilter.triggered, self.focusSidebarSearchBar, ) grid = QGridLayout() grid.addWidget(self.sidebar.searchBar, 0, 0) grid.addWidget(self.sidebar.toolbar, 0, 1) grid.addWidget(self.sidebar, 1, 0, 1, 2) grid.setContentsMargins(0, 0, 0, 0) grid.setSpacing(0) w = QWidget() w.setLayout(grid) dw.setWidget(w) self.sidebarDockWidget.setFloating(False) self.sidebarDockWidget.setTitleBarWidget(QWidget()) self.addDockWidget(Qt.LeftDockWidgetArea, dw) # schedule sidebar to refresh after browser window has loaded, so the # UI is more responsive self.mw.progress.timer(10, self.sidebar.refresh, False) def showSidebar(self) -> None: # workaround for PyQt focus bug self.editor.hideCompleters() self.sidebarDockWidget.setVisible(True) def focusSidebar(self) -> None: self.showSidebar() self.sidebar.setFocus() def focusSidebarSearchBar(self) -> None: self.showSidebar() self.sidebar.searchBar.setFocus() def toggle_sidebar(self) -> None: want_visible = not self.sidebarDockWidget.isVisible() self.sidebarDockWidget.setVisible(want_visible) if want_visible: self.sidebar.refresh() # legacy def setFilter(self, *terms: str) -> None: self.sidebar.update_search(*terms) # Info ###################################################################### def showCardInfo(self) -> None: if not self.card: return info, cs = self._cardInfoData() reps = self._revlogData(cs) card_info_dialog = CardInfoDialog(self) l = QVBoxLayout() l.setContentsMargins(0, 0, 0, 0) w = AnkiWebView(title="browser card info") l.addWidget(w) w.stdHtml(info + "<p>" + reps, context=card_info_dialog) bb = QDialogButtonBox(QDialogButtonBox.Close) l.addWidget(bb) qconnect(bb.rejected, card_info_dialog.reject) card_info_dialog.setLayout(l) card_info_dialog.setWindowModality(Qt.WindowModal) card_info_dialog.resize(500, 400) restoreGeom(card_info_dialog, "revlog") card_info_dialog.show() def _cardInfoData(self) -> Tuple[str, CardStats]: cs = CardStats(self.col, self.card) rep = cs.report(include_revlog=True) return rep, cs # legacy - revlog used to be generated here, and some add-ons # wrapped this function def _revlogData(self, cs: CardStats) -> str: return "" # Menu helpers ###################################################################### def selected_cards(self) -> Sequence[CardId]: return self.table.get_selected_card_ids() def selected_notes(self) -> Sequence[NoteId]: return self.table.get_selected_note_ids() def selectedNotesAsCards(self) -> Sequence[CardId]: return self.table.get_card_ids_from_selected_note_ids() def oneModelNotes(self) -> Sequence[NoteId]: sf = self.selected_notes() if not sf: return [] mods = self.col.db.scalar(""" select count(distinct mid) from notes where id in %s""" % ids2str(sf)) if mods > 1: showInfo(tr.browsing_please_select_cards_from_only_one()) return [] return sf def onHelp(self) -> None: openHelp(HelpPage.BROWSING) # legacy selectedCards = selected_cards selectedNotes = selected_notes # Misc menu options ###################################################################### @ensure_editor_saved_on_trigger def onChangeModel(self) -> None: nids = self.oneModelNotes() if nids: ChangeModel(self, nids) def createFilteredDeck(self) -> None: search = self.form.searchEdit.lineEdit().text() if self.mw.col.schedVer() != 1 and KeyboardModifiersPressed().alt: aqt.dialogs.open("FilteredDeckConfigDialog", self.mw, search_2=search) else: aqt.dialogs.open("FilteredDeckConfigDialog", self.mw, search=search) # Preview ###################################################################### def onTogglePreview(self) -> None: if self._previewer: self._previewer.close() self._on_preview_closed() else: self._previewer = PreviewDialog(self, self.mw, self._on_preview_closed) self._previewer.open() def _renderPreview(self) -> None: if self._previewer: if self.singleCard: self._previewer.render_card() else: self.onTogglePreview() def _cleanup_preview(self) -> None: if self._previewer: self._previewer.cancel_timer() self._previewer.close() def _on_preview_closed(self) -> None: if self.editor.web: self.editor.web.eval( "$('#previewButton').removeClass('highlighted')") self._previewer = None # Card deletion ###################################################################### def delete_selected_notes(self) -> None: # ensure deletion is not accidentally triggered when the user is focused # in the editing screen or search bar focus = self.focusWidget() if focus != self.form.tableView: return # nothing selected? nids = self.table.get_selected_note_ids() if not nids: return # select the next card if there is one self.focusTo = self.editor.currentField self.table.to_next_row() remove_notes(parent=self, note_ids=nids).run_in_background() # legacy deleteNotes = delete_selected_notes # Deck change ###################################################################### @ensure_editor_saved_on_trigger def set_deck_of_selected_cards(self) -> None: from aqt.studydeck import StudyDeck cids = self.table.get_selected_card_ids() if not cids: return did = self.mw.col.db.scalar("select did from cards where id = ?", cids[0]) current = self.mw.col.decks.get(did)["name"] ret = StudyDeck( self.mw, current=current, accept=tr.browsing_move_cards(), title=tr.browsing_change_deck(), help=HelpPage.BROWSING, parent=self, ) if not ret.name: return did = self.col.decks.id(ret.name) set_card_deck(parent=self, card_ids=cids, deck_id=did).run_in_background() # legacy setDeck = set_deck_of_selected_cards # Tags ###################################################################### @ensure_editor_saved_on_trigger def add_tags_to_selected_notes( self, tags: Optional[str] = None, ) -> None: "Shows prompt if tags not provided." if not (tags := tags or self._prompt_for_tags(tr.browsing_enter_tags_to_add())): return add_tags_to_notes( parent=self, note_ids=self.selected_notes(), space_separated_tags=tags).run_in_background(initiator=self)