anki/qt/aqt/browser/browser.py
Damien Elmes e124f935a5 fix timebox causing crash
When a modal was created with another window as its parent, the other
window was being returned, when it was the current window that we
actually wanted. This caused nextCard() to be called again when it
presented the timebox modal, leading to a stack overflow.

https://forums.ankiweb.net/t/anki-2-1-45-alpha/10061/71
2021-06-01 15:35:18 +10:00

874 lines
30 KiB
Python

# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations
from typing import Callable, Optional, Sequence, Tuple, Union
import aqt
import aqt.forms
from anki.cards import Card, CardId
from anki.collection import Collection, Config, OpChanges, SearchNode
from anki.consts import *
from anki.errors import NotFoundError
from anki.lang import without_unicode_isolation
from anki.notes import NoteId
from anki.tags import MARKED_TAG
from anki.utils import ids2str, isMac
from aqt import AnkiQt, gui_hooks
from aqt.editor import Editor
from aqt.exporting import ExportDialog
from aqt.flags import load_flags
from aqt.operations.card import set_card_deck, set_card_flag
from aqt.operations.collection import redo, undo
from aqt.operations.note import remove_notes
from aqt.operations.scheduling import (
forget_cards,
reposition_new_cards_dialog,
set_due_date_dialog,
suspend_cards,
unsuspend_cards,
)
from aqt.operations.tag import (
add_tags_to_notes,
clear_unused_tags,
remove_tags_from_notes,
)
from aqt.qt import *
from aqt.switch import Switch
from aqt.undo import UndoActionsInfo
from aqt.utils import (
HelpPage,
KeyboardModifiersPressed,
current_window,
ensure_editor_saved,
getTag,
no_arg_trigger,
openHelp,
qtMenuShortcutWorkaround,
restoreGeom,
restoreSplitter,
restoreState,
saveGeom,
saveSplitter,
saveState,
showInfo,
showWarning,
skip_if_selection_is_empty,
tr,
)
from .card_info import CardInfoDialog
from .change_notetype import ChangeModel
from .find_and_replace import FindAndReplaceDialog
from .previewer import BrowserPreviewer as PreviewDialog
from .previewer import Previewer
from .sidebar import SidebarTreeView
from .table import Table
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()
# disable undo/redo
self.on_undo_state_change(mw.undo_actions_info())
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_window() == self
self.table.op_executed(changes, handler, focused)
self.sidebar.op_executed(changes, handler, focused)
if changes.editor:
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()
if changes.browser_table and changes.card:
self.card = self.table.get_current_card()
self._update_context_actions()
def on_focus_change(self, new: Optional[QWidget], old: Optional[QWidget]) -> 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 load_flags(self.col):
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.editor.cleanup()
self.table.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_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.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: 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_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:
def add_preview_button(editor: Editor) -> None:
preview_shortcut = "Ctrl+Shift+P" # TODO
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 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
CardInfoDialog(parent=self, mw=self.mw, card=self.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()
if self._is_one_notetype(ids):
ChangeModel(self, ids)
else:
showInfo(tr.browsing_please_select_cards_from_only_one())
def _is_one_notetype(self, ids: Sequence[NoteId]) -> bool:
query = f"select count(distinct mid) from notes where id in {ids2str(ids)}"
if self.col.db.scalar(query) == 1:
return True
return False
def createFilteredDeck(self) -> None:
search = self.current_search()
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(
"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.get_selected_note_ids()
# 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
######################################################################
@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: 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)
@no_arg_trigger
@skip_if_selection_is_empty
@ensure_editor_saved
def remove_tags_from_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_delete())
):
return
remove_tags_from_notes(
parent=self, note_ids=self.selected_notes(), space_separated_tags=tags
).run_in_background(initiator=self)
def _prompt_for_tags(self, prompt: str) -> Optional[str]:
(tags, ok) = getTag(self, self.col, prompt)
if not ok:
return None
else:
return tags
@no_arg_trigger
@ensure_editor_saved
def clear_unused_tags(self) -> None:
clear_unused_tags(parent=self).run_in_background()
addTags = add_tags_to_selected_notes
deleteTags = remove_tags_from_selected_notes
clearUnusedTags = clear_unused_tags
# Suspending
######################################################################
def _update_toggle_suspend_action(self) -> None:
is_suspended = bool(self.card and self.card.queue == QUEUE_TYPE_SUSPENDED)
self.form.actionToggle_Suspend.setChecked(is_suspended)
@skip_if_selection_is_empty
@ensure_editor_saved
def suspend_selected_cards(self, checked: bool) -> None:
cids = self.selected_cards()
if checked:
suspend_cards(parent=self, card_ids=cids).run_in_background()
else:
unsuspend_cards(parent=self.mw, card_ids=cids).run_in_background()
# Exporting
######################################################################
@no_arg_trigger
@skip_if_selection_is_empty
def _on_export_notes(self) -> None:
cids = self.selectedNotesAsCards()
ExportDialog(self.mw, cids=list(cids))
# Flags & Marking
######################################################################
@skip_if_selection_is_empty
@ensure_editor_saved
def set_flag_of_selected_cards(self, flag: int) -> None:
if not self.card:
return
# flag needs toggling off?
if flag == self.card.user_flag():
flag = 0
set_card_flag(
parent=self, card_ids=self.selected_cards(), flag=flag
).run_in_background()
def _update_flags_menu(self) -> None:
flag = self.card and self.card.user_flag()
flag = flag or 0
for f in load_flags(self.col):
getattr(self.form, f.action).setChecked(flag == f.index)
qtMenuShortcutWorkaround(self.form.menuFlag)
def _update_flag_labels(self) -> None:
for flag in load_flags(self.col):
getattr(self.form, flag.action).setText(flag.label)
def toggle_mark_of_selected_notes(self, checked: bool) -> None:
if checked:
self.add_tags_to_selected_notes(tags=MARKED_TAG)
else:
self.remove_tags_from_selected_notes(tags=MARKED_TAG)
def _update_toggle_mark_action(self) -> None:
is_marked = bool(self.card and self.card.note().has_tag(MARKED_TAG))
self.form.actionToggle_Mark.setChecked(is_marked)
# Scheduling
######################################################################
@no_arg_trigger
@skip_if_selection_is_empty
@ensure_editor_saved
def reposition(self) -> None:
if self.card and self.card.queue != QUEUE_TYPE_NEW:
showInfo(tr.browsing_only_new_cards_can_be_repositioned(), parent=self)
return
if op := reposition_new_cards_dialog(
parent=self, card_ids=self.selected_cards()
):
op.run_in_background()
@no_arg_trigger
@skip_if_selection_is_empty
@ensure_editor_saved
def set_due_date(self) -> None:
if op := set_due_date_dialog(
parent=self,
card_ids=self.selected_cards(),
config_key=Config.String.SET_DUE_BROWSER,
):
op.run_in_background()
@no_arg_trigger
@skip_if_selection_is_empty
@ensure_editor_saved
def forget_cards(self) -> None:
forget_cards(
parent=self,
card_ids=self.selected_cards(),
).run_in_background()
# Edit: selection
######################################################################
@no_arg_trigger
@skip_if_selection_is_empty
@ensure_editor_saved
def selectNotes(self) -> None:
nids = self.selected_notes()
# clear the selection so we don't waste energy preserving it
self.table.clear_selection()
search = self.col.build_search_string(
SearchNode(nids=SearchNode.IdList(ids=nids))
)
self.search_for(search)
self.table.select_all()
# Hooks
######################################################################
def setupHooks(self) -> None:
gui_hooks.undo_state_did_change.append(self.on_undo_state_change)
gui_hooks.backend_will_block.append(self.table.on_backend_will_block)
gui_hooks.backend_did_block.append(self.table.on_backend_did_block)
gui_hooks.operation_did_execute.append(self.on_operation_did_execute)
gui_hooks.focus_did_change.append(self.on_focus_change)
def teardownHooks(self) -> None:
gui_hooks.undo_state_did_change.remove(self.on_undo_state_change)
gui_hooks.backend_will_block.remove(self.table.on_backend_will_block)
gui_hooks.backend_did_block.remove(self.table.on_backend_will_block)
gui_hooks.operation_did_execute.remove(self.on_operation_did_execute)
gui_hooks.focus_did_change.remove(self.on_focus_change)
# Undo
######################################################################
def undo(self) -> None:
undo(parent=self)
def redo(self) -> None:
redo(parent=self)
def on_undo_state_change(self, info: UndoActionsInfo) -> None:
self.form.actionUndo.setText(info.undo_text)
self.form.actionUndo.setEnabled(info.can_undo)
self.form.actionRedo.setText(info.redo_text)
self.form.actionRedo.setEnabled(info.can_redo)
self.form.actionRedo.setVisible(info.show_redo)
# Edit: replacing
######################################################################
@no_arg_trigger
@skip_if_selection_is_empty
@ensure_editor_saved
def onFindReplace(self) -> None:
FindAndReplaceDialog(self, mw=self.mw, note_ids=self.selected_notes())
# Edit: finding dupes
######################################################################
@no_arg_trigger
@ensure_editor_saved
def onFindDupes(self) -> None:
from aqt.browser.find_duplicates import FindDuplicatesDialog
FindDuplicatesDialog(browser=self, mw=self.mw)
# Jumping
######################################################################
def has_previous_card(self) -> bool:
return self.table.has_previous()
def has_next_card(self) -> bool:
return self.table.has_next()
def onPreviousCard(self) -> None:
self.focusTo = self.editor.currentField
self.editor.call_after_note_saved(self.table.to_previous_row)
def onNextCard(self) -> None:
self.focusTo = self.editor.currentField
self.editor.call_after_note_saved(self.table.to_next_row)
def onFirstCard(self) -> None:
self.table.to_first_row()
def onLastCard(self) -> None:
self.table.to_last_row()
def onFind(self) -> None:
# workaround for PyQt focus bug
self.editor.hideCompleters()
self.form.searchEdit.setFocus()
self.form.searchEdit.lineEdit().selectAll()
def onNote(self) -> None:
# workaround for PyQt focus bug
self.editor.hideCompleters()
self.editor.web.setFocus()
self.editor.loadNote(focusTo=0)
def onCardList(self) -> None:
self.form.tableView.setFocus()