mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 06:22:22 -04:00
move drag/drop deck logic to backend
This commit is contained in:
parent
0bd94659f1
commit
8410330f94
6 changed files with 139 additions and 49 deletions
|
@ -39,6 +39,8 @@ DeckConfig = Union[FilteredDeck, Config]
|
||||||
""" New/lrn/rev conf, from deck config"""
|
""" New/lrn/rev conf, from deck config"""
|
||||||
QueueConfig = Dict[str, Any]
|
QueueConfig = Dict[str, Any]
|
||||||
|
|
||||||
|
DeckID = int
|
||||||
|
|
||||||
|
|
||||||
class DecksDictProxy:
|
class DecksDictProxy:
|
||||||
def __init__(self, col: anki.collection.Collection):
|
def __init__(self, col: anki.collection.Collection):
|
||||||
|
@ -260,43 +262,22 @@ class DeckManager:
|
||||||
# Drag/drop
|
# Drag/drop
|
||||||
#############################################################
|
#############################################################
|
||||||
|
|
||||||
def renameForDragAndDrop(
|
def drag_drop_decks(self, source_decks: List[DeckID], target_deck: DeckID) -> None:
|
||||||
self, draggedDeckDid: int, ontoDeckDid: Optional[Union[int, str]]
|
"""Rename one or more source decks that were dropped on `target_deck`.
|
||||||
) -> None:
|
If target_deck is 0, decks will be placed at the top level."""
|
||||||
draggedDeck = self.get(draggedDeckDid)
|
self.col.backend.drag_drop_decks(
|
||||||
draggedDeckName = draggedDeck["name"]
|
source_deck_ids=source_decks, target_deck_id=target_deck
|
||||||
ontoDeckName = self.get(ontoDeckDid)["name"]
|
|
||||||
|
|
||||||
if ontoDeckDid is None or ontoDeckDid == "":
|
|
||||||
if len(self.path(draggedDeckName)) > 1:
|
|
||||||
self.rename(draggedDeck, self.basename(draggedDeckName))
|
|
||||||
elif self._canDragAndDrop(draggedDeckName, ontoDeckName):
|
|
||||||
draggedDeck = self.get(draggedDeckDid)
|
|
||||||
draggedDeckName = draggedDeck["name"]
|
|
||||||
ontoDeckName = self.get(ontoDeckDid)["name"]
|
|
||||||
assert ontoDeckName.strip()
|
|
||||||
self.rename(
|
|
||||||
draggedDeck, ontoDeckName + "::" + self.basename(draggedDeckName)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _canDragAndDrop(self, draggedDeckName: str, ontoDeckName: str) -> bool:
|
# legacy
|
||||||
if (
|
def renameForDragAndDrop(
|
||||||
draggedDeckName == ontoDeckName
|
self, draggedDeckDid: Union[int, str], ontoDeckDid: Optional[Union[int, str]]
|
||||||
or self._isParent(ontoDeckName, draggedDeckName)
|
) -> None:
|
||||||
or self._isAncestor(draggedDeckName, ontoDeckName)
|
if not ontoDeckDid:
|
||||||
):
|
onto = 0
|
||||||
return False
|
|
||||||
else:
|
else:
|
||||||
return True
|
onto = int(ontoDeckDid)
|
||||||
|
self.drag_drop_decks([int(draggedDeckDid)], onto)
|
||||||
def _isParent(self, parentDeckName: str, childDeckName: str) -> bool:
|
|
||||||
return self.path(childDeckName) == self.path(parentDeckName) + [
|
|
||||||
self.basename(childDeckName)
|
|
||||||
]
|
|
||||||
|
|
||||||
def _isAncestor(self, ancestorDeckName: str, descendantDeckName: str) -> bool:
|
|
||||||
ancestorPath = self.path(ancestorDeckName)
|
|
||||||
return ancestorPath == self.path(descendantDeckName)[0 : len(ancestorPath)]
|
|
||||||
|
|
||||||
# Deck configurations
|
# Deck configurations
|
||||||
#############################################################
|
#############################################################
|
||||||
|
|
|
@ -106,15 +106,15 @@ def test_renameForDragAndDrop():
|
||||||
col.decks.renameForDragAndDrop(chinese_did, languages_did)
|
col.decks.renameForDragAndDrop(chinese_did, languages_did)
|
||||||
assert deckNames() == ["Languages", "Languages::Chinese", "Languages::Chinese::HSK"]
|
assert deckNames() == ["Languages", "Languages::Chinese", "Languages::Chinese::HSK"]
|
||||||
|
|
||||||
# Dragging a col onto itself is a no-op
|
# Dragging a deck onto itself is a no-op
|
||||||
col.decks.renameForDragAndDrop(languages_did, languages_did)
|
col.decks.renameForDragAndDrop(languages_did, languages_did)
|
||||||
assert deckNames() == ["Languages", "Languages::Chinese", "Languages::Chinese::HSK"]
|
assert deckNames() == ["Languages", "Languages::Chinese", "Languages::Chinese::HSK"]
|
||||||
|
|
||||||
# Dragging a col onto its parent is a no-op
|
# Dragging a deck onto its parent is a no-op
|
||||||
col.decks.renameForDragAndDrop(hsk_did, chinese_did)
|
col.decks.renameForDragAndDrop(hsk_did, chinese_did)
|
||||||
assert deckNames() == ["Languages", "Languages::Chinese", "Languages::Chinese::HSK"]
|
assert deckNames() == ["Languages", "Languages::Chinese", "Languages::Chinese::HSK"]
|
||||||
|
|
||||||
# Dragging a col onto a descendant is a no-op
|
# Dragging a deck onto a descendant is a no-op
|
||||||
col.decks.renameForDragAndDrop(languages_did, hsk_did)
|
col.decks.renameForDragAndDrop(languages_did, hsk_did)
|
||||||
assert deckNames() == ["Languages", "Languages::Chinese", "Languages::Chinese::HSK"]
|
assert deckNames() == ["Languages", "Languages::Chinese", "Languages::Chinese::HSK"]
|
||||||
|
|
||||||
|
@ -122,11 +122,11 @@ def test_renameForDragAndDrop():
|
||||||
col.decks.renameForDragAndDrop(hsk_did, languages_did)
|
col.decks.renameForDragAndDrop(hsk_did, languages_did)
|
||||||
assert deckNames() == ["Languages", "Languages::Chinese", "Languages::HSK"]
|
assert deckNames() == ["Languages", "Languages::Chinese", "Languages::HSK"]
|
||||||
|
|
||||||
# Can drag a col onto its sibling
|
# Can drag a deck onto its sibling
|
||||||
col.decks.renameForDragAndDrop(hsk_did, chinese_did)
|
col.decks.renameForDragAndDrop(hsk_did, chinese_did)
|
||||||
assert deckNames() == ["Languages", "Languages::Chinese", "Languages::Chinese::HSK"]
|
assert deckNames() == ["Languages", "Languages::Chinese", "Languages::Chinese::HSK"]
|
||||||
|
|
||||||
# Can drag a col back to the top level
|
# Can drag a deck back to the top level
|
||||||
col.decks.renameForDragAndDrop(chinese_did, None)
|
col.decks.renameForDragAndDrop(chinese_did, None)
|
||||||
assert deckNames() == ["Chinese", "Chinese::HSK", "Languages"]
|
assert deckNames() == ["Chinese", "Chinese::HSK", "Languages"]
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ from __future__ import annotations
|
||||||
from concurrent.futures import Future
|
from concurrent.futures import Future
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import aqt
|
import aqt
|
||||||
from anki.errors import DeckRenameError
|
from anki.errors import DeckRenameError
|
||||||
|
@ -63,7 +64,7 @@ class DeckBrowser:
|
||||||
# Event handlers
|
# Event handlers
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
def _linkHandler(self, url):
|
def _linkHandler(self, url: str) -> Any:
|
||||||
if ":" in url:
|
if ":" in url:
|
||||||
(cmd, arg) = url.split(":", 1)
|
(cmd, arg) = url.split(":", 1)
|
||||||
else:
|
else:
|
||||||
|
@ -83,8 +84,8 @@ class DeckBrowser:
|
||||||
gui_hooks.sidebar_should_refresh_decks()
|
gui_hooks.sidebar_should_refresh_decks()
|
||||||
self.refresh()
|
self.refresh()
|
||||||
elif cmd == "drag":
|
elif cmd == "drag":
|
||||||
draggedDeckDid, ontoDeckDid = arg.split(",")
|
source, target = arg.split(",")
|
||||||
self._dragDeckOnto(draggedDeckDid, ontoDeckDid)
|
self._handle_drag_and_drop(int(source), int(target or 0))
|
||||||
elif cmd == "collapse":
|
elif cmd == "collapse":
|
||||||
self._collapse(int(arg))
|
self._collapse(int(arg))
|
||||||
return False
|
return False
|
||||||
|
@ -271,13 +272,9 @@ class DeckBrowser:
|
||||||
node.collapsed = not node.collapsed
|
node.collapsed = not node.collapsed
|
||||||
self._renderPage(reuse=True)
|
self._renderPage(reuse=True)
|
||||||
|
|
||||||
def _dragDeckOnto(self, draggedDeckDid: int, ontoDeckDid: int):
|
def _handle_drag_and_drop(self, source: int, target: int) -> None:
|
||||||
try:
|
self.mw.col.decks.drag_drop_decks([source], target)
|
||||||
self.mw.col.decks.renameForDragAndDrop(draggedDeckDid, ontoDeckDid)
|
|
||||||
gui_hooks.sidebar_should_refresh_decks()
|
gui_hooks.sidebar_should_refresh_decks()
|
||||||
except DeckRenameError as e:
|
|
||||||
return showWarning(e.description)
|
|
||||||
|
|
||||||
self.show()
|
self.show()
|
||||||
|
|
||||||
def ask_delete_deck(self, did: int) -> bool:
|
def ask_delete_deck(self, did: int) -> bool:
|
||||||
|
|
|
@ -138,6 +138,7 @@ service BackendService {
|
||||||
rpc GetDeckNames(GetDeckNamesIn) returns (DeckNames);
|
rpc GetDeckNames(GetDeckNamesIn) returns (DeckNames);
|
||||||
rpc NewDeckLegacy(Bool) returns (Json);
|
rpc NewDeckLegacy(Bool) returns (Json);
|
||||||
rpc RemoveDeck(DeckID) returns (Empty);
|
rpc RemoveDeck(DeckID) returns (Empty);
|
||||||
|
rpc DragDropDecks(DragDropDecksIn) returns (Empty);
|
||||||
|
|
||||||
// deck config
|
// deck config
|
||||||
|
|
||||||
|
@ -991,6 +992,11 @@ message GetDeckNamesIn {
|
||||||
bool include_filtered = 2;
|
bool include_filtered = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message DragDropDecksIn {
|
||||||
|
repeated int64 source_deck_ids = 1;
|
||||||
|
int64 target_deck_id = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message NoteIsDuplicateOrEmptyOut {
|
message NoteIsDuplicateOrEmptyOut {
|
||||||
enum State {
|
enum State {
|
||||||
NORMAL = 0;
|
NORMAL = 0;
|
||||||
|
|
|
@ -792,6 +792,17 @@ impl BackendService for Backend {
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn drag_drop_decks(&self, input: pb::DragDropDecksIn) -> BackendResult<Empty> {
|
||||||
|
let source_dids: Vec<_> = input.source_deck_ids.into_iter().map(Into::into).collect();
|
||||||
|
let target_did = if input.target_deck_id == 0 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(input.target_deck_id.into())
|
||||||
|
};
|
||||||
|
self.with_col(|col| col.drag_drop_decks(&source_dids, target_did))
|
||||||
|
.map(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
// deck config
|
// deck config
|
||||||
//----------------------------------------------------
|
//----------------------------------------------------
|
||||||
|
|
||||||
|
|
|
@ -175,6 +175,26 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Determine name to rename a deck to, when `dragged` is dropped on `dropped`.
|
||||||
|
/// `dropped` being unset represents a drop at the top or bottom of the deck list.
|
||||||
|
/// The returned name should be used to rename `dragged`, and may be unchanged.
|
||||||
|
/// Arguments are expected in 'machine' form with an \x1f separator.
|
||||||
|
pub(crate) fn drag_drop_deck_name(dragged: &str, dropped: Option<&str>) -> String {
|
||||||
|
let dragged_base = dragged.rsplit('\x1f').next().unwrap();
|
||||||
|
if let Some(dropped) = dropped {
|
||||||
|
if dropped.starts_with(dragged) {
|
||||||
|
// foo onto foo::bar, or foo onto itself -> no-op
|
||||||
|
dragged.to_string()
|
||||||
|
} else {
|
||||||
|
// foo::bar onto baz -> baz::bar
|
||||||
|
format!("{}\x1f{}", dropped, dragged_base)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// foo::bar onto top level -> bar
|
||||||
|
dragged_base.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Collection {
|
impl Collection {
|
||||||
pub(crate) fn default_deck_is_empty(&self) -> Result<bool> {
|
pub(crate) fn default_deck_is_empty(&self) -> Result<bool> {
|
||||||
self.storage.deck_is_empty(DeckID(1))
|
self.storage.deck_is_empty(DeckID(1))
|
||||||
|
@ -522,11 +542,51 @@ impl Collection {
|
||||||
deck.set_modified(usn);
|
deck.set_modified(usn);
|
||||||
self.add_or_update_single_deck(deck, usn)
|
self.add_or_update_single_deck(deck, usn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn drag_drop_decks(
|
||||||
|
&mut self,
|
||||||
|
source_decks: &[DeckID],
|
||||||
|
target: Option<DeckID>,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.state.deck_cache.clear();
|
||||||
|
let usn = self.usn()?;
|
||||||
|
self.transact(None, |col| {
|
||||||
|
let target_deck;
|
||||||
|
let mut target_name = None;
|
||||||
|
if let Some(target) = target {
|
||||||
|
if let Some(target) = col.storage.get_deck(target)? {
|
||||||
|
target_deck = target;
|
||||||
|
target_name = Some(target_deck.name.as_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for source in source_decks {
|
||||||
|
if let Some(mut source) = col.storage.get_deck(*source)? {
|
||||||
|
let orig = source.clone();
|
||||||
|
let new_name = drag_drop_deck_name(&source.name, target_name);
|
||||||
|
if new_name == source.name {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
source.name = new_name;
|
||||||
|
col.ensure_deck_name_unique(&mut source, usn)?;
|
||||||
|
col.rename_child_decks(&orig, &source.name, usn)?;
|
||||||
|
source.set_modified(usn);
|
||||||
|
col.storage.update_deck(&source)?;
|
||||||
|
// after updating, we need to ensure all grandparents exist, which may not be the case
|
||||||
|
// in the parent->child case
|
||||||
|
col.create_missing_parents(&source.name, usn)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::{human_deck_name_to_native, immediate_parent_name, normalize_native_name};
|
use super::{human_deck_name_to_native, immediate_parent_name, normalize_native_name};
|
||||||
|
use crate::decks::drag_drop_deck_name;
|
||||||
use crate::{
|
use crate::{
|
||||||
collection::{open_test_collection, Collection},
|
collection::{open_test_collection, Collection},
|
||||||
err::Result,
|
err::Result,
|
||||||
|
@ -675,4 +735,39 @@ mod test {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn drag_drop() {
|
||||||
|
// use custom separator to make the tests easier to read
|
||||||
|
fn n(s: &str) -> String {
|
||||||
|
s.replace(":", "\x1f")
|
||||||
|
}
|
||||||
|
assert_eq!(drag_drop_deck_name("drag", Some("drop")), n("drop:drag"));
|
||||||
|
assert_eq!(&drag_drop_deck_name("drag", None), "drag");
|
||||||
|
assert_eq!(&drag_drop_deck_name(&n("drag:child"), None), "child");
|
||||||
|
assert_eq!(
|
||||||
|
drag_drop_deck_name(&n("drag:child"), Some(&n("drop:deck"))),
|
||||||
|
n("drop:deck:child")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
drag_drop_deck_name(&n("drag:child"), Some("drag")),
|
||||||
|
n("drag:child")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
drag_drop_deck_name(&n("drag:child:grandchild"), Some("drag")),
|
||||||
|
n("drag:grandchild")
|
||||||
|
);
|
||||||
|
// while the renaming code should be able to cope with renaming a parent to a child,
|
||||||
|
// it's not often useful and can be difficult for the user to clean up if done accidentally,
|
||||||
|
// so it should be a no-op
|
||||||
|
assert_eq!(
|
||||||
|
drag_drop_deck_name(&n("drag"), Some(&n("drag:child:grandchild"))),
|
||||||
|
n("drag")
|
||||||
|
);
|
||||||
|
// name doesn't change when deck dropped on itself
|
||||||
|
assert_eq!(
|
||||||
|
drag_drop_deck_name(&n("foo:bar"), Some(&n("foo:bar"))),
|
||||||
|
n("foo:bar")
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue