diff --git a/src/calibre/db/backend.py b/src/calibre/db/backend.py
index b9ca4818a0..413696fa05 100644
--- a/src/calibre/db/backend.py
+++ b/src/calibre/db/backend.py
@@ -1834,6 +1834,31 @@ class DB(object):
if not ignore_removed or not annot.get('removed'):
yield {'format': fmt, 'user_type': user_type, 'user': user, 'annotation': annot}
+ def delete_annotations(self, annot_ids):
+ from calibre.utils.date import utcnow
+ replacements = []
+ removals = []
+ for annot_id in annot_ids:
+ for (raw_annot_data, annot_type) in self.execute(
+ 'SELECT annot_data, annot_type FROM annotations WHERE id=?', (annot_id,)
+ ):
+ try:
+ annot_data = json.loads(raw_annot_data)
+ except Exception:
+ removals.append((annot_id,))
+ continue
+ new_annot = {'removed': True, 'timestamp': utcnow().isoformat(), 'type': annot_type}
+ uuid = annot_data.get('uuid')
+ if uuid is not None:
+ new_annot['uuid'] = uuid
+ else:
+ new_annot['title'] = annot_data['title']
+ replacements.append((json.dumps(new_annot), annot_id))
+ if replacements:
+ self.executemany('UPDATE annotations SET annot_data=?, searchable_text="" WHERE id=?', replacements)
+ if removals:
+ self.executemany('DELETE FROM annotations WHERE id=?', removals)
+
def all_annotations(self, restrict_to_user=None, limit=None, annotation_type=None, ignore_removed=False):
ls = json.loads
q = 'SELECT id, book, format, user_type, user, annot_data FROM annotations'
diff --git a/src/calibre/db/cache.py b/src/calibre/db/cache.py
index 4fd5be0033..0d7f098227 100644
--- a/src/calibre/db/cache.py
+++ b/src/calibre/db/cache.py
@@ -2333,6 +2333,10 @@ class Cache(object):
ignore_removed
))
+ @write_api
+ def delete_annotations(self, annot_ids):
+ self.backend.delete_annotations(annot_ids)
+
@write_api
def restore_annotations(self, book_id, annotations):
from calibre.utils.iso8601 import parse_iso8601
diff --git a/src/calibre/gui2/library/annotations.py b/src/calibre/gui2/library/annotations.py
index 7b993a2b8b..a88f1a6a59 100644
--- a/src/calibre/gui2/library/annotations.py
+++ b/src/calibre/gui2/library/annotations.py
@@ -13,7 +13,7 @@ from PyQt5.Qt import (
from calibre import prepare_string_for_xml
from calibre.ebooks.metadata import authors_to_string, fmt_sidx
-from calibre.gui2 import Application, config, gprefs
+from calibre.gui2 import Application, config, error_dialog, gprefs, question_dialog
from calibre.gui2.viewer.widgets import ResultsDelegate, SearchBox
from calibre.gui2.widgets2 import Dialog
@@ -76,6 +76,7 @@ class ResultsList(QTreeWidget):
def __init__(self, parent):
QTreeWidget.__init__(self, parent)
self.setHeaderHidden(True)
+ self.setSelectionMode(self.ExtendedSelection)
self.delegate = AnnotsResultsDelegate(self)
self.setItemDelegate(self.delegate)
self.section_font = QFont(self.font())
@@ -145,6 +146,11 @@ class ResultsList(QTreeWidget):
return
return QTreeWidget.keyPressEvent(self, ev)
+ @property
+ def selected_annot_ids(self):
+ for item in self.selectedItems():
+ yield item.data(0, Qt.UserRole)['id']
+
class Restrictions(QWidget):
@@ -310,12 +316,20 @@ class BrowsePanel(QWidget):
def effective_query_changed(self):
self.do_find()
+ def refresh(self):
+ self.current_query = None
+ self.do_find()
+
def show_next(self):
self.do_find()
def show_previous(self):
self.do_find(backwards=True)
+ @property
+ def selected_annot_ids(self):
+ return self.results_list.selected_annot_ids
+
class Details(QTextBrowser):
@@ -486,9 +500,28 @@ class AnnotationsBrowser(Dialog):
h = QHBoxLayout()
l.addLayout(h)
h.addWidget(us), h.addStretch(10), h.addWidget(self.bb)
+ self.delete_button = b = self.bb.addButton(_('Delete selected'), self.bb.ActionRole)
+ b.setToolTip(_('Delete the selected annotations'))
+ b.setIcon(QIcon(I('trash.png')))
+ b.clicked.connect(self.delete_selected)
+
+ def delete_selected(self):
+ ids = frozenset(self.browse_panel.selected_annot_ids)
+ if not ids:
+ return error_dialog(self, _('No selected annotations'), _(
+ 'No annotations have been selected'), show=True)
+ if question_dialog(self, _('Are you sure?'), ngettext(
+ 'Are you sure you want to permanently delete this annotation?',
+ 'Are you sure you want to permanently delete these {} annotations?',
+ len(ids)).format(len(ids))
+ ):
+ db = current_db()
+ db.delete_annotations(ids)
+ self.browse_panel.refresh()
def show_dialog(self):
if self.parent() is None:
+ self.browse_panel.effective_query_changed()
self.exec_()
else:
self.reinitialize()