remember last input for 'set due'; add string config; nest config types

This commit is contained in:
Damien Elmes 2021-02-08 14:10:05 +10:00
parent b56580a8c0
commit f434cff36f
12 changed files with 124 additions and 48 deletions

View file

@ -8,7 +8,8 @@ license = "AGPL-3.0-or-later"
members = ["rslib", "pylib/rsbridge"] members = ["rslib", "pylib/rsbridge"]
[lib] [lib]
name = "dummy" # dummy top level for tooling
name = "anki"
path = "src/lib.rs" path = "src/lib.rs"
[package.metadata.raze] [package.metadata.raze]

View file

@ -50,7 +50,7 @@ MediaSyncProgress = _pb.MediaSyncProgress
FullSyncProgress = _pb.FullSyncProgress FullSyncProgress = _pb.FullSyncProgress
NormalSyncProgress = _pb.NormalSyncProgress NormalSyncProgress = _pb.NormalSyncProgress
DatabaseCheckProgress = _pb.DatabaseCheckProgress DatabaseCheckProgress = _pb.DatabaseCheckProgress
ConfigBool = _pb.ConfigBool Config = _pb.Config
EmptyCardsReport = _pb.EmptyCardsReport EmptyCardsReport = _pb.EmptyCardsReport
NoteWithEmptyCards = _pb.NoteWithEmptyCards NoteWithEmptyCards = _pb.NoteWithEmptyCards
GraphPreferences = _pb.GraphPreferences GraphPreferences = _pb.GraphPreferences
@ -586,13 +586,20 @@ class Collection:
"This is a debugging aid. Prefer .get_config() when you know the key you need." "This is a debugging aid. Prefer .get_config() when you know the key you need."
return from_json_bytes(self._backend.get_all_config()) return from_json_bytes(self._backend.get_all_config())
def get_config_bool(self, key: ConfigBool.Key.V) -> bool: def get_config_bool(self, key: Config.Bool.Key.V) -> bool:
return self._backend.get_config_bool(key) return self._backend.get_config_bool(key)
def set_config_bool(self, key: ConfigBool.Key.V, value: bool) -> None: def set_config_bool(self, key: Config.Bool.Key.V, value: bool) -> None:
self.setMod() self.setMod()
self._backend.set_config_bool(key=key, value=value) self._backend.set_config_bool(key=key, value=value)
def get_config_string(self, key: Config.String.Key.V) -> str:
return self._backend.get_config_string(key)
def set_config_string(self, key: Config.String.Key.V, value: str) -> None:
self.setMod()
self._backend.set_config_string(key=key, value=value)
# Stats # Stats
########################################################################## ##########################################################################

View file

