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:
parent
42a4d11416
commit
2168dfe63d
@ -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
|
||||
|
@ -10,7 +10,8 @@ ignored-classes=
|
||||
UnburyCardsInCurrentDeckIn,
|
||||
BuryOrSuspendCardsIn,
|
||||
NoteIsDuplicateOrEmptyOut,
|
||||
BackendError
|
||||
BackendError,
|
||||
SetDeckCollapsedIn,
|
||||
|
||||
[REPORTS]
|
||||
output-format=colorized
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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"]
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
)
|
||||
)
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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))
|
||||
|
@ -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()
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user