Skip to content

Commit ab336eb

Browse files
committed
feat: support deleting selected note types
1 parent 7d8dc01 commit ab336eb

4 files changed

Lines changed: 206 additions & 8 deletions

File tree

ftl/core/notetypes.ftl

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,16 @@ notetypes-cards = Cards
2626
notetypes-clone = Clone: { $val }
2727
notetypes-copy = { $val } copy
2828
notetypes-create-scalable-images-with-dvisvgm = Create scalable images with dvisvgm
29+
notetypes-delete-selected-note-types =
30+
{ $count ->
31+
[one] Delete this selected note type?
32+
*[other] Delete { $count } selected note types?
33+
}
34+
notetypes-delete-selected-note-types-and-all =
35+
{ $count ->
36+
[one] Delete this selected note type and all its notes and cards?
37+
*[other] Delete { $count } selected note types and all their notes and cards?
38+
}
2939
notetypes-delete-this-note-type-and-all = Delete this note type and all its cards?
3040
notetypes-delete-this-unused-note-type = Delete this unused note type?
3141
notetypes-fields = Fields
@@ -34,6 +44,11 @@ notetypes-header = Header
3444
notetypes-note-types = Note Types
3545
notetypes-options = Options
3646
notetypes-please-add-another-note-type-first = Please add another note type first.
47+
notetypes-selected-note-types-removed =
48+
{ $count ->
49+
[one] Removed one selected note type.
50+
*[other] Removed { $count } selected note types.
51+
}
3752
notetypes-type = Type
3853
3954
## Image Occlusion

qt/aqt/models.py

Lines changed: 85 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
import aqt.clayout
1111
from anki import stdmodels
12-
from anki.collection import Collection, OpChangesWithId
12+
from anki.collection import Collection, OpChangesWithCount, OpChangesWithId
1313
from anki.lang import without_unicode_isolation
1414
from anki.models import NotetypeDict, NotetypeId, NotetypeNameIdUseCount
1515
from anki.notes import Note
@@ -18,6 +18,8 @@
1818
from aqt.operations.notetype import (
1919
add_notetype_legacy,
2020
remove_notetype,
21+
remove_notetypes,
22+
selected_notetype_ids_to_remove,
2123
update_notetype_legacy,
2224
)
2325
from aqt.qt import *
@@ -32,6 +34,7 @@
3234
restoreGeom,
3335
saveGeom,
3436
showInfo,
37+
tooltip,
3538
tr,
3639
)
3740

@@ -113,6 +116,7 @@ def setupModels(self) -> None:
113116
button = box.addButton(label, QDialogButtonBox.ButtonRole.ActionRole)
114117
qconnect(button.clicked, func)
115118

119+
f.modelsList.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
116120
qconnect(f.modelsList.itemDoubleClicked, self.onRename)
117121

118122
def on_done(fut: Future) -> None:
@@ -163,6 +167,17 @@ def current_notetype(self) -> NotetypeDict:
163167
row = self.form.modelsList.currentRow()
164168
return self.mm.get(NotetypeId(self.models[row].id))
165169

170+
def selected_notetype_rows(self) -> list[int]:
171+
rows = sorted({index.row() for index in self.form.modelsList.selectedIndexes()})
172+
if rows:
173+
return rows
174+
175+
row = self.form.modelsList.currentRow()
176+
if row != -1:
177+
return [row]
178+
179+
return []
180+
166181
def onAdd(self) -> None:
167182
def on_success(notetype: NotetypeDict) -> None:
168183
# if legacy add-ons already added the notetype, skip adding
@@ -190,7 +205,16 @@ def onDelete(self) -> None:
190205
if len(self.models) < 2:
191206
showInfo(tr.notetypes_please_add_another_note_type_first(), parent=self)
192207
return
193-
idx = self.form.modelsList.currentRow()
208+
209+
selected_rows = self.selected_notetype_rows()
210+
if not selected_rows:
211+
return
212+
213+
if len(selected_rows) > 1:
214+
self.onDeleteSelected(selected_rows)
215+
return
216+
217+
idx = selected_rows[0]
194218
if self.models[idx].use_count:
195219
msg = tr.notetypes_delete_this_note_type_and_all()
196220
else:
@@ -202,10 +226,65 @@ def onDelete(self) -> None:
202226
if not tracker.mark_schema():
203227
return
204228

