anki/qt/aqt/main.py

1808 lines
60 KiB
Python
Raw Normal View History

2019-02-05 04:59:03 +01:00
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations
2020-08-16 18:33:33 +02:00
import enum
2019-12-20 10:19:03 +01:00
import gc
import os
import re
import signal
import weakref
2019-12-20 10:19:03 +01:00
from argparse import Namespace
2021-02-02 14:30:53 +01:00
from concurrent.futures import Future
from typing import Any, Literal, Sequence, TypeVar, cast
2019-12-20 10:19:03 +01:00
2020-01-24 06:48:40 +01:00
import anki
import anki.cards
import anki.sound
import aqt
import aqt.forms
2019-12-20 10:19:03 +01:00
import aqt.mediasrv
import aqt.mpv
import aqt.operations
import aqt.progress
import aqt.sound
import aqt.stats
2019-12-20 10:19:03 +01:00
import aqt.toolbar
import aqt.webview
Backup improvements (#1728) * Collection needs to be closed prior to backup even when not downgrading * Backups -> BackupLimits * Some improvements to backup_task - backup_inner now returns the error instead of logging it, so that the frontend can discover the issue when they await a backup (or create another one) - start_backup() was acquiring backup_task twice, and if another thread started a backup between the two locks, the task could have been accidentally overwritten without awaiting it * Backups no longer require a collection close - Instead of closing the collection, we ensure there is no active transaction, and flush the WAL to disk. This means the undo history is no longer lost on backup, which will be particularly useful if we add a periodic backup in the future. - Because a close is no longer required, backups are now achieved with a separate command, instead of being included in CloseCollection(). - Full sync no longer requires an extra close+reopen step, and we now wait for the backup to complete before proceeding. - Create a backup before 'check db' * Add File>Create Backup https://forums.ankiweb.net/t/anki-mac-os-no-backup-on-sync/6157 * Defer checkpoint until we know we need it When running periodic backups on a timer, we don't want to be fsync()ing unnecessarily. * Skip backup if modification time has not changed We don't want the user leaving Anki open overnight, and coming back to lots of identical backups. * Periodic backups Creates an automatic backup every 30 minutes if the collection has been modified. If there's a legacy checkpoint active, tries again 5 minutes later. * Switch to a user-configurable backup duration CreateBackup() now uses a simple force argument to determine whether the user's limits should be respected or not, and only potentially destructive ops (full download, check DB) override the user's configured limit. I considered having a separate limit for collection close and automatic backups (eg keeping the previous 5 minute limit for collection close), but that had two downsides: - When the user closes their collection at the end of the day, they'd get a recent backup. When they open the collection the next day, it would get backed up again within 5 minutes, even though not much had changed. - Multiple limits are harder to communicate to users in the UI Some remaining decisions I wasn't 100% sure about: - If force is true but the collection has not been modified, the backup will be skipped. If the user manually deleted their backups without closing Anki, they wouldn't get a new one if the mtime hadn't changed. - Force takes preference over the configured backup interval - should we be ignored the user here, or take no backups at all? Did a sneaky edit of the existing ftl string, as it hasn't been live long. * Move maybe_backup() into Collection * Use a single method for manual and periodic backups When manually creating a backup via the File menu, we no longer make the user wait until the backup completes. As we continue waiting for the backup in the background, if any errors occur, the user will get notified about it fairly quickly. * Show message to user if backup was skipped due to no changes + Don't incorrectly assert a backup will be created on force * Add "automatic" to description * Ensure we backup prior to importing colpkg if collection open The backup doesn't happen when invoked from 'open backup' in the profile screen, which matches Anki's previous behaviour. The user could potentially clobber up to 30 minutes of their work if they exited to the profile screen and restored a backup, but the alternative is we create backups every time a backup is restored, which may happen a number of times if the user is trying various ones. Or we could go back to a separate throttle amount for this case, at the cost of more complexity. * Remove the 0 special case on backup interval; minimum of 5 minutes https://github.com/ankitects/anki/pull/1728#discussion_r830876833
2022-03-21 10:40:42 +01:00
from anki import hooks
from anki._backend import RustBackend as _RustBackend
2021-04-06 06:36:13 +02:00
from anki.collection import Collection, Config, OpChanges, UndoStatus
from anki.decks import DeckDict, DeckId
2020-01-15 04:49:26 +01:00
from anki.hooks import runHook
from anki.notes import NoteId
from anki.sound import AVTag, SoundOrVideoTag
from anki.utils import (
dev_mode,
ids2str,
int_time,
is_lin,
is_mac,
is_win,
point_version,
split_fields,
)
2020-01-13 05:38:05 +01:00
from aqt import gui_hooks
2020-01-19 02:31:09 +01:00
from aqt.addons import DownloadLogEntry, check_and_prompt_for_updates, show_log_to_user
2020-06-08 12:28:11 +02:00
from aqt.dbcheck import check_db
from aqt.debug_console import show_debug_console
from aqt.emptycards import show_empty_cards
from aqt.flags import FlagManager
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.import_export.exporting import ExportDialog
from aqt.import_export.importing import (
import_collection_package_op,
import_file,
prompt_for_file_then_import,
)
from aqt.legacy import install_pylib_legacy
2020-02-11 06:51:30 +01:00
from aqt.mediacheck import check_media_db
2020-02-04 02:48:51 +01:00
from aqt.mediasync import MediaSyncer
Backup improvements (#1728) * Collection needs to be closed prior to backup even when not downgrading * Backups -> BackupLimits * Some improvements to backup_task - backup_inner now returns the error instead of logging it, so that the frontend can discover the issue when they await a backup (or create another one) - start_backup() was acquiring backup_task twice, and if another thread started a backup between the two locks, the task could have been accidentally overwritten without awaiting it * Backups no longer require a collection close - Instead of closing the collection, we ensure there is no active transaction, and flush the WAL to disk. This means the undo history is no longer lost on backup, which will be particularly useful if we add a periodic backup in the future. - Because a close is no longer required, backups are now achieved with a separate command, instead of being included in CloseCollection(). - Full sync no longer requires an extra close+reopen step, and we now wait for the backup to complete before proceeding. - Create a backup before 'check db' * Add File>Create Backup https://forums.ankiweb.net/t/anki-mac-os-no-backup-on-sync/6157 * Defer checkpoint until we know we need it When running periodic backups on a timer, we don't want to be fsync()ing unnecessarily. * Skip backup if modification time has not changed We don't want the user leaving Anki open overnight, and coming back to lots of identical backups. * Periodic backups Creates an automatic backup every 30 minutes if the collection has been modified. If there's a legacy checkpoint active, tries again 5 minutes later. * Switch to a user-configurable backup duration CreateBackup() now uses a simple force argument to determine whether the user's limits should be respected or not, and only potentially destructive ops (full download, check DB) override the user's configured limit. I considered having a separate limit for collection close and automatic backups (eg keeping the previous 5 minute limit for collection close), but that had two downsides: - When the user closes their collection at the end of the day, they'd get a recent backup. When they open the collection the next day, it would get backed up again within 5 minutes, even though not much had changed. - Multiple limits are harder to communicate to users in the UI Some remaining decisions I wasn't 100% sure about: - If force is true but the collection has not been modified, the backup will be skipped. If the user manually deleted their backups without closing Anki, they wouldn't get a new one if the mtime hadn't changed. - Force takes preference over the configured backup interval - should we be ignored the user here, or take no backups at all? Did a sneaky edit of the existing ftl string, as it hasn't been live long. * Move maybe_backup() into Collection * Use a single method for manual and periodic backups When manually creating a backup via the File menu, we no longer make the user wait until the backup completes. As we continue waiting for the backup in the background, if any errors occur, the user will get notified about it fairly quickly. * Show message to user if backup was skipped due to no changes + Don't incorrectly assert a backup will be created on force * Add "automatic" to description * Ensure we backup prior to importing colpkg if collection open The backup doesn't happen when invoked from 'open backup' in the profile screen, which matches Anki's previous behaviour. The user could potentially clobber up to 30 minutes of their work if they exited to the profile screen and restored a backup, but the alternative is we create backups every time a backup is restored, which may happen a number of times if the user is trying various ones. Or we could go back to a separate throttle amount for this case, at the cost of more complexity. * Remove the 0 special case on backup interval; minimum of 5 minutes https://github.com/ankitects/anki/pull/1728#discussion_r830876833
2022-03-21 10:40:42 +01:00
from aqt.operations import QueryOp
from aqt.operations.collection import redo, undo
from aqt.operations.deck import set_current_deck
2019-12-20 10:19:03 +01:00
from aqt.profiles import ProfileManager as ProfileManagerType
from aqt.qt import *
from aqt.qt import sip
2020-05-31 02:53:54 +02:00
from aqt.sync import sync_collection, sync_login
2020-01-19 01:05:37 +01:00
from aqt.taskman import TaskManager
from aqt.theme import Theme, theme_manager
Revamp Preferences, implement Minimalist Mode and Qt widget gallery to test GUI changes (#2289) * Create widget gallery dialog * Add WidgetGallery to debug dialog * Use enum for its intended purpose * Rename "reduced-motion" to "reduce-motion" * Add another border-radius value and make former large radius a bit smaller. * Revamp preferences, add minimalist mode Also: - create additional and missing widget styles and tweak existing ones - use single profile entry to set widget styles and reduce choices to Anki and Native * Indent QTabBar style definitions * Add missing styles for QPushButton states * Fix QTableView background * Remove unused layout from Preferences * Fix QTabView focused tab style * Highlight QCheckBox and QRadioButton when focused * Fix toolbar styles * Reorder preferences * Add setting to hide bottom toolbar * Move toolbar settings above minimalist modes * Remove unused lines * Implement proper full-screen mode * Sort imports * Tweak deck overview appearance in minimalist mode * Undo TitledContainer changes since nobody asked for that * Remove dynamic toolbar background from minimalist mode * Tweak buttons in minimalist mode * Fix some issues * Reduce theme check interval to 5s on Linux * Increase hide timer interval to 2s * Collapse toolbars with slight delay when moving to review state This should ensure the bottom toolbar collapses too. * Allow users to make hiding exclusive to full screen * Rename full screen option * Fix hide mode dropdown ignoring checkbox state on startup * Fix typing issue * Refine background image handling Giving the toolbar body the main webview height ensures background-size: cover behaves exactly the same. To prevent an override of other background properties, users are advised to only set background-images via the background-image property, not the background shorthand. * Fix top toolbar getting huge when switching modes The issue was caused by the min-height hack to align the background images. A call to web.adjustHeightToFit would set the toolbar to the same height as the main webview, as the function makes use of document.offsetHeight. * Prevent scrollbar from appearing on bottom toolbar resize * Cleanup * Put review tab before editing; fix some tab orders * Rename 'network' to 'syncing' * Fix bottom toolbar disappearing on UI > 100 * Improve Preferences layout by adding vertical spacers to the bottom also make the hiding of video_driver and its label more obvious in preferences.py. * Fix bottom toolbar animating on startup Also fix bottom toolbar not appearing when unchecking hide mode in reviewer. * Hide/Show menubar in fullscreen mode along with toolbar * Attempt to fix broken native theme on macOS * Format * Improve native theme on other systems by not forcing palette with the caveat that theme switching can get weird. * Fix theme switching in native style * Remove redundant condition * Add back check for Qt5 to prevent theme issues * Add check for macOS before setting fusion theme * Do not force scrollbar styles on macOS * Remove all of that crazy theme logic * Use canvas instead of button-bg for ColorRole.Button * Make sure Anki style is always based on Fusion otherwise we can't guarantee the same look on all systems. * Explicitly apply default style when Anki style is not selected This should fix the style not switching back after it was selected. * Remove reduncant default_palette * Revert 8af4c1cc2 On Mac with native theme, both Qt5 and Qt6 look correct already. On the Anki theme, without this change, we get the fusion-style scrollbars instead of the rounded ones. * Rename AnkiStyles enum to WidgetStyle * Fix theme switching shades on same theme * Format * Remove unused placeholderText that caused an error when opening the widget gallery on Qt5. * Check for full screen windowState using bitwise operator to prevent error in Qt5. Credit: https://stackoverflow.com/a/65425151 * Hide style option on Windows also exclude native option from dropdown just in case. * Format * Minor naming tweak
2023-01-18 12:24:16 +01:00
from aqt.toolbar import BottomWebView, Toolbar, TopWebView
from aqt.undo import UndoActionsInfo
2019-12-23 01:34:10 +01:00
from aqt.utils import (
HelpPage,
KeyboardModifiersPressed,
2019-12-23 01:34:10 +01:00
askUser,
checkInvalidFilename,
current_window,
disallow_full_screen,
2019-12-23 01:34:10 +01:00
getFile,
getOnlyText,
openHelp,
openLink,
restoreGeom,
restoreState,
saveGeom,
saveState,
2019-12-23 01:34:10 +01:00
showInfo,
showWarning,
tooltip,
tr,
2019-12-23 01:34:10 +01:00
)
from aqt.webview import AnkiWebView, AnkiWebViewKind
2013-12-06 05:27:13 +01:00
install_pylib_legacy()
2021-03-14 10:54:15 +01:00
MainWindowState = Literal[
"startup", "deckBrowser", "overview", "review", "resetRequired", "profileManager"
]
T = TypeVar("T")
class MainWebView(AnkiWebView):
def __init__(self, mw: AnkiQt) -> None:
AnkiWebView.__init__(self, kind=AnkiWebViewKind.MAIN)
self.mw = mw
self.setFocusPolicy(Qt.FocusPolicy.WheelFocus)
self.setMinimumWidth(400)
self.setAcceptDrops(True)
# Importing files via drag & drop
##########################################################################
def dragEnterEvent(self, event: QDragEnterEvent) -> None:
if self.mw.state != "deckBrowser":
return super().dragEnterEvent(event)
mime = event.mimeData()
if not mime.hasUrls():
return
for url in mime.urls():
path = url.toLocalFile()
if not os.path.exists(path) or os.path.isdir(path):
return
event.accept()
def dropEvent(self, event: QDropEvent) -> None:
import aqt.importing
if self.mw.state != "deckBrowser":
return super().dropEvent(event)
mime = event.mimeData()
paths = [url.toLocalFile() for url in mime.urls()]
deck_paths = filter(lambda p: not p.endswith(".colpkg"), paths)
for path in deck_paths:
if not self.mw.pm.legacy_import_export():
import_file(self.mw, path)
else:
aqt.importing.importFile(self.mw, path)
# importing continues after the above call returns, so it is not
# currently safe for us to import more than one file at once
return
Auto-hide toolbar in Reviewer (#2262) * Give webviews a slide-in animation if reduced motion isn't set. * Auto-hide toolbar in review mode moving the mouse above the main webview expands the toolbar. When the mouse leaves the toolbar, it will collapse after a delay of 2s. * Save some space on bottom toolbars * Use props for all hard-coded transition durations and decrease most commonly used duration (200ms) to 150ms. * Move auto-hide logic into ToolbarWebView and handle auto-hide specific events in the respective webview subclasses. * Fix typing issues * Fix flickering issue * Add auto_hide_toolbar opt-in to preferences * Rename hide_toolbar to collapse_toolbar to better describe the dock-like behaviour. * Rename setting to minimize_distractions * Reduce calls to pm in eventFilter * Run formatter * Revert setting title to something more specific * Increase default animation time to 180ms * Inset toolbar in review mode when auto-hide is not enabled. * Use card background on toolbar and add glass effect * Use flatten/elevate over inset/outset * Use flatten/elevate over inset/outset * Update toolbar.py * Fix toolbar background delay * Tweak styles * Use "collapse" instead of "auto-hide" * Fix background misalignment in collapse mode * Do not collapse toolbar when pointer is outside MainWebView * Reduce hide_timer interval to 1000ms * Use CSS to hide toolbar instead of setting webview height * Add guard to prevent backdrop-filter: blur on Qt 5.14 * Apply transition to body instead of toolbar to not complicate things for #2301. * Fix Qt 5.14 and apply guard globally * Fix background image scaling difference * Tweak preference wording (dae)
2023-01-09 05:39:31 +01:00
# Main webview specific event handling
def eventFilter(self, obj, evt):
if handled := super().eventFilter(obj, evt):
return handled
if evt.type() == QEvent.Type.Leave:
Revamp Preferences, implement Minimalist Mode and Qt widget gallery to test GUI changes (#2289) * Create widget gallery dialog * Add WidgetGallery to debug dialog * Use enum for its intended purpose * Rename "reduced-motion" to "reduce-motion" * Add another border-radius value and make former large radius a bit smaller. * Revamp preferences, add minimalist mode Also: - create additional and missing widget styles and tweak existing ones - use single profile entry to set widget styles and reduce choices to Anki and Native * Indent QTabBar style definitions * Add missing styles for QPushButton states * Fix QTableView background * Remove unused layout from Preferences * Fix QTabView focused tab style * Highlight QCheckBox and QRadioButton when focused * Fix toolbar styles * Reorder preferences * Add setting to hide bottom toolbar * Move toolbar settings above minimalist modes * Remove unused lines * Implement proper full-screen mode * Sort imports * Tweak deck overview appearance in minimalist mode * Undo TitledContainer changes since nobody asked for that * Remove dynamic toolbar background from minimalist mode * Tweak buttons in minimalist mode * Fix some issues * Reduce theme check interval to 5s on Linux * Increase hide timer interval to 2s * Collapse toolbars with slight delay when moving to review state This should ensure the bottom toolbar collapses too. * Allow users to make hiding exclusive to full screen * Rename full screen option * Fix hide mode dropdown ignoring checkbox state on startup * Fix typing issue * Refine background image handling Giving the toolbar body the main webview height ensures background-size: cover behaves exactly the same. To prevent an override of other background properties, users are advised to only set background-images via the background-image property, not the background shorthand. * Fix top toolbar getting huge when switching modes The issue was caused by the min-height hack to align the background images. A call to web.adjustHeightToFit would set the toolbar to the same height as the main webview, as the function makes use of document.offsetHeight. * Prevent scrollbar from appearing on bottom toolbar resize * Cleanup * Put review tab before editing; fix some tab orders * Rename 'network' to 'syncing' * Fix bottom toolbar disappearing on UI > 100 * Improve Preferences layout by adding vertical spacers to the bottom also make the hiding of video_driver and its label more obvious in preferences.py. * Fix bottom toolbar animating on startup Also fix bottom toolbar not appearing when unchecking hide mode in reviewer. * Hide/Show menubar in fullscreen mode along with toolbar * Attempt to fix broken native theme on macOS * Format * Improve native theme on other systems by not forcing palette with the caveat that theme switching can get weird. * Fix theme switching in native style * Remove redundant condition * Add back check for Qt5 to prevent theme issues * Add check for macOS before setting fusion theme * Do not force scrollbar styles on macOS * Remove all of that crazy theme logic * Use canvas instead of button-bg for ColorRole.Button * Make sure Anki style is always based on Fusion otherwise we can't guarantee the same look on all systems. * Explicitly apply default style when Anki style is not selected This should fix the style not switching back after it was selected. * Remove reduncant default_palette * Revert 8af4c1cc2 On Mac with native theme, both Qt5 and Qt6 look correct already. On the Anki theme, without this change, we get the fusion-style scrollbars instead of the rounded ones. * Rename AnkiStyles enum to WidgetStyle * Fix theme switching shades on same theme * Format * Remove unused placeholderText that caused an error when opening the widget gallery on Qt5. * Check for full screen windowState using bitwise operator to prevent error in Qt5. Credit: https://stackoverflow.com/a/65425151 * Hide style option on Windows also exclude native option from dropdown just in case. * Format * Minor naming tweak
2023-01-18 12:24:16 +01:00
# Show toolbar when mouse moves outside main webview
# and automatically hide it with delay after mouse has entered again
if self.mw.pm.hide_top_bar() or self.mw.pm.hide_bottom_bar():
self.mw.toolbarWeb.show()
self.mw.bottomWeb.show()
Auto-hide toolbar in Reviewer (#2262) * Give webviews a slide-in animation if reduced motion isn't set. * Auto-hide toolbar in review mode moving the mouse above the main webview expands the toolbar. When the mouse leaves the toolbar, it will collapse after a delay of 2s. * Save some space on bottom toolbars * Use props for all hard-coded transition durations and decrease most commonly used duration (200ms) to 150ms. * Move auto-hide logic into ToolbarWebView and handle auto-hide specific events in the respective webview subclasses. * Fix typing issues * Fix flickering issue * Add auto_hide_toolbar opt-in to preferences * Rename hide_toolbar to collapse_toolbar to better describe the dock-like behaviour. * Rename setting to minimize_distractions * Reduce calls to pm in eventFilter * Run formatter * Revert setting title to something more specific * Increase default animation time to 180ms * Inset toolbar in review mode when auto-hide is not enabled. * Use card background on toolbar and add glass effect * Use flatten/elevate over inset/outset * Use flatten/elevate over inset/outset * Update toolbar.py * Fix toolbar background delay * Tweak styles * Use "collapse" instead of "auto-hide" * Fix background misalignment in collapse mode * Do not collapse toolbar when pointer is outside MainWebView * Reduce hide_timer interval to 1000ms * Use CSS to hide toolbar instead of setting webview height * Add guard to prevent backdrop-filter: blur on Qt 5.14 * Apply transition to body instead of toolbar to not complicate things for #2301. * Fix Qt 5.14 and apply guard globally * Fix background image scaling difference * Tweak preference wording (dae)
2023-01-09 05:39:31 +01:00
return True
if evt.type() == QEvent.Type.Enter:
Revamp Preferences, implement Minimalist Mode and Qt widget gallery to test GUI changes (#2289) * Create widget gallery dialog * Add WidgetGallery to debug dialog * Use enum for its intended purpose * Rename "reduced-motion" to "reduce-motion" * Add another border-radius value and make former large radius a bit smaller. * Revamp preferences, add minimalist mode Also: - create additional and missing widget styles and tweak existing ones - use single profile entry to set widget styles and reduce choices to Anki and Native * Indent QTabBar style definitions * Add missing styles for QPushButton states * Fix QTableView background * Remove unused layout from Preferences * Fix QTabView focused tab style * Highlight QCheckBox and QRadioButton when focused * Fix toolbar styles * Reorder preferences * Add setting to hide bottom toolbar * Move toolbar settings above minimalist modes * Remove unused lines * Implement proper full-screen mode * Sort imports * Tweak deck overview appearance in minimalist mode * Undo TitledContainer changes since nobody asked for that * Remove dynamic toolbar background from minimalist mode * Tweak buttons in minimalist mode * Fix some issues * Reduce theme check interval to 5s on Linux * Increase hide timer interval to 2s * Collapse toolbars with slight delay when moving to review state This should ensure the bottom toolbar collapses too. * Allow users to make hiding exclusive to full screen * Rename full screen option * Fix hide mode dropdown ignoring checkbox state on startup * Fix typing issue * Refine background image handling Giving the toolbar body the main webview height ensures background-size: cover behaves exactly the same. To prevent an override of other background properties, users are advised to only set background-images via the background-image property, not the background shorthand. * Fix top toolbar getting huge when switching modes The issue was caused by the min-height hack to align the background images. A call to web.adjustHeightToFit would set the toolbar to the same height as the main webview, as the function makes use of document.offsetHeight. * Prevent scrollbar from appearing on bottom toolbar resize * Cleanup * Put review tab before editing; fix some tab orders * Rename 'network' to 'syncing' * Fix bottom toolbar disappearing on UI > 100 * Improve Preferences layout by adding vertical spacers to the bottom also make the hiding of video_driver and its label more obvious in preferences.py. * Fix bottom toolbar animating on startup Also fix bottom toolbar not appearing when unchecking hide mode in reviewer. * Hide/Show menubar in fullscreen mode along with toolbar * Attempt to fix broken native theme on macOS * Format * Improve native theme on other systems by not forcing palette with the caveat that theme switching can get weird. * Fix theme switching in native style * Remove redundant condition * Add back check for Qt5 to prevent theme issues * Add check for macOS before setting fusion theme * Do not force scrollbar styles on macOS * Remove all of that crazy theme logic * Use canvas instead of button-bg for ColorRole.Button * Make sure Anki style is always based on Fusion otherwise we can't guarantee the same look on all systems. * Explicitly apply default style when Anki style is not selected This should fix the style not switching back after it was selected. * Remove reduncant default_palette * Revert 8af4c1cc2 On Mac with native theme, both Qt5 and Qt6 look correct already. On the Anki theme, without this change, we get the fusion-style scrollbars instead of the rounded ones. * Rename AnkiStyles enum to WidgetStyle * Fix theme switching shades on same theme * Format * Remove unused placeholderText that caused an error when opening the widget gallery on Qt5. * Check for full screen windowState using bitwise operator to prevent error in Qt5. Credit: https://stackoverflow.com/a/65425151 * Hide style option on Windows also exclude native option from dropdown just in case. * Format * Minor naming tweak
2023-01-18 12:24:16 +01:00
self.mw.toolbarWeb.hide_timer.start()
self.mw.bottomWeb.hide_timer.start()
return True
Auto-hide toolbar in Reviewer (#2262) * Give webviews a slide-in animation if reduced motion isn't set. * Auto-hide toolbar in review mode moving the mouse above the main webview expands the toolbar. When the mouse leaves the toolbar, it will collapse after a delay of 2s. * Save some space on bottom toolbars * Use props for all hard-coded transition durations and decrease most commonly used duration (200ms) to 150ms. * Move auto-hide logic into ToolbarWebView and handle auto-hide specific events in the respective webview subclasses. * Fix typing issues * Fix flickering issue * Add auto_hide_toolbar opt-in to preferences * Rename hide_toolbar to collapse_toolbar to better describe the dock-like behaviour. * Rename setting to minimize_distractions * Reduce calls to pm in eventFilter * Run formatter * Revert setting title to something more specific * Increase default animation time to 180ms * Inset toolbar in review mode when auto-hide is not enabled. * Use card background on toolbar and add glass effect * Use flatten/elevate over inset/outset * Use flatten/elevate over inset/outset * Update toolbar.py * Fix toolbar background delay * Tweak styles * Use "collapse" instead of "auto-hide" * Fix background misalignment in collapse mode * Do not collapse toolbar when pointer is outside MainWebView * Reduce hide_timer interval to 1000ms * Use CSS to hide toolbar instead of setting webview height * Add guard to prevent backdrop-filter: blur on Qt 5.14 * Apply transition to body instead of toolbar to not complicate things for #2301. * Fix Qt 5.14 and apply guard globally * Fix background image scaling difference * Tweak preference wording (dae)
2023-01-09 05:39:31 +01:00
return False
class AnkiQt(QMainWindow):
2020-05-20 09:56:52 +02:00
col: Collection
2019-12-20 09:43:52 +01:00
pm: ProfileManagerType
web: MainWebView
Revamp Preferences, implement Minimalist Mode and Qt widget gallery to test GUI changes (#2289) * Create widget gallery dialog * Add WidgetGallery to debug dialog * Use enum for its intended purpose * Rename "reduced-motion" to "reduce-motion" * Add another border-radius value and make former large radius a bit smaller. * Revamp preferences, add minimalist mode Also: - create additional and missing widget styles and tweak existing ones - use single profile entry to set widget styles and reduce choices to Anki and Native * Indent QTabBar style definitions * Add missing styles for QPushButton states * Fix QTableView background * Remove unused layout from Preferences * Fix QTabView focused tab style * Highlight QCheckBox and QRadioButton when focused * Fix toolbar styles * Reorder preferences * Add setting to hide bottom toolbar * Move toolbar settings above minimalist modes * Remove unused lines * Implement proper full-screen mode * Sort imports * Tweak deck overview appearance in minimalist mode * Undo TitledContainer changes since nobody asked for that * Remove dynamic toolbar background from minimalist mode * Tweak buttons in minimalist mode * Fix some issues * Reduce theme check interval to 5s on Linux * Increase hide timer interval to 2s * Collapse toolbars with slight delay when moving to review state This should ensure the bottom toolbar collapses too. * Allow users to make hiding exclusive to full screen * Rename full screen option * Fix hide mode dropdown ignoring checkbox state on startup * Fix typing issue * Refine background image handling Giving the toolbar body the main webview height ensures background-size: cover behaves exactly the same. To prevent an override of other background properties, users are advised to only set background-images via the background-image property, not the background shorthand. * Fix top toolbar getting huge when switching modes The issue was caused by the min-height hack to align the background images. A call to web.adjustHeightToFit would set the toolbar to the same height as the main webview, as the function makes use of document.offsetHeight. * Prevent scrollbar from appearing on bottom toolbar resize * Cleanup * Put review tab before editing; fix some tab orders * Rename 'network' to 'syncing' * Fix bottom toolbar disappearing on UI > 100 * Improve Preferences layout by adding vertical spacers to the bottom also make the hiding of video_driver and its label more obvious in preferences.py. * Fix bottom toolbar animating on startup Also fix bottom toolbar not appearing when unchecking hide mode in reviewer. * Hide/Show menubar in fullscreen mode along with toolbar * Attempt to fix broken native theme on macOS * Format * Improve native theme on other systems by not forcing palette with the caveat that theme switching can get weird. * Fix theme switching in native style * Remove redundant condition * Add back check for Qt5 to prevent theme issues * Add check for macOS before setting fusion theme * Do not force scrollbar styles on macOS * Remove all of that crazy theme logic * Use canvas instead of button-bg for ColorRole.Button * Make sure Anki style is always based on Fusion otherwise we can't guarantee the same look on all systems. * Explicitly apply default style when Anki style is not selected This should fix the style not switching back after it was selected. * Remove reduncant default_palette * Revert 8af4c1cc2 On Mac with native theme, both Qt5 and Qt6 look correct already. On the Anki theme, without this change, we get the fusion-style scrollbars instead of the rounded ones. * Rename AnkiStyles enum to WidgetStyle * Fix theme switching shades on same theme * Format * Remove unused placeholderText that caused an error when opening the widget gallery on Qt5. * Check for full screen windowState using bitwise operator to prevent error in Qt5. Credit: https://stackoverflow.com/a/65425151 * Hide style option on Windows also exclude native option from dropdown just in case. * Format * Minor naming tweak
2023-01-18 12:24:16 +01:00
bottomWeb: BottomWebView
2019-12-20 09:43:52 +01:00
2019-12-23 01:34:10 +01:00
def __init__(
self,
app: aqt.AnkiApp,
2019-12-23 01:34:10 +01:00
profileManager: ProfileManagerType,
backend: _RustBackend,
2019-12-23 01:34:10 +01:00
opts: Namespace,
args: list[Any],
2019-12-23 01:34:10 +01:00
) -> None:
QMainWindow.__init__(self)
2020-03-14 00:45:00 +01:00
self.backend = backend
2021-03-14 10:54:15 +01:00
self.state: MainWindowState = "startup"
self.opts = opts
self.col: Collection | None = None
self.taskman = TaskManager(self)
self.media_syncer = MediaSyncer(self)
aqt.mw = self
self.app = app
self.pm = profileManager
Revamp Preferences, implement Minimalist Mode and Qt widget gallery to test GUI changes (#2289) * Create widget gallery dialog * Add WidgetGallery to debug dialog * Use enum for its intended purpose * Rename "reduced-motion" to "reduce-motion" * Add another border-radius value and make former large radius a bit smaller. * Revamp preferences, add minimalist mode Also: - create additional and missing widget styles and tweak existing ones - use single profile entry to set widget styles and reduce choices to Anki and Native * Indent QTabBar style definitions * Add missing styles for QPushButton states * Fix QTableView background * Remove unused layout from Preferences * Fix QTabView focused tab style * Highlight QCheckBox and QRadioButton when focused * Fix toolbar styles * Reorder preferences * Add setting to hide bottom toolbar * Move toolbar settings above minimalist modes * Remove unused lines * Implement proper full-screen mode * Sort imports * Tweak deck overview appearance in minimalist mode * Undo TitledContainer changes since nobody asked for that * Remove dynamic toolbar background from minimalist mode * Tweak buttons in minimalist mode * Fix some issues * Reduce theme check interval to 5s on Linux * Increase hide timer interval to 2s * Collapse toolbars with slight delay when moving to review state This should ensure the bottom toolbar collapses too. * Allow users to make hiding exclusive to full screen * Rename full screen option * Fix hide mode dropdown ignoring checkbox state on startup * Fix typing issue * Refine background image handling Giving the toolbar body the main webview height ensures background-size: cover behaves exactly the same. To prevent an override of other background properties, users are advised to only set background-images via the background-image property, not the background shorthand. * Fix top toolbar getting huge when switching modes The issue was caused by the min-height hack to align the background images. A call to web.adjustHeightToFit would set the toolbar to the same height as the main webview, as the function makes use of document.offsetHeight. * Prevent scrollbar from appearing on bottom toolbar resize * Cleanup * Put review tab before editing; fix some tab orders * Rename 'network' to 'syncing' * Fix bottom toolbar disappearing on UI > 100 * Improve Preferences layout by adding vertical spacers to the bottom also make the hiding of video_driver and its label more obvious in preferences.py. * Fix bottom toolbar animating on startup Also fix bottom toolbar not appearing when unchecking hide mode in reviewer. * Hide/Show menubar in fullscreen mode along with toolbar * Attempt to fix broken native theme on macOS * Format * Improve native theme on other systems by not forcing palette with the caveat that theme switching can get weird. * Fix theme switching in native style * Remove redundant condition * Add back check for Qt5 to prevent theme issues * Add check for macOS before setting fusion theme * Do not force scrollbar styles on macOS * Remove all of that crazy theme logic * Use canvas instead of button-bg for ColorRole.Button * Make sure Anki style is always based on Fusion otherwise we can't guarantee the same look on all systems. * Explicitly apply default style when Anki style is not selected This should fix the style not switching back after it was selected. * Remove reduncant default_palette * Revert 8af4c1cc2 On Mac with native theme, both Qt5 and Qt6 look correct already. On the Anki theme, without this change, we get the fusion-style scrollbars instead of the rounded ones. * Rename AnkiStyles enum to WidgetStyle * Fix theme switching shades on same theme * Format * Remove unused placeholderText that caused an error when opening the widget gallery on Qt5. * Check for full screen windowState using bitwise operator to prevent error in Qt5. Credit: https://stackoverflow.com/a/65425151 * Hide style option on Windows also exclude native option from dropdown just in case. * Format * Minor naming tweak
2023-01-18 12:24:16 +01:00
self.fullscreen = False
# init rest of app
self.safeMode = (
bool(self.app.queryKeyboardModifiers() & Qt.KeyboardModifier.ShiftModifier)
or self.opts.safemode
)
try:
self.setupUI()
self.setupAddons(args)
self.finish_ui_setup()
except:
showInfo(tr.qt_misc_error_during_startup(val=traceback.format_exc()))
sys.exit(1)
2013-05-17 08:32:17 +02:00
# must call this after ui set up
if self.safeMode:
2021-03-26 04:48:26 +01:00
tooltip(tr.qt_misc_shift_key_was_held_down_skipping())
# were we given a file to import?
if args and args[0] and not self._isAddon(args[0]):
self.onAppMsg(args[0])
# Load profile in a timer so we can let the window finish init and not
# close on profile load error.
if is_win:
fn = self.setupProfileAfterWebviewsLoaded
else:
fn = self.setupProfile
2021-02-01 14:28:21 +01:00
def on_window_init() -> None:
fn()
gui_hooks.main_window_did_init()
self.progress.single_shot(10, on_window_init, False)
2019-12-20 09:43:52 +01:00
def setupUI(self) -> None:
self.col = None
self.disable_automatic_garbage_collection()
self.setupAppMsg()
self.setupKeys()
self.setupThreads()
self.setupMediaServer()
self.setupSpellCheck()
self.setupProgress()
self.setupStyle()
self.setupMainWindow()
self.setupSystemSpecific()
self.setupMenus()
self.setupErrorHandler()
self.setupSignals()
2013-05-22 05:27:37 +02:00
self.setupHooks()
2020-02-05 03:38:36 +01:00
self.setup_timers()
self.updateTitleBar()
self.setup_focus()
# screens
self.setupDeckBrowser()
self.setupOverview()
self.setupReviewer()
def finish_ui_setup(self) -> None:
"Actions that are deferred until after add-on loading."
self.toolbar.draw()
# add-ons are only available here after setupAddons
gui_hooks.reviewer_did_init(self.reviewer)
2021-02-01 14:28:21 +01:00
def setupProfileAfterWebviewsLoaded(self) -> None:
for w in (self.web, self.bottomWeb):
if not w._domDone:
self.progress.single_shot(
2019-12-23 01:34:10 +01:00
10,
self.setupProfileAfterWebviewsLoaded,
False,
)
return
else:
w.requiresCol = True
self.setupProfile()
def weakref(self) -> AnkiQt:
"Shortcut to create a weak reference that doesn't break code completion."
return weakref.proxy(self) # type: ignore
def setup_focus(self) -> None:
qconnect(self.app.focusChanged, self.on_focus_changed)
def on_focus_changed(self, old: QWidget, new: QWidget) -> None:
gui_hooks.focus_did_change(new, old)
# Profiles
##########################################################################
class ProfileManager(QMainWindow):
onClose = pyqtSignal()
closeFires = True
def closeEvent(self, evt: QCloseEvent) -> None:
if self.closeFires:
self.onClose.emit() # type: ignore
evt.accept()
2021-02-01 14:28:21 +01:00
def closeWithoutQuitting(self) -> None:
self.closeFires = False
self.close()
self.closeFires = True
2019-12-20 09:43:52 +01:00
def setupProfile(self) -> None:
2019-12-23 01:34:10 +01:00
if self.pm.meta["firstRun"]:
# load the new deck user profile
self.pm.load(self.pm.profiles()[0])
2019-12-23 01:34:10 +01:00
self.pm.meta["firstRun"] = False
self.pm.save()
self.pendingImport: str | None = None
Backups (#1685) * Add zstd dep * Implement backend backup with zstd * Implement backup thinning * Write backup meta * Use new file ending anki21b * Asynchronously backup on collection close in Rust * Revert "Add zstd dep" This reverts commit 3fcb2141d2be15f907269d13275c41971431385c. * Add zstd again * Take backup col path from col struct * Fix formatting * Implement backup restoring on backend * Normalize restored media file names * Refactor `extract_legacy_data()` A bit cumbersome due to borrowing rules. * Refactor * Make thinning calendar-based and gradual * Consider last kept backups of previous stages * Import full apkgs and colpkgs with backend * Expose new backup settings * Test `BackupThinner` and make it deterministic * Mark backup_path when closing optional * Delete leaky timer * Add progress updates for restoring media * Write restored collection to tempfile first * Do collection compression in the background thread This has us currently storing an uncompressed and compressed copy of the collection in memory (not ideal), but means the collection can be closed without waiting for compression to complete. On a large collection, this takes a close and reopen from about 0.55s to about 0.07s. The old backup code for comparison: about 0.35s for compression off, about 8.5s for zip compression. * Use multithreading in zstd compression On my system, this reduces the compression time of a large collection from about 0.55s to 0.08s. * Stream compressed collection data into zip file * Tweak backup explanation + Fix incorrect tab order for ignore accents option * Decouple restoring backup and full import In the first case, no profile is opened, unless the new collection succeeds to load. In the second case, either the old collection is reloaded or the new one is loaded. * Fix number gap in Progress message * Don't revert backup when media fails but report it * Tweak error flow * Remove native BackupLimits enum * Fix type annotation * Add thinning test for whole year * Satisfy linter * Await async backup to finish * Move restart disclaimer out of backup tab Should be visible regardless of the current tab. * Write restored collection in chunks * Refactor * Write media in chunks and refactor * Log error if removing file fails * join_backup_task -> await_backup_completion * Refactor backup.rs * Refactor backup meta and collection extraction * Fix wrong error being returned * Call sync_all() on new collection * Add ImportError * Store logger in Backend, instead of creating one on demand init_backend() accepts a Logger rather than a log file, to allow other callers to customize the logger if they wish. In the future we may want to explore using the tracing crate as an alternative; it's a bit more ergonomic, as a logger doesn't need to be passed around, and it plays more nicely with async code. * Sync file contents prior to rename; sync folder after rename. * Limit backup creation to once per 30 min * Use zstd::stream::copy_decode * Make importing abortable * Don't revert if backup media is aborted * Set throttle implicitly * Change force flag to minimum_backup_interval * Don't attempt to open folders on Windows * Join last backup thread before starting new one Also refactor. * Disable auto sync and backup when restoring again * Force backup on full download * Include the reason why a media file import failed, and the file path - Introduce a FileIoError that contains a string representation of the underlying I/O error, and an associated path. There are a few places in the code where we're currently manually including the filename in a custom error message, and this is a step towards a more consistent approach (but we may be better served with a more general approach in the future similar to Anyhow's .context()) - Move the error message into importing.ftl, as it's a bit neater when error messages live in the same file as the rest of the messages associated with some functionality. * Fix importing of media files * Minor wording tweaks * Save an allocation I18n strings with replacements are already strings, so we can skip the extra allocation. Not that it matters here at all. * Terminate import if file missing from archive If a third-party tool is creating invalid archives, the user should know about it. This should be rare, so I did not attempt to make it translatable. * Skip multithreaded compression on small collections Co-authored-by: Damien Elmes <gpg@ankiweb.net>
2022-03-07 06:11:31 +01:00
self.restoring_backup = False
# - if a valid profile was provided on commandline, we load it
# - if an invalid profile was provided, we skip this step and show the picker
# - if no profile was provided, we use this step
if not self.pm.name and not self.pm.invalid_profile_provided_on_commandline:
profs = self.pm.profiles()
name = self.pm.last_loaded_profile_name()
if len(profs) == 1:
self.pm.load(profs[0])
elif name in profs:
self.pm.load(name)
if not self.pm.name:
self.showProfileManager()
else:
self.loadProfile()
2019-12-20 09:43:52 +01:00
def showProfileManager(self) -> None:
self.pm.profile = None
self.moveToState("profileManager")
d = self.profileDiag = self.ProfileManager()
f = self.profileForm = aqt.forms.profiles.Ui_MainWindow()
f.setupUi(d)
qconnect(f.login.clicked, self.onOpenProfile)
qconnect(f.profiles.itemDoubleClicked, self.onOpenProfile)
qconnect(f.openBackup.clicked, self.onOpenBackup)
qconnect(f.quit.clicked, d.close)
qconnect(d.onClose, self.cleanupAndExit)
qconnect(f.add.clicked, self.onAddProfile)
qconnect(f.rename.clicked, self.onRenameProfile)
qconnect(f.delete_2.clicked, self.onRemProfile)
qconnect(f.profiles.currentRowChanged, self.onProfileRowChange)
f.statusbar.setVisible(False)
qconnect(f.downgrade_button.clicked, self._on_downgrade)
2021-03-26 04:48:26 +01:00
f.downgrade_button.setText(tr.profiles_downgrade_and_quit())
# enter key opens profile
2019-12-23 01:34:10 +01:00
QShortcut(QKeySequence("Return"), d, activated=self.onOpenProfile) # type: ignore
self.refreshProfilesList()
# raise first, for osx testing
d.show()
d.activateWindow()
d.raise_()
2019-12-20 09:43:52 +01:00
def refreshProfilesList(self) -> None:
f = self.profileForm
f.profiles.clear()
profs = self.pm.profiles()
f.profiles.addItems(profs)
try:
idx = profs.index(self.pm.name)
except:
idx = 0
f.profiles.setCurrentRow(idx)
2019-12-20 09:43:52 +01:00
def onProfileRowChange(self, n: int) -> None:
if n < 0:
# called on .clear()
return
name = self.pm.profiles()[n]
self.pm.load(name)
2021-02-01 14:28:21 +01:00
def openProfile(self) -> None:
name = self.pm.profiles()[self.profileForm.profiles.currentRow()]
2021-02-01 14:28:21 +01:00
self.pm.load(name)
return
Backups (#1685) * Add zstd dep * Implement backend backup with zstd * Implement backup thinning * Write backup meta * Use new file ending anki21b * Asynchronously backup on collection close in Rust * Revert "Add zstd dep" This reverts commit 3fcb2141d2be15f907269d13275c41971431385c. * Add zstd again * Take backup col path from col struct * Fix formatting * Implement backup restoring on backend * Normalize restored media file names * Refactor `extract_legacy_data()` A bit cumbersome due to borrowing rules. * Refactor * Make thinning calendar-based and gradual * Consider last kept backups of previous stages * Import full apkgs and colpkgs with backend * Expose new backup settings * Test `BackupThinner` and make it deterministic * Mark backup_path when closing optional * Delete leaky timer * Add progress updates for restoring media * Write restored collection to tempfile first * Do collection compression in the background thread This has us currently storing an uncompressed and compressed copy of the collection in memory (not ideal), but means the collection can be closed without waiting for compression to complete. On a large collection, this takes a close and reopen from about 0.55s to about 0.07s. The old backup code for comparison: about 0.35s for compression off, about 8.5s for zip compression. * Use multithreading in zstd compression On my system, this reduces the compression time of a large collection from about 0.55s to 0.08s. * Stream compressed collection data into zip file * Tweak backup explanation + Fix incorrect tab order for ignore accents option * Decouple restoring backup and full import In the first case, no profile is opened, unless the new collection succeeds to load. In the second case, either the old collection is reloaded or the new one is loaded. * Fix number gap in Progress message * Don't revert backup when media fails but report it * Tweak error flow * Remove native BackupLimits enum * Fix type annotation * Add thinning test for whole year * Satisfy linter * Await async backup to finish * Move restart disclaimer out of backup tab Should be visible regardless of the current tab. * Write restored collection in chunks * Refactor * Write media in chunks and refactor * Log error if removing file fails * join_backup_task -> await_backup_completion * Refactor backup.rs * Refactor backup meta and collection extraction * Fix wrong error being returned * Call sync_all() on new collection * Add ImportError * Store logger in Backend, instead of creating one on demand init_backend() accepts a Logger rather than a log file, to allow other callers to customize the logger if they wish. In the future we may want to explore using the tracing crate as an alternative; it's a bit more ergonomic, as a logger doesn't need to be passed around, and it plays more nicely with async code. * Sync file contents prior to rename; sync folder after rename. * Limit backup creation to once per 30 min * Use zstd::stream::copy_decode * Make importing abortable * Don't revert if backup media is aborted * Set throttle implicitly * Change force flag to minimum_backup_interval * Don't attempt to open folders on Windows * Join last backup thread before starting new one Also refactor. * Disable auto sync and backup when restoring again * Force backup on full download * Include the reason why a media file import failed, and the file path - Introduce a FileIoError that contains a string representation of the underlying I/O error, and an associated path. There are a few places in the code where we're currently manually including the filename in a custom error message, and this is a step towards a more consistent approach (but we may be better served with a more general approach in the future similar to Anyhow's .context()) - Move the error message into importing.ftl, as it's a bit neater when error messages live in the same file as the rest of the messages associated with some functionality. * Fix importing of media files * Minor wording tweaks * Save an allocation I18n strings with replacements are already strings, so we can skip the extra allocation. Not that it matters here at all. * Terminate import if file missing from archive If a third-party tool is creating invalid archives, the user should know about it. This should be rare, so I did not attempt to make it translatable. * Skip multithreaded compression on small collections Co-authored-by: Damien Elmes <gpg@ankiweb.net>
2022-03-07 06:11:31 +01:00
def onOpenProfile(self, *, callback: Callable[[], None] | None = None) -> None:
def on_done() -> None:
self.profileDiag.closeWithoutQuitting()
if callback:
callback()
self.profileDiag.hide()
# code flow is confusing here - if load fails, profile dialog
# will be shown again
Backups (#1685) * Add zstd dep * Implement backend backup with zstd * Implement backup thinning * Write backup meta * Use new file ending anki21b * Asynchronously backup on collection close in Rust * Revert "Add zstd dep" This reverts commit 3fcb2141d2be15f907269d13275c41971431385c. * Add zstd again * Take backup col path from col struct * Fix formatting * Implement backup restoring on backend * Normalize restored media file names * Refactor `extract_legacy_data()` A bit cumbersome due to borrowing rules. * Refactor * Make thinning calendar-based and gradual * Consider last kept backups of previous stages * Import full apkgs and colpkgs with backend * Expose new backup settings * Test `BackupThinner` and make it deterministic * Mark backup_path when closing optional * Delete leaky timer * Add progress updates for restoring media * Write restored collection to tempfile first * Do collection compression in the background thread This has us currently storing an uncompressed and compressed copy of the collection in memory (not ideal), but means the collection can be closed without waiting for compression to complete. On a large collection, this takes a close and reopen from about 0.55s to about 0.07s. The old backup code for comparison: about 0.35s for compression off, about 8.5s for zip compression. * Use multithreading in zstd compression On my system, this reduces the compression time of a large collection from about 0.55s to 0.08s. * Stream compressed collection data into zip file * Tweak backup explanation + Fix incorrect tab order for ignore accents option * Decouple restoring backup and full import In the first case, no profile is opened, unless the new collection succeeds to load. In the second case, either the old collection is reloaded or the new one is loaded. * Fix number gap in Progress message * Don't revert backup when media fails but report it * Tweak error flow * Remove native BackupLimits enum * Fix type annotation * Add thinning test for whole year * Satisfy linter * Await async backup to finish * Move restart disclaimer out of backup tab Should be visible regardless of the current tab. * Write restored collection in chunks * Refactor * Write media in chunks and refactor * Log error if removing file fails * join_backup_task -> await_backup_completion * Refactor backup.rs * Refactor backup meta and collection extraction * Fix wrong error being returned * Call sync_all() on new collection * Add ImportError * Store logger in Backend, instead of creating one on demand init_backend() accepts a Logger rather than a log file, to allow other callers to customize the logger if they wish. In the future we may want to explore using the tracing crate as an alternative; it's a bit more ergonomic, as a logger doesn't need to be passed around, and it plays more nicely with async code. * Sync file contents prior to rename; sync folder after rename. * Limit backup creation to once per 30 min * Use zstd::stream::copy_decode * Make importing abortable * Don't revert if backup media is aborted * Set throttle implicitly * Change force flag to minimum_backup_interval * Don't attempt to open folders on Windows * Join last backup thread before starting new one Also refactor. * Disable auto sync and backup when restoring again * Force backup on full download * Include the reason why a media file import failed, and the file path - Introduce a FileIoError that contains a string representation of the underlying I/O error, and an associated path. There are a few places in the code where we're currently manually including the filename in a custom error message, and this is a step towards a more consistent approach (but we may be better served with a more general approach in the future similar to Anyhow's .context()) - Move the error message into importing.ftl, as it's a bit neater when error messages live in the same file as the rest of the messages associated with some functionality. * Fix importing of media files * Minor wording tweaks * Save an allocation I18n strings with replacements are already strings, so we can skip the extra allocation. Not that it matters here at all. * Terminate import if file missing from archive If a third-party tool is creating invalid archives, the user should know about it. This should be rare, so I did not attempt to make it translatable. * Skip multithreaded compression on small collections Co-authored-by: Damien Elmes <gpg@ankiweb.net>
2022-03-07 06:11:31 +01:00
self.loadProfile(on_done)
def profileNameOk(self, name: str) -> bool:
return not checkInvalidFilename(name) and name != "addons21"
2021-02-01 14:28:21 +01:00
def onAddProfile(self) -> None:
2021-03-26 04:48:26 +01:00
name = getOnlyText(tr.actions_name()).strip()
if name:
if name in self.pm.profiles():
2021-03-26 04:48:26 +01:00
showWarning(tr.qt_misc_name_exists())
2021-02-01 14:28:21 +01:00
return
if not self.profileNameOk(name):
return
self.pm.create(name)
self.pm.name = name
self.refreshProfilesList()
2021-02-01 14:28:21 +01:00
def onRenameProfile(self) -> None:
2021-03-26 04:48:26 +01:00
name = getOnlyText(tr.actions_new_name(), default=self.pm.name).strip()
if not name:
return
if name == self.pm.name:
return
if name in self.pm.profiles():
2021-03-26 04:48:26 +01:00
showWarning(tr.qt_misc_name_exists())
2021-02-01 14:28:21 +01:00
return
if not self.profileNameOk(name):
return
self.pm.rename(name)
self.refreshProfilesList()
2021-02-01 14:28:21 +01:00
def onRemProfile(self) -> None:
profs = self.pm.profiles()
if len(profs) < 2:
2021-03-26 04:48:26 +01:00
showWarning(tr.qt_misc_there_must_be_at_least_one())
2021-02-01 14:28:21 +01:00
return
# sure?
2019-12-23 01:34:10 +01:00
if not askUser(
2021-03-26 04:48:26 +01:00
tr.qt_misc_all_cards_notes_and_media_for(),
2019-12-23 01:34:10 +01:00
msgfunc=QMessageBox.warning,
defaultno=True,
):
return
self.pm.remove(self.pm.name)
self.refreshProfilesList()
2021-02-01 14:28:21 +01:00
def onOpenBackup(self) -> None:
2019-12-23 01:34:10 +01:00
if not askUser(
2021-03-26 04:48:26 +01:00
tr.qt_misc_replace_your_collection_with_an_earlier(),
2019-12-23 01:34:10 +01:00
msgfunc=QMessageBox.warning,
defaultno=True,
):
return
2019-12-23 01:34:10 +01:00
2021-02-02 14:30:53 +01:00
def doOpen(path: str) -> None:
self._openBackup(path)
2019-12-23 01:34:10 +01:00
getFile(
self.profileDiag,
2021-03-26 04:48:26 +01:00
tr.qt_misc_revert_to_backup(),
2021-02-02 14:30:53 +01:00
cb=doOpen, # type: ignore
2019-12-23 01:34:10 +01:00
filter="*.colpkg",
dir=self.pm.backupFolder(),
)
def _openBackup(self, path: str) -> None:
Backups (#1685) * Add zstd dep * Implement backend backup with zstd * Implement backup thinning * Write backup meta * Use new file ending anki21b * Asynchronously backup on collection close in Rust * Revert "Add zstd dep" This reverts commit 3fcb2141d2be15f907269d13275c41971431385c. * Add zstd again * Take backup col path from col struct * Fix formatting * Implement backup restoring on backend * Normalize restored media file names * Refactor `extract_legacy_data()` A bit cumbersome due to borrowing rules. * Refactor * Make thinning calendar-based and gradual * Consider last kept backups of previous stages * Import full apkgs and colpkgs with backend * Expose new backup settings * Test `BackupThinner` and make it deterministic * Mark backup_path when closing optional * Delete leaky timer * Add progress updates for restoring media * Write restored collection to tempfile first * Do collection compression in the background thread This has us currently storing an uncompressed and compressed copy of the collection in memory (not ideal), but means the collection can be closed without waiting for compression to complete. On a large collection, this takes a close and reopen from about 0.55s to about 0.07s. The old backup code for comparison: about 0.35s for compression off, about 8.5s for zip compression. * Use multithreading in zstd compression On my system, this reduces the compression time of a large collection from about 0.55s to 0.08s. * Stream compressed collection data into zip file * Tweak backup explanation + Fix incorrect tab order for ignore accents option * Decouple restoring backup and full import In the first case, no profile is opened, unless the new collection succeeds to load. In the second case, either the old collection is reloaded or the new one is loaded. * Fix number gap in Progress message * Don't revert backup when media fails but report it * Tweak error flow * Remove native BackupLimits enum * Fix type annotation * Add thinning test for whole year * Satisfy linter * Await async backup to finish * Move restart disclaimer out of backup tab Should be visible regardless of the current tab. * Write restored collection in chunks * Refactor * Write media in chunks and refactor * Log error if removing file fails * join_backup_task -> await_backup_completion * Refactor backup.rs * Refactor backup meta and collection extraction * Fix wrong error being returned * Call sync_all() on new collection * Add ImportError * Store logger in Backend, instead of creating one on demand init_backend() accepts a Logger rather than a log file, to allow other callers to customize the logger if they wish. In the future we may want to explore using the tracing crate as an alternative; it's a bit more ergonomic, as a logger doesn't need to be passed around, and it plays more nicely with async code. * Sync file contents prior to rename; sync folder after rename. * Limit backup creation to once per 30 min * Use zstd::stream::copy_decode * Make importing abortable * Don't revert if backup media is aborted * Set throttle implicitly * Change force flag to minimum_backup_interval * Don't attempt to open folders on Windows * Join last backup thread before starting new one Also refactor. * Disable auto sync and backup when restoring again * Force backup on full download * Include the reason why a media file import failed, and the file path - Introduce a FileIoError that contains a string representation of the underlying I/O error, and an associated path. There are a few places in the code where we're currently manually including the filename in a custom error message, and this is a step towards a more consistent approach (but we may be better served with a more general approach in the future similar to Anyhow's .context()) - Move the error message into importing.ftl, as it's a bit neater when error messages live in the same file as the rest of the messages associated with some functionality. * Fix importing of media files * Minor wording tweaks * Save an allocation I18n strings with replacements are already strings, so we can skip the extra allocation. Not that it matters here at all. * Terminate import if file missing from archive If a third-party tool is creating invalid archives, the user should know about it. This should be rare, so I did not attempt to make it translatable. * Skip multithreaded compression on small collections Co-authored-by: Damien Elmes <gpg@ankiweb.net>
2022-03-07 06:11:31 +01:00
self.restoring_backup = True
2021-03-26 04:48:26 +01:00
showInfo(tr.qt_misc_automatic_syncing_and_backups_have_been())
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
import_collection_package_op(
self, path, success=self.onOpenProfile
).run_in_background()
2021-02-01 14:28:21 +01:00
def _on_downgrade(self) -> None:
self.progress.start()
profiles = self.pm.profiles()
def downgrade() -> list[str]:
return self.pm.downgrade(profiles)
2021-02-02 14:30:53 +01:00
def on_done(future: Future) -> None:
self.progress.finish()
problems = future.result()
if not problems:
showInfo("Profiles can now be opened with an older version of Anki.")
else:
showWarning(
"The following profiles could not be downgraded: {}".format(
", ".join(problems)
)
)
return
self.profileDiag.close()
self.taskman.run_in_background(downgrade, on_done)
def loadProfile(self, onsuccess: Callable | None = None) -> None:
if not self.loadCollection():
return
self.setup_sound()
self.flags = FlagManager(self)
# show main window
if self.pm.profile.get("mainWindowState"):
restoreGeom(self, "mainWindow")
restoreState(self, "mainWindow")
2013-05-22 06:04:45 +02:00
# titlebar
self.setWindowTitle(f"{self.pm.name} - Anki")
# show and raise window for osx
self.show()
self.activateWindow()
self.raise_()
# import pending?
if self.pendingImport:
if self._isAddon(self.pendingImport):
self.installAddon(self.pendingImport)
else:
self.handleImport(self.pendingImport)
self.pendingImport = None
2020-01-15 07:53:24 +01:00
gui_hooks.profile_did_open()
2020-05-31 02:53:54 +02:00
2021-02-01 14:28:21 +01:00
def _onsuccess() -> None:
self._refresh_after_sync()
if onsuccess:
onsuccess()
self.maybe_auto_sync_on_open_close(_onsuccess)
2019-12-20 09:43:52 +01:00
def unloadProfile(self, onsuccess: Callable) -> None:
2021-02-01 14:28:21 +01:00
def callback() -> None:
self._unloadProfile()
onsuccess()
2020-01-15 07:53:24 +01:00
gui_hooks.profile_will_close()
self.unloadCollection(callback)
2019-12-20 09:43:52 +01:00
def _unloadProfile(self) -> None:
self.cleanup_sound()
saveGeom(self, "mainWindow")
saveState(self, "mainWindow")
self.pm.save()
self.hide()
Backups (#1685) * Add zstd dep * Implement backend backup with zstd * Implement backup thinning * Write backup meta * Use new file ending anki21b * Asynchronously backup on collection close in Rust * Revert "Add zstd dep" This reverts commit 3fcb2141d2be15f907269d13275c41971431385c. * Add zstd again * Take backup col path from col struct * Fix formatting * Implement backup restoring on backend * Normalize restored media file names * Refactor `extract_legacy_data()` A bit cumbersome due to borrowing rules. * Refactor * Make thinning calendar-based and gradual * Consider last kept backups of previous stages * Import full apkgs and colpkgs with backend * Expose new backup settings * Test `BackupThinner` and make it deterministic * Mark backup_path when closing optional * Delete leaky timer * Add progress updates for restoring media * Write restored collection to tempfile first * Do collection compression in the background thread This has us currently storing an uncompressed and compressed copy of the collection in memory (not ideal), but means the collection can be closed without waiting for compression to complete. On a large collection, this takes a close and reopen from about 0.55s to about 0.07s. The old backup code for comparison: about 0.35s for compression off, about 8.5s for zip compression. * Use multithreading in zstd compression On my system, this reduces the compression time of a large collection from about 0.55s to 0.08s. * Stream compressed collection data into zip file * Tweak backup explanation + Fix incorrect tab order for ignore accents option * Decouple restoring backup and full import In the first case, no profile is opened, unless the new collection succeeds to load. In the second case, either the old collection is reloaded or the new one is loaded. * Fix number gap in Progress message * Don't revert backup when media fails but report it * Tweak error flow * Remove native BackupLimits enum * Fix type annotation * Add thinning test for whole year * Satisfy linter * Await async backup to finish * Move restart disclaimer out of backup tab Should be visible regardless of the current tab. * Write restored collection in chunks * Refactor * Write media in chunks and refactor * Log error if removing file fails * join_backup_task -> await_backup_completion * Refactor backup.rs * Refactor backup meta and collection extraction * Fix wrong error being returned * Call sync_all() on new collection * Add ImportError * Store logger in Backend, instead of creating one on demand init_backend() accepts a Logger rather than a log file, to allow other callers to customize the logger if they wish. In the future we may want to explore using the tracing crate as an alternative; it's a bit more ergonomic, as a logger doesn't need to be passed around, and it plays more nicely with async code. * Sync file contents prior to rename; sync folder after rename. * Limit backup creation to once per 30 min * Use zstd::stream::copy_decode * Make importing abortable * Don't revert if backup media is aborted * Set throttle implicitly * Change force flag to minimum_backup_interval * Don't attempt to open folders on Windows * Join last backup thread before starting new one Also refactor. * Disable auto sync and backup when restoring again * Force backup on full download * Include the reason why a media file import failed, and the file path - Introduce a FileIoError that contains a string representation of the underlying I/O error, and an associated path. There are a few places in the code where we're currently manually including the filename in a custom error message, and this is a step towards a more consistent approach (but we may be better served with a more general approach in the future similar to Anyhow's .context()) - Move the error message into importing.ftl, as it's a bit neater when error messages live in the same file as the rest of the messages associated with some functionality. * Fix importing of media files * Minor wording tweaks * Save an allocation I18n strings with replacements are already strings, so we can skip the extra allocation. Not that it matters here at all. * Terminate import if file missing from archive If a third-party tool is creating invalid archives, the user should know about it. This should be rare, so I did not attempt to make it translatable. * Skip multithreaded compression on small collections Co-authored-by: Damien Elmes <gpg@ankiweb.net>
2022-03-07 06:11:31 +01:00
self.restoring_backup = False
# at this point there should be no windows left
self._checkForUnclosedWidgets()
2019-12-20 09:43:52 +01:00
def _checkForUnclosedWidgets(self) -> None:
for w in self.app.topLevelWidgets():
if w.isVisible():
# windows with this property are safe to close immediately
2017-09-08 10:42:26 +02:00
if getattr(w, "silentlyClose", None):
w.close()
else:
print(f"Window should have been closed: {w}")
2019-12-20 09:43:52 +01:00
def unloadProfileAndExit(self) -> None:
self.unloadProfile(self.cleanupAndExit)
2021-02-01 14:28:21 +01:00
def unloadProfileAndShowProfileManager(self) -> None:
self.unloadProfile(self.showProfileManager)
2019-12-20 09:43:52 +01:00
def cleanupAndExit(self) -> None:
self.errorHandler.unload()
self.mediaServer.shutdown()
Backups (#1685) * Add zstd dep * Implement backend backup with zstd * Implement backup thinning * Write backup meta * Use new file ending anki21b * Asynchronously backup on collection close in Rust * Revert "Add zstd dep" This reverts commit 3fcb2141d2be15f907269d13275c41971431385c. * Add zstd again * Take backup col path from col struct * Fix formatting * Implement backup restoring on backend * Normalize restored media file names * Refactor `extract_legacy_data()` A bit cumbersome due to borrowing rules. * Refactor * Make thinning calendar-based and gradual * Consider last kept backups of previous stages * Import full apkgs and colpkgs with backend * Expose new backup settings * Test `BackupThinner` and make it deterministic * Mark backup_path when closing optional * Delete leaky timer * Add progress updates for restoring media * Write restored collection to tempfile first * Do collection compression in the background thread This has us currently storing an uncompressed and compressed copy of the collection in memory (not ideal), but means the collection can be closed without waiting for compression to complete. On a large collection, this takes a close and reopen from about 0.55s to about 0.07s. The old backup code for comparison: about 0.35s for compression off, about 8.5s for zip compression. * Use multithreading in zstd compression On my system, this reduces the compression time of a large collection from about 0.55s to 0.08s. * Stream compressed collection data into zip file * Tweak backup explanation + Fix incorrect tab order for ignore accents option * Decouple restoring backup and full import In the first case, no profile is opened, unless the new collection succeeds to load. In the second case, either the old collection is reloaded or the new one is loaded. * Fix number gap in Progress message * Don't revert backup when media fails but report it * Tweak error flow * Remove native BackupLimits enum * Fix type annotation * Add thinning test for whole year * Satisfy linter * Await async backup to finish * Move restart disclaimer out of backup tab Should be visible regardless of the current tab. * Write restored collection in chunks * Refactor * Write media in chunks and refactor * Log error if removing file fails * join_backup_task -> await_backup_completion * Refactor backup.rs * Refactor backup meta and collection extraction * Fix wrong error being returned * Call sync_all() on new collection * Add ImportError * Store logger in Backend, instead of creating one on demand init_backend() accepts a Logger rather than a log file, to allow other callers to customize the logger if they wish. In the future we may want to explore using the tracing crate as an alternative; it's a bit more ergonomic, as a logger doesn't need to be passed around, and it plays more nicely with async code. * Sync file contents prior to rename; sync folder after rename. * Limit backup creation to once per 30 min * Use zstd::stream::copy_decode * Make importing abortable * Don't revert if backup media is aborted * Set throttle implicitly * Change force flag to minimum_backup_interval * Don't attempt to open folders on Windows * Join last backup thread before starting new one Also refactor. * Disable auto sync and backup when restoring again * Force backup on full download * Include the reason why a media file import failed, and the file path - Introduce a FileIoError that contains a string representation of the underlying I/O error, and an associated path. There are a few places in the code where we're currently manually including the filename in a custom error message, and this is a step towards a more consistent approach (but we may be better served with a more general approach in the future similar to Anyhow's .context()) - Move the error message into importing.ftl, as it's a bit neater when error messages live in the same file as the rest of the messages associated with some functionality. * Fix importing of media files * Minor wording tweaks * Save an allocation I18n strings with replacements are already strings, so we can skip the extra allocation. Not that it matters here at all. * Terminate import if file missing from archive If a third-party tool is creating invalid archives, the user should know about it. This should be rare, so I did not attempt to make it translatable. * Skip multithreaded compression on small collections Co-authored-by: Damien Elmes <gpg@ankiweb.net>
2022-03-07 06:11:31 +01:00
# Rust background jobs are not awaited implicitly
self.backend.await_backup_completion()
self.deleteLater()
app = self.app
def exit():
# try to ensure Qt objects are deleted in a logical order,
# to prevent crashes on shutdown
gc.collect()
app.exit(0)
self.progress.single_shot(100, exit, False)
2017-10-05 05:48:24 +02:00
# Sound/video
##########################################################################
def setup_sound(self) -> None:
aqt.sound.setup_audio(self.taskman, self.pm.base, self.col.media.dir())
def cleanup_sound(self) -> None:
aqt.sound.cleanup_audio()
2017-10-05 05:48:24 +02:00
def _add_play_buttons(self, text: str) -> str:
"Return card text with play buttons added, or stripped."
if self.col.get_config_bool(Config.Bool.HIDE_AUDIO_PLAY_BUTTONS):
2020-01-24 06:48:40 +01:00
return anki.sound.strip_av_refs(text)
else:
return aqt.sound.av_refs_to_play_icons(text)
def prepare_card_text_for_display(self, text: str) -> str:
text = self.col.media.escape_media_filenames(text)
text = self._add_play_buttons(text)
return text
# Collection load/unload
##########################################################################
2019-12-20 09:43:52 +01:00
def loadCollection(self) -> bool:
try:
self._loadCollection()
except Exception as e:
if "FileTooNew" in str(e):
showWarning(
2021-03-26 02:27:22 +01:00
"This profile requires a newer version of Anki to open. Did you forget to use the Downgrade button prior to switching Anki versions?"
)
else:
showWarning(
2021-03-26 04:48:26 +01:00
f"{tr.errors_unable_open_collection()}\n{traceback.format_exc()}"
)
# clean up open collection if possible
try:
Backup improvements (#1728) * Collection needs to be closed prior to backup even when not downgrading * Backups -> BackupLimits * Some improvements to backup_task - backup_inner now returns the error instead of logging it, so that the frontend can discover the issue when they await a backup (or create another one) - start_backup() was acquiring backup_task twice, and if another thread started a backup between the two locks, the task could have been accidentally overwritten without awaiting it * Backups no longer require a collection close - Instead of closing the collection, we ensure there is no active transaction, and flush the WAL to disk. This means the undo history is no longer lost on backup, which will be particularly useful if we add a periodic backup in the future. - Because a close is no longer required, backups are now achieved with a separate command, instead of being included in CloseCollection(). - Full sync no longer requires an extra close+reopen step, and we now wait for the backup to complete before proceeding. - Create a backup before 'check db' * Add File>Create Backup https://forums.ankiweb.net/t/anki-mac-os-no-backup-on-sync/6157 * Defer checkpoint until we know we need it When running periodic backups on a timer, we don't want to be fsync()ing unnecessarily. * Skip backup if modification time has not changed We don't want the user leaving Anki open overnight, and coming back to lots of identical backups. * Periodic backups Creates an automatic backup every 30 minutes if the collection has been modified. If there's a legacy checkpoint active, tries again 5 minutes later. * Switch to a user-configurable backup duration CreateBackup() now uses a simple force argument to determine whether the user's limits should be respected or not, and only potentially destructive ops (full download, check DB) override the user's configured limit. I considered having a separate limit for collection close and automatic backups (eg keeping the previous 5 minute limit for collection close), but that had two downsides: - When the user closes their collection at the end of the day, they'd get a recent backup. When they open the collection the next day, it would get backed up again within 5 minutes, even though not much had changed. - Multiple limits are harder to communicate to users in the UI Some remaining decisions I wasn't 100% sure about: - If force is true but the collection has not been modified, the backup will be skipped. If the user manually deleted their backups without closing Anki, they wouldn't get a new one if the mtime hadn't changed. - Force takes preference over the configured backup interval - should we be ignored the user here, or take no backups at all? Did a sneaky edit of the existing ftl string, as it hasn't been live long. * Move maybe_backup() into Collection * Use a single method for manual and periodic backups When manually creating a backup via the File menu, we no longer make the user wait until the backup completes. As we continue waiting for the backup in the background, if any errors occur, the user will get notified about it fairly quickly. * Show message to user if backup was skipped due to no changes + Don't incorrectly assert a backup will be created on force * Add "automatic" to description * Ensure we backup prior to importing colpkg if collection open The backup doesn't happen when invoked from 'open backup' in the profile screen, which matches Anki's previous behaviour. The user could potentially clobber up to 30 minutes of their work if they exited to the profile screen and restored a backup, but the alternative is we create backups every time a backup is restored, which may happen a number of times if the user is trying various ones. Or we could go back to a separate throttle amount for this case, at the cost of more complexity. * Remove the 0 special case on backup interval; minimum of 5 minutes https://github.com/ankitects/anki/pull/1728#discussion_r830876833
2022-03-21 10:40:42 +01:00
self.backend.close_collection(downgrade_to_schema11=False)
except Exception as e:
print("unable to close collection:", e)
self.col = None
# return to profile manager
self.hide()
self.showProfileManager()
return False
# make sure we don't get into an inconsistent state if an add-on
# has broken the deck browser or the did_load hook
try:
self.update_undo_actions()
gui_hooks.collection_did_load(self.col)
self.apply_collection_options()
self.moveToState("deckBrowser")
except Exception as e:
# dump error to stderr so it gets picked up by errors.py
traceback.print_exc()
return True
2021-02-01 11:59:18 +01:00
def _loadCollection(self) -> None:
cpath = self.pm.collectionPath()
2021-06-27 07:12:22 +02:00
self.col = Collection(cpath, backend=self.backend)
self.setEnabled(True)
def reopen(self, after_full_sync: bool = False) -> None:
self.col.reopen(after_full_sync=after_full_sync)
gui_hooks.collection_did_temporarily_close(self.col)
2020-03-06 05:03:23 +01:00
2019-12-20 09:43:52 +01:00
def unloadCollection(self, onsuccess: Callable) -> None:
2021-02-01 14:28:21 +01:00
def after_media_sync() -> None:
self._unloadCollection()
onsuccess()
2021-02-01 14:28:21 +01:00
def after_sync() -> None:
self.media_syncer.show_diag_until_finished(after_media_sync)
2021-02-01 14:28:21 +01:00
def before_sync() -> None:
2020-05-31 02:53:54 +02:00
self.setEnabled(False)
self.maybe_auto_sync_on_open_close(after_sync)
self.closeAllWindows(before_sync)
2019-12-20 09:43:52 +01:00
def _unloadCollection(self) -> None:
if not self.col:
return
Backups (#1685) * Add zstd dep * Implement backend backup with zstd * Implement backup thinning * Write backup meta * Use new file ending anki21b * Asynchronously backup on collection close in Rust * Revert "Add zstd dep" This reverts commit 3fcb2141d2be15f907269d13275c41971431385c. * Add zstd again * Take backup col path from col struct * Fix formatting * Implement backup restoring on backend * Normalize restored media file names * Refactor `extract_legacy_data()` A bit cumbersome due to borrowing rules. * Refactor * Make thinning calendar-based and gradual * Consider last kept backups of previous stages * Import full apkgs and colpkgs with backend * Expose new backup settings * Test `BackupThinner` and make it deterministic * Mark backup_path when closing optional * Delete leaky timer * Add progress updates for restoring media * Write restored collection to tempfile first * Do collection compression in the background thread This has us currently storing an uncompressed and compressed copy of the collection in memory (not ideal), but means the collection can be closed without waiting for compression to complete. On a large collection, this takes a close and reopen from about 0.55s to about 0.07s. The old backup code for comparison: about 0.35s for compression off, about 8.5s for zip compression. * Use multithreading in zstd compression On my system, this reduces the compression time of a large collection from about 0.55s to 0.08s. * Stream compressed collection data into zip file * Tweak backup explanation + Fix incorrect tab order for ignore accents option * Decouple restoring backup and full import In the first case, no profile is opened, unless the new collection succeeds to load. In the second case, either the old collection is reloaded or the new one is loaded. * Fix number gap in Progress message * Don't revert backup when media fails but report it * Tweak error flow * Remove native BackupLimits enum * Fix type annotation * Add thinning test for whole year * Satisfy linter * Await async backup to finish * Move restart disclaimer out of backup tab Should be visible regardless of the current tab. * Write restored collection in chunks * Refactor * Write media in chunks and refactor * Log error if removing file fails * join_backup_task -> await_backup_completion * Refactor backup.rs * Refactor backup meta and collection extraction * Fix wrong error being returned * Call sync_all() on new collection * Add ImportError * Store logger in Backend, instead of creating one on demand init_backend() accepts a Logger rather than a log file, to allow other callers to customize the logger if they wish. In the future we may want to explore using the tracing crate as an alternative; it's a bit more ergonomic, as a logger doesn't need to be passed around, and it plays more nicely with async code. * Sync file contents prior to rename; sync folder after rename. * Limit backup creation to once per 30 min * Use zstd::stream::copy_decode * Make importing abortable * Don't revert if backup media is aborted * Set throttle implicitly * Change force flag to minimum_backup_interval * Don't attempt to open folders on Windows * Join last backup thread before starting new one Also refactor. * Disable auto sync and backup when restoring again * Force backup on full download * Include the reason why a media file import failed, and the file path - Introduce a FileIoError that contains a string representation of the underlying I/O error, and an associated path. There are a few places in the code where we're currently manually including the filename in a custom error message, and this is a step towards a more consistent approach (but we may be better served with a more general approach in the future similar to Anyhow's .context()) - Move the error message into importing.ftl, as it's a bit neater when error messages live in the same file as the rest of the messages associated with some functionality. * Fix importing of media files * Minor wording tweaks * Save an allocation I18n strings with replacements are already strings, so we can skip the extra allocation. Not that it matters here at all. * Terminate import if file missing from archive If a third-party tool is creating invalid archives, the user should know about it. This should be rare, so I did not attempt to make it translatable. * Skip multithreaded compression on small collections Co-authored-by: Damien Elmes <gpg@ankiweb.net>
2022-03-07 06:11:31 +01:00
label = (
tr.qt_misc_closing() if self.restoring_backup else tr.qt_misc_backing_up()
)
self.progress.start(label=label)
Backups (#1685) * Add zstd dep * Implement backend backup with zstd * Implement backup thinning * Write backup meta * Use new file ending anki21b * Asynchronously backup on collection close in Rust * Revert "Add zstd dep" This reverts commit 3fcb2141d2be15f907269d13275c41971431385c. * Add zstd again * Take backup col path from col struct * Fix formatting * Implement backup restoring on backend * Normalize restored media file names * Refactor `extract_legacy_data()` A bit cumbersome due to borrowing rules. * Refactor * Make thinning calendar-based and gradual * Consider last kept backups of previous stages * Import full apkgs and colpkgs with backend * Expose new backup settings * Test `BackupThinner` and make it deterministic * Mark backup_path when closing optional * Delete leaky timer * Add progress updates for restoring media * Write restored collection to tempfile first * Do collection compression in the background thread This has us currently storing an uncompressed and compressed copy of the collection in memory (not ideal), but means the collection can be closed without waiting for compression to complete. On a large collection, this takes a close and reopen from about 0.55s to about 0.07s. The old backup code for comparison: about 0.35s for compression off, about 8.5s for zip compression. * Use multithreading in zstd compression On my system, this reduces the compression time of a large collection from about 0.55s to 0.08s. * Stream compressed collection data into zip file * Tweak backup explanation + Fix incorrect tab order for ignore accents option * Decouple restoring backup and full import In the first case, no profile is opened, unless the new collection succeeds to load. In the second case, either the old collection is reloaded or the new one is loaded. * Fix number gap in Progress message * Don't revert backup when media fails but report it * Tweak error flow * Remove native BackupLimits enum * Fix type annotation * Add thinning test for whole year * Satisfy linter * Await async backup to finish * Move restart disclaimer out of backup tab Should be visible regardless of the current tab. * Write restored collection in chunks * Refactor * Write media in chunks and refactor * Log error if removing file fails * join_backup_task -> await_backup_completion * Refactor backup.rs * Refactor backup meta and collection extraction * Fix wrong error being returned * Call sync_all() on new collection * Add ImportError * Store logger in Backend, instead of creating one on demand init_backend() accepts a Logger rather than a log file, to allow other callers to customize the logger if they wish. In the future we may want to explore using the tracing crate as an alternative; it's a bit more ergonomic, as a logger doesn't need to be passed around, and it plays more nicely with async code. * Sync file contents prior to rename; sync folder after rename. * Limit backup creation to once per 30 min * Use zstd::stream::copy_decode * Make importing abortable * Don't revert if backup media is aborted * Set throttle implicitly * Change force flag to minimum_backup_interval * Don't attempt to open folders on Windows * Join last backup thread before starting new one Also refactor. * Disable auto sync and backup when restoring again * Force backup on full download * Include the reason why a media file import failed, and the file path - Introduce a FileIoError that contains a string representation of the underlying I/O error, and an associated path. There are a few places in the code where we're currently manually including the filename in a custom error message, and this is a step towards a more consistent approach (but we may be better served with a more general approach in the future similar to Anyhow's .context()) - Move the error message into importing.ftl, as it's a bit neater when error messages live in the same file as the rest of the messages associated with some functionality. * Fix importing of media files * Minor wording tweaks * Save an allocation I18n strings with replacements are already strings, so we can skip the extra allocation. Not that it matters here at all. * Terminate import if file missing from archive If a third-party tool is creating invalid archives, the user should know about it. This should be rare, so I did not attempt to make it translatable. * Skip multithreaded compression on small collections Co-authored-by: Damien Elmes <gpg@ankiweb.net>
2022-03-07 06:11:31 +01:00
corrupt = False
Backups (#1685) * Add zstd dep * Implement backend backup with zstd * Implement backup thinning * Write backup meta * Use new file ending anki21b * Asynchronously backup on collection close in Rust * Revert "Add zstd dep" This reverts commit 3fcb2141d2be15f907269d13275c41971431385c. * Add zstd again * Take backup col path from col struct * Fix formatting * Implement backup restoring on backend * Normalize restored media file names * Refactor `extract_legacy_data()` A bit cumbersome due to borrowing rules. * Refactor * Make thinning calendar-based and gradual * Consider last kept backups of previous stages * Import full apkgs and colpkgs with backend * Expose new backup settings * Test `BackupThinner` and make it deterministic * Mark backup_path when closing optional * Delete leaky timer * Add progress updates for restoring media * Write restored collection to tempfile first * Do collection compression in the background thread This has us currently storing an uncompressed and compressed copy of the collection in memory (not ideal), but means the collection can be closed without waiting for compression to complete. On a large collection, this takes a close and reopen from about 0.55s to about 0.07s. The old backup code for comparison: about 0.35s for compression off, about 8.5s for zip compression. * Use multithreading in zstd compression On my system, this reduces the compression time of a large collection from about 0.55s to 0.08s. * Stream compressed collection data into zip file * Tweak backup explanation + Fix incorrect tab order for ignore accents option * Decouple restoring backup and full import In the first case, no profile is opened, unless the new collection succeeds to load. In the second case, either the old collection is reloaded or the new one is loaded. * Fix number gap in Progress message * Don't revert backup when media fails but report it * Tweak error flow * Remove native BackupLimits enum * Fix type annotation * Add thinning test for whole year * Satisfy linter * Await async backup to finish * Move restart disclaimer out of backup tab Should be visible regardless of the current tab. * Write restored collection in chunks * Refactor * Write media in chunks and refactor * Log error if removing file fails * join_backup_task -> await_backup_completion * Refactor backup.rs * Refactor backup meta and collection extraction * Fix wrong error being returned * Call sync_all() on new collection * Add ImportError * Store logger in Backend, instead of creating one on demand init_backend() accepts a Logger rather than a log file, to allow other callers to customize the logger if they wish. In the future we may want to explore using the tracing crate as an alternative; it's a bit more ergonomic, as a logger doesn't need to be passed around, and it plays more nicely with async code. * Sync file contents prior to rename; sync folder after rename. * Limit backup creation to once per 30 min * Use zstd::stream::copy_decode * Make importing abortable * Don't revert if backup media is aborted * Set throttle implicitly * Change force flag to minimum_backup_interval * Don't attempt to open folders on Windows * Join last backup thread before starting new one Also refactor. * Disable auto sync and backup when restoring again * Force backup on full download * Include the reason why a media file import failed, and the file path - Introduce a FileIoError that contains a string representation of the underlying I/O error, and an associated path. There are a few places in the code where we're currently manually including the filename in a custom error message, and this is a step towards a more consistent approach (but we may be better served with a more general approach in the future similar to Anyhow's .context()) - Move the error message into importing.ftl, as it's a bit neater when error messages live in the same file as the rest of the messages associated with some functionality. * Fix importing of media files * Minor wording tweaks * Save an allocation I18n strings with replacements are already strings, so we can skip the extra allocation. Not that it matters here at all. * Terminate import if file missing from archive If a third-party tool is creating invalid archives, the user should know about it. This should be rare, so I did not attempt to make it translatable. * Skip multithreaded compression on small collections Co-authored-by: Damien Elmes <gpg@ankiweb.net>
2022-03-07 06:11:31 +01:00
try:
self.maybeOptimize()
if not dev_mode:
corrupt = self.col.db.scalar("pragma quick_check") != "ok"
except:
corrupt = True
Backups (#1685) * Add zstd dep * Implement backend backup with zstd * Implement backup thinning * Write backup meta * Use new file ending anki21b * Asynchronously backup on collection close in Rust * Revert "Add zstd dep" This reverts commit 3fcb2141d2be15f907269d13275c41971431385c. * Add zstd again * Take backup col path from col struct * Fix formatting * Implement backup restoring on backend * Normalize restored media file names * Refactor `extract_legacy_data()` A bit cumbersome due to borrowing rules. * Refactor * Make thinning calendar-based and gradual * Consider last kept backups of previous stages * Import full apkgs and colpkgs with backend * Expose new backup settings * Test `BackupThinner` and make it deterministic * Mark backup_path when closing optional * Delete leaky timer * Add progress updates for restoring media * Write restored collection to tempfile first * Do collection compression in the background thread This has us currently storing an uncompressed and compressed copy of the collection in memory (not ideal), but means the collection can be closed without waiting for compression to complete. On a large collection, this takes a close and reopen from about 0.55s to about 0.07s. The old backup code for comparison: about 0.35s for compression off, about 8.5s for zip compression. * Use multithreading in zstd compression On my system, this reduces the compression time of a large collection from about 0.55s to 0.08s. * Stream compressed collection data into zip file * Tweak backup explanation + Fix incorrect tab order for ignore accents option * Decouple restoring backup and full import In the first case, no profile is opened, unless the new collection succeeds to load. In the second case, either the old collection is reloaded or the new one is loaded. * Fix number gap in Progress message * Don't revert backup when media fails but report it * Tweak error flow * Remove native BackupLimits enum * Fix type annotation * Add thinning test for whole year * Satisfy linter * Await async backup to finish * Move restart disclaimer out of backup tab Should be visible regardless of the current tab. * Write restored collection in chunks * Refactor * Write media in chunks and refactor * Log error if removing file fails * join_backup_task -> await_backup_completion * Refactor backup.rs * Refactor backup meta and collection extraction * Fix wrong error being returned * Call sync_all() on new collection * Add ImportError * Store logger in Backend, instead of creating one on demand init_backend() accepts a Logger rather than a log file, to allow other callers to customize the logger if they wish. In the future we may want to explore using the tracing crate as an alternative; it's a bit more ergonomic, as a logger doesn't need to be passed around, and it plays more nicely with async code. * Sync file contents prior to rename; sync folder after rename. * Limit backup creation to once per 30 min * Use zstd::stream::copy_decode * Make importing abortable * Don't revert if backup media is aborted * Set throttle implicitly * Change force flag to minimum_backup_interval * Don't attempt to open folders on Windows * Join last backup thread before starting new one Also refactor. * Disable auto sync and backup when restoring again * Force backup on full download * Include the reason why a media file import failed, and the file path - Introduce a FileIoError that contains a string representation of the underlying I/O error, and an associated path. There are a few places in the code where we're currently manually including the filename in a custom error message, and this is a step towards a more consistent approach (but we may be better served with a more general approach in the future similar to Anyhow's .context()) - Move the error message into importing.ftl, as it's a bit neater when error messages live in the same file as the rest of the messages associated with some functionality. * Fix importing of media files * Minor wording tweaks * Save an allocation I18n strings with replacements are already strings, so we can skip the extra allocation. Not that it matters here at all. * Terminate import if file missing from archive If a third-party tool is creating invalid archives, the user should know about it. This should be rare, so I did not attempt to make it translatable. * Skip multithreaded compression on small collections Co-authored-by: Damien Elmes <gpg@ankiweb.net>
2022-03-07 06:11:31 +01:00
try:
Backup improvements (#1728) * Collection needs to be closed prior to backup even when not downgrading * Backups -> BackupLimits * Some improvements to backup_task - backup_inner now returns the error instead of logging it, so that the frontend can discover the issue when they await a backup (or create another one) - start_backup() was acquiring backup_task twice, and if another thread started a backup between the two locks, the task could have been accidentally overwritten without awaiting it * Backups no longer require a collection close - Instead of closing the collection, we ensure there is no active transaction, and flush the WAL to disk. This means the undo history is no longer lost on backup, which will be particularly useful if we add a periodic backup in the future. - Because a close is no longer required, backups are now achieved with a separate command, instead of being included in CloseCollection(). - Full sync no longer requires an extra close+reopen step, and we now wait for the backup to complete before proceeding. - Create a backup before 'check db' * Add File>Create Backup https://forums.ankiweb.net/t/anki-mac-os-no-backup-on-sync/6157 * Defer checkpoint until we know we need it When running periodic backups on a timer, we don't want to be fsync()ing unnecessarily. * Skip backup if modification time has not changed We don't want the user leaving Anki open overnight, and coming back to lots of identical backups. * Periodic backups Creates an automatic backup every 30 minutes if the collection has been modified. If there's a legacy checkpoint active, tries again 5 minutes later. * Switch to a user-configurable backup duration CreateBackup() now uses a simple force argument to determine whether the user's limits should be respected or not, and only potentially destructive ops (full download, check DB) override the user's configured limit. I considered having a separate limit for collection close and automatic backups (eg keeping the previous 5 minute limit for collection close), but that had two downsides: - When the user closes their collection at the end of the day, they'd get a recent backup. When they open the collection the next day, it would get backed up again within 5 minutes, even though not much had changed. - Multiple limits are harder to communicate to users in the UI Some remaining decisions I wasn't 100% sure about: - If force is true but the collection has not been modified, the backup will be skipped. If the user manually deleted their backups without closing Anki, they wouldn't get a new one if the mtime hadn't changed. - Force takes preference over the configured backup interval - should we be ignored the user here, or take no backups at all? Did a sneaky edit of the existing ftl string, as it hasn't been live long. * Move maybe_backup() into Collection * Use a single method for manual and periodic backups When manually creating a backup via the File menu, we no longer make the user wait until the backup completes. As we continue waiting for the backup in the background, if any errors occur, the user will get notified about it fairly quickly. * Show message to user if backup was skipped due to no changes + Don't incorrectly assert a backup will be created on force * Add "automatic" to description * Ensure we backup prior to importing colpkg if collection open The backup doesn't happen when invoked from 'open backup' in the profile screen, which matches Anki's previous behaviour. The user could potentially clobber up to 30 minutes of their work if they exited to the profile screen and restored a backup, but the alternative is we create backups every time a backup is restored, which may happen a number of times if the user is trying various ones. Or we could go back to a separate throttle amount for this case, at the cost of more complexity. * Remove the 0 special case on backup interval; minimum of 5 minutes https://github.com/ankitects/anki/pull/1728#discussion_r830876833
2022-03-21 10:40:42 +01:00
if not corrupt and not dev_mode and not self.restoring_backup:
try:
# default 5 minute throttle
self.col.create_backup(
backup_folder=self.pm.backupFolder(),
force=False,
wait_for_completion=False,
)
except:
print("backup on close failed")
Backup improvements (#1728) * Collection needs to be closed prior to backup even when not downgrading * Backups -> BackupLimits * Some improvements to backup_task - backup_inner now returns the error instead of logging it, so that the frontend can discover the issue when they await a backup (or create another one) - start_backup() was acquiring backup_task twice, and if another thread started a backup between the two locks, the task could have been accidentally overwritten without awaiting it * Backups no longer require a collection close - Instead of closing the collection, we ensure there is no active transaction, and flush the WAL to disk. This means the undo history is no longer lost on backup, which will be particularly useful if we add a periodic backup in the future. - Because a close is no longer required, backups are now achieved with a separate command, instead of being included in CloseCollection(). - Full sync no longer requires an extra close+reopen step, and we now wait for the backup to complete before proceeding. - Create a backup before 'check db' * Add File>Create Backup https://forums.ankiweb.net/t/anki-mac-os-no-backup-on-sync/6157 * Defer checkpoint until we know we need it When running periodic backups on a timer, we don't want to be fsync()ing unnecessarily. * Skip backup if modification time has not changed We don't want the user leaving Anki open overnight, and coming back to lots of identical backups. * Periodic backups Creates an automatic backup every 30 minutes if the collection has been modified. If there's a legacy checkpoint active, tries again 5 minutes later. * Switch to a user-configurable backup duration CreateBackup() now uses a simple force argument to determine whether the user's limits should be respected or not, and only potentially destructive ops (full download, check DB) override the user's configured limit. I considered having a separate limit for collection close and automatic backups (eg keeping the previous 5 minute limit for collection close), but that had two downsides: - When the user closes their collection at the end of the day, they'd get a recent backup. When they open the collection the next day, it would get backed up again within 5 minutes, even though not much had changed. - Multiple limits are harder to communicate to users in the UI Some remaining decisions I wasn't 100% sure about: - If force is true but the collection has not been modified, the backup will be skipped. If the user manually deleted their backups without closing Anki, they wouldn't get a new one if the mtime hadn't changed. - Force takes preference over the configured backup interval - should we be ignored the user here, or take no backups at all? Did a sneaky edit of the existing ftl string, as it hasn't been live long. * Move maybe_backup() into Collection * Use a single method for manual and periodic backups When manually creating a backup via the File menu, we no longer make the user wait until the backup completes. As we continue waiting for the backup in the background, if any errors occur, the user will get notified about it fairly quickly. * Show message to user if backup was skipped due to no changes + Don't incorrectly assert a backup will be created on force * Add "automatic" to description * Ensure we backup prior to importing colpkg if collection open The backup doesn't happen when invoked from 'open backup' in the profile screen, which matches Anki's previous behaviour. The user could potentially clobber up to 30 minutes of their work if they exited to the profile screen and restored a backup, but the alternative is we create backups every time a backup is restored, which may happen a number of times if the user is trying various ones. Or we could go back to a separate throttle amount for this case, at the cost of more complexity. * Remove the 0 special case on backup interval; minimum of 5 minutes https://github.com/ankitects/anki/pull/1728#discussion_r830876833
2022-03-21 10:40:42 +01:00
self.col.close(downgrade=False)
2020-04-08 02:05:33 +02:00
except Exception as e:
print(e)
corrupt = True
finally:
self.col = None
self.progress.finish()
Backups (#1685) * Add zstd dep * Implement backend backup with zstd * Implement backup thinning * Write backup meta * Use new file ending anki21b * Asynchronously backup on collection close in Rust * Revert "Add zstd dep" This reverts commit 3fcb2141d2be15f907269d13275c41971431385c. * Add zstd again * Take backup col path from col struct * Fix formatting * Implement backup restoring on backend * Normalize restored media file names * Refactor `extract_legacy_data()` A bit cumbersome due to borrowing rules. * Refactor * Make thinning calendar-based and gradual * Consider last kept backups of previous stages * Import full apkgs and colpkgs with backend * Expose new backup settings * Test `BackupThinner` and make it deterministic * Mark backup_path when closing optional * Delete leaky timer * Add progress updates for restoring media * Write restored collection to tempfile first * Do collection compression in the background thread This has us currently storing an uncompressed and compressed copy of the collection in memory (not ideal), but means the collection can be closed without waiting for compression to complete. On a large collection, this takes a close and reopen from about 0.55s to about 0.07s. The old backup code for comparison: about 0.35s for compression off, about 8.5s for zip compression. * Use multithreading in zstd compression On my system, this reduces the compression time of a large collection from about 0.55s to 0.08s. * Stream compressed collection data into zip file * Tweak backup explanation + Fix incorrect tab order for ignore accents option * Decouple restoring backup and full import In the first case, no profile is opened, unless the new collection succeeds to load. In the second case, either the old collection is reloaded or the new one is loaded. * Fix number gap in Progress message * Don't revert backup when media fails but report it * Tweak error flow * Remove native BackupLimits enum * Fix type annotation * Add thinning test for whole year * Satisfy linter * Await async backup to finish * Move restart disclaimer out of backup tab Should be visible regardless of the current tab. * Write restored collection in chunks * Refactor * Write media in chunks and refactor * Log error if removing file fails * join_backup_task -> await_backup_completion * Refactor backup.rs * Refactor backup meta and collection extraction * Fix wrong error being returned * Call sync_all() on new collection * Add ImportError * Store logger in Backend, instead of creating one on demand init_backend() accepts a Logger rather than a log file, to allow other callers to customize the logger if they wish. In the future we may want to explore using the tracing crate as an alternative; it's a bit more ergonomic, as a logger doesn't need to be passed around, and it plays more nicely with async code. * Sync file contents prior to rename; sync folder after rename. * Limit backup creation to once per 30 min * Use zstd::stream::copy_decode * Make importing abortable * Don't revert if backup media is aborted * Set throttle implicitly * Change force flag to minimum_backup_interval * Don't attempt to open folders on Windows * Join last backup thread before starting new one Also refactor. * Disable auto sync and backup when restoring again * Force backup on full download * Include the reason why a media file import failed, and the file path - Introduce a FileIoError that contains a string representation of the underlying I/O error, and an associated path. There are a few places in the code where we're currently manually including the filename in a custom error message, and this is a step towards a more consistent approach (but we may be better served with a more general approach in the future similar to Anyhow's .context()) - Move the error message into importing.ftl, as it's a bit neater when error messages live in the same file as the rest of the messages associated with some functionality. * Fix importing of media files * Minor wording tweaks * Save an allocation I18n strings with replacements are already strings, so we can skip the extra allocation. Not that it matters here at all. * Terminate import if file missing from archive If a third-party tool is creating invalid archives, the user should know about it. This should be rare, so I did not attempt to make it translatable. * Skip multithreaded compression on small collections Co-authored-by: Damien Elmes <gpg@ankiweb.net>
2022-03-07 06:11:31 +01:00
if corrupt:
2021-03-26 04:48:26 +01:00
showWarning(tr.qt_misc_your_collection_file_appears_to_be())
def apply_collection_options(self) -> None:
"Setup audio after collection loaded."
aqt.sound.av_player.interrupt_current_audio = self.col.get_config_bool(
Config.Bool.INTERRUPT_AUDIO_WHEN_ANSWERING
)
Backups (#1685) * Add zstd dep * Implement backend backup with zstd * Implement backup thinning * Write backup meta * Use new file ending anki21b * Asynchronously backup on collection close in Rust * Revert "Add zstd dep" This reverts commit 3fcb2141d2be15f907269d13275c41971431385c. * Add zstd again * Take backup col path from col struct * Fix formatting * Implement backup restoring on backend * Normalize restored media file names * Refactor `extract_legacy_data()` A bit cumbersome due to borrowing rules. * Refactor * Make thinning calendar-based and gradual * Consider last kept backups of previous stages * Import full apkgs and colpkgs with backend * Expose new backup settings * Test `BackupThinner` and make it deterministic * Mark backup_path when closing optional * Delete leaky timer * Add progress updates for restoring media * Write restored collection to tempfile first * Do collection compression in the background thread This has us currently storing an uncompressed and compressed copy of the collection in memory (not ideal), but means the collection can be closed without waiting for compression to complete. On a large collection, this takes a close and reopen from about 0.55s to about 0.07s. The old backup code for comparison: about 0.35s for compression off, about 8.5s for zip compression. * Use multithreading in zstd compression On my system, this reduces the compression time of a large collection from about 0.55s to 0.08s. * Stream compressed collection data into zip file * Tweak backup explanation + Fix incorrect tab order for ignore accents option * Decouple restoring backup and full import In the first case, no profile is opened, unless the new collection succeeds to load. In the second case, either the old collection is reloaded or the new one is loaded. * Fix number gap in Progress message * Don't revert backup when media fails but report it * Tweak error flow * Remove native BackupLimits enum * Fix type annotation * Add thinning test for whole year * Satisfy linter * Await async backup to finish * Move restart disclaimer out of backup tab Should be visible regardless of the current tab. * Write restored collection in chunks * Refactor * Write media in chunks and refactor * Log error if removing file fails * join_backup_task -> await_backup_completion * Refactor backup.rs * Refactor backup meta and collection extraction * Fix wrong error being returned * Call sync_all() on new collection * Add ImportError * Store logger in Backend, instead of creating one on demand init_backend() accepts a Logger rather than a log file, to allow other callers to customize the logger if they wish. In the future we may want to explore using the tracing crate as an alternative; it's a bit more ergonomic, as a logger doesn't need to be passed around, and it plays more nicely with async code. * Sync file contents prior to rename; sync folder after rename. * Limit backup creation to once per 30 min * Use zstd::stream::copy_decode * Make importing abortable * Don't revert if backup media is aborted * Set throttle implicitly * Change force flag to minimum_backup_interval * Don't attempt to open folders on Windows * Join last backup thread before starting new one Also refactor. * Disable auto sync and backup when restoring again * Force backup on full download * Include the reason why a media file import failed, and the file path - Introduce a FileIoError that contains a string representation of the underlying I/O error, and an associated path. There are a few places in the code where we're currently manually including the filename in a custom error message, and this is a step towards a more consistent approach (but we may be better served with a more general approach in the future similar to Anyhow's .context()) - Move the error message into importing.ftl, as it's a bit neater when error messages live in the same file as the rest of the messages associated with some functionality. * Fix importing of media files * Minor wording tweaks * Save an allocation I18n strings with replacements are already strings, so we can skip the extra allocation. Not that it matters here at all. * Terminate import if file missing from archive If a third-party tool is creating invalid archives, the user should know about it. This should be rare, so I did not attempt to make it translatable. * Skip multithreaded compression on small collections Co-authored-by: Damien Elmes <gpg@ankiweb.net>
2022-03-07 06:11:31 +01:00
# Auto-optimize
##########################################################################
2019-12-20 09:43:52 +01:00
def maybeOptimize(self) -> None:
# have two weeks passed?
if (last_optimize := self.pm.profile.get("lastOptimize")) is not None:
if (int_time() - last_optimize) < 86400 * 14:
return
2021-03-26 04:48:26 +01:00
self.progress.start(label=tr.qt_misc_optimizing())
self.col.optimize()
PEP8 for rest of pylib (#1451) * PEP8 dbproxy.py * PEP8 errors.py * PEP8 httpclient.py * PEP8 lang.py * PEP8 latex.py * Add decorator to deprectate key words * Make replacement for deprecated attribute optional * Use new helper `_print_replacement_warning()` * PEP8 media.py * PEP8 rsbackend.py * PEP8 sound.py * PEP8 stdmodels.py * PEP8 storage.py * PEP8 sync.py * PEP8 tags.py * PEP8 template.py * PEP8 types.py * Fix DeprecatedNamesMixinForModule The class methods need to be overridden with instance methods, so every module has its own dicts. * Use `# pylint: disable=invalid-name` instead of id * PEP8 utils.py * Only decorate `__getattr__` with `@no_type_check` * Fix mypy issue with snakecase Importing it from `anki._vendor` raises attribute errors. * Format * Remove inheritance of DeprecatedNamesMixin There's almost no shared code now and overriding classmethods with instance methods raises mypy issues. * Fix traceback frames of deprecation warnings * remove fn/TimedLog (dae) Neither Anki nor add-ons appear to have been using it * fix some issues with stringcase use (dae) - the wheel was depending on the PyPI version instead of our vendored version - _vendor:stringcase should not have been listed in the anki py_library. We already include the sources in py_srcs, and need to refer to them directly. By listing _vendor:stringcase as well, we were making a top-level stringcase library available, which would have only worked for distributing because the wheel definition was also incorrect. - mypy errors are what caused me to mistakenly add the above - they were because the type: ignore at the top of stringcase.py was causing mypy to completely ignore the file, so it was not aware of any attributes it contained.
2021-10-25 06:50:13 +02:00
self.pm.profile["lastOptimize"] = int_time()
self.pm.save()
self.progress.finish()
2021-03-14 10:54:15 +01:00
# Tracking main window state (deck browser, reviewer, etc)
##########################################################################
2021-03-14 10:54:15 +01:00
def moveToState(self, state: MainWindowState, *args: Any) -> None:
2019-12-23 01:34:10 +01:00
# print("-> move from", self.state, "to", state)
2021-03-14 10:54:15 +01:00
oldState = self.state
cleanup = getattr(self, f"_{oldState}Cleanup", None)
if cleanup:
# pylint: disable=not-callable
cleanup(state)
self.clearStateShortcuts()
self.state = state
2020-01-15 07:53:24 +01:00
gui_hooks.state_will_change(state, oldState)
getattr(self, f"_{state}State", lambda *_: None)(oldState, *args)
if state != "resetRequired":
Revamp Preferences, implement Minimalist Mode and Qt widget gallery to test GUI changes (#2289) * Create widget gallery dialog * Add WidgetGallery to debug dialog * Use enum for its intended purpose * Rename "reduced-motion" to "reduce-motion" * Add another border-radius value and make former large radius a bit smaller. * Revamp preferences, add minimalist mode Also: - create additional and missing widget styles and tweak existing ones - use single profile entry to set widget styles and reduce choices to Anki and Native * Indent QTabBar style definitions * Add missing styles for QPushButton states * Fix QTableView background * Remove unused layout from Preferences * Fix QTabView focused tab style * Highlight QCheckBox and QRadioButton when focused * Fix toolbar styles * Reorder preferences * Add setting to hide bottom toolbar * Move toolbar settings above minimalist modes * Remove unused lines * Implement proper full-screen mode * Sort imports * Tweak deck overview appearance in minimalist mode * Undo TitledContainer changes since nobody asked for that * Remove dynamic toolbar background from minimalist mode * Tweak buttons in minimalist mode * Fix some issues * Reduce theme check interval to 5s on Linux * Increase hide timer interval to 2s * Collapse toolbars with slight delay when moving to review state This should ensure the bottom toolbar collapses too. * Allow users to make hiding exclusive to full screen * Rename full screen option * Fix hide mode dropdown ignoring checkbox state on startup * Fix typing issue * Refine background image handling Giving the toolbar body the main webview height ensures background-size: cover behaves exactly the same. To prevent an override of other background properties, users are advised to only set background-images via the background-image property, not the background shorthand. * Fix top toolbar getting huge when switching modes The issue was caused by the min-height hack to align the background images. A call to web.adjustHeightToFit would set the toolbar to the same height as the main webview, as the function makes use of document.offsetHeight. * Prevent scrollbar from appearing on bottom toolbar resize * Cleanup * Put review tab before editing; fix some tab orders * Rename 'network' to 'syncing' * Fix bottom toolbar disappearing on UI > 100 * Improve Preferences layout by adding vertical spacers to the bottom also make the hiding of video_driver and its label more obvious in preferences.py. * Fix bottom toolbar animating on startup Also fix bottom toolbar not appearing when unchecking hide mode in reviewer. * Hide/Show menubar in fullscreen mode along with toolbar * Attempt to fix broken native theme on macOS * Format * Improve native theme on other systems by not forcing palette with the caveat that theme switching can get weird. * Fix theme switching in native style * Remove redundant condition * Add back check for Qt5 to prevent theme issues * Add check for macOS before setting fusion theme * Do not force scrollbar styles on macOS * Remove all of that crazy theme logic * Use canvas instead of button-bg for ColorRole.Button * Make sure Anki style is always based on Fusion otherwise we can't guarantee the same look on all systems. * Explicitly apply default style when Anki style is not selected This should fix the style not switching back after it was selected. * Remove reduncant default_palette * Revert 8af4c1cc2 On Mac with native theme, both Qt5 and Qt6 look correct already. On the Anki theme, without this change, we get the fusion-style scrollbars instead of the rounded ones. * Rename AnkiStyles enum to WidgetStyle * Fix theme switching shades on same theme * Format * Remove unused placeholderText that caused an error when opening the widget gallery on Qt5. * Check for full screen windowState using bitwise operator to prevent error in Qt5. Credit: https://stackoverflow.com/a/65425151 * Hide style option on Windows also exclude native option from dropdown just in case. * Format * Minor naming tweak
2023-01-18 12:24:16 +01:00
self.bottomWeb.adjustHeightToFit()
2020-01-15 07:53:24 +01:00
gui_hooks.state_did_change(state, oldState)
def _deckBrowserState(self, oldState: MainWindowState) -> None:
self.deckBrowser.show()
def _selectedDeck(self) -> DeckDict | None:
did = self.col.decks.selected()
Simplify note adding and the deck/notetype choosers The existing code was really difficult to reason about: - The default notetype depended on the selected deck, and vice versa, and this logic was buried in the deck and notetype choosing screens, and models.py. - Changes to the notetype were not passed back directly, but were fired via a hook, which changed any screen in the app that had a notetype selector. It also wasn't great for performance, as the most recent deck and tags were embedded in the notetype, which can be expensive to save and sync for large notetypes. To address these points: - The current deck for a notetype, and notetype for a deck, are now stored in separate config variables, instead of directly in the deck or notetype. These are cheap to read and write, and we'll be able to sync them individually in the future once config syncing is updated in the future. I seem to recall some users not wanting the tag saving behaviour, so I've dropped that for now, but if people end up missing it, it would be simple to add as an extra auxiliary config variable. - The logic for getting the starting deck and notetype has been moved into the backend. It should be the same as the older Python code, with one exception: when "change deck depending on notetype" is enabled in the preferences, it will start with the current notetype ("curModel"), instead of first trying to get a deck-specific notetype. - ModelChooser has been duplicated into notetypechooser.py, and it has been updated to solely be concerned with keeping track of a selected notetype - it no longer alters global state.
2021-03-08 14:23:24 +01:00
if not self.col.decks.name_if_exists(did):
2021-03-26 04:48:26 +01:00
showInfo(tr.qt_misc_please_select_a_deck())
2019-12-20 09:43:52 +01:00
return None
return self.col.decks.get(did)
def _overviewState(self, oldState: MainWindowState) -> None:
if not self._selectedDeck():
return self.moveToState("deckBrowser")
self.overview.show()
def _reviewState(self, oldState: MainWindowState) -> None:
self.reviewer.show()
Revamp Preferences, implement Minimalist Mode and Qt widget gallery to test GUI changes (#2289) * Create widget gallery dialog * Add WidgetGallery to debug dialog * Use enum for its intended purpose * Rename "reduced-motion" to "reduce-motion" * Add another border-radius value and make former large radius a bit smaller. * Revamp preferences, add minimalist mode Also: - create additional and missing widget styles and tweak existing ones - use single profile entry to set widget styles and reduce choices to Anki and Native * Indent QTabBar style definitions * Add missing styles for QPushButton states * Fix QTableView background * Remove unused layout from Preferences * Fix QTabView focused tab style * Highlight QCheckBox and QRadioButton when focused * Fix toolbar styles * Reorder preferences * Add setting to hide bottom toolbar * Move toolbar settings above minimalist modes * Remove unused lines * Implement proper full-screen mode * Sort imports * Tweak deck overview appearance in minimalist mode * Undo TitledContainer changes since nobody asked for that * Remove dynamic toolbar background from minimalist mode * Tweak buttons in minimalist mode * Fix some issues * Reduce theme check interval to 5s on Linux * Increase hide timer interval to 2s * Collapse toolbars with slight delay when moving to review state This should ensure the bottom toolbar collapses too. * Allow users to make hiding exclusive to full screen * Rename full screen option * Fix hide mode dropdown ignoring checkbox state on startup * Fix typing issue * Refine background image handling Giving the toolbar body the main webview height ensures background-size: cover behaves exactly the same. To prevent an override of other background properties, users are advised to only set background-images via the background-image property, not the background shorthand. * Fix top toolbar getting huge when switching modes The issue was caused by the min-height hack to align the background images. A call to web.adjustHeightToFit would set the toolbar to the same height as the main webview, as the function makes use of document.offsetHeight. * Prevent scrollbar from appearing on bottom toolbar resize * Cleanup * Put review tab before editing; fix some tab orders * Rename 'network' to 'syncing' * Fix bottom toolbar disappearing on UI > 100 * Improve Preferences layout by adding vertical spacers to the bottom also make the hiding of video_driver and its label more obvious in preferences.py. * Fix bottom toolbar animating on startup Also fix bottom toolbar not appearing when unchecking hide mode in reviewer. * Hide/Show menubar in fullscreen mode along with toolbar * Attempt to fix broken native theme on macOS * Format * Improve native theme on other systems by not forcing palette with the caveat that theme switching can get weird. * Fix theme switching in native style * Remove redundant condition * Add back check for Qt5 to prevent theme issues * Add check for macOS before setting fusion theme * Do not force scrollbar styles on macOS * Remove all of that crazy theme logic * Use canvas instead of button-bg for ColorRole.Button * Make sure Anki style is always based on Fusion otherwise we can't guarantee the same look on all systems. * Explicitly apply default style when Anki style is not selected This should fix the style not switching back after it was selected. * Remove reduncant default_palette * Revert 8af4c1cc2 On Mac with native theme, both Qt5 and Qt6 look correct already. On the Anki theme, without this change, we get the fusion-style scrollbars instead of the rounded ones. * Rename AnkiStyles enum to WidgetStyle * Fix theme switching shades on same theme * Format * Remove unused placeholderText that caused an error when opening the widget gallery on Qt5. * Check for full screen windowState using bitwise operator to prevent error in Qt5. Credit: https://stackoverflow.com/a/65425151 * Hide style option on Windows also exclude native option from dropdown just in case. * Format * Minor naming tweak
2023-01-18 12:24:16 +01:00
if self.pm.hide_top_bar():
self.toolbarWeb.hide_timer.setInterval(500)
self.toolbarWeb.hide_timer.start()
Auto-hide toolbar in Reviewer (#2262) * Give webviews a slide-in animation if reduced motion isn't set. * Auto-hide toolbar in review mode moving the mouse above the main webview expands the toolbar. When the mouse leaves the toolbar, it will collapse after a delay of 2s. * Save some space on bottom toolbars * Use props for all hard-coded transition durations and decrease most commonly used duration (200ms) to 150ms. * Move auto-hide logic into ToolbarWebView and handle auto-hide specific events in the respective webview subclasses. * Fix typing issues * Fix flickering issue * Add auto_hide_toolbar opt-in to preferences * Rename hide_toolbar to collapse_toolbar to better describe the dock-like behaviour. * Rename setting to minimize_distractions * Reduce calls to pm in eventFilter * Run formatter * Revert setting title to something more specific * Increase default animation time to 180ms * Inset toolbar in review mode when auto-hide is not enabled. * Use card background on toolbar and add glass effect * Use flatten/elevate over inset/outset * Use flatten/elevate over inset/outset * Update toolbar.py * Fix toolbar background delay * Tweak styles * Use "collapse" instead of "auto-hide" * Fix background misalignment in collapse mode * Do not collapse toolbar when pointer is outside MainWebView * Reduce hide_timer interval to 1000ms * Use CSS to hide toolbar instead of setting webview height * Add guard to prevent backdrop-filter: blur on Qt 5.14 * Apply transition to body instead of toolbar to not complicate things for #2301. * Fix Qt 5.14 and apply guard globally * Fix background image scaling difference * Tweak preference wording (dae)
2023-01-09 05:39:31 +01:00
else:
self.toolbarWeb.flatten()
Revamp Preferences, implement Minimalist Mode and Qt widget gallery to test GUI changes (#2289) * Create widget gallery dialog * Add WidgetGallery to debug dialog * Use enum for its intended purpose * Rename "reduced-motion" to "reduce-motion" * Add another border-radius value and make former large radius a bit smaller. * Revamp preferences, add minimalist mode Also: - create additional and missing widget styles and tweak existing ones - use single profile entry to set widget styles and reduce choices to Anki and Native * Indent QTabBar style definitions * Add missing styles for QPushButton states * Fix QTableView background * Remove unused layout from Preferences * Fix QTabView focused tab style * Highlight QCheckBox and QRadioButton when focused * Fix toolbar styles * Reorder preferences * Add setting to hide bottom toolbar * Move toolbar settings above minimalist modes * Remove unused lines * Implement proper full-screen mode * Sort imports * Tweak deck overview appearance in minimalist mode * Undo TitledContainer changes since nobody asked for that * Remove dynamic toolbar background from minimalist mode * Tweak buttons in minimalist mode * Fix some issues * Reduce theme check interval to 5s on Linux * Increase hide timer interval to 2s * Collapse toolbars with slight delay when moving to review state This should ensure the bottom toolbar collapses too. * Allow users to make hiding exclusive to full screen * Rename full screen option * Fix hide mode dropdown ignoring checkbox state on startup * Fix typing issue * Refine background image handling Giving the toolbar body the main webview height ensures background-size: cover behaves exactly the same. To prevent an override of other background properties, users are advised to only set background-images via the background-image property, not the background shorthand. * Fix top toolbar getting huge when switching modes The issue was caused by the min-height hack to align the background images. A call to web.adjustHeightToFit would set the toolbar to the same height as the main webview, as the function makes use of document.offsetHeight. * Prevent scrollbar from appearing on bottom toolbar resize * Cleanup * Put review tab before editing; fix some tab orders * Rename 'network' to 'syncing' * Fix bottom toolbar disappearing on UI > 100 * Improve Preferences layout by adding vertical spacers to the bottom also make the hiding of video_driver and its label more obvious in preferences.py. * Fix bottom toolbar animating on startup Also fix bottom toolbar not appearing when unchecking hide mode in reviewer. * Hide/Show menubar in fullscreen mode along with toolbar * Attempt to fix broken native theme on macOS * Format * Improve native theme on other systems by not forcing palette with the caveat that theme switching can get weird. * Fix theme switching in native style * Remove redundant condition * Add back check for Qt5 to prevent theme issues * Add check for macOS before setting fusion theme * Do not force scrollbar styles on macOS * Remove all of that crazy theme logic * Use canvas instead of button-bg for ColorRole.Button * Make sure Anki style is always based on Fusion otherwise we can't guarantee the same look on all systems. * Explicitly apply default style when Anki style is not selected This should fix the style not switching back after it was selected. * Remove reduncant default_palette * Revert 8af4c1cc2 On Mac with native theme, both Qt5 and Qt6 look correct already. On the Anki theme, without this change, we get the fusion-style scrollbars instead of the rounded ones. * Rename AnkiStyles enum to WidgetStyle * Fix theme switching shades on same theme * Format * Remove unused placeholderText that caused an error when opening the widget gallery on Qt5. * Check for full screen windowState using bitwise operator to prevent error in Qt5. Credit: https://stackoverflow.com/a/65425151 * Hide style option on Windows also exclude native option from dropdown just in case. * Format * Minor naming tweak
2023-01-18 12:24:16 +01:00
if self.pm.hide_bottom_bar():
self.bottomWeb.hide_timer.setInterval(500)
self.bottomWeb.hide_timer.start()
def _reviewCleanup(self, newState: MainWindowState) -> None:
if newState != "resetRequired" and newState != "review":
self.reviewer.cleanup()
Auto-hide toolbar in Reviewer (#2262) * Give webviews a slide-in animation if reduced motion isn't set. * Auto-hide toolbar in review mode moving the mouse above the main webview expands the toolbar. When the mouse leaves the toolbar, it will collapse after a delay of 2s. * Save some space on bottom toolbars * Use props for all hard-coded transition durations and decrease most commonly used duration (200ms) to 150ms. * Move auto-hide logic into ToolbarWebView and handle auto-hide specific events in the respective webview subclasses. * Fix typing issues * Fix flickering issue * Add auto_hide_toolbar opt-in to preferences * Rename hide_toolbar to collapse_toolbar to better describe the dock-like behaviour. * Rename setting to minimize_distractions * Reduce calls to pm in eventFilter * Run formatter * Revert setting title to something more specific * Increase default animation time to 180ms * Inset toolbar in review mode when auto-hide is not enabled. * Use card background on toolbar and add glass effect * Use flatten/elevate over inset/outset * Use flatten/elevate over inset/outset * Update toolbar.py * Fix toolbar background delay * Tweak styles * Use "collapse" instead of "auto-hide" * Fix background misalignment in collapse mode * Do not collapse toolbar when pointer is outside MainWebView * Reduce hide_timer interval to 1000ms * Use CSS to hide toolbar instead of setting webview height * Add guard to prevent backdrop-filter: blur on Qt 5.14 * Apply transition to body instead of toolbar to not complicate things for #2301. * Fix Qt 5.14 and apply guard globally * Fix background image scaling difference * Tweak preference wording (dae)
2023-01-09 05:39:31 +01:00
self.toolbarWeb.elevate()
Revamp Preferences, implement Minimalist Mode and Qt widget gallery to test GUI changes (#2289) * Create widget gallery dialog * Add WidgetGallery to debug dialog * Use enum for its intended purpose * Rename "reduced-motion" to "reduce-motion" * Add another border-radius value and make former large radius a bit smaller. * Revamp preferences, add minimalist mode Also: - create additional and missing widget styles and tweak existing ones - use single profile entry to set widget styles and reduce choices to Anki and Native * Indent QTabBar style definitions * Add missing styles for QPushButton states * Fix QTableView background * Remove unused layout from Preferences * Fix QTabView focused tab style * Highlight QCheckBox and QRadioButton when focused * Fix toolbar styles * Reorder preferences * Add setting to hide bottom toolbar * Move toolbar settings above minimalist modes * Remove unused lines * Implement proper full-screen mode * Sort imports * Tweak deck overview appearance in minimalist mode * Undo TitledContainer changes since nobody asked for that * Remove dynamic toolbar background from minimalist mode * Tweak buttons in minimalist mode * Fix some issues * Reduce theme check interval to 5s on Linux * Increase hide timer interval to 2s * Collapse toolbars with slight delay when moving to review state This should ensure the bottom toolbar collapses too. * Allow users to make hiding exclusive to full screen * Rename full screen option * Fix hide mode dropdown ignoring checkbox state on startup * Fix typing issue * Refine background image handling Giving the toolbar body the main webview height ensures background-size: cover behaves exactly the same. To prevent an override of other background properties, users are advised to only set background-images via the background-image property, not the background shorthand. * Fix top toolbar getting huge when switching modes The issue was caused by the min-height hack to align the background images. A call to web.adjustHeightToFit would set the toolbar to the same height as the main webview, as the function makes use of document.offsetHeight. * Prevent scrollbar from appearing on bottom toolbar resize * Cleanup * Put review tab before editing; fix some tab orders * Rename 'network' to 'syncing' * Fix bottom toolbar disappearing on UI > 100 * Improve Preferences layout by adding vertical spacers to the bottom also make the hiding of video_driver and its label more obvious in preferences.py. * Fix bottom toolbar animating on startup Also fix bottom toolbar not appearing when unchecking hide mode in reviewer. * Hide/Show menubar in fullscreen mode along with toolbar * Attempt to fix broken native theme on macOS * Format * Improve native theme on other systems by not forcing palette with the caveat that theme switching can get weird. * Fix theme switching in native style * Remove redundant condition * Add back check for Qt5 to prevent theme issues * Add check for macOS before setting fusion theme * Do not force scrollbar styles on macOS * Remove all of that crazy theme logic * Use canvas instead of button-bg for ColorRole.Button * Make sure Anki style is always based on Fusion otherwise we can't guarantee the same look on all systems. * Explicitly apply default style when Anki style is not selected This should fix the style not switching back after it was selected. * Remove reduncant default_palette * Revert 8af4c1cc2 On Mac with native theme, both Qt5 and Qt6 look correct already. On the Anki theme, without this change, we get the fusion-style scrollbars instead of the rounded ones. * Rename AnkiStyles enum to WidgetStyle * Fix theme switching shades on same theme * Format * Remove unused placeholderText that caused an error when opening the widget gallery on Qt5. * Check for full screen windowState using bitwise operator to prevent error in Qt5. Credit: https://stackoverflow.com/a/65425151 * Hide style option on Windows also exclude native option from dropdown just in case. * Format * Minor naming tweak
2023-01-18 12:24:16 +01:00
self.toolbarWeb.show()
self.bottomWeb.show()
# Resetting state
##########################################################################
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 _increase_background_ops(self) -> None:
if not self._background_op_count:
2021-03-18 01:54:02 +01:00
gui_hooks.backend_will_block()
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
self._background_op_count += 1
def _decrease_background_ops(self) -> None:
self._background_op_count -= 1
if not self._background_op_count:
2021-03-18 01:54:02 +01:00
gui_hooks.backend_did_block()
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
if self._background_op_count < 0:
raise Exception("no background ops active")
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
2021-03-14 10:54:15 +01:00
def _synthesize_op_did_execute_from_reset(self) -> None:
"""Fire the `operation_did_execute` hook with everything marked as changed,
after legacy code has called .reset()"""
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
op = OpChanges()
for field in op.DESCRIPTOR.fields:
if field.name != "kind":
setattr(op, field.name, True)
gui_hooks.operation_did_execute(op, None)
2021-03-14 10:54:15 +01:00
def on_operation_did_execute(
self, changes: OpChanges, handler: object | None
) -> None:
"Notify current screen of changes."
focused = current_window() == self
if self.state == "review":
dirty = self.reviewer.op_executed(changes, handler, focused)
elif self.state == "overview":
dirty = self.overview.op_executed(changes, handler, focused)
elif self.state == "deckBrowser":
dirty = self.deckBrowser.op_executed(changes, handler, focused)
else:
dirty = False
if not focused and dirty:
self.fade_out_webview()
if changes.mtime:
self.toolbar.update_sync_status()
def on_focus_did_change(
self, new_focus: QWidget | None, _old: QWidget | None
) -> None:
"If main window has received focus, ensure current UI state is updated."
if new_focus and new_focus.window() == self:
if self.state == "review":
self.reviewer.refresh_if_needed()
elif self.state == "overview":
self.overview.refresh_if_needed()
elif self.state == "deckBrowser":
self.deckBrowser.refresh_if_needed()
def fade_out_webview(self) -> None:
self.web.eval("document.body.style.opacity = 0.3")
def fade_in_webview(self) -> None:
self.web.eval("document.body.style.opacity = 1")
2021-03-14 10:54:15 +01:00
def reset(self, unused_arg: bool = False) -> None:
"""Legacy method of telling UI to refresh after changes made to DB.
New code should use CollectionOp() instead."""
if self.col:
2021-03-14 10:54:15 +01:00
# fire new `operation_did_execute` hook first. If the overview
# or review screen are currently open, they will rebuild the study
# queues (via mw.col.reset())
self._synthesize_op_did_execute_from_reset()
# fire the old reset hook
2020-01-15 07:53:24 +01:00
gui_hooks.state_did_reset()
self.update_undo_actions()
2021-03-14 10:54:15 +01:00
# legacy
2021-02-01 11:59:18 +01:00
def requireReset(
self,
modal: bool = False,
reason: Any = None,
2021-02-01 11:59:18 +01:00
context: Any = None,
) -> None:
traceback.print_stack(file=sys.stdout)
print("requireReset() is obsolete; please use CollectionOp()")
self.reset()
2019-12-20 09:43:52 +01:00
def maybeReset(self) -> None:
pass
2021-02-01 14:28:21 +01:00
def delayedMaybeReset(self) -> None:
pass
2021-03-14 10:54:15 +01:00
def _resetRequiredState(self, oldState: MainWindowState) -> None:
pass
# HTML helpers
##########################################################################
2019-12-23 01:34:10 +01:00
def button(
self,
link: str,
name: str,
key: str | None = None,
2019-12-23 01:34:10 +01:00
class_: str = "",
id: str = "",
extra: str = "",
) -> str:
class_ = f"but {class_}"
if key:
key = tr.actions_shortcut_key(val=key)
else:
key = ""
2019-12-23 01:34:10 +01:00
return """
<button id="{}" class="{}" onclick="pycmd('{}');return false;"
title="{}" {}>{}</button>""".format(
2019-12-23 01:34:10 +01:00
id,
class_,
link,
key,
extra,
name,
)
# Main window setup
##########################################################################
2019-12-20 09:43:52 +01:00
def setupMainWindow(self) -> None:
# main window
self.form = aqt.forms.main.Ui_MainWindow()
self.form.setupUi(self)
# toolbar
tweb = self.toolbarWeb = TopWebView(self)
Auto-hide toolbar in Reviewer (#2262) * Give webviews a slide-in animation if reduced motion isn't set. * Auto-hide toolbar in review mode moving the mouse above the main webview expands the toolbar. When the mouse leaves the toolbar, it will collapse after a delay of 2s. * Save some space on bottom toolbars * Use props for all hard-coded transition durations and decrease most commonly used duration (200ms) to 150ms. * Move auto-hide logic into ToolbarWebView and handle auto-hide specific events in the respective webview subclasses. * Fix typing issues * Fix flickering issue * Add auto_hide_toolbar opt-in to preferences * Rename hide_toolbar to collapse_toolbar to better describe the dock-like behaviour. * Rename setting to minimize_distractions * Reduce calls to pm in eventFilter * Run formatter * Revert setting title to something more specific * Increase default animation time to 180ms * Inset toolbar in review mode when auto-hide is not enabled. * Use card background on toolbar and add glass effect * Use flatten/elevate over inset/outset * Use flatten/elevate over inset/outset * Update toolbar.py * Fix toolbar background delay * Tweak styles * Use "collapse" instead of "auto-hide" * Fix background misalignment in collapse mode * Do not collapse toolbar when pointer is outside MainWebView * Reduce hide_timer interval to 1000ms * Use CSS to hide toolbar instead of setting webview height * Add guard to prevent backdrop-filter: blur on Qt 5.14 * Apply transition to body instead of toolbar to not complicate things for #2301. * Fix Qt 5.14 and apply guard globally * Fix background image scaling difference * Tweak preference wording (dae)
2023-01-09 05:39:31 +01:00
self.toolbar = Toolbar(self, tweb)
# main area
self.web = MainWebView(self)
# bottom area
sweb = self.bottomWeb = BottomWebView(self)
sweb.setFocusPolicy(Qt.FocusPolicy.WheelFocus)
sweb.disable_zoom()
# add in a layout
self.mainLayout = QVBoxLayout()
2019-12-23 01:34:10 +01:00
self.mainLayout.setContentsMargins(0, 0, 0, 0)
self.mainLayout.setSpacing(0)
self.mainLayout.addWidget(tweb)
self.mainLayout.addWidget(self.web)
self.mainLayout.addWidget(sweb)
self.form.centralwidget.setLayout(self.mainLayout)
# force webengine processes to load before cwd is changed
if is_win:
for webview in self.web, self.bottomWeb:
webview.force_load_hack()
gui_hooks.card_review_webview_did_init(self.web, AnkiWebViewKind.MAIN)
2019-12-20 09:43:52 +01:00
def closeAllWindows(self, onsuccess: Callable) -> None:
aqt.dialogs.closeAll(onsuccess)
# Components
##########################################################################
2019-12-20 09:43:52 +01:00
def setupSignals(self) -> None:
signal.signal(signal.SIGINT, self.onUnixSignal)
signal.signal(signal.SIGTERM, self.onUnixSignal)
2021-02-02 14:30:53 +01:00
def onUnixSignal(self, signum: Any, frame: Any) -> None:
2020-03-02 11:50:17 +01:00
# schedule a rollback & quit
2021-02-01 14:28:21 +01:00
def quit() -> None:
self.col.db.rollback()
self.close()
2019-12-23 01:34:10 +01:00
self.progress.single_shot(100, quit)
2019-12-20 09:43:52 +01:00
def setupProgress(self) -> None:
self.progress = aqt.progress.ProgressManager(self)
2019-12-20 09:43:52 +01:00
def setupErrorHandler(self) -> None:
import aqt.errors
2019-12-23 01:34:10 +01:00
self.errorHandler = aqt.errors.ErrorHandler(self)
def setupAddons(self, args: list | None) -> None:
import aqt.addons
2019-12-23 01:34:10 +01:00
self.addonManager = aqt.addons.AddonManager(self)
if args and args[0] and self._isAddon(args[0]):
self.installAddon(args[0], startup=True)
if not self.safeMode:
self.addonManager.loadAddons()
2020-01-19 02:31:09 +01:00
def maybe_check_for_addon_updates(
self, on_done: Callable[[], None] | None = None
) -> None:
2020-01-19 02:31:09 +01:00
last_check = self.pm.last_addon_update_check()
PEP8 for rest of pylib (#1451) * PEP8 dbproxy.py * PEP8 errors.py * PEP8 httpclient.py * PEP8 lang.py * PEP8 latex.py * Add decorator to deprectate key words * Make replacement for deprecated attribute optional * Use new helper `_print_replacement_warning()` * PEP8 media.py * PEP8 rsbackend.py * PEP8 sound.py * PEP8 stdmodels.py * PEP8 storage.py * PEP8 sync.py * PEP8 tags.py * PEP8 template.py * PEP8 types.py * Fix DeprecatedNamesMixinForModule The class methods need to be overridden with instance methods, so every module has its own dicts. * Use `# pylint: disable=invalid-name` instead of id * PEP8 utils.py * Only decorate `__getattr__` with `@no_type_check` * Fix mypy issue with snakecase Importing it from `anki._vendor` raises attribute errors. * Format * Remove inheritance of DeprecatedNamesMixin There's almost no shared code now and overriding classmethods with instance methods raises mypy issues. * Fix traceback frames of deprecation warnings * remove fn/TimedLog (dae) Neither Anki nor add-ons appear to have been using it * fix some issues with stringcase use (dae) - the wheel was depending on the PyPI version instead of our vendored version - _vendor:stringcase should not have been listed in the anki py_library. We already include the sources in py_srcs, and need to refer to them directly. By listing _vendor:stringcase as well, we were making a top-level stringcase library available, which would have only worked for distributing because the wheel definition was also incorrect. - mypy errors are what caused me to mistakenly add the above - they were because the type: ignore at the top of stringcase.py was causing mypy to completely ignore the file, so it was not aware of any attributes it contained.
2021-10-25 06:50:13 +02:00
elap = int_time() - last_check
2020-01-19 02:31:09 +01:00
def wrap_on_updates_installed(log: list[DownloadLogEntry]) -> None:
self.on_updates_installed(log)
if on_done:
on_done()
if elap > 86_400 or self.pm.last_run_version() != point_version():
2020-01-19 02:31:09 +01:00
check_and_prompt_for_updates(
self,
self.addonManager,
wrap_on_updates_installed,
requested_by_user=False,
2020-01-19 02:31:09 +01:00
)
PEP8 for rest of pylib (#1451) * PEP8 dbproxy.py * PEP8 errors.py * PEP8 httpclient.py * PEP8 lang.py * PEP8 latex.py * Add decorator to deprectate key words * Make replacement for deprecated attribute optional * Use new helper `_print_replacement_warning()` * PEP8 media.py * PEP8 rsbackend.py * PEP8 sound.py * PEP8 stdmodels.py * PEP8 storage.py * PEP8 sync.py * PEP8 tags.py * PEP8 template.py * PEP8 types.py * Fix DeprecatedNamesMixinForModule The class methods need to be overridden with instance methods, so every module has its own dicts. * Use `# pylint: disable=invalid-name` instead of id * PEP8 utils.py * Only decorate `__getattr__` with `@no_type_check` * Fix mypy issue with snakecase Importing it from `anki._vendor` raises attribute errors. * Format * Remove inheritance of DeprecatedNamesMixin There's almost no shared code now and overriding classmethods with instance methods raises mypy issues. * Fix traceback frames of deprecation warnings * remove fn/TimedLog (dae) Neither Anki nor add-ons appear to have been using it * fix some issues with stringcase use (dae) - the wheel was depending on the PyPI version instead of our vendored version - _vendor:stringcase should not have been listed in the anki py_library. We already include the sources in py_srcs, and need to refer to them directly. By listing _vendor:stringcase as well, we were making a top-level stringcase library available, which would have only worked for distributing because the wheel definition was also incorrect. - mypy errors are what caused me to mistakenly add the above - they were because the type: ignore at the top of stringcase.py was causing mypy to completely ignore the file, so it was not aware of any attributes it contained.
2021-10-25 06:50:13 +02:00
self.pm.set_last_addon_update_check(int_time())
elif on_done:
on_done()
2020-01-19 02:31:09 +01:00
def on_updates_installed(self, log: list[DownloadLogEntry]) -> None:
2020-01-19 02:31:09 +01:00
if log:
show_log_to_user(self, log)
2019-12-20 09:43:52 +01:00
def setupSpellCheck(self) -> None:
2019-12-23 01:34:10 +01:00
os.environ["QTWEBENGINE_DICTIONARIES_PATH"] = os.path.join(
self.pm.base, "dictionaries"
)
2019-12-20 09:43:52 +01:00
def setupThreads(self) -> None:
self._mainThread = QThread.currentThread()
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
self._background_op_count = 0
2019-12-20 09:43:52 +01:00
def inMainThread(self) -> bool:
return self._mainThread == QThread.currentThread()
2019-12-20 09:43:52 +01:00
def setupDeckBrowser(self) -> None:
from aqt.deckbrowser import DeckBrowser
2019-12-23 01:34:10 +01:00
self.deckBrowser = DeckBrowser(self)
2019-12-20 09:43:52 +01:00
def setupOverview(self) -> None:
from aqt.overview import Overview
2019-12-23 01:34:10 +01:00
self.overview = Overview(self)
2019-12-20 09:43:52 +01:00
def setupReviewer(self) -> None:
from aqt.reviewer import Reviewer
2019-12-23 01:34:10 +01:00
self.reviewer = Reviewer(self)
# Syncing
##########################################################################
2021-02-01 11:59:18 +01:00
def on_sync_button_clicked(self) -> None:
if self.media_syncer.is_syncing():
self.media_syncer.show_sync_log()
else:
2020-05-31 02:53:54 +02:00
auth = self.pm.sync_auth()
if not auth:
sync_login(
self,
lambda: self._sync_collection_and_media(self._refresh_after_sync),
)
2020-05-31 02:53:54 +02:00
else:
self._sync_collection_and_media(self._refresh_after_sync)
2021-02-01 11:59:18 +01:00
def _refresh_after_sync(self) -> None:
self.toolbar.redraw()
self.flags.require_refresh()
2021-02-01 14:28:21 +01:00
def _sync_collection_and_media(self, after_sync: Callable[[], None]) -> None:
2020-05-31 02:53:54 +02:00
"Caller should ensure auth available."
# start media sync if not already running
if not self.media_syncer.is_syncing():
self.media_syncer.start()
2021-02-01 14:28:21 +01:00
def on_collection_sync_finished() -> None:
self.col.clear_python_undo()
self.col.models._clear_cache()
gui_hooks.sync_did_finish()
2020-09-15 13:06:11 +02:00
self.reset()
after_sync()
gui_hooks.sync_will_start()
2020-05-31 02:53:54 +02:00
sync_collection(self, on_done=on_collection_sync_finished)
def maybe_auto_sync_on_open_close(self, after_sync: Callable[[], None]) -> None:
"If disabled, after_sync() is called immediately."
def after_sync_and_call_addon_update() -> None:
after_sync()
if not self.safeMode:
self.maybe_check_for_addon_updates(self.setupAutoUpdate)
2020-05-31 02:53:54 +02:00
if self.can_auto_sync():
self._sync_collection_and_media(after_sync_and_call_addon_update)
2020-05-31 02:53:54 +02:00
else:
after_sync_and_call_addon_update()
2020-02-05 02:55:46 +01:00
def maybe_auto_sync_media(self) -> None:
2020-05-31 02:53:54 +02:00
if self.can_auto_sync():
2020-02-05 02:55:46 +01:00
return
2020-05-31 02:53:54 +02:00
# media_syncer takes care of media syncing preference check
2020-02-05 02:55:46 +01:00
self.media_syncer.start()
2020-05-31 02:53:54 +02:00
def can_auto_sync(self) -> bool:
return (
self.pm.auto_syncing_enabled()
2020-05-31 03:49:05 +02:00
and bool(self.pm.sync_auth())
2020-05-31 02:53:54 +02:00
and not self.safeMode
Backups (#1685) * Add zstd dep * Implement backend backup with zstd * Implement backup thinning * Write backup meta * Use new file ending anki21b * Asynchronously backup on collection close in Rust * Revert "Add zstd dep" This reverts commit 3fcb2141d2be15f907269d13275c41971431385c. * Add zstd again * Take backup col path from col struct * Fix formatting * Implement backup restoring on backend * Normalize restored media file names * Refactor `extract_legacy_data()` A bit cumbersome due to borrowing rules. * Refactor * Make thinning calendar-based and gradual * Consider last kept backups of previous stages * Import full apkgs and colpkgs with backend * Expose new backup settings * Test `BackupThinner` and make it deterministic * Mark backup_path when closing optional * Delete leaky timer * Add progress updates for restoring media * Write restored collection to tempfile first * Do collection compression in the background thread This has us currently storing an uncompressed and compressed copy of the collection in memory (not ideal), but means the collection can be closed without waiting for compression to complete. On a large collection, this takes a close and reopen from about 0.55s to about 0.07s. The old backup code for comparison: about 0.35s for compression off, about 8.5s for zip compression. * Use multithreading in zstd compression On my system, this reduces the compression time of a large collection from about 0.55s to 0.08s. * Stream compressed collection data into zip file * Tweak backup explanation + Fix incorrect tab order for ignore accents option * Decouple restoring backup and full import In the first case, no profile is opened, unless the new collection succeeds to load. In the second case, either the old collection is reloaded or the new one is loaded. * Fix number gap in Progress message * Don't revert backup when media fails but report it * Tweak error flow * Remove native BackupLimits enum * Fix type annotation * Add thinning test for whole year * Satisfy linter * Await async backup to finish * Move restart disclaimer out of backup tab Should be visible regardless of the current tab. * Write restored collection in chunks * Refactor * Write media in chunks and refactor * Log error if removing file fails * join_backup_task -> await_backup_completion * Refactor backup.rs * Refactor backup meta and collection extraction * Fix wrong error being returned * Call sync_all() on new collection * Add ImportError * Store logger in Backend, instead of creating one on demand init_backend() accepts a Logger rather than a log file, to allow other callers to customize the logger if they wish. In the future we may want to explore using the tracing crate as an alternative; it's a bit more ergonomic, as a logger doesn't need to be passed around, and it plays more nicely with async code. * Sync file contents prior to rename; sync folder after rename. * Limit backup creation to once per 30 min * Use zstd::stream::copy_decode * Make importing abortable * Don't revert if backup media is aborted * Set throttle implicitly * Change force flag to minimum_backup_interval * Don't attempt to open folders on Windows * Join last backup thread before starting new one Also refactor. * Disable auto sync and backup when restoring again * Force backup on full download * Include the reason why a media file import failed, and the file path - Introduce a FileIoError that contains a string representation of the underlying I/O error, and an associated path. There are a few places in the code where we're currently manually including the filename in a custom error message, and this is a step towards a more consistent approach (but we may be better served with a more general approach in the future similar to Anyhow's .context()) - Move the error message into importing.ftl, as it's a bit neater when error messages live in the same file as the rest of the messages associated with some functionality. * Fix importing of media files * Minor wording tweaks * Save an allocation I18n strings with replacements are already strings, so we can skip the extra allocation. Not that it matters here at all. * Terminate import if file missing from archive If a third-party tool is creating invalid archives, the user should know about it. This should be rare, so I did not attempt to make it translatable. * Skip multithreaded compression on small collections Co-authored-by: Damien Elmes <gpg@ankiweb.net>
2022-03-07 06:11:31 +01:00
and not self.restoring_backup
)
2020-05-30 04:28:22 +02:00
2020-05-31 02:53:54 +02:00
# legacy
2021-02-01 14:28:21 +01:00
def _sync(self) -> None:
2020-05-31 02:53:54 +02:00
pass
2020-05-31 02:53:54 +02:00
onSync = on_sync_button_clicked
2020-05-30 04:28:22 +02:00
# Tools
##########################################################################
def raiseMain(self) -> bool:
if not self.app.activeWindow():
# make sure window is shown
self.setWindowState(self.windowState() & ~Qt.WindowState.WindowMinimized) # type: ignore
return True
2019-12-20 09:43:52 +01:00
def setupStyle(self) -> None:
theme_manager.apply_style()
if is_lin:
# On Linux, the check requires invoking an external binary,
# and can potentially produce verbose logs on systems where
# the preferred theme cannot be determined,
# which we don't want to be doing frequently
interval_secs = 300
Revamp Preferences, implement Minimalist Mode and Qt widget gallery to test GUI changes (#2289) * Create widget gallery dialog * Add WidgetGallery to debug dialog * Use enum for its intended purpose * Rename "reduced-motion" to "reduce-motion" * Add another border-radius value and make former large radius a bit smaller. * Revamp preferences, add minimalist mode Also: - create additional and missing widget styles and tweak existing ones - use single profile entry to set widget styles and reduce choices to Anki and Native * Indent QTabBar style definitions * Add missing styles for QPushButton states * Fix QTableView background * Remove unused layout from Preferences * Fix QTabView focused tab style * Highlight QCheckBox and QRadioButton when focused * Fix toolbar styles * Reorder preferences * Add setting to hide bottom toolbar * Move toolbar settings above minimalist modes * Remove unused lines * Implement proper full-screen mode * Sort imports * Tweak deck overview appearance in minimalist mode * Undo TitledContainer changes since nobody asked for that * Remove dynamic toolbar background from minimalist mode * Tweak buttons in minimalist mode * Fix some issues * Reduce theme check interval to 5s on Linux * Increase hide timer interval to 2s * Collapse toolbars with slight delay when moving to review state This should ensure the bottom toolbar collapses too. * Allow users to make hiding exclusive to full screen * Rename full screen option * Fix hide mode dropdown ignoring checkbox state on startup * Fix typing issue * Refine background image handling Giving the toolbar body the main webview height ensures background-size: cover behaves exactly the same. To prevent an override of other background properties, users are advised to only set background-images via the background-image property, not the background shorthand. * Fix top toolbar getting huge when switching modes The issue was caused by the min-height hack to align the background images. A call to web.adjustHeightToFit would set the toolbar to the same height as the main webview, as the function makes use of document.offsetHeight. * Prevent scrollbar from appearing on bottom toolbar resize * Cleanup * Put review tab before editing; fix some tab orders * Rename 'network' to 'syncing' * Fix bottom toolbar disappearing on UI > 100 * Improve Preferences layout by adding vertical spacers to the bottom also make the hiding of video_driver and its label more obvious in preferences.py. * Fix bottom toolbar animating on startup Also fix bottom toolbar not appearing when unchecking hide mode in reviewer. * Hide/Show menubar in fullscreen mode along with toolbar * Attempt to fix broken native theme on macOS * Format * Improve native theme on other systems by not forcing palette with the caveat that theme switching can get weird. * Fix theme switching in native style * Remove redundant condition * Add back check for Qt5 to prevent theme issues * Add check for macOS before setting fusion theme * Do not force scrollbar styles on macOS * Remove all of that crazy theme logic * Use canvas instead of button-bg for ColorRole.Button * Make sure Anki style is always based on Fusion otherwise we can't guarantee the same look on all systems. * Explicitly apply default style when Anki style is not selected This should fix the style not switching back after it was selected. * Remove reduncant default_palette * Revert 8af4c1cc2 On Mac with native theme, both Qt5 and Qt6 look correct already. On the Anki theme, without this change, we get the fusion-style scrollbars instead of the rounded ones. * Rename AnkiStyles enum to WidgetStyle * Fix theme switching shades on same theme * Format * Remove unused placeholderText that caused an error when opening the widget gallery on Qt5. * Check for full screen windowState using bitwise operator to prevent error in Qt5. Credit: https://stackoverflow.com/a/65425151 * Hide style option on Windows also exclude native option from dropdown just in case. * Format * Minor naming tweak
2023-01-18 12:24:16 +01:00
else:
interval_secs = 2
self.progress.timer(
interval_secs * 1000,
Revamp Preferences, implement Minimalist Mode and Qt widget gallery to test GUI changes (#2289) * Create widget gallery dialog * Add WidgetGallery to debug dialog * Use enum for its intended purpose * Rename "reduced-motion" to "reduce-motion" * Add another border-radius value and make former large radius a bit smaller. * Revamp preferences, add minimalist mode Also: - create additional and missing widget styles and tweak existing ones - use single profile entry to set widget styles and reduce choices to Anki and Native * Indent QTabBar style definitions * Add missing styles for QPushButton states * Fix QTableView background * Remove unused layout from Preferences * Fix QTabView focused tab style * Highlight QCheckBox and QRadioButton when focused * Fix toolbar styles * Reorder preferences * Add setting to hide bottom toolbar * Move toolbar settings above minimalist modes * Remove unused lines * Implement proper full-screen mode * Sort imports * Tweak deck overview appearance in minimalist mode * Undo TitledContainer changes since nobody asked for that * Remove dynamic toolbar background from minimalist mode * Tweak buttons in minimalist mode * Fix some issues * Reduce theme check interval to 5s on Linux * Increase hide timer interval to 2s * Collapse toolbars with slight delay when moving to review state This should ensure the bottom toolbar collapses too. * Allow users to make hiding exclusive to full screen * Rename full screen option * Fix hide mode dropdown ignoring checkbox state on startup * Fix typing issue * Refine background image handling Giving the toolbar body the main webview height ensures background-size: cover behaves exactly the same. To prevent an override of other background properties, users are advised to only set background-images via the background-image property, not the background shorthand. * Fix top toolbar getting huge when switching modes The issue was caused by the min-height hack to align the background images. A call to web.adjustHeightToFit would set the toolbar to the same height as the main webview, as the function makes use of document.offsetHeight. * Prevent scrollbar from appearing on bottom toolbar resize * Cleanup * Put review tab before editing; fix some tab orders * Rename 'network' to 'syncing' * Fix bottom toolbar disappearing on UI > 100 * Improve Preferences layout by adding vertical spacers to the bottom also make the hiding of video_driver and its label more obvious in preferences.py. * Fix bottom toolbar animating on startup Also fix bottom toolbar not appearing when unchecking hide mode in reviewer. * Hide/Show menubar in fullscreen mode along with toolbar * Attempt to fix broken native theme on macOS * Format * Improve native theme on other systems by not forcing palette with the caveat that theme switching can get weird. * Fix theme switching in native style * Remove redundant condition * Add back check for Qt5 to prevent theme issues * Add check for macOS before setting fusion theme * Do not force scrollbar styles on macOS * Remove all of that crazy theme logic * Use canvas instead of button-bg for ColorRole.Button * Make sure Anki style is always based on Fusion otherwise we can't guarantee the same look on all systems. * Explicitly apply default style when Anki style is not selected This should fix the style not switching back after it was selected. * Remove reduncant default_palette * Revert 8af4c1cc2 On Mac with native theme, both Qt5 and Qt6 look correct already. On the Anki theme, without this change, we get the fusion-style scrollbars instead of the rounded ones. * Rename AnkiStyles enum to WidgetStyle * Fix theme switching shades on same theme * Format * Remove unused placeholderText that caused an error when opening the widget gallery on Qt5. * Check for full screen windowState using bitwise operator to prevent error in Qt5. Credit: https://stackoverflow.com/a/65425151 * Hide style option on Windows also exclude native option from dropdown just in case. * Format * Minor naming tweak
2023-01-18 12:24:16 +01:00
theme_manager.apply_style,
True,
False,
parent=self,
)
def set_theme(self, theme: Theme) -> None:
self.pm.set_theme(theme)
self.setupStyle()
# Key handling
##########################################################################
2019-12-20 09:43:52 +01:00
def setupKeys(self) -> None:
globalShortcuts = [
("Ctrl+:", show_debug_console),
("d", lambda: self.moveToState("deckBrowser")),
("s", self.onStudyKey),
("a", self.onAddCard),
("b", self.onBrowse),
2018-05-31 05:05:30 +02:00
("t", self.onStats),
2020-05-31 02:53:54 +02:00
("y", self.on_sync_button_clicked),
]
self.applyShortcuts(globalShortcuts)
self.stateShortcuts: list[QShortcut] = []
def _normalize_shortcuts(
self, shortcuts: Sequence[tuple[str, Callable]]
) -> Sequence[tuple[QKeySequence, Callable]]:
"""
Remove duplicate shortcuts (possibly added by add-ons)
by normalizing them and filtering through a dictionary.
The last duplicate shortcut wins, so add-ons will override
standard shortcuts if they append to the shortcut list.
"""
return tuple({QKeySequence(key): fn for key, fn in shortcuts}.items())
2019-12-23 01:34:10 +01:00
def applyShortcuts(
self, shortcuts: Sequence[tuple[str, Callable]]
) -> list[QShortcut]:
qshortcuts = []
for key, fn in self._normalize_shortcuts(shortcuts):
scut = QShortcut(key, self, activated=fn) # type: ignore
scut.setAutoRepeat(False)
qshortcuts.append(scut)
return qshortcuts
def setStateShortcuts(self, shortcuts: list[tuple[str, Callable]]) -> None:
2020-01-15 07:53:24 +01:00
gui_hooks.state_shortcuts_will_change(self.state, shortcuts)
# legacy hook
runHook(f"{self.state}StateShortcuts", shortcuts)
self.stateShortcuts = self.applyShortcuts(shortcuts)
2019-12-20 09:43:52 +01:00
def clearStateShortcuts(self) -> None:
for qs in self.stateShortcuts:
sip.delete(qs) # type: ignore
self.stateShortcuts = []
2019-12-20 09:43:52 +01:00
def onStudyKey(self) -> None:
if self.state == "overview":
self.col.startTimebox()
self.moveToState("review")
else:
self.moveToState("overview")
# App exit
##########################################################################
2019-12-20 09:43:52 +01:00
def closeEvent(self, event: QCloseEvent) -> None:
self.pm.set_last_run_version()
if self.state == "profileManager":
# if profile manager active, this event may fire via OS X menu bar's
# quit option
self.profileDiag.close()
event.accept()
else:
# ignore the event for now, as we need time to clean up
event.ignore()
self.unloadProfileAndExit()
# Undo & autosave
##########################################################################
def undo(self) -> None:
"Call operations/collection.py:undo() directly instead."
2021-04-06 06:36:13 +02:00
undo(parent=self)
def redo(self) -> None:
"Call operations/collection.py:redo() directly instead."
redo(parent=self)
def undo_actions_info(self) -> UndoActionsInfo:
"Info about the current undo/redo state for updating menus."
status = self.col.undo_status() if self.col else UndoStatus()
return UndoActionsInfo.from_undo_status(status)
def update_undo_actions(self) -> None:
"""Tell the UI to redraw the undo/redo menu actions based on the current state.
Usually you do not need to call this directly; it is called when a
CollectionOp is run, and will be called when the legacy .reset() or
.checkpoint() methods are used."""
info = self.undo_actions_info()
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)
gui_hooks.undo_state_did_change(info)
2021-03-14 10:54:15 +01:00
2021-02-01 11:59:18 +01:00
def checkpoint(self, name: str) -> None:
self.col.save(name)
self.update_undo_actions()
2019-12-20 09:43:52 +01:00
def autosave(self) -> None:
self.col.autosave()
self.update_undo_actions()
maybeEnableUndo = update_undo_actions
onUndo = undo
# Other menu operations
##########################################################################
2019-12-20 09:43:52 +01:00
def onAddCard(self) -> None:
aqt.dialogs.open("AddCards", self)
2019-12-20 09:43:52 +01:00
def onBrowse(self) -> None:
aqt.dialogs.open("Browser", self, card=self.reviewer.card)
2021-02-01 11:59:18 +01:00
def onEditCurrent(self) -> None:
aqt.dialogs.open("EditCurrent", self)
2021-02-01 14:28:21 +01:00
def onOverview(self) -> None:
self.col.reset()
self.moveToState("overview")
2021-02-01 11:59:18 +01:00
def onStats(self) -> None:
deck = self._selectedDeck()
if not deck:
return
want_old = KeyboardModifiersPressed().shift
if want_old:
aqt.dialogs.open("DeckStats", self)
else:
aqt.dialogs.open("NewDeckStats", self)
2021-02-01 14:28:21 +01:00
def onPrefs(self) -> None:
aqt.dialogs.open("Preferences", self)
2021-02-01 11:59:18 +01:00
def onNoteTypes(self) -> None:
2012-12-22 00:21:24 +01:00
import aqt.models
2019-12-23 01:34:10 +01:00
2012-12-22 00:21:24 +01:00
aqt.models.Models(self, self, fromMain=True)
2021-02-01 14:28:21 +01:00
def onAbout(self) -> None:
aqt.dialogs.open("About", self)
2021-02-01 14:28:21 +01:00
def onDonate(self) -> None:
openLink(aqt.appDonate)
2021-02-01 14:28:21 +01:00
def onDocumentation(self) -> None:
openHelp(HelpPage.INDEX)
# legacy
def onDeckConf(self, deck: DeckDict | None = None) -> None:
pass
# Importing & exporting
##########################################################################
2019-12-20 09:43:52 +01:00
def handleImport(self, path: str) -> None:
"Importing triggered via file double-click, or dragging file onto Anki icon."
import aqt.importing
2019-12-23 01:34:10 +01:00
if not os.path.exists(path):
# there were instances in the distant past where the received filename was not
# valid (encoding issues?), so this was added to direct users to try
# file>import instead.
2023-01-29 03:55:01 +01:00
showInfo(f"{tr.qt_misc_please_use_fileimport_to_import_this()} ({path})")
2019-12-20 09:43:52 +01:00
return None
if not self.pm.legacy_import_export():
import_file(self, path)
else:
aqt.importing.importFile(self, path)
2021-02-01 11:59:18 +01:00
def onImport(self) -> None:
"Importing triggered via File>Import."
import aqt.importing
2019-12-23 01:34:10 +01:00
if not self.pm.legacy_import_export():
prompt_for_file_then_import(self)
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
else:
aqt.importing.onImport(self)
def onExport(self, did: DeckId | None = None) -> None:
import aqt.exporting
2019-12-23 01:34:10 +01:00
if not self.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
ExportDialog(self, did=did)
else:
aqt.exporting.ExportDialog(self, did=did)
# Installing add-ons from CLI / mimetype handler
##########################################################################
2021-02-01 14:28:21 +01:00
def installAddon(self, path: str, startup: bool = False) -> None:
from aqt.addons import installAddonPackages
2020-01-03 18:23:28 +01:00
installAddonPackages(
self.addonManager,
[path],
warn=True,
advise_restart=not startup,
strictly_modal=startup,
parent=None if startup else self,
force_enable=True,
)
# Cramming
##########################################################################
def onCram(self) -> None:
2021-03-24 04:17:12 +01:00
aqt.dialogs.open("FilteredDeckConfigDialog", self)
# Menu, title bar & status
##########################################################################
2019-12-20 09:43:52 +01:00
def setupMenus(self) -> None:
m = self.form
# File
qconnect(
m.actionSwitchProfile.triggered, self.unloadProfileAndShowProfileManager
)
qconnect(m.actionImport.triggered, self.onImport)
qconnect(m.actionExport.triggered, self.onExport)
Backup improvements (#1728) * Collection needs to be closed prior to backup even when not downgrading * Backups -> BackupLimits * Some improvements to backup_task - backup_inner now returns the error instead of logging it, so that the frontend can discover the issue when they await a backup (or create another one) - start_backup() was acquiring backup_task twice, and if another thread started a backup between the two locks, the task could have been accidentally overwritten without awaiting it * Backups no longer require a collection close - Instead of closing the collection, we ensure there is no active transaction, and flush the WAL to disk. This means the undo history is no longer lost on backup, which will be particularly useful if we add a periodic backup in the future. - Because a close is no longer required, backups are now achieved with a separate command, instead of being included in CloseCollection(). - Full sync no longer requires an extra close+reopen step, and we now wait for the backup to complete before proceeding. - Create a backup before 'check db' * Add File>Create Backup https://forums.ankiweb.net/t/anki-mac-os-no-backup-on-sync/6157 * Defer checkpoint until we know we need it When running periodic backups on a timer, we don't want to be fsync()ing unnecessarily. * Skip backup if modification time has not changed We don't want the user leaving Anki open overnight, and coming back to lots of identical backups. * Periodic backups Creates an automatic backup every 30 minutes if the collection has been modified. If there's a legacy checkpoint active, tries again 5 minutes later. * Switch to a user-configurable backup duration CreateBackup() now uses a simple force argument to determine whether the user's limits should be respected or not, and only potentially destructive ops (full download, check DB) override the user's configured limit. I considered having a separate limit for collection close and automatic backups (eg keeping the previous 5 minute limit for collection close), but that had two downsides: - When the user closes their collection at the end of the day, they'd get a recent backup. When they open the collection the next day, it would get backed up again within 5 minutes, even though not much had changed. - Multiple limits are harder to communicate to users in the UI Some remaining decisions I wasn't 100% sure about: - If force is true but the collection has not been modified, the backup will be skipped. If the user manually deleted their backups without closing Anki, they wouldn't get a new one if the mtime hadn't changed. - Force takes preference over the configured backup interval - should we be ignored the user here, or take no backups at all? Did a sneaky edit of the existing ftl string, as it hasn't been live long. * Move maybe_backup() into Collection * Use a single method for manual and periodic backups When manually creating a backup via the File menu, we no longer make the user wait until the backup completes. As we continue waiting for the backup in the background, if any errors occur, the user will get notified about it fairly quickly. * Show message to user if backup was skipped due to no changes + Don't incorrectly assert a backup will be created on force * Add "automatic" to description * Ensure we backup prior to importing colpkg if collection open The backup doesn't happen when invoked from 'open backup' in the profile screen, which matches Anki's previous behaviour. The user could potentially clobber up to 30 minutes of their work if they exited to the profile screen and restored a backup, but the alternative is we create backups every time a backup is restored, which may happen a number of times if the user is trying various ones. Or we could go back to a separate throttle amount for this case, at the cost of more complexity. * Remove the 0 special case on backup interval; minimum of 5 minutes https://github.com/ankitects/anki/pull/1728#discussion_r830876833
2022-03-21 10:40:42 +01:00
qconnect(m.action_create_backup.triggered, self.on_create_backup_now)
qconnect(m.actionExit.triggered, self.close)
# Help
qconnect(m.actionDocumentation.triggered, self.onDocumentation)
qconnect(m.actionDonate.triggered, self.onDonate)
qconnect(m.actionAbout.triggered, self.onAbout)
# Edit
qconnect(m.actionUndo.triggered, self.undo)
qconnect(m.actionRedo.triggered, self.redo)
# Tools
qconnect(m.actionFullDatabaseCheck.triggered, self.onCheckDB)
qconnect(m.actionCheckMediaDatabase.triggered, self.on_check_media_db)
qconnect(m.actionStudyDeck.triggered, self.onStudyDeck)
qconnect(m.actionCreateFiltered.triggered, self.onCram)
qconnect(m.actionEmptyCards.triggered, self.onEmptyCards)
qconnect(m.actionNoteTypes.triggered, self.onNoteTypes)
qconnect(m.actionPreferences.triggered, self.onPrefs)
# View
qconnect(
m.actionZoomIn.triggered,
lambda: self.web.setZoomFactor(self.web.zoomFactor() + 0.1),
)
qconnect(
m.actionZoomOut.triggered,
lambda: self.web.setZoomFactor(self.web.zoomFactor() - 0.1),
)
qconnect(m.actionResetZoom.triggered, lambda: self.web.setZoomFactor(1))
# app-wide shortcut
qconnect(m.actionFullScreen.triggered, self.on_toggle_full_screen)
m.actionFullScreen.setShortcut(
QKeySequence("F11") if is_lin else QKeySequence.StandardKey.FullScreen
)
m.actionFullScreen.setShortcutContext(Qt.ShortcutContext.ApplicationShortcut)
2019-12-20 09:43:52 +01:00
def updateTitleBar(self) -> None:
self.setWindowTitle("Anki")
# View
##########################################################################
def on_toggle_full_screen(self) -> None:
if disallow_full_screen():
showWarning(
tr.actions_currently_unsupported(),
parent=self,
help=HelpPage.FULL_SCREEN_ISSUE,
)
return
else:
window = self.app.activeWindow()
window.setWindowState(
window.windowState() ^ Qt.WindowState.WindowFullScreen
)
Revamp Preferences, implement Minimalist Mode and Qt widget gallery to test GUI changes (#2289) * Create widget gallery dialog * Add WidgetGallery to debug dialog * Use enum for its intended purpose * Rename "reduced-motion" to "reduce-motion" * Add another border-radius value and make former large radius a bit smaller. * Revamp preferences, add minimalist mode Also: - create additional and missing widget styles and tweak existing ones - use single profile entry to set widget styles and reduce choices to Anki and Native * Indent QTabBar style definitions * Add missing styles for QPushButton states * Fix QTableView background * Remove unused layout from Preferences * Fix QTabView focused tab style * Highlight QCheckBox and QRadioButton when focused * Fix toolbar styles * Reorder preferences * Add setting to hide bottom toolbar * Move toolbar settings above minimalist modes * Remove unused lines * Implement proper full-screen mode * Sort imports * Tweak deck overview appearance in minimalist mode * Undo TitledContainer changes since nobody asked for that * Remove dynamic toolbar background from minimalist mode * Tweak buttons in minimalist mode * Fix some issues * Reduce theme check interval to 5s on Linux * Increase hide timer interval to 2s * Collapse toolbars with slight delay when moving to review state This should ensure the bottom toolbar collapses too. * Allow users to make hiding exclusive to full screen * Rename full screen option * Fix hide mode dropdown ignoring checkbox state on startup * Fix typing issue * Refine background image handling Giving the toolbar body the main webview height ensures background-size: cover behaves exactly the same. To prevent an override of other background properties, users are advised to only set background-images via the background-image property, not the background shorthand. * Fix top toolbar getting huge when switching modes The issue was caused by the min-height hack to align the background images. A call to web.adjustHeightToFit would set the toolbar to the same height as the main webview, as the function makes use of document.offsetHeight. * Prevent scrollbar from appearing on bottom toolbar resize * Cleanup * Put review tab before editing; fix some tab orders * Rename 'network' to 'syncing' * Fix bottom toolbar disappearing on UI > 100 * Improve Preferences layout by adding vertical spacers to the bottom also make the hiding of video_driver and its label more obvious in preferences.py. * Fix bottom toolbar animating on startup Also fix bottom toolbar not appearing when unchecking hide mode in reviewer. * Hide/Show menubar in fullscreen mode along with toolbar * Attempt to fix broken native theme on macOS * Format * Improve native theme on other systems by not forcing palette with the caveat that theme switching can get weird. * Fix theme switching in native style * Remove redundant condition * Add back check for Qt5 to prevent theme issues * Add check for macOS before setting fusion theme * Do not force scrollbar styles on macOS * Remove all of that crazy theme logic * Use canvas instead of button-bg for ColorRole.Button * Make sure Anki style is always based on Fusion otherwise we can't guarantee the same look on all systems. * Explicitly apply default style when Anki style is not selected This should fix the style not switching back after it was selected. * Remove reduncant default_palette * Revert 8af4c1cc2 On Mac with native theme, both Qt5 and Qt6 look correct already. On the Anki theme, without this change, we get the fusion-style scrollbars instead of the rounded ones. * Rename AnkiStyles enum to WidgetStyle * Fix theme switching shades on same theme * Format * Remove unused placeholderText that caused an error when opening the widget gallery on Qt5. * Check for full screen windowState using bitwise operator to prevent error in Qt5. Credit: https://stackoverflow.com/a/65425151 * Hide style option on Windows also exclude native option from dropdown just in case. * Format * Minor naming tweak
2023-01-18 12:24:16 +01:00
# Hide Menubar on Windows and Linux
if window.windowState() & Qt.WindowState.WindowFullScreen and not is_mac:
self.fullscreen = True
self.hide_menubar()
else:
self.fullscreen = False
self.show_menubar()
# Update Toolbar states
self.toolbarWeb.hide_if_allowed()
self.bottomWeb.hide_if_allowed()
def hide_menubar(self) -> None:
self.form.menubar.setFixedHeight(0)
def show_menubar(self) -> None:
self.form.menubar.setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX)
self.form.menubar.setMinimumSize(0, 0)
Auto-hide toolbar in Reviewer (#2262) * Give webviews a slide-in animation if reduced motion isn't set. * Auto-hide toolbar in review mode moving the mouse above the main webview expands the toolbar. When the mouse leaves the toolbar, it will collapse after a delay of 2s. * Save some space on bottom toolbars * Use props for all hard-coded transition durations and decrease most commonly used duration (200ms) to 150ms. * Move auto-hide logic into ToolbarWebView and handle auto-hide specific events in the respective webview subclasses. * Fix typing issues * Fix flickering issue * Add auto_hide_toolbar opt-in to preferences * Rename hide_toolbar to collapse_toolbar to better describe the dock-like behaviour. * Rename setting to minimize_distractions * Reduce calls to pm in eventFilter * Run formatter * Revert setting title to something more specific * Increase default animation time to 180ms * Inset toolbar in review mode when auto-hide is not enabled. * Use card background on toolbar and add glass effect * Use flatten/elevate over inset/outset * Use flatten/elevate over inset/outset * Update toolbar.py * Fix toolbar background delay * Tweak styles * Use "collapse" instead of "auto-hide" * Fix background misalignment in collapse mode * Do not collapse toolbar when pointer is outside MainWebView * Reduce hide_timer interval to 1000ms * Use CSS to hide toolbar instead of setting webview height * Add guard to prevent backdrop-filter: blur on Qt 5.14 * Apply transition to body instead of toolbar to not complicate things for #2301. * Fix Qt 5.14 and apply guard globally * Fix background image scaling difference * Tweak preference wording (dae)
2023-01-09 05:39:31 +01:00
# Auto update
##########################################################################
2019-12-20 09:43:52 +01:00
def setupAutoUpdate(self) -> None:
import aqt.update
2019-12-23 01:34:10 +01:00
self.autoUpdate = aqt.update.LatestVersionFinder(self)
qconnect(self.autoUpdate.newVerAvail, self.newVerAvail)
qconnect(self.autoUpdate.newMsg, self.newMsg)
qconnect(self.autoUpdate.clockIsOff, self.clockIsOff)
self.autoUpdate.start()
2021-02-02 14:30:53 +01:00
def newVerAvail(self, ver: str) -> None:
2019-12-23 01:34:10 +01:00
if self.pm.meta.get("suppressUpdate", None) != ver:
aqt.update.askAndUpdate(self, ver)
def newMsg(self, data: dict) -> None:
aqt.update.showMessages(self, data)
2021-02-02 14:30:53 +01:00
def clockIsOff(self, diff: int) -> None:
if dev_mode:
2020-11-15 09:29:16 +01:00
print("clock is off; ignoring")
return
diffText = tr.qt_misc_second(count=diff)
warn = tr.qt_misc_in_order_to_ensure_your_collection(val="%s") % diffText
showWarning(warn)
self.app.closeAllWindows()
2020-02-05 03:38:36 +01:00
# Timers
##########################################################################
2020-02-05 03:38:36 +01:00
def setup_timers(self) -> None:
# refresh decks every 10 minutes
self.progress.timer(10 * 60 * 1000, self.onRefreshTimer, True, parent=self)
2020-02-05 03:38:36 +01:00
# check media sync every 5 minutes
self.progress.timer(5 * 60 * 1000, self.on_autosync_timer, True, parent=self)
# periodic garbage collection
self.progress.timer(
15 * 60 * 1000, self.garbage_collect_now, True, False, parent=self
)
# ensure Python interpreter runs at least once per second, so that
# SIGINT/SIGTERM is processed without a long delay
self.progress.timer(1000, lambda: None, True, False, parent=self)
Backup improvements (#1728) * Collection needs to be closed prior to backup even when not downgrading * Backups -> BackupLimits * Some improvements to backup_task - backup_inner now returns the error instead of logging it, so that the frontend can discover the issue when they await a backup (or create another one) - start_backup() was acquiring backup_task twice, and if another thread started a backup between the two locks, the task could have been accidentally overwritten without awaiting it * Backups no longer require a collection close - Instead of closing the collection, we ensure there is no active transaction, and flush the WAL to disk. This means the undo history is no longer lost on backup, which will be particularly useful if we add a periodic backup in the future. - Because a close is no longer required, backups are now achieved with a separate command, instead of being included in CloseCollection(). - Full sync no longer requires an extra close+reopen step, and we now wait for the backup to complete before proceeding. - Create a backup before 'check db' * Add File>Create Backup https://forums.ankiweb.net/t/anki-mac-os-no-backup-on-sync/6157 * Defer checkpoint until we know we need it When running periodic backups on a timer, we don't want to be fsync()ing unnecessarily. * Skip backup if modification time has not changed We don't want the user leaving Anki open overnight, and coming back to lots of identical backups. * Periodic backups Creates an automatic backup every 30 minutes if the collection has been modified. If there's a legacy checkpoint active, tries again 5 minutes later. * Switch to a user-configurable backup duration CreateBackup() now uses a simple force argument to determine whether the user's limits should be respected or not, and only potentially destructive ops (full download, check DB) override the user's configured limit. I considered having a separate limit for collection close and automatic backups (eg keeping the previous 5 minute limit for collection close), but that had two downsides: - When the user closes their collection at the end of the day, they'd get a recent backup. When they open the collection the next day, it would get backed up again within 5 minutes, even though not much had changed. - Multiple limits are harder to communicate to users in the UI Some remaining decisions I wasn't 100% sure about: - If force is true but the collection has not been modified, the backup will be skipped. If the user manually deleted their backups without closing Anki, they wouldn't get a new one if the mtime hadn't changed. - Force takes preference over the configured backup interval - should we be ignored the user here, or take no backups at all? Did a sneaky edit of the existing ftl string, as it hasn't been live long. * Move maybe_backup() into Collection * Use a single method for manual and periodic backups When manually creating a backup via the File menu, we no longer make the user wait until the backup completes. As we continue waiting for the backup in the background, if any errors occur, the user will get notified about it fairly quickly. * Show message to user if backup was skipped due to no changes + Don't incorrectly assert a backup will be created on force * Add "automatic" to description * Ensure we backup prior to importing colpkg if collection open The backup doesn't happen when invoked from 'open backup' in the profile screen, which matches Anki's previous behaviour. The user could potentially clobber up to 30 minutes of their work if they exited to the profile screen and restored a backup, but the alternative is we create backups every time a backup is restored, which may happen a number of times if the user is trying various ones. Or we could go back to a separate throttle amount for this case, at the cost of more complexity. * Remove the 0 special case on backup interval; minimum of 5 minutes https://github.com/ankitects/anki/pull/1728#discussion_r830876833
2022-03-21 10:40:42 +01:00
# periodic backups are checked every 5 minutes
self.progress.timer(
5 * 60 * 1000,
self.on_periodic_backup_timer,
True,
parent=self,
)
2021-02-01 14:28:21 +01:00
def onRefreshTimer(self) -> None:
if self.state == "deckBrowser":
self.deckBrowser.refresh()
elif self.state == "overview":
self.overview.refresh()
2021-02-01 11:59:18 +01:00
def on_autosync_timer(self) -> None:
2020-02-05 03:38:36 +01:00
elap = self.media_syncer.seconds_since_last_sync()
minutes = self.pm.auto_sync_media_minutes()
if not minutes:
return
if elap > minutes * 60:
2020-02-05 03:38:36 +01:00
self.maybe_auto_sync_media()
Backup improvements (#1728) * Collection needs to be closed prior to backup even when not downgrading * Backups -> BackupLimits * Some improvements to backup_task - backup_inner now returns the error instead of logging it, so that the frontend can discover the issue when they await a backup (or create another one) - start_backup() was acquiring backup_task twice, and if another thread started a backup between the two locks, the task could have been accidentally overwritten without awaiting it * Backups no longer require a collection close - Instead of closing the collection, we ensure there is no active transaction, and flush the WAL to disk. This means the undo history is no longer lost on backup, which will be particularly useful if we add a periodic backup in the future. - Because a close is no longer required, backups are now achieved with a separate command, instead of being included in CloseCollection(). - Full sync no longer requires an extra close+reopen step, and we now wait for the backup to complete before proceeding. - Create a backup before 'check db' * Add File>Create Backup https://forums.ankiweb.net/t/anki-mac-os-no-backup-on-sync/6157 * Defer checkpoint until we know we need it When running periodic backups on a timer, we don't want to be fsync()ing unnecessarily. * Skip backup if modification time has not changed We don't want the user leaving Anki open overnight, and coming back to lots of identical backups. * Periodic backups Creates an automatic backup every 30 minutes if the collection has been modified. If there's a legacy checkpoint active, tries again 5 minutes later. * Switch to a user-configurable backup duration CreateBackup() now uses a simple force argument to determine whether the user's limits should be respected or not, and only potentially destructive ops (full download, check DB) override the user's configured limit. I considered having a separate limit for collection close and automatic backups (eg keeping the previous 5 minute limit for collection close), but that had two downsides: - When the user closes their collection at the end of the day, they'd get a recent backup. When they open the collection the next day, it would get backed up again within 5 minutes, even though not much had changed. - Multiple limits are harder to communicate to users in the UI Some remaining decisions I wasn't 100% sure about: - If force is true but the collection has not been modified, the backup will be skipped. If the user manually deleted their backups without closing Anki, they wouldn't get a new one if the mtime hadn't changed. - Force takes preference over the configured backup interval - should we be ignored the user here, or take no backups at all? Did a sneaky edit of the existing ftl string, as it hasn't been live long. * Move maybe_backup() into Collection * Use a single method for manual and periodic backups When manually creating a backup via the File menu, we no longer make the user wait until the backup completes. As we continue waiting for the backup in the background, if any errors occur, the user will get notified about it fairly quickly. * Show message to user if backup was skipped due to no changes + Don't incorrectly assert a backup will be created on force * Add "automatic" to description * Ensure we backup prior to importing colpkg if collection open The backup doesn't happen when invoked from 'open backup' in the profile screen, which matches Anki's previous behaviour. The user could potentially clobber up to 30 minutes of their work if they exited to the profile screen and restored a backup, but the alternative is we create backups every time a backup is restored, which may happen a number of times if the user is trying various ones. Or we could go back to a separate throttle amount for this case, at the cost of more complexity. * Remove the 0 special case on backup interval; minimum of 5 minutes https://github.com/ankitects/anki/pull/1728#discussion_r830876833
2022-03-21 10:40:42 +01:00
# Backups
##########################################################################
def on_periodic_backup_timer(self) -> None:
"""Create a backup if enough time has elapsed and collection changed."""
self._create_backup_with_progress(user_initiated=False)
def on_create_backup_now(self) -> None:
self._create_backup_with_progress(user_initiated=True)
def create_backup_now(self) -> None:
"""Create a backup immediately, regardless of when the last one was created.
Waits until the backup completes. Intended to be used as part of a longer-running
CollectionOp/QueryOp."""
self.col.create_backup(
backup_folder=self.pm.backupFolder(),
force=True,
wait_for_completion=True,
)
def _create_backup_with_progress(self, user_initiated: bool) -> None:
# if there's a legacy undo op, try again later
if not user_initiated and self.col.legacy_checkpoint_pending():
return
# The initial copy will display a progress window if it takes too long
def backup(col: Collection) -> bool:
return col.create_backup(
backup_folder=self.pm.backupFolder(),
force=user_initiated,
wait_for_completion=False,
)
def on_success(val: None) -> None:
if user_initiated:
tooltip(tr.profiles_backup_created(), parent=self)
def on_failure(exc: Exception) -> None:
showWarning(
tr.profiles_backup_creation_failed(reason=str(exc)), parent=self
)
def after_backup_started(created: bool) -> None:
# Legacy checkpoint may have expired.
self.update_undo_actions()
if user_initiated and not created:
tooltip(tr.profiles_backup_unchanged(), parent=self)
return
# We await backup completion to confirm it was successful, but this step
# does not block collection access, so we don't need to show the progress
# window anymore.
QueryOp(
parent=self,
op=lambda col: col.await_backup_completion(),
success=on_success,
).failure(on_failure).run_in_background()
QueryOp(parent=self, op=backup, success=after_backup_started).failure(
on_failure
).with_progress(tr.profiles_creating_backup()).run_in_background()
# Permanent hooks
##########################################################################
2019-12-20 09:43:52 +01:00
def setupHooks(self) -> None:
2020-01-15 07:53:24 +01:00
hooks.schema_will_change.append(self.onSchemaMod)
hooks.notes_will_be_deleted.append(self.onRemNotes)
2020-01-15 07:53:24 +01:00
hooks.card_odue_was_invalid.append(self.onOdueInvalid)
gui_hooks.av_player_will_play.append(self.on_av_player_will_play)
2020-01-22 05:39:18 +01:00
gui_hooks.av_player_did_end_playing.append(self.on_av_player_did_end_playing)
gui_hooks.operation_did_execute.append(self.on_operation_did_execute)
gui_hooks.focus_did_change.append(self.on_focus_did_change)
2020-01-13 05:38:05 +01:00
self._activeWindowOnPlay: QWidget | None = None
2021-02-01 14:28:21 +01:00
def onOdueInvalid(self) -> None:
2021-03-26 04:48:26 +01:00
showWarning(tr.qt_misc_invalid_property_found_on_card_please())
2013-05-22 05:27:37 +02:00
def _isVideo(self, tag: AVTag) -> bool:
if isinstance(tag, SoundOrVideoTag):
head, ext = os.path.splitext(tag.filename.lower())
return ext in (".mp4", ".mov", ".mpg", ".mpeg", ".mkv", ".avi")
return False
def on_av_player_will_play(self, tag: AVTag) -> None:
"Record active window to restore after video playing."
if not self._isVideo(tag):
return
self._activeWindowOnPlay = self.app.activeWindow() or self._activeWindowOnPlay
2020-01-22 05:39:18 +01:00
def on_av_player_did_end_playing(self, player: Any) -> None:
"Restore window focus after a video was played."
w = self._activeWindowOnPlay
if not self.app.activeWindow() and w and not sip.isdeleted(w) and w.isVisible():
w.activateWindow()
w.raise_()
self._activeWindowOnPlay = None
2013-05-22 05:27:37 +02:00
# Log note deletion
##########################################################################
def onRemNotes(self, col: Collection, nids: Sequence[NoteId]) -> None:
2013-05-22 05:27:37 +02:00
path = os.path.join(self.pm.profileFolder(), "deleted.txt")
existed = os.path.exists(path)
2017-01-08 11:44:52 +01:00
with open(path, "ab") as f:
2013-05-22 05:27:37 +02:00
if not existed:
2017-01-08 11:44:52 +01:00
f.write(b"nid\tmid\tfields\n")
for id, mid, flds in col.db.execute(
f"select id, mid, flds from notes where id in {ids2str(nids)}"
2019-12-23 01:34:10 +01:00
):
PEP8 for rest of pylib (#1451) * PEP8 dbproxy.py * PEP8 errors.py * PEP8 httpclient.py * PEP8 lang.py * PEP8 latex.py * Add decorator to deprectate key words * Make replacement for deprecated attribute optional * Use new helper `_print_replacement_warning()` * PEP8 media.py * PEP8 rsbackend.py * PEP8 sound.py * PEP8 stdmodels.py * PEP8 storage.py * PEP8 sync.py * PEP8 tags.py * PEP8 template.py * PEP8 types.py * Fix DeprecatedNamesMixinForModule The class methods need to be overridden with instance methods, so every module has its own dicts. * Use `# pylint: disable=invalid-name` instead of id * PEP8 utils.py * Only decorate `__getattr__` with `@no_type_check` * Fix mypy issue with snakecase Importing it from `anki._vendor` raises attribute errors. * Format * Remove inheritance of DeprecatedNamesMixin There's almost no shared code now and overriding classmethods with instance methods raises mypy issues. * Fix traceback frames of deprecation warnings * remove fn/TimedLog (dae) Neither Anki nor add-ons appear to have been using it * fix some issues with stringcase use (dae) - the wheel was depending on the PyPI version instead of our vendored version - _vendor:stringcase should not have been listed in the anki py_library. We already include the sources in py_srcs, and need to refer to them directly. By listing _vendor:stringcase as well, we were making a top-level stringcase library available, which would have only worked for distributing because the wheel definition was also incorrect. - mypy errors are what caused me to mistakenly add the above - they were because the type: ignore at the top of stringcase.py was causing mypy to completely ignore the file, so it was not aware of any attributes it contained.
2021-10-25 06:50:13 +02:00
fields = split_fields(flds)
2017-01-08 11:44:52 +01:00
f.write(("\t".join([str(id), str(mid)] + fields)).encode("utf8"))
f.write(b"\n")
2013-05-22 05:27:37 +02:00
# Schema modifications
##########################################################################
# this will gradually be phased out
2021-02-01 11:59:18 +01:00
def onSchemaMod(self, arg: bool) -> bool:
if not self.inMainThread():
raise Exception("not in main thread")
progress_shown = self.progress.busy()
if progress_shown:
self.progress.finish()
2021-03-26 04:48:26 +01:00
ret = askUser(tr.qt_misc_the_requested_change_will_require_a())
if progress_shown:
self.progress.start()
return ret
# in favour of this
def confirm_schema_modification(self) -> bool:
"""If schema unmodified, ask user to confirm change.
True if confirmed or already modified."""
2021-06-27 07:12:22 +02:00
if self.col.schema_changed():
return True
2021-03-26 04:48:26 +01:00
return askUser(tr.qt_misc_the_requested_change_will_require_a())
# Advanced features
##########################################################################
2021-02-01 11:59:18 +01:00
def onCheckDB(self) -> None:
2020-06-08 12:28:11 +02:00
check_db(self)
2020-02-10 08:58:54 +01:00
def on_check_media_db(self) -> None:
2020-11-09 10:45:14 +01:00
gui_hooks.media_check_will_start()
2020-02-10 08:58:54 +01:00
check_media_db(self)
2021-02-01 11:59:18 +01:00
def onStudyDeck(self) -> None:
from aqt.studydeck import StudyDeck
2019-12-23 01:34:10 +01:00
def callback(ret: StudyDeck) -> None:
if not ret.name:
return
deck_id = self.col.decks.id(ret.name)
set_current_deck(parent=self, deck_id=deck_id).success(
lambda out: self.moveToState("overview")
).run_in_background()
StudyDeck(
self,
parent=self,
dyn=True,
current=self.col.decks.current()["name"],
callback=callback,
)
def onEmptyCards(self) -> None:
show_empty_cards(self)
# System specific code
##########################################################################
2019-12-20 09:43:52 +01:00
def setupSystemSpecific(self) -> None:
self.hideMenuAccels = False
if is_mac:
# mac users expect a minimize option
self.minimizeShortcut = QShortcut("Ctrl+M", self)
qconnect(self.minimizeShortcut.activated, self.onMacMinimize)
self.hideMenuAccels = True
self.maybeHideAccelerators()
self.hideStatusTips()
elif is_win:
# make sure ctypes is bundled
2019-12-23 01:34:10 +01:00
from ctypes import windll, wintypes # type: ignore
_dummy1 = windll
_dummy2 = wintypes
def maybeHideAccelerators(self, tgt: Any | None = None) -> None:
if not self.hideMenuAccels:
return
tgt = tgt or self
for action_ in tgt.findChildren(QAction):
action = cast(QAction, action_)
txt = str(action.text())
2017-12-11 08:25:51 +01:00
m = re.match(r"^(.+)\(&.+\)(.+)?", txt)
if m:
action.setText(m.group(1) + (m.group(2) or ""))
2019-12-20 09:43:52 +01:00
def hideStatusTips(self) -> None:
for action in self.findChildren(QAction):
cast(QAction, action).setStatusTip("")
def onMacMinimize(self) -> None:
self.setWindowState(self.windowState() | Qt.WindowState.WindowMinimized) # type: ignore
# Single instance support
##########################################################################
2019-12-20 09:43:52 +01:00
def setupAppMsg(self) -> None:
qconnect(self.app.appMsg, self.onAppMsg)
def onAppMsg(self, buf: str) -> None:
is_addon = self._isAddon(buf)
2020-01-03 18:23:28 +01:00
if self.state == "startup":
# try again in a second
self.progress.single_shot(
1000,
lambda: self.onAppMsg(buf),
False,
2019-12-23 01:34:10 +01:00
)
return
elif self.state == "profileManager":
# can't raise window while in profile manager
if buf == "raise":
2019-12-20 09:43:52 +01:00
return None
self.pendingImport = buf
if is_addon:
2021-03-26 04:48:26 +01:00
msg = tr.qt_misc_addon_will_be_installed_when_a()
else:
2021-03-26 04:48:26 +01:00
msg = tr.qt_misc_deck_will_be_imported_when_a()
tooltip(msg)
return
if not self.interactiveState() or self.progress.busy():
# we can't raise the main window while in profile dialog, syncing, etc
if buf != "raise":
2019-12-23 01:34:10 +01:00
showInfo(
2021-03-26 04:48:26 +01:00
tr.qt_misc_please_ensure_a_profile_is_open(),
2019-12-23 01:34:10 +01:00
parent=None,
)
2019-12-20 09:43:52 +01:00
return None
# raise window
if is_win:
# on windows we can raise the window by minimizing and restoring
self.showMinimized()
self.setWindowState(Qt.WindowState.WindowActive)
self.showNormal()
else:
# on osx we can raise the window. on unity the icon in the tray will just flash.
self.activateWindow()
self.raise_()
if buf == "raise":
2019-12-20 09:43:52 +01:00
return None
2020-01-03 18:23:28 +01:00
# import / add-on installation
if is_addon:
self.installAddon(buf)
else:
self.handleImport(buf)
2020-01-03 18:23:28 +01:00
2019-12-20 09:43:52 +01:00
return None
def _isAddon(self, buf: str) -> bool:
# only accept primary extension here to avoid conflicts with deck packages
return buf.endswith(self.addonManager.exts[0])
def interactiveState(self) -> bool:
"True if not in profile manager, syncing, etc."
return self.state in ("overview", "review", "deckBrowser")
# GC
##########################################################################
# The default Python garbage collection can trigger on any thread. This can
# cause crashes if Qt objects are garbage-collected, as Qt expects access
# only on the main thread. So Anki disables the default GC on startup, and
# instead runs it on a timer, and after dialog close.
# The gc after dialog close is necessary to free up the memory and extra
# processes that webviews spawn, as a lot of the GUI code creates ref cycles.
def garbage_collect_on_dialog_finish(self, dialog: QDialog) -> None:
qconnect(
dialog.finished, lambda: self.deferred_delete_and_garbage_collect(dialog)
)
def deferred_delete_and_garbage_collect(self, obj: QObject) -> None:
obj.deleteLater()
self.progress.single_shot(1000, self.garbage_collect_now, False)
def disable_automatic_garbage_collection(self) -> None:
gc.collect()
gc.disable()
def garbage_collect_now(self) -> None:
# gc.collect() has optional arguments that will cause problems if
# it's passed directly to a QTimer, and pylint complains if we
# wrap it in a lambda, so we use this trivial wrapper
gc.collect()
2016-07-07 15:39:48 +02:00
# legacy aliases
setupDialogGC = garbage_collect_on_dialog_finish
gcWindow = deferred_delete_and_garbage_collect
2016-07-07 15:39:48 +02:00
# Media server
##########################################################################
2019-12-20 09:43:52 +01:00
def setupMediaServer(self) -> None:
self.mediaServer = aqt.mediasrv.MediaServer(self)
2016-07-07 15:39:48 +02:00
self.mediaServer.start()
2019-12-20 09:43:52 +01:00
def baseHTML(self) -> str:
return f'<base href="{self.serverURL()}">'
2019-12-20 09:43:52 +01:00
def serverURL(self) -> str:
return "http://127.0.0.1:%d/" % self.mediaServer.getPort()
# legacy
class ResetReason(enum.Enum):
Unknown = "unknown"
AddCardsAddNote = "addCardsAddNote"
EditCurrentInit = "editCurrentInit"
EditorBridgeCmd = "editorBridgeCmd"
BrowserSetDeck = "browserSetDeck"
BrowserAddTags = "browserAddTags"
BrowserRemoveTags = "browserRemoveTags"
BrowserSuspend = "browserSuspend"
BrowserReposition = "browserReposition"
BrowserReschedule = "browserReschedule"
BrowserFindReplace = "browserFindReplace"
BrowserTagDupes = "browserTagDupes"
BrowserDeleteDeck = "browserDeleteDeck"