mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
Optionally restore original position and reset counts when forgetting (#1714)
* Add forget prompt with options - Restore original position - Reset reps and lapses * Restore position when resetting for export * Add config context to avoid passing keys * Add routine to fetch defaults; use method-specific enum (dae) * Keep original position by default (dae) * Fix code completion for forget dialog (dae) Needs to be a symbolic link to the generated file
This commit is contained in:
parent
c21e6e2b97
commit
b9c3b12f71
17 changed files with 278 additions and 20 deletions
|
@ -135,6 +135,8 @@ scheduling-new-options-group-name = New options group name:
|
|||
scheduling-options-group = Options group:
|
||||
scheduling-order = Order
|
||||
scheduling-parent-limit = (parent limit: { $val })
|
||||
scheduling-reset-counts = Reset repetition and lapse counts
|
||||
scheduling-restore-position = Restore original position where possible
|
||||
scheduling-review = Review
|
||||
scheduling-reviews = Reviews
|
||||
scheduling-seconds = seconds
|
||||
|
|
|
@ -41,6 +41,10 @@ message ConfigKey {
|
|||
PASTE_STRIPS_FORMATTING = 16;
|
||||
NORMALIZE_NOTE_TEXT = 17;
|
||||
IGNORE_ACCENTS_IN_SEARCH = 18;
|
||||
RESTORE_POSITION_BROWSER = 19;
|
||||
RESTORE_POSITION_REVIEWER = 20;
|
||||
RESET_COUNTS_BROWSER = 21;
|
||||
RESET_COUNTS_REVIEWER = 22;
|
||||
}
|
||||
enum String {
|
||||
SET_DUE_BROWSER = 0;
|
||||
|
|
|
@ -30,6 +30,8 @@ service SchedulerService {
|
|||
rpc RebuildFilteredDeck(decks.DeckId) returns (collection.OpChangesWithCount);
|
||||
rpc ScheduleCardsAsNew(ScheduleCardsAsNewRequest)
|
||||
returns (collection.OpChanges);
|
||||
rpc ScheduleCardsAsNewDefaults(ScheduleCardsAsNewDefaultsRequest)
|
||||
returns (ScheduleCardsAsNewDefaultsResponse);
|
||||
rpc SetDueDate(SetDueDateRequest) returns (collection.OpChanges);
|
||||
rpc SortCards(SortCardsRequest) returns (collection.OpChangesWithCount);
|
||||
rpc SortDeck(SortDeckRequest) returns (collection.OpChangesWithCount);
|
||||
|
@ -172,8 +174,24 @@ message BuryOrSuspendCardsRequest {
|
|||
}
|
||||
|
||||
message ScheduleCardsAsNewRequest {
|
||||
enum Context {
|
||||
BROWSER = 0;
|
||||
REVIEWER = 1;
|
||||
}
|
||||
repeated int64 card_ids = 1;
|
||||
bool log = 2;
|
||||
bool restore_position = 3;
|
||||
bool reset_counts = 4;
|
||||
optional Context context = 5;
|
||||
}
|
||||
|
||||
message ScheduleCardsAsNewDefaultsRequest {
|
||||
ScheduleCardsAsNewRequest.Context context = 1;
|
||||
}
|
||||
|
||||
message ScheduleCardsAsNewDefaultsResponse {
|
||||
bool restore_position = 1;
|
||||
bool reset_counts = 2;
|
||||
}
|
||||
|
||||
message SetDueDateRequest {
|
||||
|
|
|
@ -21,6 +21,7 @@ ignored-classes=
|
|||
StripHtmlRequest,
|
||||
CustomStudyRequest,
|
||||
Cram,
|
||||
ScheduleCardsAsNewRequest,
|
||||
|
||||
[REPORTS]
|
||||
output-format=colorized
|
||||
|
|
|
@ -15,6 +15,8 @@ CongratsInfo = scheduler_pb2.CongratsInfoResponse
|
|||
UnburyDeck = scheduler_pb2.UnburyDeckRequest
|
||||
BuryOrSuspend = scheduler_pb2.BuryOrSuspendCardsRequest
|
||||
CustomStudyRequest = scheduler_pb2.CustomStudyRequest
|
||||
ScheduleCardsAsNew = scheduler_pb2.ScheduleCardsAsNewRequest
|
||||
ScheduleCardsAsNewDefaults = scheduler_pb2.ScheduleCardsAsNewDefaultsResponse
|
||||
FilteredDeckForUpdate = decks_pb2.FilteredDeckForUpdate
|
||||
|
||||
|
||||
|
@ -163,9 +165,28 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
|
|||
# Resetting/rescheduling
|
||||
##########################################################################
|
||||
|
||||
def schedule_cards_as_new(self, card_ids: Sequence[CardId]) -> OpChanges:
|
||||
"Put cards at the end of the new queue."
|
||||
return self.col._backend.schedule_cards_as_new(card_ids=card_ids, log=True)
|
||||
def schedule_cards_as_new(
|
||||
self,
|
||||
card_ids: Sequence[CardId],
|
||||
*,
|
||||
restore_position: bool = False,
|
||||
reset_counts: bool = False,
|
||||
context: ScheduleCardsAsNew.Context.V | None = None,
|
||||
) -> OpChanges:
|
||||
"Place cards back into the new queue."
|
||||
request = ScheduleCardsAsNew(
|
||||
card_ids=card_ids,
|
||||
log=True,
|
||||
restore_position=restore_position,
|
||||
reset_counts=reset_counts,
|
||||
context=context,
|
||||
)
|
||||
return self.col._backend.schedule_cards_as_new(request)
|
||||
|
||||
def schedule_cards_as_new_defaults(
|
||||
self, context: ScheduleCardsAsNew.Context.V
|
||||
) -> ScheduleCardsAsNewDefaults:
|
||||
return self.col._backend.schedule_cards_as_new_defaults(context)
|
||||
|
||||
def set_due_date(
|
||||
self,
|
||||
|
@ -203,7 +224,8 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l
|
|||
" where id in %s" % sids
|
||||
)
|
||||
# and forget any non-new cards, changing their due numbers
|
||||
self.col._backend.schedule_cards_as_new(card_ids=non_new, log=False)
|
||||
request = ScheduleCardsAsNew(card_ids=non_new, log=False, restore_position=True)
|
||||
self.col._backend.schedule_cards_as_new(request)
|
||||
|
||||
# Repositioning new cards
|
||||
##########################################################################
|
||||
|
|
|
@ -17,6 +17,7 @@ ignored-classes=
|
|||
ChangeNotetypeRequest,
|
||||
CustomStudyRequest,
|
||||
Cram,
|
||||
ScheduleCardsAsNewRequest,
|
||||
|
||||
[REPORTS]
|
||||
output-format=colorized
|
||||
|
|
|
@ -18,6 +18,7 @@ from anki.consts import *
|
|||
from anki.errors import NotFoundError
|
||||
from anki.lang import without_unicode_isolation
|
||||
from anki.notes import NoteId
|
||||
from anki.scheduler.base import ScheduleCardsAsNew
|
||||
from anki.tags import MARKED_TAG
|
||||
from anki.utils import is_mac
|
||||
from aqt import AnkiQt, gui_hooks
|
||||
|
@ -862,10 +863,12 @@ class Browser(QMainWindow):
|
|||
@skip_if_selection_is_empty
|
||||
@ensure_editor_saved
|
||||
def forget_cards(self) -> None:
|
||||
forget_cards(
|
||||
if op := forget_cards(
|
||||
parent=self,
|
||||
card_ids=self.selected_cards(),
|
||||
).run_in_background()
|
||||
context=ScheduleCardsAsNew.Context.BROWSER,
|
||||
):
|
||||
op.run_in_background()
|
||||
|
||||
# Edit: selection
|
||||
######################################################################
|
||||
|
|
|
@ -14,6 +14,7 @@ from . import customstudy
|
|||
from . import dconf
|
||||
from . import debug
|
||||
from . import filtered_deck
|
||||
from . import forget
|
||||
from . import editaddon
|
||||
from . import editcurrent
|
||||
from . import edithtml
|
||||
|
|
6
qt/aqt/forms/forget.py
Normal file
6
qt/aqt/forms/forget.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from aqt.qt import qtmajor
|
||||
|
||||
if qtmajor > 5:
|
||||
from .forget_qt6 import *
|
||||
else:
|
||||
from .forget_qt5 import * # type: ignore
|
94
qt/aqt/forms/forget.ui
Normal file
94
qt/aqt/forms/forget.ui
Normal file
|
@ -0,0 +1,94 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Dialog</class>
|
||||
<widget class="QDialog" name="Dialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>235</width>
|
||||
<height>118</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>actions_forget_card</string>
|
||||
</property>
|
||||
<property name="modal">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="restore_position">
|
||||
<property name="text">
|
||||
<string>scheduling_restore_position</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="reset_counts">
|
||||
<property name="text">
|
||||
<string>scheduling_reset_counts</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>Dialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>Dialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
1
qt/aqt/forms/forget_qt6.py
Symbolic link
1
qt/aqt/forms/forget_qt6.py
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../../.bazel/bin/qt/aqt/forms/forget_qt6.py
|
|
@ -19,6 +19,7 @@ from anki.collection import (
|
|||
from anki.decks import DeckId
|
||||
from anki.notes import NoteId
|
||||
from anki.scheduler import CustomStudyRequest, FilteredDeckForUpdate, UnburyDeck
|
||||
from anki.scheduler.base import ScheduleCardsAsNew
|
||||
from anki.scheduler.v3 import CardAnswer
|
||||
from anki.scheduler.v3 import Scheduler as V3Scheduler
|
||||
from aqt.operations import CollectionOp
|
||||
|
@ -65,10 +66,37 @@ def set_due_date_dialog(
|
|||
|
||||
|
||||
def forget_cards(
|
||||
*, parent: QWidget, card_ids: Sequence[CardId]
|
||||
) -> CollectionOp[OpChanges]:
|
||||
*,
|
||||
parent: QWidget,
|
||||
card_ids: Sequence[CardId],
|
||||
context: ScheduleCardsAsNew.Context.V | None = None,
|
||||
) -> CollectionOp[OpChanges] | None:
|
||||
assert aqt.mw
|
||||
|
||||
dialog = QDialog(parent)
|
||||
disable_help_button(dialog)
|
||||
form = aqt.forms.forget.Ui_Dialog()
|
||||
form.setupUi(dialog)
|
||||
|
||||
if context is not None:
|
||||
defaults = aqt.mw.col.sched.schedule_cards_as_new_defaults(context)
|
||||
form.restore_position.setChecked(defaults.restore_position)
|
||||
form.reset_counts.setChecked(defaults.reset_counts)
|
||||
|
||||
if not dialog.exec():
|
||||
return None
|
||||
|
||||
restore_position = form.restore_position.isChecked()
|
||||
reset_counts = form.reset_counts.isChecked()
|
||||
|
||||
return CollectionOp(
|
||||
parent, lambda col: col.sched.schedule_cards_as_new(card_ids)
|
||||
parent,
|
||||
lambda col: col.sched.schedule_cards_as_new(
|
||||
card_ids,
|
||||
restore_position=restore_position,
|
||||
reset_counts=reset_counts,
|
||||
context=context,
|
||||
),
|
||||
).success(
|
||||
lambda _: tooltip(
|
||||
tr.scheduling_forgot_cards(cards=len(card_ids)), parent=parent
|
||||
|
|
|
@ -19,6 +19,7 @@ import aqt.operations
|
|||
from anki import hooks
|
||||
from anki.cards import Card, CardId
|
||||
from anki.collection import Config, OpChanges, OpChangesWithCount
|
||||
from anki.scheduler.base import ScheduleCardsAsNew
|
||||
from anki.scheduler.v3 import CardAnswer, NextStates, QueuedCards
|
||||
from anki.scheduler.v3 import Scheduler as V3Scheduler
|
||||
from anki.tags import MARKED_TAG
|
||||
|
@ -1067,10 +1068,12 @@ time = %(time)d;
|
|||
).run_in_background()
|
||||
|
||||
def forget_current_card(self) -> None:
|
||||
forget_cards(
|
||||
if op := forget_cards(
|
||||
parent=self.mw,
|
||||
card_ids=[self.card.id],
|
||||
).run_in_background()
|
||||
context=ScheduleCardsAsNew.Context.REVIEWER,
|
||||
):
|
||||
op.run_in_background()
|
||||
|
||||
def on_create_copy(self) -> None:
|
||||
if self.card:
|
||||
|
|
|
@ -32,6 +32,10 @@ impl From<BoolKeyProto> for BoolKey {
|
|||
BoolKeyProto::PasteStripsFormatting => BoolKey::PasteStripsFormatting,
|
||||
BoolKeyProto::NormalizeNoteText => BoolKey::NormalizeNoteText,
|
||||
BoolKeyProto::IgnoreAccentsInSearch => BoolKey::IgnoreAccentsInSearch,
|
||||
BoolKeyProto::RestorePositionBrowser => BoolKey::RestorePositionBrowser,
|
||||
BoolKeyProto::RestorePositionReviewer => BoolKey::RestorePositionReviewer,
|
||||
BoolKeyProto::ResetCountsBrowser => BoolKey::ResetCountsBrowser,
|
||||
BoolKeyProto::ResetCountsReviewer => BoolKey::ResetCountsReviewer,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ mod states;
|
|||
use super::Backend;
|
||||
pub(super) use crate::backend_proto::scheduler_service::Service as SchedulerService;
|
||||
use crate::{
|
||||
backend_proto::{self as pb},
|
||||
backend_proto as pb,
|
||||
prelude::*,
|
||||
scheduler::{
|
||||
new::NewCardDueOrder,
|
||||
|
@ -111,11 +111,26 @@ impl SchedulerService for Backend {
|
|||
fn schedule_cards_as_new(&self, input: pb::ScheduleCardsAsNewRequest) -> Result<pb::OpChanges> {
|
||||
self.with_col(|col| {
|
||||
let cids = input.card_ids.into_newtype(CardId);
|
||||
let log = input.log;
|
||||
col.reschedule_cards_as_new(&cids, log).map(Into::into)
|
||||
col.reschedule_cards_as_new(
|
||||
&cids,
|
||||
input.log,
|
||||
input.restore_position,
|
||||
input.reset_counts,
|
||||
input
|
||||
.context
|
||||
.and_then(pb::schedule_cards_as_new_request::Context::from_i32),
|
||||
)
|
||||
.map(Into::into)
|
||||
})
|
||||
}
|
||||
|
||||
fn schedule_cards_as_new_defaults(
|
||||
&self,
|
||||
input: pb::ScheduleCardsAsNewDefaultsRequest,
|
||||
) -> Result<pb::ScheduleCardsAsNewDefaultsResponse> {
|
||||
self.with_col(|col| Ok(col.reschedule_cards_as_new_defaults(input.context())))
|
||||
}
|
||||
|
||||
fn set_due_date(&self, input: pb::SetDueDateRequest) -> Result<pb::OpChanges> {
|
||||
let config = input.config_key.map(|v| v.key().into());
|
||||
let days = input.days;
|
||||
|
|
|
@ -27,6 +27,10 @@ pub enum BoolKey {
|
|||
PreviewBothSides,
|
||||
Sched2021,
|
||||
IgnoreAccentsInSearch,
|
||||
RestorePositionBrowser,
|
||||
RestorePositionReviewer,
|
||||
ResetCountsBrowser,
|
||||
ResetCountsReviewer,
|
||||
|
||||
#[strum(to_string = "normalize_note_text")]
|
||||
NormalizeNoteText,
|
||||
|
@ -55,6 +59,8 @@ impl Collection {
|
|||
| BoolKey::FutureDueShowBacklog
|
||||
| BoolKey::ShowRemainingDueCountsInStudy
|
||||
| BoolKey::CardCountsSeparateInactive
|
||||
| BoolKey::RestorePositionBrowser
|
||||
| BoolKey::RestorePositionReviewer
|
||||
| BoolKey::NormalizeNoteText => self.get_config_optional(key).unwrap_or(true),
|
||||
|
||||
// other options default to false
|
||||
|
|
|
@ -5,16 +5,20 @@ use std::collections::{HashMap, HashSet};
|
|||
|
||||
use rand::seq::SliceRandom;
|
||||
|
||||
pub use crate::backend_proto::scheduler::{
|
||||
schedule_cards_as_new_request::Context as ScheduleAsNewContext,
|
||||
ScheduleCardsAsNewDefaultsResponse,
|
||||
};
|
||||
use crate::{
|
||||
card::{CardQueue, CardType},
|
||||
config::SchedulerVersion,
|
||||
config::{BoolKey, SchedulerVersion},
|
||||
deckconfig::NewCardInsertOrder,
|
||||
prelude::*,
|
||||
search::{SearchNode, SortMode, StateKind},
|
||||
};
|
||||
|
||||
impl Card {
|
||||
fn schedule_as_new(&mut self, position: u32) {
|
||||
fn schedule_as_new(&mut self, position: u32, reset_counts: bool) {
|
||||
self.remove_from_filtered_deck_before_reschedule();
|
||||
self.due = position as i32;
|
||||
self.ctype = CardType::New;
|
||||
|
@ -22,6 +26,10 @@ impl Card {
|
|||
self.interval = 0;
|
||||
self.ease_factor = 0;
|
||||
self.original_position = None;
|
||||
if reset_counts {
|
||||
self.reps = 0;
|
||||
self.lapses = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// If the card is new, change its position, and return true.
|
||||
|
@ -112,7 +120,14 @@ fn nids_in_preserved_order(cards: &[Card]) -> Vec<NoteId> {
|
|||
}
|
||||
|
||||
impl Collection {
|
||||
pub fn reschedule_cards_as_new(&mut self, cids: &[CardId], log: bool) -> Result<OpOutput<()>> {
|
||||
pub fn reschedule_cards_as_new(
|
||||
&mut self,
|
||||
cids: &[CardId],
|
||||
log: bool,
|
||||
restore_position: bool,
|
||||
reset_counts: bool,
|
||||
context: Option<ScheduleAsNewContext>,
|
||||
) -> Result<OpOutput<()>> {
|
||||
let usn = self.usn()?;
|
||||
let mut position = self.get_next_card_position();
|
||||
self.transact(Op::ScheduleAsNew, |col| {
|
||||
|
@ -120,18 +135,52 @@ impl Collection {
|
|||
let cards = col.storage.all_searched_cards_in_search_order()?;
|
||||
for mut card in cards {
|
||||
let original = card.clone();
|
||||
card.schedule_as_new(position);
|
||||
if restore_position && card.original_position.is_some() {
|
||||
card.schedule_as_new(card.original_position.unwrap(), reset_counts);
|
||||
} else {
|
||||
card.schedule_as_new(position, reset_counts);
|
||||
position += 1;
|
||||
}
|
||||
if log {
|
||||
col.log_manually_scheduled_review(&card, &original, usn)?;
|
||||
}
|
||||
col.update_card_inner(&mut card, original, usn)?;
|
||||
position += 1;
|
||||
}
|
||||
col.set_next_card_position(position)?;
|
||||
col.storage.clear_searched_cards_table()
|
||||
col.storage.clear_searched_cards_table()?;
|
||||
|
||||
match context {
|
||||
Some(ScheduleAsNewContext::Browser) => {
|
||||
col.set_config_bool_inner(BoolKey::RestorePositionBrowser, restore_position)?;
|
||||
col.set_config_bool_inner(BoolKey::ResetCountsBrowser, reset_counts)?;
|
||||
}
|
||||
Some(ScheduleAsNewContext::Reviewer) => {
|
||||
col.set_config_bool_inner(BoolKey::RestorePositionReviewer, restore_position)?;
|
||||
col.set_config_bool_inner(BoolKey::ResetCountsReviewer, reset_counts)?;
|
||||
}
|
||||
None => (),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn reschedule_cards_as_new_defaults(
|
||||
&self,
|
||||
context: ScheduleAsNewContext,
|
||||
) -> ScheduleCardsAsNewDefaultsResponse {
|
||||
match context {
|
||||
ScheduleAsNewContext::Browser => ScheduleCardsAsNewDefaultsResponse {
|
||||
restore_position: self.get_config_bool(BoolKey::RestorePositionBrowser),
|
||||
reset_counts: self.get_config_bool(BoolKey::ResetCountsBrowser),
|
||||
},
|
||||
ScheduleAsNewContext::Reviewer => ScheduleCardsAsNewDefaultsResponse {
|
||||
restore_position: self.get_config_bool(BoolKey::RestorePositionReviewer),
|
||||
reset_counts: self.get_config_bool(BoolKey::ResetCountsReviewer),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sort_cards(
|
||||
&mut self,
|
||||
cids: &[CardId],
|
||||
|
|
Loading…
Reference in a new issue