add routine to set deck collapse state

Updating a deck via protobuf is now exposed on the backend, but not
currently on the frontend - I suspect we'll be better off writing
separate routines for the actions we need instead, and we get a better
undo description for free.

This is currently causing an ugly redraw in the browse screen, which
will need fixing.
This commit is contained in:
Damien Elmes 2021-04-05 10:21:50 +10:00
parent 42a4d11416
commit 2168dfe63d
14 changed files with 168 additions and 67 deletions

View File

@ -23,3 +23,4 @@ undo-update-deck = Update Deck
undo-forget-card = Forget Card
undo-set-flag = Set Flag
undo-build-filtered-deck = Build Deck
undo-expand-collapse = Expand/Collapse

View File

@ -10,7 +10,8 @@ ignored-classes=
UnburyCardsInCurrentDeckIn,
BuryOrSuspendCardsIn,
NoteIsDuplicateOrEmptyOut,
BackendError
BackendError,
SetDeckCollapsedIn,
[REPORTS]
output-format=colorized

View File

@ -36,6 +36,9 @@ from . import backend_pb2 as pb
from . import rsbridge
from .fluent import GeneratedTranslations, LegacyTranslationEnum
# the following comment is required to suppress a warning that only shows up
# when there are other pylint failures
# pylint: disable=c-extension-no-member
assert rsbridge.buildhash() == anki.buildinfo.buildhash

View File

@ -332,9 +332,13 @@ class Collection:
)
def get_deck(self, id: DeckId) -> Deck:
"Get a new-style deck object. Currently read-only."
"Get a new-style deck object."
return self._backend.get_deck(id)
def update_deck(self, deck: Deck) -> OpChanges:
"Save updates to an existing deck."
return self._backend.update_deck(deck)
def get_deck_config(self, id: DeckConfigId) -> DeckConfig:
"Get a new-style deck config object. Currently read-only."
return self._backend.get_deck_config(id)

View File

@ -21,6 +21,7 @@ from anki.utils import from_json_bytes, ids2str, intTime, legacy_func, to_json_b
DeckTreeNode = _pb.DeckTreeNode
DeckNameId = _pb.DeckNameId
FilteredDeckConfig = _pb.Deck.Filtered
DeckCollapseScope = _pb.SetDeckCollapsedIn.Scope
# legacy code may pass this in as the type argument to .id()
defaultDeck = 0
@ -224,6 +225,13 @@ class DeckManager:
)
]
def set_collapsed(
self, deck_id: DeckId, collapsed: bool, scope: DeckCollapseScope.V
) -> OpChanges:
return self.col._backend.set_deck_collapsed(
deck_id=deck_id, collapsed=collapsed, scope=scope
)
def collapse(self, did: DeckId) -> None:
deck = self.get(did)
deck["collapsed"] = not deck["collapsed"]

View File

