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:
RumovZ 2022-03-09 07:51:41 +01:00 committed by GitHub
parent c21e6e2b97
commit b9c3b12f71
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 278 additions and 20 deletions

View file

@ -135,6 +135,8 @@ scheduling-new-options-group-name = New options group name:
scheduling-options-group = Options group: scheduling-options-group = Options group:
scheduling-order = Order scheduling-order = Order
scheduling-parent-limit = (parent limit: { $val }) 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-review = Review
scheduling-reviews = Reviews scheduling-reviews = Reviews
scheduling-seconds = seconds scheduling-seconds = seconds

View file

@ -41,6 +41,10 @@ message ConfigKey {
PASTE_STRIPS_FORMATTING = 16; PASTE_STRIPS_FORMATTING = 16;
NORMALIZE_NOTE_TEXT = 17; NORMALIZE_NOTE_TEXT = 17;
IGNORE_ACCENTS_IN_SEARCH = 18; IGNORE_ACCENTS_IN_SEARCH = 18;
RESTORE_POSITION_BROWSER = 19;
RESTORE_POSITION_REVIEWER = 20;
RESET_COUNTS_BROWSER = 21;
RESET_COUNTS_REVIEWER = 22;
} }
enum String { enum String {
SET_DUE_BROWSER = 0; SET_DUE_BROWSER = 0;

View file

@ -30,6 +30,8 @@ service SchedulerService {
rpc RebuildFilteredDeck(decks.DeckId) returns (collection.OpChangesWithCount); rpc RebuildFilteredDeck(decks.DeckId) returns (collection.OpChangesWithCount);
rpc ScheduleCardsAsNew(ScheduleCardsAsNewRequest) rpc ScheduleCardsAsNew(ScheduleCardsAsNewRequest)
returns (collection.OpChanges); returns (collection.OpChanges);
rpc ScheduleCardsAsNewDefaults(ScheduleCardsAsNewDefaultsRequest)
returns (ScheduleCardsAsNewDefaultsResponse);
rpc SetDueDate(SetDueDateRequest) returns (collection.OpChanges); rpc SetDueDate(SetDueDateRequest) returns (collection.OpChanges);
rpc SortCards(SortCardsRequest) returns (collection.OpChangesWithCount); rpc SortCards(SortCardsRequest) returns (collection.OpChangesWithCount);
rpc SortDeck(SortDeckRequest) returns (collection.OpChangesWithCount); rpc SortDeck(SortDeckRequest) returns (collection.OpChangesWithCount);
@ -172,8 +174,24 @@ message BuryOrSuspendCardsRequest {
} }
message ScheduleCardsAsNewRequest { message ScheduleCardsAsNewRequest {
enum Context {
BROWSER = 0;
REVIEWER = 1;
}
repeated int64 card_ids = 1; repeated int64 card_ids = 1;
bool log = 2; 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 { message SetDueDateRequest {

View file

@ -21,6 +21,7 @@ ignored-classes=
StripHtmlRequest, StripHtmlRequest,
CustomStudyRequest, CustomStudyRequest,
Cram, Cram,
ScheduleCardsAsNewRequest,
[REPORTS] [REPORTS]
output-format=colorized output-format=colorized

View file

@ -15,6 +15,8 @@ CongratsInfo = scheduler_pb2.CongratsInfoResponse
UnburyDeck = scheduler_pb2.UnburyDeckRequest UnburyDeck = scheduler_pb2.UnburyDeckRequest
BuryOrSuspend = scheduler_pb2.BuryOrSuspendCardsRequest BuryOrSuspend = scheduler_pb2.BuryOrSuspendCardsRequest
CustomStudyRequest = scheduler_pb2.CustomStudyRequest CustomStudyRequest = scheduler_pb2.CustomStudyRequest
ScheduleCardsAsNew = scheduler_pb2.ScheduleCardsAsNewRequest
ScheduleCardsAsNewDefaults = scheduler_pb2.ScheduleCardsAsNewDefaultsResponse
FilteredDeckForUpdate = decks_pb2.FilteredDeckForUpdate 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 # Resetting/rescheduling
########################################################################## ##########################################################################
def schedule_cards_as_new(self, card_ids: Sequence[CardId]) -> OpChanges: def schedule_cards_as_new(
"Put cards at the end of the new queue." self,
return self.col._backend.schedule_cards_as_new(card_ids=card_ids, log=True) 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( def set_due_date(
self, 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 " where id in %s" % sids
) )
# and forget any non-new cards, changing their due numbers # 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 # Repositioning new cards
########################################################################## ##########################################################################

View file

@ -17,6 +17,7 @@ ignored-classes=
ChangeNotetypeRequest, ChangeNotetypeRequest,
CustomStudyRequest, CustomStudyRequest,
Cram, Cram,
ScheduleCardsAsNewRequest,
[REPORTS] [REPORTS]
output-format=colorized output-format=colorized

View file

@ -18,6 +18,7 @@ from anki.consts import *
from anki.errors import NotFoundError from anki.errors import NotFoundError
from anki.lang import without_unicode_isolation from anki.lang import without_unicode_isolation
from anki.notes import NoteId from anki.notes import NoteId
from anki.scheduler.base import ScheduleCardsAsNew
from anki.tags import MARKED_TAG from anki.tags import MARKED_TAG
from anki.utils import is_mac from anki.utils import is_mac
from aqt import AnkiQt, gui_hooks from aqt import AnkiQt, gui_hooks
@ -862,10 +863,12 @@ class Browser(QMainWindow):
@skip_if_selection_is_empty @skip_if_selection_is_empty
@ensure_editor_saved @ensure_editor_saved
def forget_cards(self) -> None: def forget_cards(self) -> None:
forget_cards( if op := forget_cards(
parent=self, parent=self,
card_ids=self.selected_cards(), card_ids=self.selected_cards(),
).run_in_background() context=ScheduleCardsAsNew.Context.BROWSER,
):
op.run_in_background()
# Edit: selection # Edit: selection
###################################################################### ######################################################################

View file

@ -14,6 +14,7 @@ from . import customstudy
from . import dconf from . import dconf
from . import debug from . import debug
from . import filtered_deck from . import filtered_deck
from . import forget
from . import editaddon from . import editaddon
from . import editcurrent from . import editcurrent
from . import edithtml from . import edithtml

6
qt/aqt/forms/forget.py Normal file
View 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
View 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
View file

@ -0,0 +1 @@
../../../.bazel/bin/qt/aqt/forms/forget_qt6.py

View file

@ -19,6 +19,7 @@ from anki.collection import (
from anki.decks import DeckId from anki.decks import DeckId
from anki.notes import NoteId from anki.notes import NoteId
from anki.scheduler import CustomStudyRequest, FilteredDeckForUpdate, UnburyDeck from anki.scheduler import CustomStudyRequest, FilteredDeckForUpdate, UnburyDeck
from anki.scheduler.base import ScheduleCardsAsNew
from anki.scheduler.v3 import CardAnswer from anki.scheduler.v3 import CardAnswer
from anki.scheduler.v3 import Scheduler as V3Scheduler from anki.scheduler.v3 import Scheduler as V3Scheduler
from aqt.operations import CollectionOp from aqt.operations import CollectionOp
@ -65,10 +66,37 @@ def set_due_date_dialog(
def forget_cards( 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( 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( ).success(
lambda _: tooltip( lambda _: tooltip(
tr.scheduling_forgot_cards(cards=len(card_ids)), parent=parent tr.scheduling_forgot_cards(cards=len(card_ids)), parent=parent

View file

@ -19,6 +19,7 @@ import aqt.operations
from anki import hooks from anki import hooks
from anki.cards import Card, CardId from anki.cards import Card, CardId
from anki.collection import Config, OpChanges, OpChangesWithCount 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 CardAnswer, NextStates, QueuedCards
from anki.scheduler.v3 import Scheduler as V3Scheduler from anki.scheduler.v3 import Scheduler as V3Scheduler
from anki.tags import MARKED_TAG from anki.tags import MARKED_TAG
@ -1067,10 +1068,12 @@ time = %(time)d;
).run_in_background() ).run_in_background()
def forget_current_card(self) -> None: def forget_current_card(self) -> None:
forget_cards( if op := forget_cards(
parent=self.mw, parent=self.mw,
card_ids=[self.card.id], card_ids=[self.card.id],
).run_in_background() context=ScheduleCardsAsNew.Context.REVIEWER,
):
op.run_in_background()
def on_create_copy(self) -> None: def on_create_copy(self) -> None:
if self.card: if self.card:

View file

@ -32,6 +32,10 @@ impl From<BoolKeyProto> for BoolKey {
BoolKeyProto::PasteStripsFormatting => BoolKey::PasteStripsFormatting, BoolKeyProto::PasteStripsFormatting => BoolKey::PasteStripsFormatting,
BoolKeyProto::NormalizeNoteText => BoolKey::NormalizeNoteText, BoolKeyProto::NormalizeNoteText => BoolKey::NormalizeNoteText,
BoolKeyProto::IgnoreAccentsInSearch => BoolKey::IgnoreAccentsInSearch, BoolKeyProto::IgnoreAccentsInSearch => BoolKey::IgnoreAccentsInSearch,
BoolKeyProto::RestorePositionBrowser => BoolKey::RestorePositionBrowser,
BoolKeyProto::RestorePositionReviewer => BoolKey::RestorePositionReviewer,
BoolKeyProto::ResetCountsBrowser => BoolKey::ResetCountsBrowser,
BoolKeyProto::ResetCountsReviewer => BoolKey::ResetCountsReviewer,
} }
} }
} }

View file

@ -7,7 +7,7 @@ mod states;
use super::Backend; use super::Backend;
pub(super) use crate::backend_proto::scheduler_service::Service as SchedulerService; pub(super) use crate::backend_proto::scheduler_service::Service as SchedulerService;
use crate::{ use crate::{
backend_proto::{self as pb}, backend_proto as pb,
prelude::*, prelude::*,
scheduler::{ scheduler::{
new::NewCardDueOrder, new::NewCardDueOrder,
@ -111,11 +111,26 @@ impl SchedulerService for Backend {
fn schedule_cards_as_new(&self, input: pb::ScheduleCardsAsNewRequest) -> Result<pb::OpChanges> { fn schedule_cards_as_new(&self, input: pb::ScheduleCardsAsNewRequest) -> Result<pb::OpChanges> {
self.with_col(|col| { self.with_col(|col| {
let cids = input.card_ids.into_newtype(CardId); let cids = input.card_ids.into_newtype(CardId);
let log = input.log; col.reschedule_cards_as_new(
col.reschedule_cards_as_new(&cids, log).map(Into::into) &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> { fn set_due_date(&self, input: pb::SetDueDateRequest) -> Result<pb::OpChanges> {
let config = input.config_key.map(|v| v.key().into()); let config = input.config_key.map(|v| v.key().into());
let days = input.days; let days = input.days;

View file

@ -27,6 +27,10 @@ pub enum BoolKey {
PreviewBothSides, PreviewBothSides,
Sched2021, Sched2021,
IgnoreAccentsInSearch, IgnoreAccentsInSearch,
RestorePositionBrowser,
RestorePositionReviewer,
ResetCountsBrowser,
ResetCountsReviewer,
#[strum(to_string = "normalize_note_text")] #[strum(to_string = "normalize_note_text")]
NormalizeNoteText, NormalizeNoteText,
@ -55,6 +59,8 @@ impl Collection {
| BoolKey::FutureDueShowBacklog | BoolKey::FutureDueShowBacklog
| BoolKey::ShowRemainingDueCountsInStudy | BoolKey::ShowRemainingDueCountsInStudy
| BoolKey::CardCountsSeparateInactive | BoolKey::CardCountsSeparateInactive
| BoolKey::RestorePositionBrowser
| BoolKey::RestorePositionReviewer
| BoolKey::NormalizeNoteText => self.get_config_optional(key).unwrap_or(true), | BoolKey::NormalizeNoteText => self.get_config_optional(key).unwrap_or(true),
// other options default to false // other options default to false

View file

@ -5,16 +5,20 @@ use std::collections::{HashMap, HashSet};
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
pub use crate::backend_proto::scheduler::{
schedule_cards_as_new_request::Context as ScheduleAsNewContext,
ScheduleCardsAsNewDefaultsResponse,
};
use crate::{ use crate::{
card::{CardQueue, CardType}, card::{CardQueue, CardType},
config::SchedulerVersion, config::{BoolKey, SchedulerVersion},
deckconfig::NewCardInsertOrder, deckconfig::NewCardInsertOrder,
prelude::*, prelude::*,
search::{SearchNode, SortMode, StateKind}, search::{SearchNode, SortMode, StateKind},
}; };
impl Card { 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.remove_from_filtered_deck_before_reschedule();
self.due = position as i32; self.due = position as i32;
self.ctype = CardType::New; self.ctype = CardType::New;
@ -22,6 +26,10 @@ impl Card {
self.interval = 0; self.interval = 0;
self.ease_factor = 0; self.ease_factor = 0;
self.original_position = None; self.original_position = None;
if reset_counts {
self.reps = 0;
self.lapses = 0;
}
} }
/// If the card is new, change its position, and return true. /// 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 { 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 usn = self.usn()?;
let mut position = self.get_next_card_position(); let mut position = self.get_next_card_position();
self.transact(Op::ScheduleAsNew, |col| { self.transact(Op::ScheduleAsNew, |col| {
@ -120,18 +135,52 @@ impl Collection {
let cards = col.storage.all_searched_cards_in_search_order()?; let cards = col.storage.all_searched_cards_in_search_order()?;
for mut card in cards { for mut card in cards {
let original = card.clone(); 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 { if log {
col.log_manually_scheduled_review(&card, &original, usn)?; col.log_manually_scheduled_review(&card, &original, usn)?;
} }
col.update_card_inner(&mut card, original, usn)?; col.update_card_inner(&mut card, original, usn)?;
position += 1;
} }
col.set_next_card_position(position)?; 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( pub fn sort_cards(
&mut self, &mut self,
cids: &[CardId], cids: &[CardId],