2019-02-05 04:59:03 +01:00
|
|
|
# Copyright: Ankitects Pty Ltd and contributors
|
2012-12-21 08:51:59 +01:00
|
|
|
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
2021-03-16 13:40:37 +01:00
|
|
|
|
2020-02-11 23:28:21 +01:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2021-12-20 11:23:50 +01:00
|
|
|
import json
|
2022-09-20 04:56:59 +02:00
|
|
|
import math
|
2022-05-07 01:16:57 +02:00
|
|
|
import re
|
2021-10-03 10:59:42 +02:00
|
|
|
from typing import Callable, Sequence
|
2019-12-19 12:11:12 +01:00
|
|
|
|
2020-03-29 21:10:30 +02:00
|
|
|
import aqt
|
2022-02-13 04:40:47 +01:00
|
|
|
import aqt.browser
|
|
|
|
import aqt.editor
|
2019-12-20 10:19:03 +01:00
|
|
|
import aqt.forms
|
2022-02-13 04:40:47 +01:00
|
|
|
import aqt.operations
|
2021-07-12 12:36:26 +02:00
|
|
|
from anki._legacy import deprecated
|
2021-03-27 12:38:20 +01:00
|
|
|
from anki.cards import Card, CardId
|
2021-09-25 17:28:38 +02:00
|
|
|
from anki.collection import Collection, Config, OpChanges, SearchNode
|
2019-12-20 10:19:03 +01:00
|
|
|
from anki.consts import *
|
rework filtered deck screen & search errors
- Filtered deck creation now happens as an atomic operation, and is
undoable.
- The logic for initial search text, normalizing searches and so on
has been pushed into the backend.
- Use protobuf to pass the filtered deck to the updated dialog, so
we don't need to deal with untyped JSON.
- Change the "revise your search?" prompt to be a simple info box -
user has access to cancel and build buttons, and doesn't need a separate
prompt. Tweak the wording so the 'show excluded' button should be more
obvious.
- Filtered decks have a time appended to them instead of a number,
primarily because it's easier to implement. No objections going back to
the old behaviour if someone wants to contribute a clean patch.
The standard de-duplication will happen if two decks are created in the
same minute with the same name.
- Tweak the default sort order, and start with two searches. The UI
will still hide the second search by default, but by starting with two,
the frontend doesn't need logic for creating the starting text.
- Search errors now have their own error type, instead of using
InvalidInput, as that was intended mainly for bad API calls. The markdown
conversion is done when the error is converted from the backend, allowing
errors to printed as a string without any special handling by the calling
code.
TODO: when building a new filtered deck, update_active() is clobbering
the undo log when the overview is refreshed
2021-03-24 12:52:48 +01:00
|
|
|
from anki.errors import NotFoundError
|
2020-11-18 04:48:23 +01:00
|
|
|
from anki.lang import without_unicode_isolation
|
2021-03-27 12:38:20 +01:00
|
|
|
from anki.notes import NoteId
|
2022-03-09 07:51:41 +01:00
|
|
|
from anki.scheduler.base import ScheduleCardsAsNew
|
2021-03-18 03:06:45 +01:00
|
|
|
from anki.tags import MARKED_TAG
|
2021-11-25 00:06:16 +01:00
|
|
|
from anki.utils import is_mac
|
2021-03-29 08:12:26 +02:00
|
|
|
from aqt import AnkiQt, gui_hooks
|
2020-01-15 22:41:23 +01:00
|
|
|
from aqt.editor import Editor
|
Add apkg import/export on backend (#1743)
* Add apkg export on backend
* Filter out missing media-paths at write time
* Make TagMatcher::new() infallible
* Gather export data instead of copying directly
* Revert changes to rslib/src/tags/
* Reuse filename_is_safe/check_filename_safe()
* Accept func to produce MediaIter in export_apkg()
* Only store file folder once in MediaIter
* Use temporary tables for gathering
export_apkg() now accepts a search instead of a deck id. Decks are
gathered according to the matched notes' cards.
* Use schedule_as_new() to reset cards
* ExportData → ExchangeData
* Ignore ascii case when filtering system tags
* search_notes_cards_into_table →
search_cards_of_notes_into_table
* Start on apkg importing on backend
* Fix due dates in days for apkg export
* Refactor import-export/package
- Move media and meta code into appropriate modules.
- Normalize/check for normalization when deserializing media entries.
* Add SafeMediaEntry for deserialized MediaEntries
* Prepare media based on checksums
- Ensure all existing media files are hashed.
- Hash incoming files during preparation to detect conflicts.
- Uniquify names of conflicting files with hash (not notetype id).
- Mark media files as used while importing notes.
- Finally copy used media.
* Handle encoding in `replace_media_refs()`
* Add trait to keep down cow boilerplate
* Add notetypes immediately instaed of preparing
* Move target_col into Context
* Add notes immediately instaed of preparing
* Note id, not guid of conflicting notes
* Add import_decks()
* decks_configs → deck_configs
* Add import_deck_configs()
* Add import_cards(), import_revlog()
* Use dyn instead of generic for media_fn
Otherwise, would have to pass None with type annotation in the default
case.
* Fix signature of import_apkg()
* Fix search_cards_of_notes_into_table()
* Test new functions in text.rs
* Add roundtrip test for apkg (stub)
* Keep source id of imported cards (or skip)
* Keep source ids of imported revlog (or skip)
* Try to keep source ids of imported notes
* Make adding notetype with id undoable
* Wrap apkg import in transaction
* Keep source ids of imported deck configs (or skip)
* Handle card due dates and original due/did
* Fix importing cards/revlog
Card ids are manually uniquified.
* Factor out card importing
* Refactor card and revlog importing
* Factor out card importing
Also handle missing parents .
* Factor out note importing
* Factor out media importing
* Maybe upgrade scheduler of apkg
* Fix parent deck gathering
* Unconditionally import static media
* Fix deck importing edge cases
Test those edge cases, and add some global test helpers.
* Test note importing
* Let import_apkg() take a progress func
* Expand roundtrip apkg test
* Use fat pointer to avoid propogating generics
* Fix progress_fn type
* Expose apkg export/import on backend
* Return note log when importing apkg
* Fix archived collection name on apkg import
* Add CollectionOpWithBackendProgress
* Fix wrong Interrupted Exception being checked
* Add ClosedCollectionOp
* Add note ids to log and strip HTML
* Update progress when checking incoming media too
* Conditionally enable new importing in GUI
* Fix all_checksums() for media import
Entries of deleted files are nulled, not removed.
* Make apkg exporting on backend abortable
* Return number of notes imported from apkg
* Fix exception printing for QueryOp as well
* Add QueryOpWithBackendProgress
Also support backend exporting progress.
* Expose new apkg and colpkg exporting
* Open transaction in insert_data()
Was slowing down exporting by several orders of magnitude.
* Handle zstd-compressed apkg
* Add legacy arg to ExportAnkiPackage
Currently not exposed on the frontend
* Remove unused import in proto file
* Add symlink for typechecking of import_export_pb2
* Avoid kwargs in pb message creation, so typechecking is not lost
Protobuf's behaviour is rather subtle and I had to dig through the docs
to figure it out: set a field on a submessage to automatically assign
the submessage to the parent, or call SetInParent() to persist a default
version of the field you specified.
* Avoid re-exporting protobuf msgs we only use internally
* Stop after one test failure
mypy often fails much faster than pylint
* Avoid an extra allocation when extracting media checksums
* Update progress after prepare_media() finishes
Otherwise the bulk of the import ends up being shown as "Checked: 0"
in the progress window.
* Show progress of note imports
Note import is the slowest part, so showing progress here makes the UI
feel more responsive.
* Reset filtered decks at import time
Before this change, filtered decks exported with scheduling remained
filtered on import, and maybe_remove_from_filtered_deck() moved cards
into them as their home deck, leading to errors during review.
We may still want to provide a way to preserve filtered decks on import,
but to do that we'll need to ensure we don't rewrite the home decks of
cards, and we'll need to ensure the home decks are included as part of
the import (or give an error if they're not).
https://github.com/ankitects/anki/pull/1743/files#r839346423
* Fix a corner-case where due dates were shifted by a day
This issue existed in the old Python code as well. We need to include
the user's UTC offset in the exported file, or days_elapsed falls back
on the v1 cutoff calculation, which may be a day earlier or later than
the v2 calculation.
* Log conflicting note in remapped nt case
* take_fields() → into_fields()
* Alias `[u8; 20]` with `Sha1Hash`
* Truncate logged fields
* Rework apkg note import tests
- Use macros for more helpful errors.
- Split monolith into unit tests.
- Fix some unknown error with the previous test along the way.
(Was failing after 969484de4388d225c9f17d94534b3ba0094c3568.)
* Fix sorting of imported decks
Also adjust the test, so it fails without the patch. It was only passing
before, because the parent deck happened to come before the
inconsistently capitalised child alphabetically. But we want all parent
decks to be imported before their child decks, so their children can
adopt their capitalisation.
* target[_id]s → existing_card[_id]s
* export_collection_extracting_media() → ...
export_into_collection_file()
* target_already_exists→card_ordinal_already_exists
* Add search_cards_of_notes_into_table.sql
* Imrove type of apkg export selector/limit
* Remove redundant call to mod_schema()
* Parent tooltips to mw
* Fix a crash when truncating note text
String::truncate() is a bit of a footgun, and I've hit this before
too :-)
* Remove ExportLimit in favour of separate classes
* Remove OpWithBackendProgress and ClosedCollectionOp
Backend progress logic is now in ProgressManager. QueryOp can be used
for running on closed collection.
Also fix aborting of colpkg exports, which slipped through in #1817.
* Tidy up import log
* Avoid QDialog.exec()
* Default to excluding scheuling for deck list deck
* Use IncrementalProgress in whole import_export code
* Compare checksums when importing colpkgs
* Avoid registering changes if hashes are not needed
* ImportProgress::Collection → ImportProgress::File
* Make downgrading apkgs depend on meta version
* Generalise IncrementableProgress
And use it in entire import_export code instead.
* Fix type complexity lint
* Take count_map for IncrementableProgress::get_inner
* Replace import/export env with Shift click
* Accept all args from update() for backend progress
* Pass fields of ProgressUpdate explicitly
* Move update_interval into IncrementableProgress
* Outsource incrementing into Incrementor
* Mutate ProgressUpdate in progress_update callback
* Switch import/export legacy toggle to profile setting
Shift would have been nice, but the existing shortcuts complicate things.
If the user triggers an import with ctrl+shift+i, shift is unlikely to
have been released by the time our code runs, meaning the user accidentally
triggers the new code. We could potentially wait a while before bringing
up the dialog, but then we're forced to guess at how long it will take the
user to release the key.
One alternative would be to use alt instead of shift, but then we need to
trigger our shortcut when that key is pressed as well, and it could
potentially cause a conflict with an add-on that already uses that
combination.
* Show extension in export dialog
* Continue to provide separate options for schema 11+18 colpkg export
* Default to colpkg export when using File>Export
* Improve appearance of combo boxes when switching between apkg/colpkg
+ Deal with long deck names
* Convert newlines to spaces when showing fields from import
Ensures each imported note appears on a separate line
* Don't separate total note count from the other summary lines
This may come down to personal preference, but I feel the other counts
are equally as important, and separating them feels like it makes it
a bit easier to ignore them.
* Fix 'deck not normal' error when importing a filtered deck for the 2nd time
* Fix [Identical] being shown on first import
* Revert "Continue to provide separate options for schema 11+18 colpkg export"
This reverts commit 8f0b2c175f4794d642823b60414d142a12768441.
Will use a different approach
* Move legacy support into a separate exporter option; add to apkg export
* Adjust 'too new' message to also apply to .apkg import case
* Show a better message when attempting to import new apkg into old code
Previously the user could end seeing a message like:
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xb5 in position 1: invalid start byte
Unfortunately we can't retroactively fix this for older clients.
* Hide legacy support option in older exporting screen
* Reflect change from paths to fnames in type & name
* Make imported decks normal at once
Then skip special casing in update_deck(). Also skip updating
description if new one is empty.
Co-authored-by: Damien Elmes <gpg@ankiweb.net>
2022-05-02 13:12:46 +02:00
|
|
|
from aqt.exporting import ExportDialog as LegacyExportDialog
|
|
|
|
from aqt.import_export.exporting import ExportDialog
|
2021-04-03 08:26:10 +02:00
|
|
|
from aqt.operations.card import set_card_deck, set_card_flag
|
2021-05-19 07:18:39 +02:00
|
|
|
from aqt.operations.collection import redo, undo
|
2021-04-03 08:26:10 +02:00
|
|
|
from aqt.operations.note import remove_notes
|
|
|
|
from aqt.operations.scheduling import (
|
2023-01-30 10:21:06 +01:00
|
|
|
bury_cards,
|
undoable ops now return changes directly; add new *_ops.py files
- Introduced a new transact() method that wraps the return value
in a separate struct that describes the changes that were made.
- Changes are now gathered from the undo log, so we don't need to
guess at what was changed - eg if update_note() is called with identical
note contents, no changes are returned. Card changes will only be set
if cards were actually generated by the update_note() call, and tag
will only be set if a new tag was added.
- mw.perform_op() has been updated to expect the op to return the changes,
or a structure with the changes in it, and it will use them to fire the
change hook, instead of fetching the changes from undo_status(), so there
is no risk of race conditions.
- the various calls to mw.perform_op() have been split into separate
files like card_ops.py. Aside from making the code cleaner, this works
around a rather annoying issue with mypy. Because we run it with
no_strict_optional, mypy is happy to accept an operation that returns None,
despite the type signature saying it requires changes to be returned.
Turning no_strict_optional on for the whole codebase is not practical
at the moment, but we can enable it for individual files.
Still todo:
- The cursor keeps moving back to the start of a field when typing -
we need to ignore the refresh hook when we are the initiator.
- The busy cursor icon should probably be delayed a few hundreds ms.
- Still need to think about a nicer way of handling saveNow()
- op_made_changes(), op_affects_study_queue() might be better embedded
as properties in the object instead
2021-03-16 05:26:42 +01:00
|
|
|
forget_cards,
|
2021-03-18 02:46:11 +01:00
|
|
|
reposition_new_cards_dialog,
|
undoable ops now return changes directly; add new *_ops.py files
- Introduced a new transact() method that wraps the return value
in a separate struct that describes the changes that were made.
- Changes are now gathered from the undo log, so we don't need to
guess at what was changed - eg if update_note() is called with identical
note contents, no changes are returned. Card changes will only be set
if cards were actually generated by the update_note() call, and tag
will only be set if a new tag was added.
- mw.perform_op() has been updated to expect the op to return the changes,
or a structure with the changes in it, and it will use them to fire the
change hook, instead of fetching the changes from undo_status(), so there
is no risk of race conditions.
- the various calls to mw.perform_op() have been split into separate
files like card_ops.py. Aside from making the code cleaner, this works
around a rather annoying issue with mypy. Because we run it with
no_strict_optional, mypy is happy to accept an operation that returns None,
despite the type signature saying it requires changes to be returned.
Turning no_strict_optional on for the whole codebase is not practical
at the moment, but we can enable it for individual files.
Still todo:
- The cursor keeps moving back to the start of a field when typing -
we need to ignore the refresh hook when we are the initiator.
- The busy cursor icon should probably be delayed a few hundreds ms.
- Still need to think about a nicer way of handling saveNow()
- op_made_changes(), op_affects_study_queue() might be better embedded
as properties in the object instead
2021-03-16 05:26:42 +01:00
|
|
|
set_due_date_dialog,
|
|
|
|
suspend_cards,
|
2023-01-30 10:21:06 +01:00
|
|
|
unbury_cards,
|
undoable ops now return changes directly; add new *_ops.py files
- Introduced a new transact() method that wraps the return value
in a separate struct that describes the changes that were made.
- Changes are now gathered from the undo log, so we don't need to
guess at what was changed - eg if update_note() is called with identical
note contents, no changes are returned. Card changes will only be set
if cards were actually generated by the update_note() call, and tag
will only be set if a new tag was added.
- mw.perform_op() has been updated to expect the op to return the changes,
or a structure with the changes in it, and it will use them to fire the
change hook, instead of fetching the changes from undo_status(), so there
is no risk of race conditions.
- the various calls to mw.perform_op() have been split into separate
files like card_ops.py. Aside from making the code cleaner, this works
around a rather annoying issue with mypy. Because we run it with
no_strict_optional, mypy is happy to accept an operation that returns None,
despite the type signature saying it requires changes to be returned.
Turning no_strict_optional on for the whole codebase is not practical
at the moment, but we can enable it for individual files.
Still todo:
- The cursor keeps moving back to the start of a field when typing -
we need to ignore the refresh hook when we are the initiator.
- The busy cursor icon should probably be delayed a few hundreds ms.
- Still need to think about a nicer way of handling saveNow()
- op_made_changes(), op_affects_study_queue() might be better embedded
as properties in the object instead
2021-03-16 05:26:42 +01:00
|
|
|
unsuspend_cards,
|
|
|
|
)
|
2021-04-03 08:26:10 +02:00
|
|
|
from aqt.operations.tag import (
|
|
|
|
add_tags_to_notes,
|
|
|
|
clear_unused_tags,
|
|
|
|
remove_tags_from_notes,
|
|
|
|
)
|
|
|
|
from aqt.qt import *
|
2021-12-20 11:23:50 +01:00
|
|
|
from aqt.sound import av_player
|
2021-03-29 12:24:24 +02:00
|
|
|
from aqt.switch import Switch
|
2021-05-19 07:18:39 +02:00
|
|
|
from aqt.undo import UndoActionsInfo
|
2019-12-23 01:34:10 +01:00
|
|
|
from aqt.utils import (
|
2021-01-25 14:45:47 +01:00
|
|
|
HelpPage,
|
2021-03-17 05:51:59 +01:00
|
|
|
KeyboardModifiersPressed,
|
2021-12-07 23:40:48 +01:00
|
|
|
add_ellipsis_to_action_label,
|
2021-06-01 07:16:53 +02:00
|
|
|
current_window,
|
2021-03-16 13:40:37 +01:00
|
|
|
ensure_editor_saved,
|
2019-12-23 01:34:10 +01:00
|
|
|
getTag,
|
2021-04-26 08:46:08 +02:00
|
|
|
no_arg_trigger,
|
2019-12-23 01:34:10 +01:00
|
|
|
openHelp,
|
|
|
|
qtMenuShortcutWorkaround,
|
|
|
|
restoreGeom,
|
|
|
|
restoreSplitter,
|
|
|
|
restoreState,
|
|
|
|
saveGeom,
|
|
|
|
saveSplitter,
|
|
|
|
saveState,
|
rework filtered deck screen & search errors
- Filtered deck creation now happens as an atomic operation, and is
undoable.
- The logic for initial search text, normalizing searches and so on
has been pushed into the backend.
- Use protobuf to pass the filtered deck to the updated dialog, so
we don't need to deal with untyped JSON.
- Change the "revise your search?" prompt to be a simple info box -
user has access to cancel and build buttons, and doesn't need a separate
prompt. Tweak the wording so the 'show excluded' button should be more
obvious.
- Filtered decks have a time appended to them instead of a number,
primarily because it's easier to implement. No objections going back to
the old behaviour if someone wants to contribute a clean patch.
The standard de-duplication will happen if two decks are created in the
same minute with the same name.
- Tweak the default sort order, and start with two searches. The UI
will still hide the second search by default, but by starting with two,
the frontend doesn't need logic for creating the starting text.
- Search errors now have their own error type, instead of using
InvalidInput, as that was intended mainly for bad API calls. The markdown
conversion is done when the error is converted from the backend, allowing
errors to printed as a string without any special handling by the calling
code.
TODO: when building a new filtered deck, update_active() is clobbering
the undo log when the overview is refreshed
2021-03-24 12:52:48 +01:00
|
|
|
showWarning,
|
2021-04-25 08:47:15 +02:00
|
|
|
skip_if_selection_is_empty,
|
2022-09-20 04:56:59 +02:00
|
|
|
tooltip,
|
2020-02-17 05:41:01 +01:00
|
|
|
tr,
|
2019-12-23 01:34:10 +01:00
|
|
|
)
|
2021-05-08 07:08:45 +02:00
|
|
|
|
2021-06-10 13:30:39 +02:00
|
|
|
from ..changenotetype import change_notetype_dialog
|
2021-10-16 23:38:11 +02:00
|
|
|
from .card_info import BrowserCardInfo
|
2021-05-08 07:08:45 +02:00
|
|
|
from .find_and_replace import FindAndReplaceDialog
|
2022-11-03 03:13:48 +01:00
|
|
|
from .layout import BrowserLayout, QSplitterHandleEventFilter
|
2021-05-08 07:08:45 +02:00
|
|
|
from .previewer import BrowserPreviewer as PreviewDialog
|
|
|
|
from .previewer import Previewer
|
|
|
|
from .sidebar import SidebarTreeView
|
|
|
|
from .table import Table
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-02-08 23:59:29 +01:00
|
|
|
|
2021-07-12 12:36:26 +02:00
|
|
|
class MockModel:
|
|
|
|
"""This class only exists to support some legacy aliases."""
|
|
|
|
|
|
|
|
def __init__(self, browser: aqt.browser.Browser) -> None:
|
|
|
|
self.browser = browser
|
|
|
|
|
|
|
|
@deprecated(replaced_by=aqt.operations.CollectionOp)
|
|
|
|
def beginReset(self) -> None:
|
|
|
|
self.browser.begin_reset()
|
|
|
|
|
|
|
|
@deprecated(replaced_by=aqt.operations.CollectionOp)
|
|
|
|
def endReset(self) -> None:
|
|
|
|
self.browser.end_reset()
|
|
|
|
|
|
|
|
@deprecated(replaced_by=aqt.operations.CollectionOp)
|
|
|
|
def reset(self) -> None:
|
|
|
|
self.browser.begin_reset()
|
|
|
|
self.browser.end_reset()
|
|
|
|
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
class Browser(QMainWindow):
|
2019-12-20 08:55:19 +01:00
|
|
|
mw: AnkiQt
|
2020-05-20 09:56:52 +02:00
|
|
|
col: Collection
|
2021-10-03 10:59:42 +02:00
|
|
|
editor: Editor | None
|
2021-03-29 08:12:26 +02:00
|
|
|
table: Table
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 11:54:28 +01:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
mw: AnkiQt,
|
2021-10-03 10:59:42 +02:00
|
|
|
card: Card | None = None,
|
|
|
|
search: tuple[str | SearchNode] | None = None,
|
2021-02-01 11:54:28 +01:00
|
|
|
) -> None:
|
2021-02-02 09:48:55 +01:00
|
|
|
"""
|
2021-06-07 16:42:17 +02:00
|
|
|
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
|
2021-02-02 09:48:55 +01:00
|
|
|
"""
|
|
|
|
|
2021-10-05 05:53:01 +02:00
|
|
|
QMainWindow.__init__(self, None, Qt.WindowType.Window)
|
2012-12-21 08:51:59 +01:00
|
|
|
self.mw = mw
|
|
|
|
self.col = self.mw.col
|
|
|
|
self.lastFilter = ""
|
2021-10-03 10:59:42 +02:00
|
|
|
self.focusTo: int | None = None
|
|
|
|
self._previewer: Previewer | None = None
|
2021-10-16 23:38:11 +02:00
|
|
|
self._card_info = BrowserCardInfo(self.mw)
|
2016-07-14 12:23:44 +02:00
|
|
|
self._closeEventHasCleanedUp = False
|
2012-12-21 08:51:59 +01:00
|
|
|
self.form = aqt.forms.browser.Ui_Dialog()
|
|
|
|
self.form.setupUi(self)
|
|
|
|
self.form.splitter.setChildrenCollapsible(False)
|
2022-11-03 03:13:48 +01:00
|
|
|
splitter_handle_event_filter = QSplitterHandleEventFilter(self.form.splitter)
|
|
|
|
self.form.splitter.handle(1).installEventFilter(splitter_handle_event_filter)
|
2021-10-01 16:40:25 +02:00
|
|
|
# set if exactly 1 row is selected; used by the previewer
|
2021-10-03 10:59:42 +02:00
|
|
|
self.card: Card | None = None
|
|
|
|
self.current_card: Card | None = None
|
2021-10-25 05:27:19 +02:00
|
|
|
self.setupSidebar()
|
2021-03-29 08:12:26 +02:00
|
|
|
self.setup_table()
|
2012-12-21 08:51:59 +01:00
|
|
|
self.setupMenus()
|
|
|
|
self.setupHooks()
|
|
|
|
self.setupEditor()
|
2022-11-21 01:18:59 +01:00
|
|
|
gui_hooks.browser_will_show(self)
|
2022-10-14 02:19:20 +02:00
|
|
|
|
|
|
|
# restoreXXX() should be called after all child widgets have been created
|
|
|
|
# and attached to QMainWindow
|
2022-11-03 03:14:52 +01:00
|
|
|
self._editor_state_key = (
|
|
|
|
"editorRTL"
|
|
|
|
if self.layoutDirection() == Qt.LayoutDirection.RightToLeft
|
|
|
|
else "editor"
|
|
|
|
)
|
2023-05-18 00:53:42 +02:00
|
|
|
restoreGeom(self, self._editor_state_key)
|
2022-10-14 02:19:20 +02:00
|
|
|
restoreSplitter(self.form.splitter, "editor3")
|
2022-11-03 03:14:52 +01:00
|
|
|
restoreState(self, self._editor_state_key)
|
2022-10-14 02:19:20 +02:00
|
|
|
|
2022-09-20 04:56:59 +02:00
|
|
|
# responsive layout
|
2023-03-16 07:02:16 +01:00
|
|
|
self.aspect_ratio = self.width() / self.height() if self.height() != 0 else 0
|
2022-09-20 04:56:59 +02:00
|
|
|
self.set_layout(self.mw.pm.browser_layout(), True)
|
2021-05-19 07:18:39 +02:00
|
|
|
# disable undo/redo
|
|
|
|
self.on_undo_state_change(mw.undo_actions_info())
|
2021-07-12 12:36:26 +02:00
|
|
|
# legacy alias
|
|
|
|
self.model = MockModel(self)
|
2022-04-02 13:42:49 +02:00
|
|
|
self.setupSearch(card, search)
|
2012-12-21 08:51:59 +01:00
|
|
|
self.show()
|
|
|
|
|
2021-04-06 02:14:11 +02:00
|
|
|
def on_operation_did_execute(
|
2021-10-03 10:59:42 +02:00
|
|
|
self, changes: OpChanges, handler: object | None
|
2021-04-06 02:14:11 +02:00
|
|
|
) -> None:
|
2021-06-01 07:16:53 +02:00
|
|
|
focused = current_window() == self
|
2021-04-06 02:14:11 +02:00
|
|
|
self.table.op_executed(changes, handler, focused)
|
|
|
|
self.sidebar.op_executed(changes, handler, focused)
|
2021-06-08 04:09:35 +02:00
|
|
|
if changes.note_text:
|
2021-04-06 02:14:11 +02:00
|
|
|
if handler is not self.editor:
|
2021-03-18 15:06:54 +01:00
|
|
|
# fixme: this will leave the splitter shown, but with no current
|
|
|
|
# note being edited
|
2021-03-16 09:30:54 +01:00
|
|
|
note = self.editor.note
|
|
|
|
if note:
|
2021-03-18 15:06:54 +01:00
|
|
|
try:
|
|
|
|
note.load()
|
|
|
|
except NotFoundError:
|
|
|
|
self.editor.set_note(None)
|
|
|
|
return
|
2021-03-16 09:30:54 +01:00
|
|
|
self.editor.set_note(note)
|
|
|
|
|
2021-05-21 09:50:41 +02:00
|
|
|
if changes.browser_table and changes.card:
|
2021-10-01 16:40:25 +02:00
|
|
|
self.card = self.table.get_single_selected_card()
|
|
|
|
self.current_card = self.table.get_current_card()
|
2021-10-16 23:38:11 +02:00
|
|
|
self._update_card_info()
|
2021-10-01 16:40:25 +02:00
|
|
|
self._update_current_actions()
|
2021-03-14 13:08:37 +01:00
|
|
|
|
2021-07-26 07:28:38 +02:00
|
|
|
# changes.card is required for updating flag icon
|
|
|
|
if changes.note_text or changes.card:
|
|
|
|
self._renderPreview()
|
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
def on_focus_change(self, new: QWidget | None, old: QWidget | None) -> None:
|
2021-06-01 07:16:53 +02:00
|
|
|
if current_window() == self:
|
2021-03-16 09:30:54 +01:00
|
|
|
self.setUpdatesEnabled(True)
|
2021-03-29 08:12:26 +02:00
|
|
|
self.table.redraw_cells()
|
2021-03-16 10:21:18 +01:00
|
|
|
self.sidebar.refresh_if_needed()
|
2021-03-12 08:53:14 +01:00
|
|
|
|
2022-09-20 04:56:59 +02:00
|
|
|
def set_layout(self, mode: BrowserLayout, init: bool = False) -> None:
|
|
|
|
self.mw.pm.set_browser_layout(mode)
|
|
|
|
|
|
|
|
if mode == BrowserLayout.AUTO:
|
|
|
|
self.auto_layout = True
|
|
|
|
self.maybe_update_layout(self.aspect_ratio, True)
|
|
|
|
self.form.actionLayoutAuto.setChecked(True)
|
|
|
|
self.form.actionLayoutVertical.setChecked(False)
|
|
|
|
self.form.actionLayoutHorizontal.setChecked(False)
|
|
|
|
if not init:
|
|
|
|
tooltip(tr.qt_misc_layout_auto_enabled())
|
|
|
|
else:
|
|
|
|
self.auto_layout = False
|
|
|
|
self.form.actionLayoutAuto.setChecked(False)
|
|
|
|
|
|
|
|
if mode == BrowserLayout.VERTICAL:
|
|
|
|
self.form.splitter.setOrientation(Qt.Orientation.Vertical)
|
|
|
|
self.form.actionLayoutVertical.setChecked(True)
|
|
|
|
self.form.actionLayoutHorizontal.setChecked(False)
|
|
|
|
if not init:
|
|
|
|
tooltip(tr.qt_misc_layout_vertical_enabled())
|
|
|
|
|
|
|
|
elif mode == BrowserLayout.HORIZONTAL:
|
|
|
|
self.form.splitter.setOrientation(Qt.Orientation.Horizontal)
|
|
|
|
self.form.actionLayoutHorizontal.setChecked(True)
|
|
|
|
self.form.actionLayoutVertical.setChecked(False)
|
|
|
|
if not init:
|
|
|
|
tooltip(tr.qt_misc_layout_horizontal_enabled())
|
|
|
|
|
|
|
|
def maybe_update_layout(self, aspect_ratio: float, force: bool = False) -> None:
|
|
|
|
if force or math.floor(aspect_ratio) != math.floor(self.aspect_ratio):
|
|
|
|
if aspect_ratio < 1:
|
|
|
|
self.form.splitter.setOrientation(Qt.Orientation.Vertical)
|
|
|
|
else:
|
|
|
|
self.form.splitter.setOrientation(Qt.Orientation.Horizontal)
|
|
|
|
|
|
|
|
def resizeEvent(self, event: QResizeEvent) -> None:
|
2023-03-16 07:02:16 +01:00
|
|
|
if self.height() != 0:
|
|
|
|
aspect_ratio = self.width() / self.height()
|
2022-09-20 04:56:59 +02:00
|
|
|
|
2023-03-16 07:02:16 +01:00
|
|
|
if self.auto_layout:
|
|
|
|
self.maybe_update_layout(aspect_ratio)
|
2022-09-20 04:56:59 +02:00
|
|
|
|
2023-03-16 07:02:16 +01:00
|
|
|
self.aspect_ratio = aspect_ratio
|
2022-09-20 04:56:59 +02:00
|
|
|
|
|
|
|
QMainWindow.resizeEvent(self, event)
|
|
|
|
|
2020-01-15 22:41:23 +01:00
|
|
|
def setupMenus(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
# actions
|
2016-05-31 10:51:40 +02:00
|
|
|
f = self.form
|
2022-02-17 07:31:46 +01:00
|
|
|
|
2017-08-11 06:40:51 +02:00
|
|
|
# edit
|
2021-03-12 08:56:13 +01:00
|
|
|
qconnect(f.actionUndo.triggered, self.undo)
|
2021-05-19 07:18:39 +02:00
|
|
|
qconnect(f.actionRedo.triggered, self.redo)
|
2021-03-29 08:12:26 +02:00
|
|
|
qconnect(f.actionInvertSelection.triggered, self.table.invert_selection)
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(f.actionSelectNotes.triggered, self.selectNotes)
|
2021-11-25 00:06:16 +01:00
|
|
|
if not is_mac:
|
2017-08-11 06:40:51 +02:00
|
|
|
f.actionClose.setVisible(False)
|
2021-02-01 12:09:37 +01:00
|
|
|
qconnect(f.actionCreateFilteredDeck.triggered, self.createFilteredDeck)
|
2021-02-02 09:29:09 +01:00
|
|
|
f.actionCreateFilteredDeck.setShortcuts(["Ctrl+G", "Ctrl+Alt+G"])
|
2022-02-17 07:31:46 +01:00
|
|
|
|
|
|
|
# view
|
|
|
|
qconnect(f.actionFullScreen.triggered, self.mw.on_toggle_full_screen)
|
|
|
|
qconnect(
|
|
|
|
f.actionZoomIn.triggered,
|
|
|
|
lambda: self.editor.web.setZoomFactor(self.editor.web.zoomFactor() + 0.1),
|
|
|
|
)
|
|
|
|
qconnect(
|
|
|
|
f.actionZoomOut.triggered,
|
|
|
|
lambda: self.editor.web.setZoomFactor(self.editor.web.zoomFactor() - 0.1),
|
|
|
|
)
|
|
|
|
qconnect(
|
|
|
|
f.actionResetZoom.triggered,
|
|
|
|
lambda: self.editor.web.setZoomFactor(1),
|
|
|
|
)
|
2022-09-20 04:56:59 +02:00
|
|
|
qconnect(
|
|
|
|
self.form.actionLayoutAuto.triggered,
|
|
|
|
lambda: self.set_layout(BrowserLayout.AUTO),
|
|
|
|
)
|
|
|
|
qconnect(
|
|
|
|
self.form.actionLayoutVertical.triggered,
|
|
|
|
lambda: self.set_layout(BrowserLayout.VERTICAL),
|
|
|
|
)
|
|
|
|
qconnect(
|
|
|
|
self.form.actionLayoutHorizontal.triggered,
|
|
|
|
lambda: self.set_layout(BrowserLayout.HORIZONTAL),
|
|
|
|
)
|
2022-02-17 07:31:46 +01:00
|
|
|
|
2017-08-11 06:40:51 +02:00
|
|
|
# notes
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(f.actionAdd.triggered, self.mw.onAddCard)
|
2021-12-07 23:40:48 +01:00
|
|
|
qconnect(f.actionCopy.triggered, self.on_create_copy)
|
2021-04-25 19:51:57 +02:00
|
|
|
qconnect(f.actionAdd_Tags.triggered, self.add_tags_to_selected_notes)
|
|
|
|
qconnect(f.actionRemove_Tags.triggered, self.remove_tags_from_selected_notes)
|
clear_unused_tags and browser redraw improvements
- clear_unused_tags() is now undoable, and returns the number of removed
notes
- add a new mw.query_op() helper for immutable queries
- decouple "freeze/unfreeze ui state" hooks from the "interface update
required" hook, so that the former is fired even on error, and can be
made re-entrant
- use a 'block_updates' flag in Python, instead of setUpdatesEnabled(),
as the latter has the side-effect of preventing child windows like
tooltips from appearing, and forces a full redrawn when updates are
enabled again. The new behaviour leads to the card list blanking out
when a long-running op is running, but in the future if we cache the
cell values we can just display them from the cache instead.
- we were indiscriminately saving the note with saveNow(), due to the
call to saveTags(). Changed so that it only saves when the tags field
is focused.
- drain the "on_done" queue on main before launching a new background
task, to lower the chances of something in on_done making a small query
to the DB and hanging until a long op finishes
- the duplicate check in the editor was executed after the webview loads,
leading to it hanging until the sidebar finishes loading. Run it at
set_note() time instead, so that the editor loads first.
- don't throw an error when a long-running op started with with_progress()
finishes after the window it was launched from has closed
- don't throw an error when the browser is closed before the sidebar
has finished loading
2021-03-17 12:27:42 +01:00
|
|
|
qconnect(f.actionClear_Unused_Tags.triggered, self.clear_unused_tags)
|
2021-03-31 10:05:44 +02:00
|
|
|
qconnect(f.actionToggle_Mark.triggered, self.toggle_mark_of_selected_notes)
|
2020-05-04 05:23:08 +02:00
|
|
|
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)
|
2021-03-12 08:53:14 +01:00
|
|
|
qconnect(f.actionDelete.triggered, self.delete_selected_notes)
|
2022-02-17 07:31:46 +01:00
|
|
|
|
2017-08-11 06:40:51 +02:00
|
|
|
# cards
|
2021-03-12 08:53:14 +01:00
|
|
|
qconnect(f.actionChange_Deck.triggered, self.set_deck_of_selected_cards)
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(f.action_Info.triggered, self.showCardInfo)
|
|
|
|
qconnect(f.actionReposition.triggered, self.reposition)
|
Rework reschedule tool
The old rescheduling dialog's two options have been split into two
separate menu items, "Forget", and "Set Due Date"
For cards that are not review cards, "Set Due Date" behaves like the
old reschedule option, changing the cards into a review card, and
and setting both the interval and due date to the provided number of
days.
When "Set Due Date" is applied to a review card, it no longer resets
the card's interval. Instead, it looks at how much the provided number
of days will change the original interval, and adjusts the interval by
that amount, so that cards that are answered earlier receive a smaller
next interval, and cards that are answered after a longer delay receive
a bonus.
For example, imagine a card was answered on day 5, and given an interval
of 10 days, so it has a due date of day 15.
- if on day 10 the due date is changed to day 12 (today+2), the card
is being scheduled 3 days earlier than it was supposed to be, so the
interval will be adjusted to 7 days.
- and if on day 10 the due date is changed to day 20, the interval will
be changed from 10 days to 15 days.
There is no separate option to reset the interval of a review card, but
it can be accomplished by forgetting the card(s), and then setting the
desired due date.
Other notes:
- Added the action to the review screen as well.
- Set the shortcut to Ctrl+Shift+D, and changed the existing Delete
Tags shortcut to Ctrl+Alt+Shift+A.
2021-02-07 11:58:16 +01:00
|
|
|
qconnect(f.action_set_due_date.triggered, self.set_due_date)
|
|
|
|
qconnect(f.action_forget.triggered, self.forget_cards)
|
2021-03-04 13:14:35 +01:00
|
|
|
qconnect(f.actionToggle_Suspend.triggered, self.suspend_selected_cards)
|
2023-01-30 10:21:06 +01:00
|
|
|
qconnect(f.action_toggle_bury.triggered, self.bury_selected_cards)
|
2021-05-19 19:17:43 +02:00
|
|
|
|
|
|
|
def set_flag_func(desired_flag: int) -> Callable:
|
|
|
|
return lambda: self.set_flag_of_selected_cards(desired_flag)
|
|
|
|
|
2021-07-02 11:16:10 +02:00
|
|
|
for flag in self.mw.flags.all():
|
2021-05-19 19:17:43 +02:00
|
|
|
qconnect(
|
|
|
|
getattr(self.form, flag.action).triggered, set_flag_func(flag.index)
|
|
|
|
)
|
2021-05-19 10:34:36 +02:00
|
|
|
self._update_flag_labels()
|
2021-04-25 19:51:57 +02:00
|
|
|
qconnect(f.actionExport.triggered, self._on_export_notes)
|
2022-02-17 07:31:46 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# jumps
|
2020-05-04 05:23:08 +02:00
|
|
|
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)
|
2022-02-17 07:31:46 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# help
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(f.actionGuide.triggered, self.onHelp)
|
2022-02-17 07:31:46 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# keyboard shortcut for shift+home/end
|
|
|
|
self.pgUpCut = QShortcut(QKeySequence("Shift+Home"), self)
|
2020-01-15 22:41:23 +01:00
|
|
|
qconnect(self.pgUpCut.activated, self.onFirstCard)
|
2012-12-21 08:51:59 +01:00
|
|
|
self.pgDownCut = QShortcut(QKeySequence("Shift+End"), self)
|
2020-01-15 22:41:23 +01:00
|
|
|
qconnect(self.pgDownCut.activated, self.onLastCard)
|
2022-02-17 07:31:46 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# add-on hook
|
2020-01-15 08:18:11 +01:00
|
|
|
gui_hooks.browser_menus_did_init(self)
|
2012-12-21 08:51:59 +01:00
|
|
|
self.mw.maybeHideAccelerators(self)
|
|
|
|
|
2021-12-07 23:40:48 +01:00
|
|
|
add_ellipsis_to_action_label(f.actionCopy)
|
2022-06-20 02:25:50 +02:00
|
|
|
add_ellipsis_to_action_label(f.action_forget)
|
2021-12-07 23:40:48 +01:00
|
|
|
|
2021-02-01 00:39:55 +01:00
|
|
|
def closeEvent(self, evt: QCloseEvent) -> None:
|
2017-08-16 04:45:33 +02:00
|
|
|
if self._closeEventHasCleanedUp:
|
2016-07-14 12:23:44 +02:00
|
|
|
evt.accept()
|
2017-08-16 04:45:33 +02:00
|
|
|
return
|
2021-03-16 13:40:37 +01:00
|
|
|
self.editor.call_after_note_saved(self._closeWindow)
|
2017-08-16 04:45:33 +02:00
|
|
|
evt.ignore()
|
2016-07-14 12:23:44 +02:00
|
|
|
|
2021-02-01 00:39:55 +01:00
|
|
|
def _closeWindow(self) -> None:
|
2020-04-10 10:42:28 +02:00
|
|
|
self._cleanup_preview()
|
2021-10-16 23:38:11 +02:00
|
|
|
self._card_info.close()
|
2017-08-16 04:45:33 +02:00
|
|
|
self.editor.cleanup()
|
2021-04-11 12:28:11 +02:00
|
|
|
self.table.cleanup()
|
2021-07-02 11:16:10 +02:00
|
|
|
self.sidebar.cleanup()
|
2012-12-21 08:51:59 +01:00
|
|
|
saveSplitter(self.form.splitter, "editor3")
|
2022-11-03 03:14:52 +01:00
|
|
|
saveGeom(self, self._editor_state_key)
|
|
|
|
saveState(self, self._editor_state_key)
|
2012-12-21 08:51:59 +01:00
|
|
|
self.teardownHooks()
|
|
|
|
self.mw.maybeReset()
|
2017-08-16 04:45:33 +02:00
|
|
|
aqt.dialogs.markClosed("Browser")
|
2016-07-14 12:23:44 +02:00
|
|
|
self._closeEventHasCleanedUp = True
|
2021-03-05 04:07:52 +01:00
|
|
|
self.mw.deferred_delete_and_garbage_collect(self)
|
2017-08-16 04:45:33 +02:00
|
|
|
self.close()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-03-16 13:40:37 +01:00
|
|
|
@ensure_editor_saved
|
2021-02-01 00:39:55 +01:00
|
|
|
def closeWithCallback(self, onsuccess: Callable) -> None:
|
2021-03-16 13:40:37 +01:00
|
|
|
self._closeWindow()
|
|
|
|
onsuccess()
|
2013-04-11 12:23:32 +02:00
|
|
|
|
2021-02-01 00:39:55 +01:00
|
|
|
def keyPressEvent(self, evt: QKeyEvent) -> None:
|
2021-10-05 05:53:01 +02:00
|
|
|
if evt.key() == Qt.Key.Key_Escape:
|
2012-12-21 08:51:59 +01:00
|
|
|
self.close()
|
2017-08-13 11:11:40 +02:00
|
|
|
else:
|
|
|
|
super().keyPressEvent(evt)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 11:54:28 +01:00
|
|
|
def reopen(
|
|
|
|
self,
|
|
|
|
_mw: AnkiQt,
|
2021-10-03 10:59:42 +02:00
|
|
|
card: Card | None = None,
|
|
|
|
search: tuple[str | SearchNode] | None = None,
|
2021-02-01 11:54:28 +01:00
|
|
|
) -> None:
|
|
|
|
if search is not None:
|
|
|
|
self.search_for_terms(*search)
|
|
|
|
self.form.searchEdit.setFocus()
|
2021-06-07 16:42:17 +02:00
|
|
|
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)
|
2021-02-01 11:54:28 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# Searching
|
|
|
|
######################################################################
|
|
|
|
|
2021-02-01 11:54:28 +01:00
|
|
|
def setupSearch(
|
|
|
|
self,
|
2021-10-03 10:59:42 +02:00
|
|
|
card: Card | None = None,
|
|
|
|
search: tuple[str | SearchNode] | None = None,
|
2021-02-01 23:33:41 +01:00
|
|
|
) -> None:
|
2020-05-04 05:23:08 +02:00
|
|
|
qconnect(self.form.searchEdit.lineEdit().returnPressed, self.onSearchActivated)
|
2012-12-21 08:51:59 +01:00
|
|
|
self.form.searchEdit.setCompleter(None)
|
2021-01-29 21:07:42 +01:00
|
|
|
self.form.searchEdit.lineEdit().setPlaceholderText(
|
2021-03-26 04:48:26 +01:00
|
|
|
tr.browsing_search_bar_hint()
|
2019-12-23 01:34:10 +01:00
|
|
|
)
|
2023-04-26 11:11:48 +02:00
|
|
|
self.form.searchEdit.addItems(
|
|
|
|
[""] + self.mw.pm.profile.get("searchHistory", [])
|
|
|
|
)
|
2021-02-01 11:54:28 +01:00
|
|
|
if search is not None:
|
|
|
|
self.search_for_terms(*search)
|
|
|
|
else:
|
2021-06-07 16:42:17 +02:00
|
|
|
self._default_search(card)
|
2016-07-14 12:23:44 +02:00
|
|
|
self.form.searchEdit.setFocus()
|
2021-06-07 16:42:17 +02:00
|
|
|
if card:
|
|
|
|
self.table.select_single_card(card.id)
|
2016-07-14 12:23:44 +02:00
|
|
|
|
|
|
|
# search triggered by user
|
2021-03-16 13:40:37 +01:00
|
|
|
@ensure_editor_saved
|
2021-02-01 00:39:55 +01:00
|
|
|
def onSearchActivated(self) -> None:
|
2021-04-14 10:42:26 +02:00
|
|
|
text = self.current_search()
|
2021-01-30 10:51:31 +01:00
|
|
|
try:
|
|
|
|
normed = self.col.build_search_string(text)
|
rework filtered deck screen & search errors
- Filtered deck creation now happens as an atomic operation, and is
undoable.
- The logic for initial search text, normalizing searches and so on
has been pushed into the backend.
- Use protobuf to pass the filtered deck to the updated dialog, so
we don't need to deal with untyped JSON.
- Change the "revise your search?" prompt to be a simple info box -
user has access to cancel and build buttons, and doesn't need a separate
prompt. Tweak the wording so the 'show excluded' button should be more
obvious.
- Filtered decks have a time appended to them instead of a number,
primarily because it's easier to implement. No objections going back to
the old behaviour if someone wants to contribute a clean patch.
The standard de-duplication will happen if two decks are created in the
same minute with the same name.
- Tweak the default sort order, and start with two searches. The UI
will still hide the second search by default, but by starting with two,
the frontend doesn't need logic for creating the starting text.
- Search errors now have their own error type, instead of using
InvalidInput, as that was intended mainly for bad API calls. The markdown
conversion is done when the error is converted from the backend, allowing
errors to printed as a string without any special handling by the calling
code.
TODO: when building a new filtered deck, update_active() is clobbering
the undo log when the overview is refreshed
2021-03-24 12:52:48 +01:00
|
|
|
except Exception as err:
|
|
|
|
showWarning(str(err))
|
2021-01-30 10:51:31 +01:00
|
|
|
else:
|
|
|
|
self.search_for(normed)
|
2021-01-29 23:05:51 +01:00
|
|
|
self.update_history()
|
2018-11-12 03:43:54 +01:00
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
def search_for(self, search: str, prompt: str | None = None) -> None:
|
2021-01-30 11:05:48 +01:00
|
|
|
"""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.
|
|
|
|
"""
|
2016-07-14 12:23:44 +02:00
|
|
|
|
2020-10-19 20:37:17 +02:00
|
|
|
self._lastSearchTxt = search
|
Move away from Bazel (#2202)
(for upgrading users, please see the notes at the bottom)
Bazel brought a lot of nice things to the table, such as rebuilds based on
content changes instead of modification times, caching of build products,
detection of incorrect build rules via a sandbox, and so on. Rewriting the build
in Bazel was also an opportunity to improve on the Makefile-based build we had
prior, which was pretty poor: most dependencies were external or not pinned, and
the build graph was poorly defined and mostly serialized. It was not uncommon
for fresh checkouts to fail due to floating dependencies, or for things to break
when trying to switch to an older commit.
For day-to-day development, I think Bazel served us reasonably well - we could
generally switch between branches while being confident that builds would be
correct and reasonably fast, and not require full rebuilds (except on Windows,
where the lack of a sandbox and the TS rules would cause build breakages when TS
files were renamed/removed).
Bazel achieves that reliability by defining rules for each programming language
that define how source files should be turned into outputs. For the rules to
work with Bazel's sandboxing approach, they often have to reimplement or
partially bypass the standard tools that each programming language provides. The
Rust rules call Rust's compiler directly for example, instead of using Cargo,
and the Python rules extract each PyPi package into a separate folder that gets
added to sys.path.
These separate language rules allow proper declaration of inputs and outputs,
and offer some advantages such as caching of build products and fine-grained
dependency installation. But they also bring some downsides:
- The rules don't always support use-cases/platforms that the standard language
tools do, meaning they need to be patched to be used. I've had to contribute a
number of patches to the Rust, Python and JS rules to unblock various issues.
- The dependencies we use with each language sometimes make assumptions that do
not hold in Bazel, meaning they either need to be pinned or patched, or the
language rules need to be adjusted to accommodate them.
I was hopeful that after the initial setup work, things would be relatively
smooth-sailing. Unfortunately, that has not proved to be the case. Things
frequently broke when dependencies or the language rules were updated, and I
began to get frustrated at the amount of Anki development time I was instead
spending on build system upkeep. It's now about 2 years since switching to
Bazel, and I think it's time to cut losses, and switch to something else that's
a better fit.
The new build system is based on a small build tool called Ninja, and some
custom Rust code in build/. This means that to build Anki, Bazel is no longer
required, but Ninja and Rust need to be installed on your system. Python and
Node toolchains are automatically downloaded like in Bazel.
This new build system should result in faster builds in some cases:
- Because we're using cargo to build now, Rust builds are able to take advantage
of pipelining and incremental debug builds, which we didn't have with Bazel.
It's also easier to override the default linker on Linux/macOS, which can
further improve speeds.
- External Rust crates are now built with opt=1, which improves performance
of debug builds.
- Esbuild is now used to transpile TypeScript, instead of invoking the TypeScript
compiler. This results in faster builds, by deferring typechecking to test/check
time, and by allowing more work to happen in parallel.
As an example of the differences, when testing with the mold linker on Linux,
adding a new message to tags.proto (which triggers a recompile of the bulk of
the Rust and TypeScript code) results in a compile that goes from about 22s on
Bazel to about 7s in the new system. With the standard linker, it's about 9s.
Some other changes of note:
- Our Rust workspace now uses cargo-hakari to ensure all packages agree on
available features, preventing unnecessary rebuilds.
- pylib/anki is now a PEP420 implicit namespace, avoiding the need to merge
source files and generated files into a single folder for running. By telling
VSCode about the extra search path, code completion now works with generated
files without needing to symlink them into the source folder.
- qt/aqt can't use PEP420 as it's difficult to get rid of aqt/__init__.py.
Instead, the generated files are now placed in a separate _aqt package that's
added to the path.
- ts/lib is now exposed as @tslib, so the source code and generated code can be
provided under the same namespace without a merging step.
- MyPy and PyLint are now invoked once for the entire codebase.
- dprint will be used to format TypeScript/json files in the future instead of
the slower prettier (currently turned off to avoid causing conflicts). It can
automatically defer to prettier when formatting Svelte files.
- svelte-check is now used for typechecking our Svelte code, which revealed a
few typing issues that went undetected with the old system.
- The Jest unit tests now work on Windows as well.
If you're upgrading from Bazel, updated usage instructions are in docs/development.md and docs/build.md. A summary of the changes:
- please remove node_modules and .bazel
- install rustup (https://rustup.rs/)
- install rsync if not already installed (on windows, use pacman - see docs/windows.md)
- install Ninja (unzip from https://github.com/ninja-build/ninja/releases/tag/v1.11.1 and
place on your path, or from your distro/homebrew if it's 1.10+)
- update .vscode/settings.json from .vscode.dist
2022-11-27 06:24:20 +01:00
|
|
|
prompt = search if prompt is None else prompt
|
2022-04-14 10:46:40 +02:00
|
|
|
self.form.searchEdit.setCurrentIndex(-1)
|
2021-01-30 10:51:31 +01:00
|
|
|
self.form.searchEdit.lineEdit().setText(prompt)
|
2016-07-14 12:23:44 +02:00
|
|
|
self.search()
|
|
|
|
|
2021-02-09 01:50:39 +01:00
|
|
|
def current_search(self) -> str:
|
|
|
|
return self.form.searchEdit.lineEdit().text()
|
|
|
|
|
2021-02-01 00:39:55 +01:00
|
|
|
def search(self) -> None:
|
2021-01-30 10:51:31 +01:00
|
|
|
"""Search triggered programmatically. Caller must have saved note first."""
|
2016-07-14 12:23:44 +02:00
|
|
|
|
2021-01-29 23:05:51 +01:00
|
|
|
try:
|
2021-03-29 08:12:26 +02:00
|
|
|
self.table.search(self._lastSearchTxt)
|
2021-01-30 10:51:31 +01:00
|
|
|
except Exception as err:
|
rework filtered deck screen & search errors
- Filtered deck creation now happens as an atomic operation, and is
undoable.
- The logic for initial search text, normalizing searches and so on
has been pushed into the backend.
- Use protobuf to pass the filtered deck to the updated dialog, so
we don't need to deal with untyped JSON.
- Change the "revise your search?" prompt to be a simple info box -
user has access to cancel and build buttons, and doesn't need a separate
prompt. Tweak the wording so the 'show excluded' button should be more
obvious.
- Filtered decks have a time appended to them instead of a number,
primarily because it's easier to implement. No objections going back to
the old behaviour if someone wants to contribute a clean patch.
The standard de-duplication will happen if two decks are created in the
same minute with the same name.
- Tweak the default sort order, and start with two searches. The UI
will still hide the second search by default, but by starting with two,
the frontend doesn't need logic for creating the starting text.
- Search errors now have their own error type, instead of using
InvalidInput, as that was intended mainly for bad API calls. The markdown
conversion is done when the error is converted from the backend, allowing
errors to printed as a string without any special handling by the calling
code.
TODO: when building a new filtered deck, update_active() is clobbering
the undo log when the overview is refreshed
2021-03-24 12:52:48 +01:00
|
|
|
showWarning(str(err))
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 00:39:55 +01:00
|
|
|
def update_history(self) -> None:
|
2023-04-26 11:11:48 +02:00
|
|
|
sh = self.mw.pm.profile.get("searchHistory", [])
|
2021-01-29 23:05:51 +01:00
|
|
|
if self._lastSearchTxt in sh:
|
|
|
|
sh.remove(self._lastSearchTxt)
|
|
|
|
sh.insert(0, self._lastSearchTxt)
|
2012-12-21 08:51:59 +01:00
|
|
|
sh = sh[:30]
|
|
|
|
self.form.searchEdit.clear()
|
|
|
|
self.form.searchEdit.addItems(sh)
|
2019-12-23 01:34:10 +01:00
|
|
|
self.mw.pm.profile["searchHistory"] = sh
|
2020-12-22 11:08:47 +01:00
|
|
|
|
2021-03-29 08:12:26 +02:00
|
|
|
def updateTitle(self) -> None:
|
|
|
|
selected = self.table.len_selection()
|
|
|
|
cur = self.table.len()
|
|
|
|
tr_title = (
|
2021-03-29 15:51:34 +02:00
|
|
|
tr.browsing_window_title_notes
|
|
|
|
if self.table.is_notes_mode()
|
|
|
|
else tr.browsing_window_title
|
2021-03-29 08:12:26 +02:00
|
|
|
)
|
2019-12-23 01:34:10 +01:00
|
|
|
self.setWindowTitle(
|
2021-03-29 08:12:26 +02:00
|
|
|
without_unicode_isolation(tr_title(total=cur, selected=selected))
|
2019-12-23 01:34:10 +01:00
|
|
|
)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
def search_for_terms(self, *search_terms: str | SearchNode) -> None:
|
2021-02-01 11:54:28 +01:00
|
|
|
search = self.col.build_search_string(*search_terms)
|
|
|
|
self.form.searchEdit.setEditText(search)
|
|
|
|
self.onSearchActivated()
|
2021-01-30 11:05:48 +01:00
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
def _default_search(self, card: Card | None = None) -> None:
|
2021-06-24 03:23:25 +02:00
|
|
|
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 = ""
|
2021-06-07 16:42:17 +02:00
|
|
|
if card is not None:
|
|
|
|
search = gui_hooks.default_search(search, card)
|
2021-06-24 03:23:25 +02:00
|
|
|
self.search_for(search, prompt)
|
2021-01-29 21:07:42 +01:00
|
|
|
|
2021-02-01 00:39:55 +01:00
|
|
|
def onReset(self) -> None:
|
2021-01-23 10:59:12 +01:00
|
|
|
self.sidebar.refresh()
|
2021-03-29 08:12:26 +02:00
|
|
|
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()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-03-29 08:12:26 +02:00
|
|
|
def end_reset(self) -> None:
|
|
|
|
self.table.end_reset()
|
|
|
|
self.mw.progress.finish()
|
|
|
|
|
|
|
|
# Table & Editor
|
2012-12-21 08:51:59 +01:00
|
|
|
######################################################################
|
|
|
|
|
2021-03-29 08:12:26 +02:00
|
|
|
def setup_table(self) -> None:
|
|
|
|
self.table = Table(self)
|
|
|
|
self.table.set_view(self.form.tableView)
|
2022-10-10 10:36:11 +02:00
|
|
|
switch = Switch(12, tr.browsing_cards(), tr.browsing_notes())
|
2021-03-29 15:51:34 +02:00
|
|
|
switch.setChecked(self.table.is_notes_mode())
|
2021-04-06 11:41:18 +02:00
|
|
|
switch.setToolTip(tr.browsing_toggle_showing_cards_notes())
|
2021-03-31 18:53:36 +02:00
|
|
|
qconnect(self.form.action_toggle_mode.triggered, switch.toggle)
|
2021-03-29 12:24:24 +02:00
|
|
|
qconnect(switch.toggled, self.on_table_state_changed)
|
|
|
|
self.form.gridLayout.addWidget(switch, 0, 0)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 00:39:55 +01:00
|
|
|
def setupEditor(self) -> None:
|
2021-07-17 04:17:28 +02:00
|
|
|
QShortcut(QKeySequence("Ctrl+Shift+P"), self, self.onTogglePreview)
|
2021-04-14 20:41:03 +02:00
|
|
|
|
2021-07-17 03:39:27 +02:00
|
|
|
def add_preview_button(editor: Editor) -> None:
|
2021-04-14 20:41:03 +02:00
|
|
|
editor._links["preview"] = lambda _editor: self.onTogglePreview()
|
2021-01-09 22:34:53 +01:00
|
|
|
|
2021-04-14 20:41:03 +02:00
|
|
|
gui_hooks.editor_did_init.append(add_preview_button)
|
2022-01-12 05:51:43 +01:00
|
|
|
self.editor = aqt.editor.Editor(
|
|
|
|
self.mw,
|
|
|
|
self.form.fieldsArea,
|
|
|
|
self,
|
2022-01-12 06:01:43 +01:00
|
|
|
editor_mode=aqt.editor.EditorMode.BROWSER,
|
2022-01-12 05:51:43 +01:00
|
|
|
)
|
2021-04-14 20:41:03 +02:00
|
|
|
gui_hooks.editor_did_init.remove(add_preview_button)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-03-16 13:40:37 +01:00
|
|
|
@ensure_editor_saved
|
2021-10-01 16:40:25 +02:00
|
|
|
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.
|
|
|
|
"""
|
2020-04-02 11:44:51 +02:00
|
|
|
if self._closeEventHasCleanedUp:
|
|
|
|
return
|
2021-03-29 08:12:26 +02:00
|
|
|
|
|
|
|
self.updateTitle()
|
|
|
|
# if there is only one selected card, use it in the editor
|
|
|
|
# it might differ from the current card
|
2021-10-01 16:40:25 +02:00
|
|
|
self.card = self.table.get_single_selected_card()
|
|
|
|
self.singleCard = bool(self.card)
|
2021-03-29 08:12:26 +02:00
|
|
|
self.form.splitter.widget(1).setVisible(self.singleCard)
|
|
|
|
if self.singleCard:
|
2021-10-01 16:40:25 +02:00
|
|
|
self.editor.set_note(self.card.note(), focusTo=self.focusTo)
|
2021-03-29 08:12:26 +02:00
|
|
|
self.focusTo = None
|
2021-10-01 16:40:25 +02:00
|
|
|
self.editor.card = self.card
|
2021-03-29 08:12:26 +02:00
|
|
|
else:
|
2021-03-16 07:39:41 +01:00
|
|
|
self.editor.set_note(None)
|
2021-03-30 22:06:58 +02:00
|
|
|
self._renderPreview()
|
2021-10-01 16:40:25 +02:00
|
|
|
self._update_row_actions()
|
|
|
|
self._update_selection_actions()
|
2020-01-15 08:45:35 +01:00
|
|
|
gui_hooks.browser_did_change_row(self)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-10-01 18:36:12 +02:00
|
|
|
@deprecated(info="please use on_all_or_selected_rows_changed() instead.")
|
|
|
|
def onRowChanged(self, *args: Any) -> None:
|
|
|
|
self.on_all_or_selected_rows_changed()
|
|
|
|
|
2021-10-01 16:40:25 +02:00
|
|
|
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()
|
2021-10-16 23:38:11 +02:00
|
|
|
self._update_card_info()
|
2021-03-31 10:05:44 +02:00
|
|
|
|
2021-10-01 16:40:25 +02:00
|
|
|
def _update_row_actions(self) -> None:
|
2021-09-17 16:16:34 +02:00
|
|
|
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)
|
|
|
|
|
2021-10-01 16:40:25 +02:00
|
|
|
def _update_selection_actions(self) -> None:
|
2021-09-17 16:16:34 +02:00
|
|
|
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)
|
|
|
|
|
2021-10-01 16:40:25 +02:00
|
|
|
def _update_current_actions(self) -> None:
|
|
|
|
self._update_flags_menu()
|
2023-01-30 10:21:06 +01:00
|
|
|
self._update_toggle_bury_action()
|
2021-10-01 16:40:25 +02:00
|
|
|
self._update_toggle_mark_action()
|
|
|
|
self._update_toggle_suspend_action()
|
2021-12-07 23:40:48 +01:00
|
|
|
self.form.actionCopy.setEnabled(self.table.has_current())
|
2021-10-01 16:40:25 +02:00
|
|
|
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())
|
|
|
|
|
2021-03-29 12:24:24 +02:00
|
|
|
@ensure_editor_saved
|
|
|
|
def on_table_state_changed(self, checked: bool) -> None:
|
2021-03-29 08:12:26 +02:00
|
|
|
self.mw.progress.start()
|
2021-03-29 12:24:24 +02:00
|
|
|
self.table.toggle_state(checked, self._lastSearchTxt)
|
2021-03-29 08:12:26 +02:00
|
|
|
self.mw.progress.finish()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-03-29 08:12:26 +02:00
|
|
|
# Sidebar
|
|
|
|
######################################################################
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2019-12-19 12:11:12 +01:00
|
|
|
def setupSidebar(self) -> None:
|
2021-03-26 04:48:26 +01:00
|
|
|
dw = self.sidebarDockWidget = QDockWidget(tr.browsing_sidebar(), self)
|
2021-10-05 05:53:01 +02:00
|
|
|
dw.setFeatures(QDockWidget.DockWidgetFeature.NoDockWidgetFeatures)
|
2017-08-13 11:11:40 +02:00
|
|
|
dw.setObjectName("Sidebar")
|
2021-10-25 05:27:19 +02:00
|
|
|
dock_area = (
|
|
|
|
Qt.DockWidgetArea.RightDockWidgetArea
|
|
|
|
if self.layoutDirection() == Qt.LayoutDirection.RightToLeft
|
|
|
|
else Qt.DockWidgetArea.LeftDockWidgetArea
|
|
|
|
)
|
|
|
|
dw.setAllowedAreas(dock_area)
|
2020-10-10 03:42:49 +02:00
|
|
|
|
2021-01-23 10:59:12 +01:00
|
|
|
self.sidebar = SidebarTreeView(self)
|
|
|
|
self.sidebarTree = self.sidebar # legacy alias
|
|
|
|
dw.setWidget(self.sidebar)
|
2021-01-26 23:35:40 +01:00
|
|
|
qconnect(
|
2021-02-05 05:32:56 +01:00
|
|
|
self.form.actionSidebarFilter.triggered,
|
2021-01-26 23:35:40 +01:00
|
|
|
self.focusSidebarSearchBar,
|
|
|
|
)
|
2021-02-26 13:04:30 +01:00
|
|
|
grid = QGridLayout()
|
2021-03-17 05:51:59 +01:00
|
|
|
grid.addWidget(self.sidebar.searchBar, 0, 0)
|
|
|
|
grid.addWidget(self.sidebar.toolbar, 0, 1)
|
2021-02-26 13:04:30 +01:00
|
|
|
grid.addWidget(self.sidebar, 1, 0, 1, 2)
|
2022-08-15 05:08:44 +02:00
|
|
|
grid.setContentsMargins(8, 4, 0, 0)
|
2021-02-26 13:04:30 +01:00
|
|
|
grid.setSpacing(0)
|
2021-01-26 23:35:40 +01:00
|
|
|
w = QWidget()
|
2021-02-26 13:04:30 +01:00
|
|
|
w.setLayout(grid)
|
2021-01-26 23:35:40 +01:00
|
|
|
dw.setWidget(w)
|
2017-08-15 06:54:13 +02:00
|
|
|
self.sidebarDockWidget.setFloating(False)
|
2021-01-23 10:59:12 +01:00
|
|
|
|
2017-08-15 07:19:22 +02:00
|
|
|
self.sidebarDockWidget.setTitleBarWidget(QWidget())
|
2021-10-25 05:27:19 +02:00
|
|
|
self.addDockWidget(dock_area, dw)
|
2017-08-13 11:11:40 +02:00
|
|
|
|
2021-01-23 10:59:12 +01:00
|
|
|
# schedule sidebar to refresh after browser window has loaded, so the
|
|
|
|
# UI is more responsive
|
2022-02-18 10:00:12 +01:00
|
|
|
self.mw.progress.timer(10, self.sidebar.refresh, False, parent=self.sidebar)
|
2017-08-13 11:11:40 +02:00
|
|
|
|
2021-01-26 23:35:40 +01:00
|
|
|
def showSidebar(self) -> None:
|
2017-08-13 11:11:40 +02:00
|
|
|
self.sidebarDockWidget.setVisible(True)
|
2021-01-26 23:35:40 +01:00
|
|
|
|
|
|
|
def focusSidebar(self) -> None:
|
|
|
|
self.showSidebar()
|
2021-01-23 10:59:12 +01:00
|
|
|
self.sidebar.setFocus()
|
2017-08-13 11:11:40 +02:00
|
|
|
|
2021-01-26 23:35:40 +01:00
|
|
|
def focusSidebarSearchBar(self) -> None:
|
|
|
|
self.showSidebar()
|
|
|
|
self.sidebar.searchBar.setFocus()
|
|
|
|
|
2021-02-01 00:39:55 +01:00
|
|
|
def toggle_sidebar(self) -> None:
|
2021-01-23 10:59:12 +01:00
|
|
|
want_visible = not self.sidebarDockWidget.isVisible()
|
|
|
|
self.sidebarDockWidget.setVisible(want_visible)
|
|
|
|
if want_visible:
|
|
|
|
self.sidebar.refresh()
|
2020-02-15 21:03:15 +01:00
|
|
|
|
2021-01-23 10:59:12 +01:00
|
|
|
# legacy
|
2021-02-09 01:50:39 +01:00
|
|
|
|
2021-02-01 00:39:55 +01:00
|
|
|
def setFilter(self, *terms: str) -> None:
|
2021-02-09 01:50:39 +01:00
|
|
|
self.sidebar.update_search(*terms)
|
2021-01-23 10:59:12 +01:00
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# Info
|
|
|
|
######################################################################
|
|
|
|
|
2021-02-01 00:39:55 +01:00
|
|
|
def showCardInfo(self) -> None:
|
2021-10-16 23:38:11 +02:00
|
|
|
self._card_info.toggle()
|
2020-06-15 06:14:18 +02:00
|
|
|
|
2021-10-16 23:38:11 +02:00
|
|
|
def _update_card_info(self) -> None:
|
|
|
|
self._card_info.set_card(self.current_card)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# Menu helpers
|
|
|
|
######################################################################
|
|
|
|
|
2021-03-29 08:48:33 +02:00
|
|
|
def selected_cards(self) -> Sequence[CardId]:
|
2021-03-29 08:12:26 +02:00
|
|
|
return self.table.get_selected_card_ids()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-03-29 08:48:33 +02:00
|
|
|
def selected_notes(self) -> Sequence[NoteId]:
|
2021-03-29 08:12:26 +02:00
|
|
|
return self.table.get_selected_note_ids()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-03-29 08:48:33 +02:00
|
|
|
def selectedNotesAsCards(self) -> Sequence[CardId]:
|
2021-03-29 08:12:26 +02:00
|
|
|
return self.table.get_card_ids_from_selected_note_ids()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 00:39:55 +01:00
|
|
|
def onHelp(self) -> None:
|
2021-01-25 14:45:47 +01:00
|
|
|
openHelp(HelpPage.BROWSING)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-03-18 02:46:11 +01:00
|
|
|
# legacy
|
|
|
|
|
|
|
|
selectedCards = selected_cards
|
|
|
|
selectedNotes = selected_notes
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# Misc menu options
|
|
|
|
######################################################################
|
|
|
|
|
2021-12-07 23:40:48 +01:00
|
|
|
def on_create_copy(self) -> None:
|
|
|
|
if note := self.table.get_current_note():
|
2021-12-31 07:45:30 +01:00
|
|
|
deck_id = self.table.get_current_card().did
|
|
|
|
aqt.dialogs.open("AddCards", self.mw).set_note(note, deck_id)
|
2021-12-07 23:40:48 +01:00
|
|
|
|
2021-04-26 08:46:08 +02:00
|
|
|
@no_arg_trigger
|
|
|
|
@skip_if_selection_is_empty
|
|
|
|
@ensure_editor_saved
|
2021-02-01 00:39:55 +01:00
|
|
|
def onChangeModel(self) -> None:
|
2021-05-22 19:11:06 +02:00
|
|
|
ids = self.selected_notes()
|
2021-06-10 13:30:39 +02:00
|
|
|
change_notetype_dialog(parent=self, note_ids=ids)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 23:46:56 +01:00
|
|
|
def createFilteredDeck(self) -> None:
|
2021-04-14 10:42:26 +02:00
|
|
|
search = self.current_search()
|
2021-06-27 07:12:22 +02:00
|
|
|
if self.mw.col.sched_ver() != 1 and KeyboardModifiersPressed().alt:
|
2021-03-24 04:17:12 +01:00
|
|
|
aqt.dialogs.open("FilteredDeckConfigDialog", self.mw, search_2=search)
|
2021-02-02 09:29:09 +01:00
|
|
|
else:
|
2021-03-24 04:17:12 +01:00
|
|
|
aqt.dialogs.open("FilteredDeckConfigDialog", self.mw, search=search)
|
2021-02-01 19:10:05 +01:00
|
|
|
|
2013-05-03 10:52:46 +02:00
|
|
|
# Preview
|
|
|
|
######################################################################
|
|
|
|
|
2021-02-01 00:39:55 +01:00
|
|
|
def onTogglePreview(self) -> None:
|
2020-03-29 21:10:30 +02:00
|
|
|
if self._previewer:
|
2020-04-02 09:35:51 +02:00
|
|
|
self._previewer.close()
|
2021-07-17 03:39:27 +02:00
|
|
|
elif self.editor.note:
|
2020-04-08 08:19:59 +02:00
|
|
|
self._previewer = PreviewDialog(self, self.mw, self._on_preview_closed)
|
2020-04-02 09:35:51 +02:00
|
|
|
self._previewer.open()
|
2021-12-20 11:23:50 +01:00
|
|
|
self.toggle_preview_button_state(True)
|
2017-08-08 08:28:53 +02:00
|
|
|
|
2021-02-01 00:39:55 +01:00
|
|
|
def _renderPreview(self) -> None:
|
2020-03-29 21:10:30 +02:00
|
|
|
if self._previewer:
|
2021-01-10 02:01:24 +01:00
|
|
|
if self.singleCard:
|
|
|
|
self._previewer.render_card()
|
|
|
|
else:
|
|
|
|
self.onTogglePreview()
|
2017-08-08 08:28:53 +02:00
|
|
|
|
2021-12-20 11:23:50 +01:00
|
|
|
def toggle_preview_button_state(self, active: bool) -> None:
|
|
|
|
if self.editor.web:
|
2022-02-03 05:52:11 +01:00
|
|
|
self.editor.web.eval(f"togglePreviewButtonState({json.dumps(active)});")
|
2021-12-20 11:23:50 +01:00
|
|
|
|
2021-02-01 00:39:55 +01:00
|
|
|
def _cleanup_preview(self) -> None:
|
2020-03-29 21:10:30 +02:00
|
|
|
if self._previewer:
|
2020-04-02 17:34:53 +02:00
|
|
|
self._previewer.cancel_timer()
|
2020-04-10 10:42:28 +02:00
|
|
|
self._previewer.close()
|
2019-02-26 02:18:32 +01:00
|
|
|
|
2021-02-01 00:39:55 +01:00
|
|
|
def _on_preview_closed(self) -> None:
|
2021-12-20 11:23:50 +01:00
|
|
|
av_player.stop_and_clear_queue()
|
|
|
|
self.toggle_preview_button_state(False)
|
2020-04-08 08:19:59 +02:00
|
|
|
self._previewer = None
|
|
|
|
|
2012-12-21 08:51:59 +01:00
|
|
|
# Card deletion
|
|
|
|
######################################################################
|
|
|
|
|
2021-04-26 08:46:08 +02:00
|
|
|
@no_arg_trigger
|
|
|
|
@skip_if_selection_is_empty
|
2021-03-12 08:53:14 +01:00
|
|
|
def delete_selected_notes(self) -> None:
|
|
|
|
# ensure deletion is not accidentally triggered when the user is focused
|
|
|
|
# in the editing screen or search bar
|
2018-03-01 04:31:52 +01:00
|
|
|
focus = self.focusWidget()
|
|
|
|
if focus != self.form.tableView:
|
|
|
|
return
|
2016-07-14 12:23:44 +02:00
|
|
|
|
2022-06-20 03:07:38 +02:00
|
|
|
self.editor.set_note(None)
|
2021-09-28 11:47:13 +02:00
|
|
|
nids = self.table.to_row_of_unselected_note()
|
2021-09-25 17:28:38 +02:00
|
|
|
remove_notes(parent=self, note_ids=nids).run_in_background()
|
2021-03-12 08:53:14 +01:00
|
|
|
|
|
|
|
# legacy
|
|
|
|
|
|
|
|
deleteNotes = delete_selected_notes
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# Deck change
|
|
|
|
######################################################################
|
|
|
|
|
2021-04-26 08:46:08 +02:00
|
|
|
@no_arg_trigger
|
|
|
|
@skip_if_selection_is_empty
|
|
|
|
@ensure_editor_saved
|
2021-03-12 08:53:14 +01:00
|
|
|
def set_deck_of_selected_cards(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
from aqt.studydeck import StudyDeck
|
2019-12-23 01:34:10 +01:00
|
|
|
|
2021-03-29 08:12:26 +02:00
|
|
|
cids = self.table.get_selected_card_ids()
|
2019-12-23 01:34:10 +01:00
|
|
|
did = self.mw.col.db.scalar("select did from cards where id = ?", cids[0])
|
|
|
|
current = self.mw.col.decks.get(did)["name"]
|
2022-02-10 00:53:13 +01:00
|
|
|
|
|
|
|
def callback(ret: StudyDeck) -> None:
|
|
|
|
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()
|
|
|
|
|
|
|
|
StudyDeck(
|
2019-12-23 01:34:10 +01:00
|
|
|
self.mw,
|
|
|
|
current=current,
|
2021-03-26 04:48:26 +01:00
|
|
|
accept=tr.browsing_move_cards(),
|
|
|
|
title=tr.browsing_change_deck(),
|
2021-01-25 14:45:47 +01:00
|
|
|
help=HelpPage.BROWSING,
|
2019-12-23 01:34:10 +01:00
|
|
|
parent=self,
|
2022-02-10 00:53:13 +01:00
|
|
|
callback=callback,
|
2019-12-23 01:34:10 +01:00
|
|
|
)
|
2021-03-12 08:53:14 +01:00
|
|
|
|
|
|
|
# legacy
|
|
|
|
|
|
|
|
setDeck = set_deck_of_selected_cards
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# Tags
|
|
|
|
######################################################################
|
|
|
|
|
2021-04-26 08:46:08 +02:00
|
|
|
@no_arg_trigger
|
|
|
|
@skip_if_selection_is_empty
|
|
|
|
@ensure_editor_saved
|
2021-03-05 11:47:51 +01:00
|
|
|
def add_tags_to_selected_notes(
|
2021-02-01 00:39:55 +01:00
|
|
|
self,
|
2021-10-03 10:59:42 +02:00
|
|
|
tags: str | None = None,
|
2021-02-01 00:39:55 +01:00
|
|
|
) -> None:
|
2021-03-05 11:47:51 +01:00
|
|
|
"Shows prompt if tags not provided."
|
2021-03-26 04:48:26 +01:00
|
|
|
if not (tags := tags or self._prompt_for_tags(tr.browsing_enter_tags_to_add())):
|
2021-03-16 13:40:37 +01:00
|
|
|
return
|
2022-05-07 01:16:57 +02:00
|
|
|
|
|
|
|
space_separated_tags = re.sub(r"[ \n\t\v]+", " ", tags)
|
2021-04-01 07:01:08 +02:00
|
|
|
add_tags_to_notes(
|
2022-05-07 01:16:57 +02:00
|
|
|
parent=self,
|
|
|
|
note_ids=self.selected_notes(),
|
|
|
|
space_separated_tags=space_separated_tags,
|
2021-04-06 06:36:13 +02:00
|
|
|
).run_in_background(initiator=self)
|
undoable ops now return changes directly; add new *_ops.py files
- Introduced a new transact() method that wraps the return value
in a separate struct that describes the changes that were made.
- Changes are now gathered from the undo log, so we don't need to
guess at what was changed - eg if update_note() is called with identical
note contents, no changes are returned. Card changes will only be set
if cards were actually generated by the update_note() call, and tag
will only be set if a new tag was added.
- mw.perform_op() has been updated to expect the op to return the changes,
or a structure with the changes in it, and it will use them to fire the
change hook, instead of fetching the changes from undo_status(), so there
is no risk of race conditions.
- the various calls to mw.perform_op() have been split into separate
files like card_ops.py. Aside from making the code cleaner, this works
around a rather annoying issue with mypy. Because we run it with
no_strict_optional, mypy is happy to accept an operation that returns None,
despite the type signature saying it requires changes to be returned.
Turning no_strict_optional on for the whole codebase is not practical
at the moment, but we can enable it for individual files.
Still todo:
- The cursor keeps moving back to the start of a field when typing -
we need to ignore the refresh hook when we are the initiator.
- The busy cursor icon should probably be delayed a few hundreds ms.
- Still need to think about a nicer way of handling saveNow()
- op_made_changes(), op_affects_study_queue() might be better embedded
as properties in the object instead
2021-03-16 05:26:42 +01:00
|
|
|
|
2021-04-26 08:46:08 +02:00
|
|
|
@no_arg_trigger
|
|
|
|
@skip_if_selection_is_empty
|
|
|
|
@ensure_editor_saved
|
2021-10-03 10:59:42 +02:00
|
|
|
def remove_tags_from_selected_notes(self, tags: str | None = None) -> None:
|
2021-03-05 11:47:51 +01:00
|
|
|
"Shows prompt if tags not provided."
|
2021-03-16 13:40:37 +01:00
|
|
|
if not (
|
2021-03-26 04:48:26 +01:00
|
|
|
tags := tags or self._prompt_for_tags(tr.browsing_enter_tags_to_delete())
|
2021-03-16 13:40:37 +01:00
|
|
|
):
|
|
|
|
return
|
2021-04-06 04:47:55 +02:00
|
|
|
|
2021-04-01 07:01:08 +02:00
|
|
|
remove_tags_from_notes(
|
2021-04-06 04:47:55 +02:00
|
|
|
parent=self, note_ids=self.selected_notes(), space_separated_tags=tags
|
2021-04-06 06:36:13 +02:00
|
|
|
).run_in_background(initiator=self)
|
undoable ops now return changes directly; add new *_ops.py files
- Introduced a new transact() method that wraps the return value
in a separate struct that describes the changes that were made.
- Changes are now gathered from the undo log, so we don't need to
guess at what was changed - eg if update_note() is called with identical
note contents, no changes are returned. Card changes will only be set
if cards were actually generated by the update_note() call, and tag
will only be set if a new tag was added.
- mw.perform_op() has been updated to expect the op to return the changes,
or a structure with the changes in it, and it will use them to fire the
change hook, instead of fetching the changes from undo_status(), so there
is no risk of race conditions.
- the various calls to mw.perform_op() have been split into separate
files like card_ops.py. Aside from making the code cleaner, this works
around a rather annoying issue with mypy. Because we run it with
no_strict_optional, mypy is happy to accept an operation that returns None,
despite the type signature saying it requires changes to be returned.
Turning no_strict_optional on for the whole codebase is not practical
at the moment, but we can enable it for individual files.
Still todo:
- The cursor keeps moving back to the start of a field when typing -
we need to ignore the refresh hook when we are the initiator.
- The busy cursor icon should probably be delayed a few hundreds ms.
- Still need to think about a nicer way of handling saveNow()
- op_made_changes(), op_affects_study_queue() might be better embedded
as properties in the object instead
2021-03-16 05:26:42 +01:00
|
|
|
|
2021-10-03 10:59:42 +02:00
|
|
|
def _prompt_for_tags(self, prompt: str) -> str | None:
|
undoable ops now return changes directly; add new *_ops.py files
- Introduced a new transact() method that wraps the return value
in a separate struct that describes the changes that were made.
- Changes are now gathered from the undo log, so we don't need to
guess at what was changed - eg if update_note() is called with identical
note contents, no changes are returned. Card changes will only be set
if cards were actually generated by the update_note() call, and tag
will only be set if a new tag was added.
- mw.perform_op() has been updated to expect the op to return the changes,
or a structure with the changes in it, and it will use them to fire the
change hook, instead of fetching the changes from undo_status(), so there
is no risk of race conditions.
- the various calls to mw.perform_op() have been split into separate
files like card_ops.py. Aside from making the code cleaner, this works
around a rather annoying issue with mypy. Because we run it with
no_strict_optional, mypy is happy to accept an operation that returns None,
despite the type signature saying it requires changes to be returned.
Turning no_strict_optional on for the whole codebase is not practical
at the moment, but we can enable it for individual files.
Still todo:
- The cursor keeps moving back to the start of a field when typing -
we need to ignore the refresh hook when we are the initiator.
- The busy cursor icon should probably be delayed a few hundreds ms.
- Still need to think about a nicer way of handling saveNow()
- op_made_changes(), op_affects_study_queue() might be better embedded
as properties in the object instead
2021-03-16 05:26:42 +01:00
|
|
|
(tags, ok) = getTag(self, self.col, prompt)
|
|
|
|
if not ok:
|
|
|
|
return None
|
|
|
|
else:
|
|
|
|
return tags
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-04-26 08:46:08 +02:00
|
|
|
@no_arg_trigger
|
|
|
|
@ensure_editor_saved
|
clear_unused_tags and browser redraw improvements
- clear_unused_tags() is now undoable, and returns the number of removed
notes
- add a new mw.query_op() helper for immutable queries
- decouple "freeze/unfreeze ui state" hooks from the "interface update
required" hook, so that the former is fired even on error, and can be
made re-entrant
- use a 'block_updates' flag in Python, instead of setUpdatesEnabled(),
as the latter has the side-effect of preventing child windows like
tooltips from appearing, and forces a full redrawn when updates are
enabled again. The new behaviour leads to the card list blanking out
when a long-running op is running, but in the future if we cache the
cell values we can just display them from the cache instead.
- we were indiscriminately saving the note with saveNow(), due to the
call to saveTags(). Changed so that it only saves when the tags field
is focused.
- drain the "on_done" queue on main before launching a new background
task, to lower the chances of something in on_done making a small query
to the DB and hanging until a long op finishes
- the duplicate check in the editor was executed after the webview loads,
leading to it hanging until the sidebar finishes loading. Run it at
set_note() time instead, so that the editor loads first.
- don't throw an error when a long-running op started with with_progress()
finishes after the window it was launched from has closed
- don't throw an error when the browser is closed before the sidebar
has finished loading
2021-03-17 12:27:42 +01:00
|
|
|
def clear_unused_tags(self) -> None:
|
2021-04-06 06:36:13 +02:00
|
|
|
clear_unused_tags(parent=self).run_in_background()
|
2017-08-11 06:40:51 +02:00
|
|
|
|
2021-03-05 11:47:51 +01:00
|
|
|
addTags = add_tags_to_selected_notes
|
|
|
|
deleteTags = remove_tags_from_selected_notes
|
clear_unused_tags and browser redraw improvements
- clear_unused_tags() is now undoable, and returns the number of removed
notes
- add a new mw.query_op() helper for immutable queries
- decouple "freeze/unfreeze ui state" hooks from the "interface update
required" hook, so that the former is fired even on error, and can be
made re-entrant
- use a 'block_updates' flag in Python, instead of setUpdatesEnabled(),
as the latter has the side-effect of preventing child windows like
tooltips from appearing, and forces a full redrawn when updates are
enabled again. The new behaviour leads to the card list blanking out
when a long-running op is running, but in the future if we cache the
cell values we can just display them from the cache instead.
- we were indiscriminately saving the note with saveNow(), due to the
call to saveTags(). Changed so that it only saves when the tags field
is focused.
- drain the "on_done" queue on main before launching a new background
task, to lower the chances of something in on_done making a small query
to the DB and hanging until a long op finishes
- the duplicate check in the editor was executed after the webview loads,
leading to it hanging until the sidebar finishes loading. Run it at
set_note() time instead, so that the editor loads first.
- don't throw an error when a long-running op started with with_progress()
finishes after the window it was launched from has closed
- don't throw an error when the browser is closed before the sidebar
has finished loading
2021-03-17 12:27:42 +01:00
|
|
|
clearUnusedTags = clear_unused_tags
|
2021-03-05 11:47:51 +01:00
|
|
|
|
2017-08-12 08:08:10 +02:00
|
|
|
# Suspending
|
2012-12-21 08:51:59 +01:00
|
|
|
######################################################################
|
|
|
|
|
2021-03-31 10:05:44 +02:00
|
|
|
def _update_toggle_suspend_action(self) -> None:
|
2021-10-01 16:40:25 +02:00
|
|
|
is_suspended = bool(
|
|
|
|
self.current_card and self.current_card.queue == QUEUE_TYPE_SUSPENDED
|
|
|
|
)
|
2021-03-31 10:05:44 +02:00
|
|
|
self.form.actionToggle_Suspend.setChecked(is_suspended)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-04-25 08:47:15 +02:00
|
|
|
@skip_if_selection_is_empty
|
2021-03-31 10:05:44 +02:00
|
|
|
@ensure_editor_saved
|
|
|
|
def suspend_selected_cards(self, checked: bool) -> None:
|
2021-03-18 02:46:11 +01:00
|
|
|
cids = self.selected_cards()
|
2021-03-31 10:05:44 +02:00
|
|
|
if checked:
|
2021-04-06 08:38:42 +02:00
|
|
|
suspend_cards(parent=self, card_ids=cids).run_in_background()
|
undoable ops now return changes directly; add new *_ops.py files
- Introduced a new transact() method that wraps the return value
in a separate struct that describes the changes that were made.
- Changes are now gathered from the undo log, so we don't need to
guess at what was changed - eg if update_note() is called with identical
note contents, no changes are returned. Card changes will only be set
if cards were actually generated by the update_note() call, and tag
will only be set if a new tag was added.
- mw.perform_op() has been updated to expect the op to return the changes,
or a structure with the changes in it, and it will use them to fire the
change hook, instead of fetching the changes from undo_status(), so there
is no risk of race conditions.
- the various calls to mw.perform_op() have been split into separate
files like card_ops.py. Aside from making the code cleaner, this works
around a rather annoying issue with mypy. Because we run it with
no_strict_optional, mypy is happy to accept an operation that returns None,
despite the type signature saying it requires changes to be returned.
Turning no_strict_optional on for the whole codebase is not practical
at the moment, but we can enable it for individual files.
Still todo:
- The cursor keeps moving back to the start of a field when typing -
we need to ignore the refresh hook when we are the initiator.
- The busy cursor icon should probably be delayed a few hundreds ms.
- Still need to think about a nicer way of handling saveNow()
- op_made_changes(), op_affects_study_queue() might be better embedded
as properties in the object instead
2021-03-16 05:26:42 +01:00
|
|
|
else:
|
2021-04-06 08:38:42 +02:00
|
|
|
unsuspend_cards(parent=self.mw, card_ids=cids).run_in_background()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2023-01-30 10:21:06 +01:00
|
|
|
# Burying
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
def _update_toggle_bury_action(self) -> None:
|
|
|
|
is_buried = bool(
|
2023-02-22 04:12:35 +01:00
|
|
|
self.current_card
|
|
|
|
and self.current_card.queue
|
|
|
|
in (QUEUE_TYPE_MANUALLY_BURIED, QUEUE_TYPE_SIBLING_BURIED)
|
2023-01-30 10:21:06 +01:00
|
|
|
)
|
|
|
|
self.form.action_toggle_bury.setChecked(is_buried)
|
|
|
|
|
|
|
|
@skip_if_selection_is_empty
|
|
|
|
@ensure_editor_saved
|
|
|
|
def bury_selected_cards(self, checked: bool) -> None:
|
|
|
|
cids = self.selected_cards()
|
|
|
|
if checked:
|
|
|
|
bury_cards(parent=self, card_ids=cids).run_in_background()
|
|
|
|
else:
|
|
|
|
unbury_cards(parent=self.mw, card_ids=cids).run_in_background()
|
|
|
|
|
2020-02-10 04:15:10 +01:00
|
|
|
# Exporting
|
|
|
|
######################################################################
|
|
|
|
|
2021-04-26 08:46:08 +02:00
|
|
|
@no_arg_trigger
|
|
|
|
@skip_if_selection_is_empty
|
2021-02-01 00:39:55 +01:00
|
|
|
def _on_export_notes(self) -> None:
|
2022-07-18 05:31:24 +02:00
|
|
|
if not self.mw.pm.legacy_import_export():
|
Add apkg import/export on backend (#1743)
* Add apkg export on backend
* Filter out missing media-paths at write time
* Make TagMatcher::new() infallible
* Gather export data instead of copying directly
* Revert changes to rslib/src/tags/
* Reuse filename_is_safe/check_filename_safe()
* Accept func to produce MediaIter in export_apkg()
* Only store file folder once in MediaIter
* Use temporary tables for gathering
export_apkg() now accepts a search instead of a deck id. Decks are
gathered according to the matched notes' cards.
* Use schedule_as_new() to reset cards
* ExportData → ExchangeData
* Ignore ascii case when filtering system tags
* search_notes_cards_into_table →
search_cards_of_notes_into_table
* Start on apkg importing on backend
* Fix due dates in days for apkg export
* Refactor import-export/package
- Move media and meta code into appropriate modules.
- Normalize/check for normalization when deserializing media entries.
* Add SafeMediaEntry for deserialized MediaEntries
* Prepare media based on checksums
- Ensure all existing media files are hashed.
- Hash incoming files during preparation to detect conflicts.
- Uniquify names of conflicting files with hash (not notetype id).
- Mark media files as used while importing notes.
- Finally copy used media.
* Handle encoding in `replace_media_refs()`
* Add trait to keep down cow boilerplate
* Add notetypes immediately instaed of preparing
* Move target_col into Context
* Add notes immediately instaed of preparing
* Note id, not guid of conflicting notes
* Add import_decks()
* decks_configs → deck_configs
* Add import_deck_configs()
* Add import_cards(), import_revlog()
* Use dyn instead of generic for media_fn
Otherwise, would have to pass None with type annotation in the default
case.
* Fix signature of import_apkg()
* Fix search_cards_of_notes_into_table()
* Test new functions in text.rs
* Add roundtrip test for apkg (stub)
* Keep source id of imported cards (or skip)
* Keep source ids of imported revlog (or skip)
* Try to keep source ids of imported notes
* Make adding notetype with id undoable
* Wrap apkg import in transaction
* Keep source ids of imported deck configs (or skip)
* Handle card due dates and original due/did
* Fix importing cards/revlog
Card ids are manually uniquified.
* Factor out card importing
* Refactor card and revlog importing
* Factor out card importing
Also handle missing parents .
* Factor out note importing
* Factor out media importing
* Maybe upgrade scheduler of apkg
* Fix parent deck gathering
* Unconditionally import static media
* Fix deck importing edge cases
Test those edge cases, and add some global test helpers.
* Test note importing
* Let import_apkg() take a progress func
* Expand roundtrip apkg test
* Use fat pointer to avoid propogating generics
* Fix progress_fn type
* Expose apkg export/import on backend
* Return note log when importing apkg
* Fix archived collection name on apkg import
* Add CollectionOpWithBackendProgress
* Fix wrong Interrupted Exception being checked
* Add ClosedCollectionOp
* Add note ids to log and strip HTML
* Update progress when checking incoming media too
* Conditionally enable new importing in GUI
* Fix all_checksums() for media import
Entries of deleted files are nulled, not removed.
* Make apkg exporting on backend abortable
* Return number of notes imported from apkg
* Fix exception printing for QueryOp as well
* Add QueryOpWithBackendProgress
Also support backend exporting progress.
* Expose new apkg and colpkg exporting
* Open transaction in insert_data()
Was slowing down exporting by several orders of magnitude.
* Handle zstd-compressed apkg
* Add legacy arg to ExportAnkiPackage
Currently not exposed on the frontend
* Remove unused import in proto file
* Add symlink for typechecking of import_export_pb2
* Avoid kwargs in pb message creation, so typechecking is not lost
Protobuf's behaviour is rather subtle and I had to dig through the docs
to figure it out: set a field on a submessage to automatically assign
the submessage to the parent, or call SetInParent() to persist a default
version of the field you specified.
* Avoid re-exporting protobuf msgs we only use internally
* Stop after one test failure
mypy often fails much faster than pylint
* Avoid an extra allocation when extracting media checksums
* Update progress after prepare_media() finishes
Otherwise the bulk of the import ends up being shown as "Checked: 0"
in the progress window.
* Show progress of note imports
Note import is the slowest part, so showing progress here makes the UI
feel more responsive.
* Reset filtered decks at import time
Before this change, filtered decks exported with scheduling remained
filtered on import, and maybe_remove_from_filtered_deck() moved cards
into them as their home deck, leading to errors during review.
We may still want to provide a way to preserve filtered decks on import,
but to do that we'll need to ensure we don't rewrite the home decks of
cards, and we'll need to ensure the home decks are included as part of
the import (or give an error if they're not).
https://github.com/ankitects/anki/pull/1743/files#r839346423
* Fix a corner-case where due dates were shifted by a day
This issue existed in the old Python code as well. We need to include
the user's UTC offset in the exported file, or days_elapsed falls back
on the v1 cutoff calculation, which may be a day earlier or later than
the v2 calculation.
* Log conflicting note in remapped nt case
* take_fields() → into_fields()
* Alias `[u8; 20]` with `Sha1Hash`
* Truncate logged fields
* Rework apkg note import tests
- Use macros for more helpful errors.
- Split monolith into unit tests.
- Fix some unknown error with the previous test along the way.
(Was failing after 969484de4388d225c9f17d94534b3ba0094c3568.)
* Fix sorting of imported decks
Also adjust the test, so it fails without the patch. It was only passing
before, because the parent deck happened to come before the
inconsistently capitalised child alphabetically. But we want all parent
decks to be imported before their child decks, so their children can
adopt their capitalisation.
* target[_id]s → existing_card[_id]s
* export_collection_extracting_media() → ...
export_into_collection_file()
* target_already_exists→card_ordinal_already_exists
* Add search_cards_of_notes_into_table.sql
* Imrove type of apkg export selector/limit
* Remove redundant call to mod_schema()
* Parent tooltips to mw
* Fix a crash when truncating note text
String::truncate() is a bit of a footgun, and I've hit this before
too :-)
* Remove ExportLimit in favour of separate classes
* Remove OpWithBackendProgress and ClosedCollectionOp
Backend progress logic is now in ProgressManager. QueryOp can be used
for running on closed collection.
Also fix aborting of colpkg exports, which slipped through in #1817.
* Tidy up import log
* Avoid QDialog.exec()
* Default to excluding scheuling for deck list deck
* Use IncrementalProgress in whole import_export code
* Compare checksums when importing colpkgs
* Avoid registering changes if hashes are not needed
* ImportProgress::Collection → ImportProgress::File
* Make downgrading apkgs depend on meta version
* Generalise IncrementableProgress
And use it in entire import_export code instead.
* Fix type complexity lint
* Take count_map for IncrementableProgress::get_inner
* Replace import/export env with Shift click
* Accept all args from update() for backend progress
* Pass fields of ProgressUpdate explicitly
* Move update_interval into IncrementableProgress
* Outsource incrementing into Incrementor
* Mutate ProgressUpdate in progress_update callback
* Switch import/export legacy toggle to profile setting
Shift would have been nice, but the existing shortcuts complicate things.
If the user triggers an import with ctrl+shift+i, shift is unlikely to
have been released by the time our code runs, meaning the user accidentally
triggers the new code. We could potentially wait a while before bringing
up the dialog, but then we're forced to guess at how long it will take the
user to release the key.
One alternative would be to use alt instead of shift, but then we need to
trigger our shortcut when that key is pressed as well, and it could
potentially cause a conflict with an add-on that already uses that
combination.
* Show extension in export dialog
* Continue to provide separate options for schema 11+18 colpkg export
* Default to colpkg export when using File>Export
* Improve appearance of combo boxes when switching between apkg/colpkg
+ Deal with long deck names
* Convert newlines to spaces when showing fields from import
Ensures each imported note appears on a separate line
* Don't separate total note count from the other summary lines
This may come down to personal preference, but I feel the other counts
are equally as important, and separating them feels like it makes it
a bit easier to ignore them.
* Fix 'deck not normal' error when importing a filtered deck for the 2nd time
* Fix [Identical] being shown on first import
* Revert "Continue to provide separate options for schema 11+18 colpkg export"
This reverts commit 8f0b2c175f4794d642823b60414d142a12768441.
Will use a different approach
* Move legacy support into a separate exporter option; add to apkg export
* Adjust 'too new' message to also apply to .apkg import case
* Show a better message when attempting to import new apkg into old code
Previously the user could end seeing a message like:
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xb5 in position 1: invalid start byte
Unfortunately we can't retroactively fix this for older clients.
* Hide legacy support option in older exporting screen
* Reflect change from paths to fnames in type & name
* Make imported decks normal at once
Then skip special casing in update_deck(). Also skip updating
description if new one is empty.
Co-authored-by: Damien Elmes <gpg@ankiweb.net>
2022-05-02 13:12:46 +02:00
|
|
|
nids = self.selected_notes()
|
|
|
|
ExportDialog(self.mw, nids=nids)
|
|
|
|
else:
|
|
|
|
cids = self.selectedNotesAsCards()
|
|
|
|
LegacyExportDialog(self.mw, cids=list(cids))
|
2020-02-10 04:15:10 +01:00
|
|
|
|
2017-08-16 12:30:29 +02:00
|
|
|
# Flags & Marking
|
2017-08-12 08:08:10 +02:00
|
|
|
######################################################################
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-04-25 08:47:15 +02:00
|
|
|
@skip_if_selection_is_empty
|
2021-03-18 03:06:45 +01:00
|
|
|
@ensure_editor_saved
|
|
|
|
def set_flag_of_selected_cards(self, flag: int) -> None:
|
2021-10-01 16:40:25 +02:00
|
|
|
if not self.current_card:
|
2019-12-06 05:23:54 +01:00
|
|
|
return
|
2020-09-27 04:31:25 +02:00
|
|
|
|
2018-11-12 02:11:53 +01:00
|
|
|
# flag needs toggling off?
|
2021-10-01 16:40:25 +02:00
|
|
|
if flag == self.current_card.user_flag():
|
undoable ops now return changes directly; add new *_ops.py files
- Introduced a new transact() method that wraps the return value
in a separate struct that describes the changes that were made.
- Changes are now gathered from the undo log, so we don't need to
guess at what was changed - eg if update_note() is called with identical
note contents, no changes are returned. Card changes will only be set
if cards were actually generated by the update_note() call, and tag
will only be set if a new tag was added.
- mw.perform_op() has been updated to expect the op to return the changes,
or a structure with the changes in it, and it will use them to fire the
change hook, instead of fetching the changes from undo_status(), so there
is no risk of race conditions.
- the various calls to mw.perform_op() have been split into separate
files like card_ops.py. Aside from making the code cleaner, this works
around a rather annoying issue with mypy. Because we run it with
no_strict_optional, mypy is happy to accept an operation that returns None,
despite the type signature saying it requires changes to be returned.
Turning no_strict_optional on for the whole codebase is not practical
at the moment, but we can enable it for individual files.
Still todo:
- The cursor keeps moving back to the start of a field when typing -
we need to ignore the refresh hook when we are the initiator.
- The busy cursor icon should probably be delayed a few hundreds ms.
- Still need to think about a nicer way of handling saveNow()
- op_made_changes(), op_affects_study_queue() might be better embedded
as properties in the object instead
2021-03-16 05:26:42 +01:00
|
|
|
flag = 0
|
more reset refactoring
'card modified' covers the common case where we need to rebuild the
study queue, but is also set when changing the card flags. We want to
avoid a queue rebuild in that case, as it causes UI flicker, and may
result in a different card being shown. Note marking doesn't trigger
a queue build, but still causes flicker, and may return the user back
to the front side when they were looking at the answer.
I still think entity-based change tracking is the simplest in the
common case, but to solve the above, I've introduced an enum describing
the last operation that was taken. This currently is not trying to list
out all possible operations, and just describes the ones we want to
special-case.
Other changes:
- Fire the old 'state_did_reset' hook after an operation is performed,
so legacy code can refresh itself after an operation is performed.
- Fire the new `operation_did_execute` hook when mw.reset() is called,
so that as the UI is updated to the use the new hook, it will still
be able to refresh after legacy code calls mw.reset()
- Update the deck browser, overview and review screens to listen to
the new hook, instead of relying on the main window to call moveToState()
- Add a 'set flag' backend action, so we can distinguish it from a
normal card update.
- Drop the separate added/modified entries in the change list in
favour of a single entry per entity.
- Add typing to mw.state
- Tweak perform_op()
- Convert a few more actions to use perform_op()
2021-03-14 10:54:15 +01:00
|
|
|
|
2021-04-06 06:36:13 +02:00
|
|
|
set_card_flag(
|
|
|
|
parent=self, card_ids=self.selected_cards(), flag=flag
|
|
|
|
).run_in_background()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-03-18 03:06:45 +01:00
|
|
|
def _update_flags_menu(self) -> None:
|
2021-10-01 16:40:25 +02:00
|
|
|
flag = self.current_card and self.current_card.user_flag()
|
2018-11-12 03:10:50 +01:00
|
|
|
flag = flag or 0
|
|
|
|
|
2021-07-02 11:16:10 +02:00
|
|
|
for f in self.mw.flags.all():
|
2021-05-19 19:17:43 +02:00
|
|
|
getattr(self.form, f.action).setChecked(flag == f.index)
|
2019-02-05 05:37:07 +01:00
|
|
|
|
|
|
|
qtMenuShortcutWorkaround(self.form.menuFlag)
|
2018-11-12 03:10:50 +01:00
|
|
|
|
2021-05-19 10:34:36 +02:00
|
|
|
def _update_flag_labels(self) -> None:
|
2021-07-02 11:16:10 +02:00
|
|
|
for flag in self.mw.flags.all():
|
2021-05-19 19:17:43 +02:00
|
|
|
getattr(self.form, flag.action).setText(flag.label)
|
2021-05-19 10:34:36 +02:00
|
|
|
|
2021-03-31 10:05:44 +02:00
|
|
|
def toggle_mark_of_selected_notes(self, checked: bool) -> None:
|
|
|
|
if checked:
|
2021-03-18 03:06:45 +01:00
|
|
|
self.add_tags_to_selected_notes(tags=MARKED_TAG)
|
2021-03-31 10:05:44 +02:00
|
|
|
else:
|
|
|
|
self.remove_tags_from_selected_notes(tags=MARKED_TAG)
|
|
|
|
|
|
|
|
def _update_toggle_mark_action(self) -> None:
|
2021-10-01 16:40:25 +02:00
|
|
|
is_marked = bool(
|
|
|
|
self.current_card and self.current_card.note().has_tag(MARKED_TAG)
|
|
|
|
)
|
2021-03-31 10:05:44 +02:00
|
|
|
self.form.actionToggle_Mark.setChecked(is_marked)
|
2017-08-16 12:30:29 +02:00
|
|
|
|
2021-03-18 02:46:11 +01:00
|
|
|
# Scheduling
|
2012-12-21 08:51:59 +01:00
|
|
|
######################################################################
|
|
|
|
|
2021-04-26 08:46:08 +02:00
|
|
|
@no_arg_trigger
|
|
|
|
@skip_if_selection_is_empty
|
|
|
|
@ensure_editor_saved
|
2021-02-01 00:39:55 +01:00
|
|
|
def reposition(self) -> None:
|
2021-04-25 11:26:55 +02:00
|
|
|
if op := reposition_new_cards_dialog(
|
2021-04-06 08:38:42 +02:00
|
|
|
parent=self, card_ids=self.selected_cards()
|
2021-04-25 11:26:55 +02:00
|
|
|
):
|
|
|
|
op.run_in_background()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-04-26 08:46:08 +02:00
|
|
|
@no_arg_trigger
|
|
|
|
@skip_if_selection_is_empty
|
|
|
|
@ensure_editor_saved
|
Rework reschedule tool
The old rescheduling dialog's two options have been split into two
separate menu items, "Forget", and "Set Due Date"
For cards that are not review cards, "Set Due Date" behaves like the
old reschedule option, changing the cards into a review card, and
and setting both the interval and due date to the provided number of
days.
When "Set Due Date" is applied to a review card, it no longer resets
the card's interval. Instead, it looks at how much the provided number
of days will change the original interval, and adjusts the interval by
that amount, so that cards that are answered earlier receive a smaller
next interval, and cards that are answered after a longer delay receive
a bonus.
For example, imagine a card was answered on day 5, and given an interval
of 10 days, so it has a due date of day 15.
- if on day 10 the due date is changed to day 12 (today+2), the card
is being scheduled 3 days earlier than it was supposed to be, so the
interval will be adjusted to 7 days.
- and if on day 10 the due date is changed to day 20, the interval will
be changed from 10 days to 15 days.
There is no separate option to reset the interval of a review card, but
it can be accomplished by forgetting the card(s), and then setting the
desired due date.
Other notes:
- Added the action to the review screen as well.
- Set the shortcut to Ctrl+Shift+D, and changed the existing Delete
Tags shortcut to Ctrl+Alt+Shift+A.
2021-02-07 11:58:16 +01:00
|
|
|
def set_due_date(self) -> None:
|
2021-04-25 11:26:55 +02:00
|
|
|
if op := set_due_date_dialog(
|
2021-03-16 13:40:37 +01:00
|
|
|
parent=self,
|
2021-03-18 02:46:11 +01:00
|
|
|
card_ids=self.selected_cards(),
|
2021-03-16 13:40:37 +01:00
|
|
|
config_key=Config.String.SET_DUE_BROWSER,
|
2021-04-25 11:26:55 +02:00
|
|
|
):
|
|
|
|
op.run_in_background()
|
Rework reschedule tool
The old rescheduling dialog's two options have been split into two
separate menu items, "Forget", and "Set Due Date"
For cards that are not review cards, "Set Due Date" behaves like the
old reschedule option, changing the cards into a review card, and
and setting both the interval and due date to the provided number of
days.
When "Set Due Date" is applied to a review card, it no longer resets
the card's interval. Instead, it looks at how much the provided number
of days will change the original interval, and adjusts the interval by
that amount, so that cards that are answered earlier receive a smaller
next interval, and cards that are answered after a longer delay receive
a bonus.
For example, imagine a card was answered on day 5, and given an interval
of 10 days, so it has a due date of day 15.
- if on day 10 the due date is changed to day 12 (today+2), the card
is being scheduled 3 days earlier than it was supposed to be, so the
interval will be adjusted to 7 days.
- and if on day 10 the due date is changed to day 20, the interval will
be changed from 10 days to 15 days.
There is no separate option to reset the interval of a review card, but
it can be accomplished by forgetting the card(s), and then setting the
desired due date.
Other notes:
- Added the action to the review screen as well.
- Set the shortcut to Ctrl+Shift+D, and changed the existing Delete
Tags shortcut to Ctrl+Alt+Shift+A.
2021-02-07 11:58:16 +01:00
|
|
|
|
2021-04-26 08:46:08 +02:00
|
|
|
@no_arg_trigger
|
|
|
|
@skip_if_selection_is_empty
|
|
|
|
@ensure_editor_saved
|
Rework reschedule tool
The old rescheduling dialog's two options have been split into two
separate menu items, "Forget", and "Set Due Date"
For cards that are not review cards, "Set Due Date" behaves like the
old reschedule option, changing the cards into a review card, and
and setting both the interval and due date to the provided number of
days.
When "Set Due Date" is applied to a review card, it no longer resets
the card's interval. Instead, it looks at how much the provided number
of days will change the original interval, and adjusts the interval by
that amount, so that cards that are answered earlier receive a smaller
next interval, and cards that are answered after a longer delay receive
a bonus.
For example, imagine a card was answered on day 5, and given an interval
of 10 days, so it has a due date of day 15.
- if on day 10 the due date is changed to day 12 (today+2), the card
is being scheduled 3 days earlier than it was supposed to be, so the
interval will be adjusted to 7 days.
- and if on day 10 the due date is changed to day 20, the interval will
be changed from 10 days to 15 days.
There is no separate option to reset the interval of a review card, but
it can be accomplished by forgetting the card(s), and then setting the
desired due date.
Other notes:
- Added the action to the review screen as well.
- Set the shortcut to Ctrl+Shift+D, and changed the existing Delete
Tags shortcut to Ctrl+Alt+Shift+A.
2021-02-07 11:58:16 +01:00
|
|
|
def forget_cards(self) -> None:
|
2022-03-09 07:51:41 +01:00
|
|
|
if op := forget_cards(
|
2021-03-16 13:40:37 +01:00
|
|
|
parent=self,
|
2021-03-18 02:46:11 +01:00
|
|
|
card_ids=self.selected_cards(),
|
2022-03-09 07:51:41 +01:00
|
|
|
context=ScheduleCardsAsNew.Context.BROWSER,
|
|
|
|
):
|
|
|
|
op.run_in_background()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# Edit: selection
|
|
|
|
######################################################################
|
|
|
|
|
2021-04-26 08:46:08 +02:00
|
|
|
@no_arg_trigger
|
|
|
|
@skip_if_selection_is_empty
|
|
|
|
@ensure_editor_saved
|
2021-02-01 00:39:55 +01:00
|
|
|
def selectNotes(self) -> None:
|
2021-03-18 02:46:11 +01:00
|
|
|
nids = self.selected_notes()
|
2012-12-21 08:51:59 +01:00
|
|
|
# clear the selection so we don't waste energy preserving it
|
2021-03-29 08:12:26 +02:00
|
|
|
self.table.clear_selection()
|
2021-01-30 02:23:32 +01:00
|
|
|
search = self.col.build_search_string(
|
2021-02-11 10:57:19 +01:00
|
|
|
SearchNode(nids=SearchNode.IdList(ids=nids))
|
2021-01-30 02:23:32 +01:00
|
|
|
)
|
2020-10-19 20:51:36 +02:00
|
|
|
self.search_for(search)
|
2021-03-29 08:12:26 +02:00
|
|
|
self.table.select_all()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-01-15 22:53:12 +01:00
|
|
|
# Hooks
|
2012-12-21 08:51:59 +01:00
|
|
|
######################################################################
|
|
|
|
|
2020-01-15 22:41:23 +01:00
|
|
|
def setupHooks(self) -> None:
|
2021-05-19 07:18:39 +02:00
|
|
|
gui_hooks.undo_state_did_change.append(self.on_undo_state_change)
|
2021-03-29 08:12:26 +02:00
|
|
|
gui_hooks.backend_will_block.append(self.table.on_backend_will_block)
|
|
|
|
gui_hooks.backend_did_block.append(self.table.on_backend_did_block)
|
2021-03-13 14:59:32 +01:00
|
|
|
gui_hooks.operation_did_execute.append(self.on_operation_did_execute)
|
2021-03-14 13:08:37 +01:00
|
|
|
gui_hooks.focus_did_change.append(self.on_focus_change)
|
2021-07-02 11:16:10 +02:00
|
|
|
gui_hooks.flag_label_did_change.append(self._update_flag_labels)
|
2022-04-19 09:10:34 +02:00
|
|
|
gui_hooks.collection_will_temporarily_close.append(self._on_temporary_close)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2020-01-15 22:41:23 +01:00
|
|
|
def teardownHooks(self) -> None:
|
2021-05-19 07:18:39 +02:00
|
|
|
gui_hooks.undo_state_did_change.remove(self.on_undo_state_change)
|
2021-03-29 08:12:26 +02:00
|
|
|
gui_hooks.backend_will_block.remove(self.table.on_backend_will_block)
|
2022-02-18 10:00:12 +01:00
|
|
|
gui_hooks.backend_did_block.remove(self.table.on_backend_did_block)
|
2021-03-13 14:59:32 +01:00
|
|
|
gui_hooks.operation_did_execute.remove(self.on_operation_did_execute)
|
2021-03-14 13:08:37 +01:00
|
|
|
gui_hooks.focus_did_change.remove(self.on_focus_change)
|
2021-07-02 11:16:10 +02:00
|
|
|
gui_hooks.flag_label_did_change.remove(self._update_flag_labels)
|
2022-04-19 09:10:34 +02:00
|
|
|
gui_hooks.collection_will_temporarily_close.remove(self._on_temporary_close)
|
|
|
|
|
|
|
|
def _on_temporary_close(self, col: Collection) -> None:
|
|
|
|
# we could reload browser columns in the future; for now we just close
|
|
|
|
self.close()
|
2020-01-15 22:53:12 +01:00
|
|
|
|
2021-03-12 08:56:13 +01:00
|
|
|
# Undo
|
|
|
|
######################################################################
|
|
|
|
|
|
|
|
def undo(self) -> None:
|
2021-04-06 06:36:13 +02:00
|
|
|
undo(parent=self)
|
2021-03-12 08:56:13 +01:00
|
|
|
|
2021-05-19 07:18:39 +02:00
|
|
|
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)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# Edit: replacing
|
|
|
|
######################################################################
|
|
|
|
|
2021-04-26 08:46:08 +02:00
|
|
|
@no_arg_trigger
|
|
|
|
@ensure_editor_saved
|
2021-02-01 00:39:55 +01:00
|
|
|
def onFindReplace(self) -> None:
|
2021-04-25 08:47:15 +02:00
|
|
|
FindAndReplaceDialog(self, mw=self.mw, note_ids=self.selected_notes())
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# Edit: finding dupes
|
|
|
|
######################################################################
|
|
|
|
|
2021-04-26 08:46:08 +02:00
|
|
|
@no_arg_trigger
|
|
|
|
@ensure_editor_saved
|
2021-02-01 00:39:55 +01:00
|
|
|
def onFindDupes(self) -> None:
|
2021-05-08 06:32:15 +02:00
|
|
|
from aqt.browser.find_duplicates import FindDuplicatesDialog
|
2013-10-03 17:07:11 +02:00
|
|
|
|
2021-05-08 06:32:15 +02:00
|
|
|
FindDuplicatesDialog(browser=self, mw=self.mw)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
|
|
|
# Jumping
|
|
|
|
######################################################################
|
|
|
|
|
2021-03-29 08:12:26 +02:00
|
|
|
def has_previous_card(self) -> bool:
|
|
|
|
return self.table.has_previous()
|
|
|
|
|
|
|
|
def has_next_card(self) -> bool:
|
|
|
|
return self.table.has_next()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 00:39:55 +01:00
|
|
|
def onPreviousCard(self) -> None:
|
2017-08-05 07:15:19 +02:00
|
|
|
self.focusTo = self.editor.currentField
|
2021-03-29 08:12:26 +02:00
|
|
|
self.editor.call_after_note_saved(self.table.to_previous_row)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 00:39:55 +01:00
|
|
|
def onNextCard(self) -> None:
|
2017-08-05 07:15:19 +02:00
|
|
|
self.focusTo = self.editor.currentField
|
2021-03-29 08:12:26 +02:00
|
|
|
self.editor.call_after_note_saved(self.table.to_next_row)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 00:39:55 +01:00
|
|
|
def onFirstCard(self) -> None:
|
2021-03-29 08:12:26 +02:00
|
|
|
self.table.to_first_row()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 00:39:55 +01:00
|
|
|
def onLastCard(self) -> None:
|
2021-03-29 08:12:26 +02:00
|
|
|
self.table.to_last_row()
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 00:39:55 +01:00
|
|
|
def onFind(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
self.form.searchEdit.setFocus()
|
|
|
|
self.form.searchEdit.lineEdit().selectAll()
|
|
|
|
|
2021-02-01 00:39:55 +01:00
|
|
|
def onNote(self) -> None:
|
2013-01-29 01:49:04 +01:00
|
|
|
self.editor.web.setFocus()
|
2019-12-06 05:22:49 +01:00
|
|
|
self.editor.loadNote(focusTo=0)
|
2012-12-21 08:51:59 +01:00
|
|
|
|
2021-02-01 00:39:55 +01:00
|
|
|
def onCardList(self) -> None:
|
2012-12-21 08:51:59 +01:00
|
|
|
self.form.tableView.setFocus()
|