205-
nt = self.current_notetype()
206-
remove_notetype(parent=self, notetype_id=nt["id"]).success(
207-
lambda _: self.refresh_list(None)
208-
).run_in_background()
229+
remove_notetype(
230+
parent=self, notetype_id=NotetypeId(self.models[idx].id)
231+
).success(lambda _: self.refresh_list(None)).run_in_background()
232+
233+
def onDeleteSelected(self, selected_rows: Sequence[int]) -> None:
234+
selected_notetype_ids = [
235+
NotetypeId(self.models[row].id)
236+
for row in selected_rows
237+
if 0 <= row < len(self.models)
238+
]
239+
current_row = self.form.modelsList.currentRow()
240+
protected_notetype_id = (
241+
NotetypeId(self.models[current_row].id)
242+
if len(selected_notetype_ids) == len(self.models) and current_row != -1
243+
else None
244+
)
245+
notetype_ids = selected_notetype_ids_to_remove(
246+
self.models,
247+
selected_notetype_ids,
248+
protected_notetype_id,
249+
)
250+
if not notetype_ids:
251+
showInfo(tr.notetypes_please_add_another_note_type_first(), parent=self)
252+
return
253+
254+
use_counts = {
255+
NotetypeId(notetype.id): notetype.use_count for notetype in self.models
256+
}
257+
has_notes = any(use_counts[notetype_id] for notetype_id in notetype_ids)
258+
msg = (
259+
tr.notetypes_delete_selected_note_types_and_all(count=len(notetype_ids))
260+
if has_notes
261+
else tr.notetypes_delete_selected_note_types(count=len(notetype_ids))
262+
)
263+
if not askUser(
264+
msg,
265+
parent=self,
266+
):
267+
return
268+
269+
tracker = ChangeTracker(self.mw)
270+
if not tracker.mark_schema():
271+
return
272+
273+
def on_success(out: OpChangesWithCount) -> None:
274+
if out.count:
275+
tooltip(
276+
tr.notetypes_selected_note_types_removed(count=out.count),
277+
parent=self,
278+
)
279+
else:
280+
showInfo(tr.notetypes_please_add_another_note_type_first(), parent=self)
281+
self.refresh_list(None)
282+
283+
remove_notetypes(
284+
parent=self,
285+
notetype_ids=notetype_ids,
286+
protected_notetype_id=protected_notetype_id,
287+
).success(on_success).run_in_background()
209288

210289
def onAdvanced(self) -> None:
211290
nt = self.current_notetype()

qt/aqt/operations/notetype.py

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,40 @@
33

44
from __future__ import annotations
55

6-
from anki.collection import OpChanges, OpChangesWithId
7-
from anki.models import ChangeNotetypeRequest, NotetypeDict, NotetypeId
6+
from collections.abc import Sequence
7+
8+
from anki.collection import OpChanges, OpChangesWithCount, OpChangesWithId
9+
from anki.models import (
10+
ChangeNotetypeRequest,
11+
NotetypeDict,
12+
NotetypeId,
13+
NotetypeNameIdUseCount,
14+
)
815
from anki.stdmodels import StockNotetypeKind
916
from aqt.operations import CollectionOp
1017
from aqt.qt import QWidget
1118

1219

