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-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

View file

@ -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;

View file

@ -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 {

View file

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

View file

@ -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
##########################################################################

View file

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

View file

@ -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
######################################################################

View file

@ -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
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.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

View file

@ -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:

View file

@ -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,
}
}
}

View file

@ -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;

View file

@ -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

View file

@ -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],