use perform_op() for deck drag&drop

This commit is contained in:
Damien Elmes 2021-03-22 18:23:56 +10:00
parent b1f4ea562a
commit 6a11c0398c
10 changed files with 112 additions and 116 deletions

View File

@ -151,3 +151,8 @@ browsing-changed-new-position =
[one] Changed position of { $count } new card.
*[other] Changed position of { $count } new cards.
}
browsing-reparented-decks =
{ $count ->
[one] Renamed { $count } deck.
*[other] Renamed { $count } decks.
}

View File

@ -19,5 +19,3 @@ undo-update-card = Update Card
undo-update-deck = Update Deck
undo-forget-card = Forget Card
undo-set-flag = Set Flag
# when dragging/dropping tags and decks in the sidebar
undo-reparent = Change Parent

View File

@ -271,11 +271,13 @@ class DeckManager:
# Drag/drop
#############################################################
def drag_drop_decks(self, source_decks: List[DeckID], target_deck: DeckID) -> None:
"""Rename one or more source decks that were dropped on `target_deck`.
If target_deck is 0, decks will be placed at the top level."""
self.col._backend.drag_drop_decks(
source_deck_ids=source_decks, target_deck_id=target_deck
def reparent(
self, deck_ids: Sequence[DeckID], new_parent: DeckID
) -> OpChangesWithCount:
"""Rename one or more source decks that were dropped on `new_parent`.
If new_parent is 0, decks will be placed at the top level."""
return self.col._backend.reparent_decks(
deck_ids=deck_ids, new_parent=new_parent
)
# legacy
@ -286,7 +288,7 @@ class DeckManager:
onto = 0
else:
onto = int(ontoDeckDid)
self.drag_drop_decks([int(draggedDeckDid)], onto)
self.reparent([int(draggedDeckDid)], onto)
# Deck configurations
#############################################################

View File

@ -5,6 +5,7 @@ from __future__ import annotations
from typing import Sequence
from anki.decks import DeckID
from anki.lang import TR
from aqt import AnkiQt, QWidget
from aqt.utils import tooltip, tr
@ -14,7 +15,7 @@ def remove_decks(
*,
mw: AnkiQt,
parent: QWidget,
deck_ids: Sequence[int],
deck_ids: Sequence[DeckID],
) -> None:
mw.perform_op(
lambda: mw.col.decks.remove(deck_ids),
@ -22,3 +23,14 @@ def remove_decks(
tr(TR.BROWSING_CARDS_DELETED, count=out.count), parent=parent
),
)
def reparent_decks(
*, mw: AnkiQt, parent: QWidget, deck_ids: Sequence[DeckID], new_parent: DeckID
) -> None:
mw.perform_op(
lambda: mw.col.decks.reparent(deck_ids=deck_ids, new_parent=new_parent),
success=lambda out: tooltip(
tr(TR.BROWSING_REPARENTED_DECKS, count=out.count), parent=parent
),
)

View File

@ -1,8 +1,8 @@
# Copyright: Ankitects Pty Ltd and contributors
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations
from concurrent.futures import Future
from copy import deepcopy
from dataclasses import dataclass
from typing import Any
@ -13,7 +13,7 @@ from anki.decks import DeckTreeNode
from anki.errors import DeckIsFilteredError
from anki.utils import intTime
from aqt import AnkiQt, gui_hooks
from aqt.deck_ops import remove_decks
from aqt.deck_ops import remove_decks, reparent_decks
from aqt.qt import *
from aqt.sound import av_player
from aqt.toolbar import BottomBar
@ -304,21 +304,7 @@ class DeckBrowser:
self._renderPage(reuse=True)
def _handle_drag_and_drop(self, source: int, target: int) -> None:
def process() -> None:
self.mw.col.decks.drag_drop_decks([source], target)
def on_done(fut: Future) -> None:
try:
fut.result()
except Exception as e:
showWarning(str(e))
return
self.mw.update_undo_actions()
gui_hooks.sidebar_should_refresh_decks()
self.show()
self.mw.taskman.with_progress(process, on_done)
reparent_decks(mw=self.mw, parent=self.mw, deck_ids=[source], new_parent=target)
def _delete(self, did: int) -> None:
remove_decks(mw=self.mw, parent=self.mw, deck_ids=[did])

View File

@ -3,7 +3,6 @@
from __future__ import annotations
from concurrent.futures import Future
from enum import Enum, auto
from typing import Dict, Iterable, List, Optional, Tuple, cast
@ -16,7 +15,7 @@ from anki.tags import TagTreeNode
from anki.types import assert_exhaustive
from aqt import colors, gui_hooks
from aqt.clayout import CardLayout
from aqt.deck_ops import remove_decks
from aqt.deck_ops import remove_decks, reparent_decks
from aqt.models import Models
from aqt.qt import *
from aqt.tag_ops import remove_tags_for_all_notes, rename_tag, reparent_tags
@ -604,30 +603,18 @@ class SidebarTreeView(QTreeView):
def _handle_drag_drop_decks(
self, sources: List[SidebarItem], target: SidebarItem
) -> bool:
source_ids = [
deck_ids = [
source.id for source in sources if source.item_type == SidebarItemType.DECK
]
if not source_ids:
if not deck_ids:
return False
def on_done(fut: Future) -> None:
self.browser.model.endReset()
try:
fut.result()
except Exception as e:
showWarning(str(e))
return
self.refresh()
self.mw.deckBrowser.refresh()
self.mw.update_undo_actions()
new_parent = target.id
def on_save() -> None:
self.browser.model.beginReset()
self.mw.taskman.with_progress(
lambda: self.col.decks.drag_drop_decks(source_ids, target.id), on_done
reparent_decks(
mw=self.mw, parent=self.browser, deck_ids=deck_ids, new_parent=new_parent
)
self.browser.editor.call_after_note_saved(on_save)
return True
def _handle_drag_drop_tags(

View File

@ -140,7 +140,7 @@ service DecksService {
rpc GetDeckNames(GetDeckNamesIn) returns (DeckNames);
rpc NewDeckLegacy(Bool) returns (Json);
rpc RemoveDecks(DeckIDs) returns (OpChangesWithCount);
rpc DragDropDecks(DragDropDecksIn) returns (OpChanges);
rpc ReparentDecks(ReparentDecksIn) returns (OpChangesWithCount);
rpc RenameDeck(RenameDeckIn) returns (OpChanges);
}
@ -1108,9 +1108,9 @@ message GetDeckNamesIn {
bool include_filtered = 2;
}
message DragDropDecksIn {
repeated int64 source_deck_ids = 1;
int64 target_deck_id = 2;
message ReparentDecksIn {
repeated int64 deck_ids = 1;
int64 new_parent = 2;
}
message NoteIsDuplicateOrEmptyOut {

View File

@ -114,14 +114,14 @@ impl DecksService for Backend {
.map(Into::into)
}
fn drag_drop_decks(&self, input: pb::DragDropDecksIn) -> Result<pb::OpChanges> {
let source_dids: Vec<_> = input.source_deck_ids.into_iter().map(Into::into).collect();
let target_did = if input.target_deck_id == 0 {
fn reparent_decks(&self, input: pb::ReparentDecksIn) -> Result<pb::OpChangesWithCount> {
let deck_ids: Vec<_> = input.deck_ids.into_iter().map(Into::into).collect();
let new_parent = if input.new_parent == 0 {
None
} else {
Some(input.target_deck_id.into())
Some(input.new_parent.into())
};
self.with_col(|col| col.drag_drop_decks(&source_dids, target_did))
self.with_col(|col| col.reparent_decks(&deck_ids, new_parent))
.map(Into::into)
}

View File

@ -230,21 +230,21 @@ pub(crate) fn immediate_parent_name(machine_name: &str) -> Option<&str> {
/// 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.
/// The returned name should be used to rename `dragged`.
/// Arguments are expected in 'machine' form with an \x1f separator.
pub(crate) fn drag_drop_deck_name(dragged: &str, dropped: Option<&str>) -> String {
pub(crate) fn reparented_name(dragged: &str, dropped: Option<&str>) -> Option<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()
None
} else {
// foo::bar onto baz -> baz::bar
format!("{}\x1f{}", dropped, dragged_base)
Some(format!("{}\x1f{}", dropped, dragged_base))
}
} else {
// foo::bar onto top level -> bar
dragged_base.into()
Some(dragged_base.into())
}
}
@ -618,17 +618,26 @@ impl Collection {
self.update_single_deck_undoable(deck, original)
}
pub fn drag_drop_decks(
pub fn reparent_decks(
&mut self,
source_decks: &[DeckID],
target: Option<DeckID>,
) -> Result<OpOutput<()>> {
deck_ids: &[DeckID],
new_parent: Option<DeckID>,
) -> Result<OpOutput<usize>> {
self.transact(Op::ReparentDeck, |col| {
col.reparent_decks_inner(deck_ids, new_parent)
})
}
pub fn reparent_decks_inner(
&mut self,
deck_ids: &[DeckID],
new_parent: Option<DeckID>,
) -> Result<usize> {
let usn = self.usn()?;
self.transact(Op::RenameDeck, |col| {
let target_deck;
let mut target_name = None;
if let Some(target) = target {
if let Some(target) = col.storage.get_deck(target)? {
if let Some(target) = new_parent {
if let Some(target) = self.storage.get_deck(target)? {
if target.is_filtered() {
return Err(AnkiError::DeckIsFiltered);
}
@ -637,40 +646,36 @@ impl Collection {
}
}
for source in source_decks {
if let Some(mut source) = col.storage.get_deck(*source)? {
let new_name = drag_drop_deck_name(&source.name, target_name);
if new_name == source.name {
continue;
}
let orig = source.clone();
let mut count = 0;
for deck in deck_ids {
if let Some(mut deck) = self.storage.get_deck(*deck)? {
if let Some(new_name) = reparented_name(&deck.name, target_name) {
count += 1;
let orig = deck.clone();
// this is basically update_deck_inner(), except:
// - we skip the normalization in prepare_for_update()
// - we skip the match_or_create_parents() step
// - we skip the final create_missing_parents(), as we don't allow parent->child
// renames
source.set_modified(usn);
source.name = new_name;
col.ensure_deck_name_unique(&mut source, usn)?;
col.rename_child_decks(&orig, &source.name, usn)?;
col.update_single_deck_undoable(&mut source, orig)?;
// after updating, we need to ensure all grandparents exist, which may not be the case
// in the parent->child case
// FIXME: maybe we only need to do this once at the end of the loop?
col.create_missing_parents(&source.name, usn)?;
deck.set_modified(usn);
deck.name = new_name;
self.ensure_deck_name_unique(&mut deck, usn)?;
self.rename_child_decks(&orig, &deck.name, usn)?;
self.update_single_deck_undoable(&mut deck, orig)?;
}
}
}
Ok(())
})
Ok(count)
}
}
#[cfg(test)]
mod test {
use super::{human_deck_name_to_native, immediate_parent_name, normalize_native_name};
use crate::decks::drag_drop_deck_name;
use crate::decks::reparented_name;
use crate::{
collection::{open_test_collection, Collection},
err::Result,
@ -843,32 +848,31 @@ mod test {
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");
fn n_opt(s: &str) -> Option<String> {
Some(n(s))
}
assert_eq!(reparented_name("drag", Some("drop")), n_opt("drop:drag"));
assert_eq!(reparented_name("drag", None), n_opt("drag"));
assert_eq!(reparented_name(&n("drag:child"), None), n_opt("child"));
assert_eq!(
drag_drop_deck_name(&n("drag:child"), Some(&n("drop:deck"))),
n("drop:deck:child")
reparented_name(&n("drag:child"), Some(&n("drop:deck"))),
n_opt("drop:deck:child")
);
assert_eq!(
drag_drop_deck_name(&n("drag:child"), Some("drag")),
n("drag:child")
reparented_name(&n("drag:child"), Some("drag")),
n_opt("drag:child")
);
assert_eq!(
drag_drop_deck_name(&n("drag:child:grandchild"), Some("drag")),
n("drag:grandchild")
reparented_name(&n("drag:child:grandchild"), Some("drag")),
n_opt("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
// drops to child not supported
assert_eq!(
drag_drop_deck_name(&n("drag"), Some(&n("drag:child:grandchild"))),
n("drag")
reparented_name(&n("drag"), Some(&n("drag:child:grandchild"))),
None
);
// 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")
);
assert_eq!(reparented_name(&n("foo:bar"), Some(&n("foo:bar"))), None);
}
}

View File

@ -15,6 +15,7 @@ pub enum Op {
RemoveNote,
RemoveTag,
RenameDeck,
ReparentDeck,
RenameTag,
ReparentTag,
ScheduleAsNew,
@ -57,7 +58,8 @@ impl Op {
Op::SortCards => TR::BrowsingReschedule,
Op::RenameTag => TR::ActionsRenameTag,
Op::RemoveTag => TR::ActionsRemoveTag,
Op::ReparentTag => TR::UndoReparent,
Op::ReparentTag => TR::ActionsRenameTag,
Op::ReparentDeck => TR::ActionsRenameDeck,
};
i18n.tr(key).to_string()