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-forget-card = Forget Card
undo-set-flag = Set Flag undo-set-flag = Set Flag
undo-build-filtered-deck = Build Deck undo-build-filtered-deck = Build Deck
undo-expand-collapse = Expand/Collapse

View File

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

View File

@ -36,6 +36,9 @@ from . import backend_pb2 as pb
from . import rsbridge from . import rsbridge
from .fluent import GeneratedTranslations, LegacyTranslationEnum 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 assert rsbridge.buildhash() == anki.buildinfo.buildhash

View File

@ -332,9 +332,13 @@ class Collection:
) )
def get_deck(self, id: DeckId) -> Deck: 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) 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: def get_deck_config(self, id: DeckConfigId) -> DeckConfig:
"Get a new-style deck config object. Currently read-only." "Get a new-style deck config object. Currently read-only."
return self._backend.get_deck_config(id) 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 DeckTreeNode = _pb.DeckTreeNode
DeckNameId = _pb.DeckNameId DeckNameId = _pb.DeckNameId
FilteredDeckConfig = _pb.Deck.Filtered FilteredDeckConfig = _pb.Deck.Filtered
DeckCollapseScope = _pb.SetDeckCollapsedIn.Scope
# legacy code may pass this in as the type argument to .id() # legacy code may pass this in as the type argument to .id()
defaultDeck = 0 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: def collapse(self, did: DeckId) -> None:
deck = self.get(did) deck = self.get(did)
deck["collapsed"] = not deck["collapsed"] deck["collapsed"] = not deck["collapsed"]

View File