@ -1,7 +1,7 @@
# coding: utf-8 # coding: utf-8
import pytest import pytest
from anki.collection import BuiltinSort, ConfigBool from anki.collection import BuiltinSort, Config
from anki.consts import * from anki.consts import *
from tests.shared import getEmptyCol, isNearCutoff from tests.shared import getEmptyCol, isNearCutoff
@ -121,7 +121,7 @@ def test_findCards():
col.flush() col.flush()
assert col.findCards("", order=True)[-1] in latestCardIds assert col.findCards("", order=True)[-1] in latestCardIds
assert col.findCards("", order=True)[0] == firstCardId assert col.findCards("", order=True)[0] == firstCardId
col.set_config_bool(ConfigBool.BROWSER_SORT_BACKWARDS, True) col.set_config_bool(Config.Bool.BROWSER_SORT_BACKWARDS, True)
col.flush() col.flush()
assert col.findCards("", order=True)[0] in latestCardIds assert col.findCards("", order=True)[0] in latestCardIds
assert ( assert (

View file

@ -5,7 +5,9 @@ ignore = forms,hooks_gen.py
[TYPECHECK] [TYPECHECK]
ignored-modules=win32file,pywintypes,socket,win32pipe,winrt,pyaudio ignored-modules=win32file,pywintypes,socket,win32pipe,winrt,pyaudio
ignored-classes=SearchTerm,ConfigBool ignored-classes=
SearchTerm,
Config,
[REPORTS] [REPORTS]
output-format=colorized output-format=colorized

View file

@ -13,7 +13,7 @@ from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union,
import aqt import aqt
import aqt.forms import aqt.forms
from anki.cards import Card from anki.cards import Card
from anki.collection import Collection, ConfigBool, SearchTerm from anki.collection import Collection, Config, SearchTerm
from anki.consts import * from anki.consts import *
from anki.errors import InvalidInput from anki.errors import InvalidInput
from anki.lang import without_unicode_isolation from anki.lang import without_unicode_isolation
@ -852,13 +852,13 @@ QTableView {{ gridline-color: {grid} }}
# default to descending for non-text fields # default to descending for non-text fields
if type == "noteFld": if type == "noteFld":
ord = not ord ord = not ord
self.col.set_config_bool(ConfigBool.BROWSER_SORT_BACKWARDS, ord) self.col.set_config_bool(Config.Bool.BROWSER_SORT_BACKWARDS, ord)
self.col.setMod() self.col.setMod()
self.col.save() self.col.save()
self.search() self.search()
else: else:
if self.col.get_config_bool(ConfigBool.BROWSER_SORT_BACKWARDS) != ord: if self.col.get_config_bool(Config.Bool.BROWSER_SORT_BACKWARDS) != ord:
self.col.set_config_bool(ConfigBool.BROWSER_SORT_BACKWARDS, ord) self.col.set_config_bool(Config.Bool.BROWSER_SORT_BACKWARDS, ord)
self.col.setMod() self.col.setMod()
self.col.save() self.col.save()
self.model.reverse() self.model.reverse()
@ -871,7 +871,7 @@ QTableView {{ gridline-color: {grid} }}
hh.setSortIndicatorShown(False) hh.setSortIndicatorShown(False)
return return
idx = self.model.activeCols.index(type) idx = self.model.activeCols.index(type)
if self.col.get_config_bool(ConfigBool.BROWSER_SORT_BACKWARDS): if self.col.get_config_bool(Config.Bool.BROWSER_SORT_BACKWARDS):
ord = Qt.DescendingOrder ord = Qt.DescendingOrder
else: else:
ord = Qt.AscendingOrder ord = Qt.AscendingOrder
@ -1399,7 +1399,7 @@ where id in %s"""
mw=self.mw, mw=self.mw,
parent=self, parent=self,
card_ids=self.selectedCards(), card_ids=self.selectedCards(),
default="0", default_key=Config.String.SET_DUE_BROWSER,
on_done=self._after_schedule, on_done=self._after_schedule,
) )
) )

View file

@ -7,7 +7,7 @@ import time
from typing import Any, Callable, Optional, Tuple, Union from typing import Any, Callable, Optional, Tuple, Union
from anki.cards import Card from anki.cards import Card
from anki.collection import ConfigBool from anki.collection import Config
from aqt import AnkiQt, gui_hooks from aqt import AnkiQt, gui_hooks
from aqt.qt import ( from aqt.qt import (
QAbstractItemView, QAbstractItemView,
@ -94,7 +94,7 @@ class Previewer(QDialog):
both_sides_button.setToolTip(tr(TR.ACTIONS_SHORTCUT_KEY, val="B")) both_sides_button.setToolTip(tr(TR.ACTIONS_SHORTCUT_KEY, val="B"))
self.bbox.addButton(both_sides_button, QDialogButtonBox.ActionRole) self.bbox.addButton(both_sides_button, QDialogButtonBox.ActionRole)
self._show_both_sides = self.mw.col.get_config_bool( self._show_both_sides = self.mw.col.get_config_bool(
ConfigBool.PREVIEW_BOTH_SIDES Config.Bool.PREVIEW_BOTH_SIDES
) )
both_sides_button.setChecked(self._show_both_sides) both_sides_button.setChecked(self._show_both_sides)
qconnect(both_sides_button.toggled, self._on_show_both_sides) qconnect(both_sides_button.toggled, self._on_show_both_sides)
@ -221,7 +221,7 @@ class Previewer(QDialog):
def _on_show_both_sides(self, toggle: bool) -> None: def _on_show_both_sides(self, toggle: bool) -> None:
self._show_both_sides = toggle self._show_both_sides = toggle
self.mw.col.set_config_bool(ConfigBool.PREVIEW_BOTH_SIDES, toggle) self.mw.col.set_config_bool(Config.Bool.PREVIEW_BOTH_SIDES, toggle)
self.mw.col.setMod() self.mw.col.setMod()
if self._state == "answer" and not toggle: if self._state == "answer" and not toggle:
self._state = "question" self._state = "question"

View file

@ -14,6 +14,7 @@ from PyQt5.QtCore import Qt
from anki import hooks from anki import hooks
from anki.cards import Card from anki.cards import Card
from anki.collection import Config
from anki.utils import stripHTML from anki.utils import stripHTML
from aqt import AnkiQt, gui_hooks from aqt import AnkiQt, gui_hooks
from aqt.profiles import VideoDriver from aqt.profiles import VideoDriver
@ -809,7 +810,7 @@ time = %(time)d;
mw=self.mw, mw=self.mw,
parent=self.mw, parent=self.mw,
card_ids=[self.card.id], card_ids=[self.card.id],
default="1", default_key=Config.String.SET_DUE_REVIEWER,
on_done=self.mw.reset, on_done=self.mw.reset,
) )

View file

@ -7,6 +7,7 @@ from concurrent.futures import Future
from typing import List from typing import List
import aqt import aqt
from anki.collection import Config
from anki.errors import InvalidInput from anki.errors import InvalidInput
from anki.lang import TR from anki.lang import TR
from aqt.qt import * from aqt.qt import *
@ -18,12 +19,14 @@ def set_due_date_dialog(
mw: aqt.AnkiQt, mw: aqt.AnkiQt,
parent: QDialog, parent: QDialog,
card_ids: List[int], card_ids: List[int],
default: str, default_key: Config.String.Key.V,
on_done: Callable[[], None], on_done: Callable[[], None],
) -> None: ) -> None:
if not card_ids: if not card_ids:
return return
default = mw.col.get_config_string(default_key)
(days, success) = getText( (days, success) = getText(
prompt=tr(TR.SCHEDULING_SET_DUE_DATE_PROMPT, cards=len(card_ids)), prompt=tr(TR.SCHEDULING_SET_DUE_DATE_PROMPT, cards=len(card_ids)),
parent=parent, parent=parent,
@ -33,7 +36,12 @@ def set_due_date_dialog(
if not success or not days.strip(): if not success or not days.strip():
return return
def on_done_wrapper(fut: Future) -> None: def set_due() -> None:
mw.col.sched.set_due_date(card_ids, days)
if days != default:
mw.col.set_config_string(default_key, days)
def after_set(fut: Future) -> None:
try: try:
fut.result() fut.result()
except Exception as e: except Exception as e:
@ -53,9 +61,7 @@ def set_due_date_dialog(
on_done() on_done()
mw.checkpoint(tr(TR.ACTIONS_SET_DUE_DATE)) mw.checkpoint(tr(TR.ACTIONS_SET_DUE_DATE))
mw.taskman.with_progress( mw.taskman.with_progress(set_due, after_set)
lambda: mw.col.sched.set_due_date(card_ids, days), on_done_wrapper
)
def forget_cards( def forget_cards(

View file

@ -9,7 +9,7 @@ from enum import Enum, auto
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, cast from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, cast
import aqt import aqt
from anki.collection import ConfigBool, SearchTerm from anki.collection import Config, SearchTerm
from anki.decks import DeckTreeNode from anki.decks import DeckTreeNode
from anki.errors import DeckRenameError, InvalidInput from anki.errors import DeckRenameError, InvalidInput
from anki.tags import TagTreeNode from anki.tags import TagTreeNode
@ -526,7 +526,7 @@ class SidebarTreeView(QTreeView):
root: SidebarItem, root: SidebarItem,
name: TR.V, name: TR.V,
icon: Union[str, ColoredIcon], icon: Union[str, ColoredIcon],
collapse_key: ConfigBool.Key.V, collapse_key: Config.Bool.Key.V,
type: Optional[SidebarItemType] = None, type: Optional[SidebarItemType] = None,
) -> SidebarItem: ) -> SidebarItem:
def update(expanded: bool) -> None: def update(expanded: bool) -> None:
@ -557,7 +557,7 @@ class SidebarTreeView(QTreeView):
root=root, root=root,
name=TR.BROWSING_SIDEBAR_SAVED_SEARCHES, name=TR.BROWSING_SIDEBAR_SAVED_SEARCHES,
icon=icon, icon=icon,
collapse_key=ConfigBool.COLLAPSE_SAVED_SEARCHES, collapse_key=Config.Bool.COLLAPSE_SAVED_SEARCHES,
type=SidebarItemType.SAVED_SEARCH_ROOT, type=SidebarItemType.SAVED_SEARCH_ROOT,
) )
@ -584,7 +584,7 @@ class SidebarTreeView(QTreeView):
root=root, root=root,
name=TR.BROWSING_SIDEBAR_RECENT, name=TR.BROWSING_SIDEBAR_RECENT,
icon=icon, icon=icon,
collapse_key=ConfigBool.COLLAPSE_RECENT, collapse_key=Config.Bool.COLLAPSE_RECENT,
type=SidebarItemType.FLAG_ROOT, type=SidebarItemType.FLAG_ROOT,
) )
type = SidebarItemType.FLAG type = SidebarItemType.FLAG
@ -646,7 +646,7 @@ class SidebarTreeView(QTreeView):
root=root, root=root,
name=TR.BROWSING_SIDEBAR_CARD_STATE, name=TR.BROWSING_SIDEBAR_CARD_STATE,
icon=icon, icon=icon,
collapse_key=ConfigBool.COLLAPSE_CARD_STATE, collapse_key=Config.Bool.COLLAPSE_CARD_STATE,
type=SidebarItemType.CARD_STATE_ROOT, type=SidebarItemType.CARD_STATE_ROOT,
) )
type = SidebarItemType.CARD_STATE type = SidebarItemType.CARD_STATE
@ -693,7 +693,7 @@ class SidebarTreeView(QTreeView):
root=root, root=root,
name=TR.BROWSING_SIDEBAR_FLAGS, name=TR.BROWSING_SIDEBAR_FLAGS,
icon=icon, icon=icon,
collapse_key=ConfigBool.COLLAPSE_FLAGS, collapse_key=Config.Bool.COLLAPSE_FLAGS,
type=SidebarItemType.FLAG_ROOT, type=SidebarItemType.FLAG_ROOT,
) )
type = SidebarItemType.FLAG type = SidebarItemType.FLAG
@ -771,7 +771,7 @@ class SidebarTreeView(QTreeView):
root=root, root=root,
name=TR.BROWSING_SIDEBAR_TAGS, name=TR.BROWSING_SIDEBAR_TAGS,
icon=icon, icon=icon,
collapse_key=ConfigBool.COLLAPSE_TAGS, collapse_key=Config.Bool.COLLAPSE_TAGS,
type=SidebarItemType.TAG_ROOT, type=SidebarItemType.TAG_ROOT,
) )
render(root, tree.children) render(root, tree.children)
@ -810,7 +810,7 @@ class SidebarTreeView(QTreeView):
root=root, root=root,
name=TR.BROWSING_SIDEBAR_DECKS, name=TR.BROWSING_SIDEBAR_DECKS,
icon=icon, icon=icon,
collapse_key=ConfigBool.COLLAPSE_DECKS, collapse_key=Config.Bool.COLLAPSE_DECKS,
type=SidebarItemType.DECK_ROOT, type=SidebarItemType.DECK_ROOT,
) )
render(root, tree.children) render(root, tree.children)
@ -824,7 +824,7 @@ class SidebarTreeView(QTreeView):
root=root, root=root,
name=TR.BROWSING_SIDEBAR_NOTETYPES, name=TR.BROWSING_SIDEBAR_NOTETYPES,
icon=icon, icon=icon,
collapse_key=ConfigBool.COLLAPSE_NOTETYPES, collapse_key=Config.Bool.COLLAPSE_NOTETYPES,
type=SidebarItemType.NOTETYPE_ROOT, type=SidebarItemType.NOTETYPE_ROOT,
) )

View file

@ -228,8 +228,10 @@ service BackendService {
rpc SetConfigJson(SetConfigJsonIn) returns (Empty); rpc SetConfigJson(SetConfigJsonIn) returns (Empty);
rpc RemoveConfig(String) returns (Empty); rpc RemoveConfig(String) returns (Empty);
rpc GetAllConfig(Empty) returns (Json); rpc GetAllConfig(Empty) returns (Json);
rpc GetConfigBool(ConfigBool) returns (Bool); rpc GetConfigBool(Config.Bool) returns (Bool);
rpc SetConfigBool(SetConfigBoolIn) returns (Empty); rpc SetConfigBool(SetConfigBoolIn) returns (Empty);
rpc GetConfigString(Config.String) returns (String);
rpc SetConfigString(SetConfigStringIn) returns (Empty);
// preferences // preferences
@ -1216,7 +1218,8 @@ message SetDeckIn {
int64 deck_id = 2; int64 deck_id = 2;
} }
message ConfigBool { message Config {
message Bool {
enum Key { enum Key {
BROWSER_SORT_BACKWARDS = 0; BROWSER_SORT_BACKWARDS = 0;
PREVIEW_BOTH_SIDES = 1; PREVIEW_BOTH_SIDES = 1;
@ -1231,7 +1234,21 @@ message ConfigBool {
Key key = 1; Key key = 1;
} }
message String {
enum Key {
SET_DUE_BROWSER = 0;
SET_DUE_REVIEWER = 1;
}
Key key = 1;
}
}
message SetConfigBoolIn { message SetConfigBoolIn {
ConfigBool.Key key = 1; Config.Bool.Key key = 1;
bool value = 2; bool value = 2;
} }
message SetConfigStringIn {
Config.String.Key key = 1;
string value = 2;
}

View file

@ -1493,7 +1493,7 @@ impl BackendService for Backend {
.map(Into::into) .map(Into::into)
} }
fn get_config_bool(&self, input: pb::ConfigBool) -> BackendResult<pb::Bool> { fn get_config_bool(&self, input: pb::config::Bool) -> BackendResult<pb::Bool> {
self.with_col(|col| { self.with_col(|col| {
Ok(pb::Bool { Ok(pb::Bool {
val: col.get_bool(input), val: col.get_bool(input),
@ -1505,6 +1505,19 @@ impl BackendService for Backend {
self.with_col(|col| col.transact(None, |col| col.set_bool(input))) self.with_col(|col| col.transact(None, |col| col.set_bool(input)))
.map(Into::into) .map(Into::into)
} }
fn get_config_string(&self, input: pb::config::String) -> BackendResult<pb::String> {
self.with_col(|col| {
Ok(pb::String {
val: col.get_string(input),
})
})
}
fn set_config_string(&self, input: pb::SetConfigStringIn) -> BackendResult<pb::Empty> {
self.with_col(|col| col.transact(None, |col| col.set_string(input)))
.map(Into::into)
}
} }
impl Backend { impl Backend {

View file

@ -5,7 +5,8 @@ use crate::{
backend_proto as pb, collection::Collection, decks::DeckID, err::Result, notetype::NoteTypeID, backend_proto as pb, collection::Collection, decks::DeckID, err::Result, notetype::NoteTypeID,
timestamp::TimestampSecs, timestamp::TimestampSecs,
}; };
use pb::config_bool::Key as BoolKey; use pb::config::bool::Key as BoolKey;
use pb::config::string::Key as StringKey;
use serde::{de::DeserializeOwned, Serialize}; use serde::{de::DeserializeOwned, Serialize};
use serde_aux::field_attributes::deserialize_bool_from_anything; use serde_aux::field_attributes::deserialize_bool_from_anything;
use serde_derive::Deserialize; use serde_derive::Deserialize;
@ -63,6 +64,8 @@ pub(crate) enum ConfigKey {
PreviewBothSides, PreviewBothSides,
Rollover, Rollover,
SchedulerVersion, SchedulerVersion,
SetDueBrowser,
SetDueReviewer,
ShowDayLearningCardsFirst, ShowDayLearningCardsFirst,
ShowIntervalsAboveAnswerButtons, ShowIntervalsAboveAnswerButtons,
ShowRemainingDueCountsInStudy, ShowRemainingDueCountsInStudy,
@ -102,6 +105,8 @@ impl From<ConfigKey> for &'static str {
ConfigKey::PreviewBothSides => "previewBothSides", ConfigKey::PreviewBothSides => "previewBothSides",
ConfigKey::Rollover => "rollover", ConfigKey::Rollover => "rollover",
ConfigKey::SchedulerVersion => "schedVer", ConfigKey::SchedulerVersion => "schedVer",
ConfigKey::SetDueBrowser => "setDueBrowser",
ConfigKey::SetDueReviewer => "setDueReviewer",
ConfigKey::ShowDayLearningCardsFirst => "dayLearnFirst", ConfigKey::ShowDayLearningCardsFirst => "dayLearnFirst",
ConfigKey::ShowIntervalsAboveAnswerButtons => "estTimes", ConfigKey::ShowIntervalsAboveAnswerButtons => "estTimes",
ConfigKey::ShowRemainingDueCountsInStudy => "dueCounts", ConfigKey::ShowRemainingDueCountsInStudy => "dueCounts",
@ -125,6 +130,15 @@ impl From<BoolKey> for ConfigKey {
} }
} }
impl From<StringKey> for ConfigKey {
fn from(key: StringKey) -> Self {
match key {
StringKey::SetDueBrowser => ConfigKey::SetDueBrowser,
StringKey::SetDueReviewer => ConfigKey::SetDueReviewer,
}
}
}
/// This is a workaround for old clients that used ints to represent boolean /// This is a workaround for old clients that used ints to represent boolean
/// values. For new config items, prefer using a bool directly. /// values. For new config items, prefer using a bool directly.
#[derive(Deserialize, Default)] #[derive(Deserialize, Default)]
@ -345,7 +359,7 @@ impl Collection {
} }
#[allow(clippy::match_single_binding)] #[allow(clippy::match_single_binding)]
pub(crate) fn get_bool(&self, config: pb::ConfigBool) -> bool { pub(crate) fn get_bool(&self, config: pb::config::Bool) -> bool {
match config.key() { match config.key() {
// all options default to false at the moment // all options default to false at the moment
other => self.get_config_default(ConfigKey::from(other)), other => self.get_config_default(ConfigKey::from(other)),
@ -355,6 +369,21 @@ impl Collection {
pub(crate) fn set_bool(&self, input: pb::SetConfigBoolIn) -> Result<()> { pub(crate) fn set_bool(&self, input: pb::SetConfigBoolIn) -> Result<()> {
self.set_config(ConfigKey::from(input.key()), &input.value) self.set_config(ConfigKey::from(input.key()), &input.value)
} }
pub(crate) fn get_string(&self, config: pb::config::String) -> String {
let key = config.key();
let default = match key {
StringKey::SetDueBrowser => "0",
StringKey::SetDueReviewer => "1",
// other => "",
};
self.get_config_optional(ConfigKey::from(key))
.unwrap_or_else(|| default.to_string())
}
pub(crate) fn set_string(&self, input: pb::SetConfigStringIn) -> Result<()> {
self.set_config(ConfigKey::from(input.key()), &input.value)
}
} }
#[derive(Deserialize, PartialEq, Debug, Clone, Copy)] #[derive(Deserialize, PartialEq, Debug, Clone, Copy)]