@ -9,7 +9,7 @@ from typing import Any
import aqt
from anki.collection import OpChanges
from anki.decks import Deck, DeckId, DeckTreeNode
from anki.decks import Deck, DeckCollapseScope, DeckId, DeckTreeNode
from anki.utils import intTime
from aqt import AnkiQt, gui_hooks
from aqt.operations.deck import (
@ -17,6 +17,7 @@ from aqt.operations.deck import (
remove_decks,
rename_deck,
reparent_decks,
set_deck_collapsed,
)
from aqt.qt import *
from aqt.sound import av_player
@ -286,11 +287,16 @@ class DeckBrowser:
self.mw.onDeckConf()
def _collapse(self, did: DeckId) -> None:
self.mw.col.decks.collapse(did)
node = self.mw.col.decks.find_deck_in_tree(self._dueTree, did)
if node:
node.collapsed = not node.collapsed
self._renderPage(reuse=True)
set_deck_collapsed(
mw=self.mw,
deck_id=did,
collapsed=node.collapsed,
scope=DeckCollapseScope.REVIEWER,
)
self._renderPage(reuse=True)
def _handle_drag_and_drop(self, source: DeckId, target: DeckId) -> None:
reparent_decks(mw=self.mw, parent=self.mw, deck_ids=[source], new_parent=target)

View File

@ -5,7 +5,7 @@ from __future__ import annotations
from typing import Callable, Sequence
from anki.decks import DeckId
from anki.decks import DeckCollapseScope, DeckId
from aqt import AnkiQt, QWidget
from aqt.main import PerformOpOptionalSuccessCallback
from aqt.utils import getOnlyText, tooltip, tr
@ -68,3 +68,13 @@ def add_deck(
lambda: mw.col.decks.add_normal_deck_with_name(name),
success=success,
)
def set_deck_collapsed(
*, mw: AnkiQt, deck_id: DeckId, collapsed: bool, scope: DeckCollapseScope.V
) -> None:
mw.perform_op(
lambda: mw.col.decks.set_collapsed(
deck_id=deck_id, collapsed=collapsed, scope=scope
)
)

View File

@ -8,7 +8,7 @@ from typing import Dict, Iterable, List, Optional, Tuple, cast
import aqt
from anki.collection import Config, OpChanges, SearchJoiner, SearchNode
from anki.decks import Deck, DeckId, DeckTreeNode
from anki.decks import Deck, DeckCollapseScope, DeckId, DeckTreeNode
from anki.models import NotetypeId
from anki.notes import Note
from anki.tags import TagTreeNode
@ -16,7 +16,12 @@ from anki.types import assert_exhaustive
from aqt import colors, gui_hooks
from aqt.clayout import CardLayout
from aqt.models import Models
from aqt.operations.deck import remove_decks, rename_deck, reparent_decks
from aqt.operations.deck import (
remove_decks,
rename_deck,
reparent_decks,
set_deck_collapsed,
)
from aqt.operations.tag import remove_tags_from_all_notes, rename_tag, reparent_tags
from aqt.qt import *
from aqt.theme import ColoredIcon, theme_manager
@ -965,17 +970,20 @@ class SidebarTreeView(QTreeView):
def render(
root: SidebarItem, nodes: Iterable[DeckTreeNode], head: str = ""
) -> None:
def toggle_expand(node: DeckTreeNode) -> Callable[[bool], None]:
return lambda expanded: set_deck_collapsed(
mw=self.mw,
deck_id=DeckId(node.deck_id),
collapsed=not expanded,
scope=DeckCollapseScope.BROWSER,
)
for node in nodes:
def toggle_expand() -> Callable[[bool], None]:
did = DeckId(node.deck_id) # pylint: disable=cell-var-from-loop
return lambda _: self.mw.col.decks.collapseBrowser(did)
item = SidebarItem(
name=node.name,
icon=icon,
search_node=SearchNode(deck=head + node.name),
on_expanded=toggle_expand(),
on_expanded=toggle_expand(node),
expanded=not node.collapsed,
item_type=SidebarItemType.DECK,
id=node.deck_id,

View File

@ -143,6 +143,8 @@ service DecksService {
rpc GetAllDecksLegacy(Empty) returns (Json);
rpc GetDeckIdByName(String) returns (DeckId);
rpc GetDeck(DeckId) returns (Deck);
rpc UpdateDeck(Deck) returns (OpChanges);
rpc SetDeckCollapsed(SetDeckCollapsedIn) returns (OpChanges);
rpc GetDeckLegacy(DeckId) returns (Json);
rpc GetDeckNames(GetDeckNamesIn) returns (DeckNames);
rpc NewDeckLegacy(Bool) returns (Json);
@ -916,6 +918,17 @@ message SetTagExpandedIn {
bool expanded = 2;
}
message SetDeckCollapsedIn {
enum Scope {
REVIEWER = 0;
BROWSER = 1;
}
int64 deck_id = 1;
bool collapsed = 2;
Scope scope = 3;
}
message GetChangedTagsOut {
repeated string tags = 1;
}

View File

@ -1,10 +1,15 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use std::convert::TryFrom;
use super::Backend;
use crate::{
backend_proto::{self as pb},
decks::{Deck, DeckId, DeckSchema11, FilteredSearchOrder},
decks::{
human_deck_name_to_native, native_deck_name_to_human, Deck, DeckId, DeckSchema11,
FilteredSearchOrder,
},
prelude::*,
scheduler::filtered::FilteredDeckForUpdate,
};
@ -89,6 +94,13 @@ impl DecksService for Backend {
})
}
fn update_deck(&self, input: pb::Deck) -> Result<pb::OpChanges> {
self.with_col(|col| {
let mut deck = Deck::try_from(input)?;
col.update_deck(&mut deck).map(Into::into)
})
}
fn get_deck_legacy(&self, input: pb::DeckId) -> Result<pb::Json> {
self.with_col(|col| {
let deck: DeckSchema11 = col
@ -168,6 +180,13 @@ impl DecksService for Backend {
fn filtered_deck_order_labels(&self, _input: pb::Empty) -> Result<pb::StringList> {
Ok(FilteredSearchOrder::labels(&self.tr).into())
}
fn set_deck_collapsed(&self, input: pb::SetDeckCollapsedIn) -> Result<pb::OpChanges> {
self.with_col(|col| {
col.set_deck_collapsed(input.deck_id.into(), input.collapsed, input.scope())
})
.map(Into::into)
}
}
impl From<pb::DeckId> for DeckId {
@ -208,8 +227,54 @@ impl From<pb::FilteredDeckForUpdate> for FilteredDeckForUpdate {
}
}
// before we can switch to returning protobuf, we need to make sure we're converting the
// deck separators
impl From<Deck> for pb::Deck {
fn from(d: Deck) -> Self {
pb::Deck {
id: d.id.0,
name: native_deck_name_to_human(&d.name),
mtime_secs: d.mtime_secs.0,
usn: d.usn.0,
common: Some(d.common),
kind: Some(d.kind.into()),
}
}
}
impl TryFrom<pb::Deck> for Deck {
type Error = AnkiError;
fn try_from(d: pb::Deck) -> Result<Self, Self::Error> {
Ok(Deck {
id: DeckId(d.id),
name: human_deck_name_to_native(&d.name),
mtime_secs: TimestampSecs(d.mtime_secs),
usn: Usn(d.usn),
common: d.common.unwrap_or_default(),
kind: d
.kind
.ok_or_else(|| AnkiError::invalid_input("missing kind"))?
.into(),
})
}
}
impl From<DeckKind> for pb::deck::Kind {
fn from(k: DeckKind) -> Self {
match k {
DeckKind::Normal(n) => pb::deck::Kind::Normal(n),
DeckKind::Filtered(f) => pb::deck::Kind::Filtered(f),
}
}
}
impl From<pb::deck::Kind> for DeckKind {
fn from(kind: pb::deck::Kind) -> Self {
match kind {
pb::deck::Kind::Normal(normal) => DeckKind::Normal(normal),
pb::deck::Kind::Filtered(filtered) => DeckKind::Filtered(filtered),
}
}
}
// fn new_deck(&self, input: pb::Bool) -> Result<pb::Deck> {
// let deck = if input.val {
@ -219,28 +284,3 @@ impl From<pb::FilteredDeckForUpdate> for FilteredDeckForUpdate {
// };
// Ok(deck.into())
// }
// impl From<pb::Deck> for Deck {
// fn from(deck: pb::Deck) -> Self {
// Self {
// id: deck.id.into(),
// name: deck.name,
// mtime_secs: deck.mtime_secs.into(),
// usn: deck.usn.into(),
// common: deck.common.unwrap_or_default(),
// kind: deck
// .kind
// .map(Into::into)
// .unwrap_or_else(|| DeckKind::Normal(NormalDeck::default())),
// }
// }
// }
// impl From<pb::deck::Kind> for DeckKind {
// fn from(kind: pb::deck::Kind) -> Self {
// match kind {
// pb::deck::Kind::Normal(normal) => DeckKind::Normal(normal),
// pb::deck::Kind::Filtered(filtered) => DeckKind::Filtered(filtered),
// }
// }
// }

View File

@ -207,6 +207,10 @@ pub(crate) fn human_deck_name_to_native(name: &str) -> String {
out.trim_end_matches('\x1f').into()
}
pub(crate) fn native_deck_name_to_human(name: &str) -> String {
name.replace('\x1f', "::")
}
impl Collection {
pub(crate) fn get_deck(&mut self, did: DeckId) -> Result<Option<Arc<Deck>>> {
if let Some(deck) = self.state.deck_cache.get(&did) {
@ -222,28 +226,6 @@ impl Collection {
}
}
impl From<Deck> for DeckProto {
fn from(d: Deck) -> Self {
DeckProto {
id: d.id.0,
name: d.name,
mtime_secs: d.mtime_secs.0,
usn: d.usn.0,
common: Some(d.common),
kind: Some(d.kind.into()),
}
}
}
impl From<DeckKind> for pb::deck::Kind {
fn from(k: DeckKind) -> Self {
match k {
DeckKind::Normal(n) => pb::deck::Kind::Normal(n),
DeckKind::Filtered(f) => pb::deck::Kind::Filtered(f),
}
}
}
pub(crate) fn immediate_parent_name(machine_name: &str) -> Option<&str> {
machine_name.rsplitn(2, '\x1f').nth(1)
}

View File

@ -3,8 +3,8 @@
use super::DeckId;
use super::{
human_deck_name_to_native, Deck, DeckCommon, DeckKind, FilteredDeck, FilteredSearchTerm,
NormalDeck,
human_deck_name_to_native, native_deck_name_to_human, Deck, DeckCommon, DeckKind, FilteredDeck,
FilteredSearchTerm, NormalDeck,
};
use crate::{
serde::{default_on_invalid, deserialize_bool_from_anything, deserialize_number_from_string},
@ -363,7 +363,7 @@ impl From<Deck> for DeckCommonSchema11 {
DeckCommonSchema11 {
id: deck.id,
mtime: deck.mtime_secs,
name: deck.name.replace("\x1f", "::"),
name: native_deck_name_to_human(&deck.name),
usn: deck.usn,
today: (&deck).into(),
study_collapsed: deck.common.study_collapsed,

View File

@ -2,6 +2,7 @@
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use super::{Deck, DeckKind, DueCounts};
pub use crate::backend_proto::set_deck_collapsed_in::Scope as DeckCollapseScope;
use crate::{
backend_proto::DeckTreeNode,
collection::Collection,
@ -9,7 +10,9 @@ use crate::{
deckconf::{DeckConf, DeckConfId},
decks::DeckId,
error::Result,
ops::OpOutput,
timestamp::TimestampSecs,
undo::Op,
};
use serde_tuple::Serialize_tuple;
use std::{
@ -312,6 +315,26 @@ impl Collection {
Ok(get_subnode(tree, target))
}
pub fn set_deck_collapsed(
&mut self,
did: DeckId,
collapsed: bool,
scope: DeckCollapseScope,
) -> Result<OpOutput<()>> {
self.transact(Op::ExpandCollapse, |col| {
if let Some(mut deck) = col.storage.get_deck(did)? {
let original = deck.clone();
let c = &mut deck.common;
match scope {
DeckCollapseScope::Reviewer => c.study_collapsed = collapsed,
DeckCollapseScope::Browser => c.browser_collapsed = collapsed,
};
col.update_deck_inner(&mut deck, original, col.usn()?)?;
}
Ok(())
})
}
pub(crate) fn legacy_deck_tree(&mut self) -> Result<LegacyDueCounts> {
let tree = self.deck_tree(Some(TimestampSecs::now()), None)?;
Ok(LegacyDueCounts::from(tree))

View File

@ -12,6 +12,7 @@ pub enum Op {
Bury,
ClearUnusedTags,
EmptyFilteredDeck,
ExpandCollapse,
FindAndReplace,
RebuildFilteredDeck,
RemoveDeck,
@ -66,6 +67,7 @@ impl Op {
Op::BuildFilteredDeck => tr.undo_build_filtered_deck(),
Op::RebuildFilteredDeck => tr.undo_build_filtered_deck(),
Op::EmptyFilteredDeck => tr.studying_empty(),
Op::ExpandCollapse => tr.undo_expand_collapse(),
}
.into()
}