@ -9,7 +9,7 @@ from typing import Any
import aqt import aqt
from anki.collection import OpChanges 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 anki.utils import intTime
from aqt import AnkiQt, gui_hooks from aqt import AnkiQt, gui_hooks
from aqt.operations.deck import ( from aqt.operations.deck import (
@ -17,6 +17,7 @@ from aqt.operations.deck import (
remove_decks, remove_decks,
rename_deck, rename_deck,
reparent_decks, reparent_decks,
set_deck_collapsed,
) )
from aqt.qt import * from aqt.qt import *
from aqt.sound import av_player from aqt.sound import av_player
@ -286,11 +287,16 @@ class DeckBrowser:
self.mw.onDeckConf() self.mw.onDeckConf()
def _collapse(self, did: DeckId) -> None: def _collapse(self, did: DeckId) -> None:
self.mw.col.decks.collapse(did)
node = self.mw.col.decks.find_deck_in_tree(self._dueTree, did) node = self.mw.col.decks.find_deck_in_tree(self._dueTree, did)
if node: if node:
node.collapsed = not node.collapsed 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: 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) 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 typing import Callable, Sequence
from anki.decks import DeckId from anki.decks import DeckCollapseScope, DeckId
from aqt import AnkiQt, QWidget from aqt import AnkiQt, QWidget
from aqt.main import PerformOpOptionalSuccessCallback from aqt.main import PerformOpOptionalSuccessCallback
from aqt.utils import getOnlyText, tooltip, tr from aqt.utils import getOnlyText, tooltip, tr
@ -68,3 +68,13 @@ def add_deck(
lambda: mw.col.decks.add_normal_deck_with_name(name), lambda: mw.col.decks.add_normal_deck_with_name(name),
success=success, 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 import aqt
from anki.collection import Config, OpChanges, SearchJoiner, SearchNode 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.models import NotetypeId
from anki.notes import Note from anki.notes import Note
from anki.tags import TagTreeNode from anki.tags import TagTreeNode
@ -16,7 +16,12 @@ from anki.types import assert_exhaustive
from aqt import colors, gui_hooks from aqt import colors, gui_hooks
from aqt.clayout import CardLayout from aqt.clayout import CardLayout
from aqt.models import Models 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.operations.tag import remove_tags_from_all_notes, rename_tag, reparent_tags
from aqt.qt import * from aqt.qt import *
from aqt.theme import ColoredIcon, theme_manager from aqt.theme import ColoredIcon, theme_manager
@ -965,17 +970,20 @@ class SidebarTreeView(QTreeView):
def render( def render(
root: SidebarItem, nodes: Iterable[DeckTreeNode], head: str = "" root: SidebarItem, nodes: Iterable[DeckTreeNode], head: str = ""
) -> None: ) -> 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: 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( item = SidebarItem(
name=node.name, name=node.name,
icon=icon, icon=icon,
search_node=SearchNode(deck=head + node.name), search_node=SearchNode(deck=head + node.name),
on_expanded=toggle_expand(), on_expanded=toggle_expand(node),
expanded=not node.collapsed, expanded=not node.collapsed,
item_type=SidebarItemType.DECK, item_type=SidebarItemType.DECK,
id=node.deck_id, id=node.deck_id,

View File

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

View File

@ -1,10 +1,15 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use std::convert::TryFrom;
use super::Backend; use super::Backend;
use crate::{ use crate::{
backend_proto::{self as pb}, 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::*, prelude::*,
scheduler::filtered::FilteredDeckForUpdate, 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> { fn get_deck_legacy(&self, input: pb::DeckId) -> Result<pb::Json> {
self.with_col(|col| { self.with_col(|col| {
let deck: DeckSchema11 = 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> { fn filtered_deck_order_labels(&self, _input: pb::Empty) -> Result<pb::StringList> {
Ok(FilteredSearchOrder::labels(&self.tr).into()) 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 { 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 impl From<Deck> for pb::Deck {
// deck separators 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> { // fn new_deck(&self, input: pb::Bool) -> Result<pb::Deck> {
// let deck = if input.val { // let deck = if input.val {
@ -219,28 +284,3 @@ impl From<pb::FilteredDeckForUpdate> for FilteredDeckForUpdate {
// }; // };
// Ok(deck.into()) // 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() out.trim_end_matches('\x1f').into()
} }
pub(crate) fn native_deck_name_to_human(name: &str) -> String {
name.replace('\x1f', "::")
}
impl Collection { impl Collection {
pub(crate) fn get_deck(&mut self, did: DeckId) -> Result<Option<Arc<Deck>>> { pub(crate) fn get_deck(&mut self, did: DeckId) -> Result<Option<Arc<Deck>>> {
if let Some(deck) = self.state.deck_cache.get(&did) { 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> { pub(crate) fn immediate_parent_name(machine_name: &str) -> Option<&str> {
machine_name.rsplitn(2, '\x1f').nth(1) machine_name.rsplitn(2, '\x1f').nth(1)
} }

View File

@ -3,8 +3,8 @@
use super::DeckId; use super::DeckId;
use super::{ use super::{
human_deck_name_to_native, Deck, DeckCommon, DeckKind, FilteredDeck, FilteredSearchTerm, human_deck_name_to_native, native_deck_name_to_human, Deck, DeckCommon, DeckKind, FilteredDeck,
NormalDeck, FilteredSearchTerm, NormalDeck,
}; };
use crate::{ use crate::{
serde::{default_on_invalid, deserialize_bool_from_anything, deserialize_number_from_string}, serde::{default_on_invalid, deserialize_bool_from_anything, deserialize_number_from_string},
@ -363,7 +363,7 @@ impl From<Deck> for DeckCommonSchema11 {
DeckCommonSchema11 { DeckCommonSchema11 {
id: deck.id, id: deck.id,
mtime: deck.mtime_secs, mtime: deck.mtime_secs,
name: deck.name.replace("\x1f", "::"), name: native_deck_name_to_human(&deck.name),
usn: deck.usn, usn: deck.usn,
today: (&deck).into(), today: (&deck).into(),
study_collapsed: deck.common.study_collapsed, 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 // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use super::{Deck, DeckKind, DueCounts}; use super::{Deck, DeckKind, DueCounts};
pub use crate::backend_proto::set_deck_collapsed_in::Scope as DeckCollapseScope;
use crate::{ use crate::{
backend_proto::DeckTreeNode, backend_proto::DeckTreeNode,
collection::Collection, collection::Collection,
@ -9,7 +10,9 @@ use crate::{
deckconf::{DeckConf, DeckConfId}, deckconf::{DeckConf, DeckConfId},
decks::DeckId, decks::DeckId,
error::Result, error::Result,
ops::OpOutput,
timestamp::TimestampSecs, timestamp::TimestampSecs,
undo::Op,
}; };
use serde_tuple::Serialize_tuple; use serde_tuple::Serialize_tuple;
use std::{ use std::{
@ -312,6 +315,26 @@ impl Collection {
Ok(get_subnode(tree, target)) 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> { pub(crate) fn legacy_deck_tree(&mut self) -> Result<LegacyDueCounts> {
let tree = self.deck_tree(Some(TimestampSecs::now()), None)?; let tree = self.deck_tree(Some(TimestampSecs::now()), None)?;
Ok(LegacyDueCounts::from(tree)) Ok(LegacyDueCounts::from(tree))

View File

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