20+
def selected_notetype_ids_to_remove(
21+
notetypes: Sequence[NotetypeNameIdUseCount],
22+
notetype_ids: Sequence[NotetypeId],
23+
protected_notetype_id: NotetypeId | None = None,
24+
) -> list[NotetypeId]:
25+
"""Return selected note type IDs to remove, keeping at least one note type."""
26+
removable_count = max(len(notetypes) - 1, 0)
27+
requested_ids = {NotetypeId(notetype_id) for notetype_id in notetype_ids}
28+
protected_id = (
29+
NotetypeId(protected_notetype_id) if protected_notetype_id is not None else None
30+
)
31+
selected_ids = [
32+
NotetypeId(notetype.id)
33+
for notetype in notetypes
34+
if NotetypeId(notetype.id) in requested_ids
35+
and NotetypeId(notetype.id) != protected_id
36+
]
37+
return selected_ids[:removable_count]
38+
39+
1340
def add_notetype_legacy(
1441
*,
1542
parent: QWidget,
@@ -37,6 +64,33 @@ def remove_notetype(
3764
return CollectionOp(parent, lambda col: col.models.remove(notetype_id))
3865

3966

67+
def remove_notetypes(
68+
*,
69+
parent: QWidget,
70+
notetype_ids: Sequence[NotetypeId],
71+
protected_notetype_id: NotetypeId | None = None,
72+
) -> CollectionOp[OpChangesWithCount]:
73+
def op(col) -> OpChangesWithCount:
74+
ids_to_remove = selected_notetype_ids_to_remove(
75+
col.models.all_use_counts(),
76+
notetype_ids,
77+
protected_notetype_id,
78+
)
79+
if not ids_to_remove:
80+
return OpChangesWithCount(changes=OpChanges(), count=0)
81+
82+
changes = col.models.remove(ids_to_remove[0])
83+
target = col.undo_status().last_step
84+
85+
for notetype_id in ids_to_remove[1:]:
86+
col.models.remove(notetype_id)
87+
changes = col.merge_undo_entries(target)
88+
89+
return OpChangesWithCount(changes=changes, count=len(ids_to_remove))
90+
91+
return CollectionOp(parent, op)
92+
93+
4094
def change_notetype_of_notes(
4195
*, parent: QWidget, input: ChangeNotetypeRequest
4296
) -> CollectionOp[OpChanges]:

qt/tests/test_notetype_delete.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Copyright: Ankitects Pty Ltd and contributors
2+
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
3+
4+
from dataclasses import dataclass
5+
6+
from anki.models import NotetypeId
7+
from aqt.operations.notetype import selected_notetype_ids_to_remove
8+
9+
10+
@dataclass
11+
class Notetype:
12+
id: int
13+
use_count: int
14+
15+
16+
def test_selected_notetype_ids_to_remove() -> None:
17+
notetypes = [Notetype(1, 2), Notetype(2, 0), Notetype(3, 0)]
18+
19+
assert selected_notetype_ids_to_remove(
20+
notetypes,
21+
[NotetypeId(1), NotetypeId(3)],
22+
) == [1, 3]
23+
24+
25+
def test_selected_notetype_ids_to_remove_keeps_one() -> None:
26+
notetypes = [Notetype(1, 0), Notetype(2, 0), Notetype(3, 0)]
27+
28+
assert selected_notetype_ids_to_remove(
29+
notetypes,
30+
[NotetypeId(1), NotetypeId(2), NotetypeId(3)],
31+
) == [1, 2]
32+
33+
34+
def test_selected_notetype_ids_to_remove_keeps_protected_notetype() -> None:
35+
notetypes = [Notetype(1, 0), Notetype(2, 0), Notetype(3, 0)]
36+
37+
assert selected_notetype_ids_to_remove(
38+
notetypes,
39+
[NotetypeId(1), NotetypeId(2), NotetypeId(3)],
40+
protected_notetype_id=NotetypeId(2),
41+
) == [1, 3]
42+
43+
44+
def test_selected_notetype_ids_to_remove_ignores_stale_ids() -> None:
45+
notetypes = [Notetype(1, 0), Notetype(2, 0), Notetype(3, 0)]
46+
47+
assert selected_notetype_ids_to_remove(
48+
notetypes,
49+
[NotetypeId(2), NotetypeId(4)],
50+
) == [2]

0 commit comments

Comments
 (0)