Book Details panel: Allow deleting tags/series/publisher/etc. by right clicking on the link in the book details panel. Fixes #1442925 [Click on metadata on Book Details to delete item](https://bugs.launchpad.net/calibre/+bug/1442925)

This commit is contained in:
Kovid Goyal 2015-04-13 12:32:59 +05:30
parent 8e77a2469c
commit d13c4c2b94
4 changed files with 74 additions and 24 deletions

View File

@ -6,7 +6,7 @@ from __future__ import (unicode_literals, division, absolute_import,
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
import os import os, cPickle
from functools import partial from functools import partial
from binascii import hexlify from binascii import hexlify
@ -50,6 +50,9 @@ def search_href(search_term, value):
search = '%s:"=%s"' % (search_term, value.replace('"', '\\"')) search = '%s:"=%s"' % (search_term, value.replace('"', '\\"'))
return prepare_string_for_xml('search:' + hexlify(search.encode('utf-8')), True) return prepare_string_for_xml('search:' + hexlify(search.encode('utf-8')), True)
def item_data(field_name, value, book_id):
return hexlify(cPickle.dumps((field_name, value, book_id), -1))
def mi_to_html(mi, field_list=None, default_author_link=None, use_roman_numbers=True, rating_font='Liberation Serif'): def mi_to_html(mi, field_list=None, default_author_link=None, use_roman_numbers=True, rating_font='Liberation Serif'):
if field_list is None: if field_list is None:
field_list = get_field_list(mi) field_list = get_field_list(mi)
@ -144,7 +147,7 @@ def mi_to_html(mi, field_list=None, default_author_link=None, use_roman_numbers=
ans.append((field, row % (name, u', '.join(fmts)))) ans.append((field, row % (name, u', '.join(fmts))))
elif field == 'identifiers': elif field == 'identifiers':
urls = urls_from_identifiers(mi.identifiers) urls = urls_from_identifiers(mi.identifiers)
links = [u'<a href="%s" title="%s:%s">%s</a>' % (a(url), a(id_typ), a(id_val), p(namel)) links = [u'<a href="%s" title="%s:%s" data-item="%s">%s</a>' % (a(url), a(id_typ), a(id_val), a(item_data(field, id_typ, mi.id)), p(namel))
for namel, id_typ, id_val, url in urls] for namel, id_typ, id_val, url in urls]
links = u', '.join(links) links = u', '.join(links)
if links: if links:
@ -181,8 +184,9 @@ def mi_to_html(mi, field_list=None, default_author_link=None, use_roman_numbers=
elif field == 'publisher': elif field == 'publisher':
if not mi.publisher: if not mi.publisher:
continue continue
val = '<a href="%s" title="%s">%s</a>' % ( val = '<a href="%s" title="%s" data-item="%s">%s</a>' % (
search_href('publisher', mi.publisher), _('Click to see books with {0}: {1}').format(metadata['name'], a(mi.publisher)), p(mi.publisher)) search_href('publisher', mi.publisher), _('Click to see books with {0}: {1}').format(metadata['name'], a(mi.publisher)),
a(item_data('publisher', mi.publisher, mi.id)), p(mi.publisher))
ans.append((field, row % (name, val))) ans.append((field, row % (name, val)))
else: else:
val = mi.format_field(field)[-1] val = mi.format_field(field)[-1]
@ -199,10 +203,11 @@ def mi_to_html(mi, field_list=None, default_author_link=None, use_roman_numbers=
st = field st = field
series = getattr(mi, field) series = getattr(mi, field)
val = _( val = _(
'%(sidx)s of <a href="%(href)s" title="%(tt)s">' '%(sidx)s of <a href="%(href)s" title="%(tt)s" data-item="%(data)s">'
'<span class="%(cls)s">%(series)s</span></a>') % dict( '<span class="%(cls)s">%(series)s</span></a>') % dict(
sidx=fmt_sidx(sidx, use_roman=use_roman_numbers), cls="series_name", sidx=fmt_sidx(sidx, use_roman=use_roman_numbers), cls="series_name",
series=p(series), href=search_href(st, series), series=p(series), href=search_href(st, series),
data=a(item_data(field, series, mi.id)),
tt=p(_('Click to see books in this series'))) tt=p(_('Click to see books in this series')))
elif metadata['datatype'] == 'datetime': elif metadata['datatype'] == 'datetime':
aval = getattr(mi, field) aval = getattr(mi, field)
@ -216,8 +221,8 @@ def mi_to_html(mi, field_list=None, default_author_link=None, use_roman_numbers=
all_vals = mi.get(field) all_vals = mi.get(field)
if field == 'tags': if field == 'tags':
all_vals = sorted(all_vals, key=sort_key) all_vals = sorted(all_vals, key=sort_key)
links = ['<a href="%s" title="%s">%s</a>' % ( links = ['<a href="%s" title="%s" data-item="%s">%s</a>' % (
search_href(st, x), _('Click to see books with {0}: {1}').format(metadata['name'], a(x)), p(x)) search_href(st, x), _('Click to see books with {0}: {1}').format(metadata['name'], a(x)), a(item_data(field, x, mi.id)), p(x))
for x in all_vals] for x in all_vals]
val = metadata['is_multiple']['list_to_ui'].join(links) val = metadata['is_multiple']['list_to_ui'].join(links)
elif metadata['datatype'] == 'enumeration': elif metadata['datatype'] == 'enumeration':
@ -225,7 +230,9 @@ def mi_to_html(mi, field_list=None, default_author_link=None, use_roman_numbers=
st = metadata['search_terms'][0] st = metadata['search_terms'][0]
except Exception: except Exception:
st = field st = field
val = '<a href="%s" title="%s">%s</a>' % (search_href(st, val), _('Click to see books with {0}: {1}').format(metadata['name'], val), val) val = '<a href="%s" title="%s" data-item="%s">%s</a>' % (
search_href(st, val), a(_('Click to see books with {0}: {1}').format(metadata['name'], val)),
a(item_data(field, val, mi.id)), p(val))
ans.append((field, row % (name, val))) ans.append((field, row % (name, val)))
@ -246,5 +253,3 @@ def mi_to_html(mi, field_list=None, default_author_link=None, use_roman_numbers=
classname(fieldl), html) for fieldl, html in ans] classname(fieldl), html) for fieldl, html in ans]
# print '\n'.join(ans) # print '\n'.join(ans)
return u'<table class="fields">%s</table>'%(u'\n'.join(ans)), comment_fields return u'<table class="fields">%s</table>'%(u'\n'.join(ans)), comment_fields

View File

@ -322,18 +322,22 @@ class EditMetadataAction(InterfaceAction):
m.refresh_rows(rows_to_refresh) m.refresh_rows(rows_to_refresh)
if changed: if changed:
m.refresh_ids(list(changed)) self.refresh_books_after_metadata_edit(changed, previous)
current = self.gui.library_view.currentIndex()
if self.gui.cover_flow:
self.gui.cover_flow.dataChanged()
m.current_changed(current, previous)
self.gui.tags_view.recount()
if self.gui.library_view.alternate_views.current_view is view: if self.gui.library_view.alternate_views.current_view is view:
if hasattr(view, 'restore_hpos'): if hasattr(view, 'restore_hpos'):
view.restore_hpos(hpos) view.restore_hpos(hpos)
else: else:
view.horizontalScrollBar().setValue(hpos) view.horizontalScrollBar().setValue(hpos)
def refresh_books_after_metadata_edit(self, book_ids, previous=None):
m = self.gui.library_view.model()
m.refresh_ids(list(book_ids))
current = self.gui.library_view.currentIndex()
if self.gui.cover_flow:
self.gui.cover_flow.dataChanged()
m.current_changed(current, previous or current)
self.gui.tags_view.recount()
def do_edit_metadata(self, row_list, current_row, editing_multiple): def do_edit_metadata(self, row_list, current_row, editing_multiple):
from calibre.gui2.metadata.single import edit_metadata from calibre.gui2.metadata.single import edit_metadata
db = self.gui.library_view.model().db db = self.gui.library_view.model().db
@ -418,8 +422,8 @@ class EditMetadataAction(InterfaceAction):
show=True) show=True)
if len(rows) > 5: if len(rows) > 5:
if not confirm('<p>'+_('You are about to merge more than 5 books. ' if not confirm('<p>'+_('You are about to merge more than 5 books. '
'Are you <b>sure</b> you want to proceed?') 'Are you <b>sure</b> you want to proceed?') +
+'</p>', 'merge_too_many_books', self.gui): '</p>', 'merge_too_many_books', self.gui):
return return
dest_id, src_ids = self.books_to_merge(rows) dest_id, src_ids = self.books_to_merge(rows)
@ -430,8 +434,8 @@ class EditMetadataAction(InterfaceAction):
'will be added to the <b>first selected book</b> (%s).<br> ' 'will be added to the <b>first selected book</b> (%s).<br> '
'The second and subsequently selected books will not ' 'The second and subsequently selected books will not '
'be deleted or changed.<br><br>' 'be deleted or changed.<br><br>'
'Please confirm you want to proceed.')%title 'Please confirm you want to proceed.')%title +
+'</p>', 'merge_books_safe', self.gui): '</p>', 'merge_books_safe', self.gui):
return return
self.add_formats(dest_id, self.formats_for_books(rows)) self.add_formats(dest_id, self.formats_for_books(rows))
self.merge_metadata(dest_id, src_ids) self.merge_metadata(dest_id, src_ids)
@ -446,8 +450,8 @@ class EditMetadataAction(InterfaceAction):
'All book formats of the first selected book will be kept ' 'All book formats of the first selected book will be kept '
'and any duplicate formats in the second and subsequently selected books ' 'and any duplicate formats in the second and subsequently selected books '
'will be permanently <b>deleted</b> from your calibre library.<br><br> ' 'will be permanently <b>deleted</b> from your calibre library.<br><br> '
'Are you <b>sure</b> you want to proceed?')%title 'Are you <b>sure</b> you want to proceed?')%title +
+'</p>', 'merge_only_formats', self.gui): '</p>', 'merge_only_formats', self.gui):
return return
self.add_formats(dest_id, self.formats_for_books(rows)) self.add_formats(dest_id, self.formats_for_books(rows))
self.delete_books_after_merge(src_ids) self.delete_books_after_merge(src_ids)
@ -460,8 +464,8 @@ class EditMetadataAction(InterfaceAction):
'All book formats of the first selected book will be kept ' 'All book formats of the first selected book will be kept '
'and any duplicate formats in the second and subsequently selected books ' 'and any duplicate formats in the second and subsequently selected books '
'will be permanently <b>deleted</b> from your calibre library.<br><br> ' 'will be permanently <b>deleted</b> from your calibre library.<br><br> '
'Are you <b>sure</b> you want to proceed?')%title 'Are you <b>sure</b> you want to proceed?')%title +
+'</p>', 'merge_books', self.gui): '</p>', 'merge_books', self.gui):
return return
self.add_formats(dest_id, self.formats_for_books(rows)) self.add_formats(dest_id, self.formats_for_books(rows))
self.merge_metadata(dest_id, src_ids) self.merge_metadata(dest_id, src_ids)
@ -758,3 +762,19 @@ class EditMetadataAction(InterfaceAction):
# }}} # }}}
def remove_metadata_item(self, book_id, field, value):
db = self.gui.current_db.new_api
fm = db.field_metadata[field]
affected_books = set()
if field == 'identifiers':
identifiers = db.field_for(field, book_id)
if identifiers.pop(value, False) is not False:
affected_books = db.set_field(field, {book_id:identifiers})
elif fm['is_multiple']:
item_id = db.get_item_id(field, value)
if item_id is not None:
affected_books = db.remove_items(field, (item_id,), {book_id})
else:
affected_books = db.set_field(field, {book_id:''})
if affected_books:
self.refresh_books_after_metadata_edit(affected_books)

View File

@ -5,6 +5,7 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import cPickle
from binascii import unhexlify from binascii import unhexlify
from functools import partial from functools import partial
@ -164,6 +165,7 @@ def details_context_menu_event(view, ev, book_info): # {{{
menu.addAction(ac) menu.addAction(ac)
else: else:
el = r.linkElement() el = r.linkElement()
data = el.attribute('data-item')
author = el.toPlainText() if unicode(el.attribute('calibre-data')) == u'authors' else None author = el.toPlainText() if unicode(el.attribute('calibre-data')) == u'authors' else None
if not url.startswith('search:'): if not url.startswith('search:'):
for a, t in [('copy', _('&Copy Link')), for a, t in [('copy', _('&Copy Link')),
@ -179,6 +181,16 @@ def details_context_menu_event(view, ev, book_info): # {{{
ac.current_fmt = author ac.current_fmt = author
ac.setText(_('Manage %s') % author) ac.setText(_('Manage %s') % author)
menu.addAction(ac) menu.addAction(ac)
if data:
try:
field, value, book_id = cPickle.loads(unhexlify(data))
except Exception:
field = value = book_id = None
if field:
ac = book_info.remove_item_action
ac.data = (field, value, book_id)
ac.setText(_('Remove %s from this book') % value)
menu.addAction(ac)
if len(menu.actions()) > 0: if len(menu.actions()) > 0:
menu.exec_(ev.globalPos()) menu.exec_(ev.globalPos())
@ -385,6 +397,7 @@ class BookInfo(QWebView):
link_clicked = pyqtSignal(object) link_clicked = pyqtSignal(object)
remove_format = pyqtSignal(int, object) remove_format = pyqtSignal(int, object)
remove_item = pyqtSignal(int, object, object)
save_format = pyqtSignal(int, object) save_format = pyqtSignal(int, object)
restore_format = pyqtSignal(int, object) restore_format = pyqtSignal(int, object)
compare_format = pyqtSignal(int, object) compare_format = pyqtSignal(int, object)
@ -415,8 +428,16 @@ class BookInfo(QWebView):
ac.current_url = None ac.current_url = None
ac.triggered.connect(getattr(self, '%s_triggerred'%x)) ac.triggered.connect(getattr(self, '%s_triggerred'%x))
setattr(self, '%s_action'%x, ac) setattr(self, '%s_action'%x, ac)
self.remove_item_action = ac = QAction(QIcon(I('minus.png')), '...', self)
ac.data = (None, None, None)
ac.triggered.connect(self.remove_item_triggered)
self.setFocusPolicy(Qt.NoFocus) self.setFocusPolicy(Qt.NoFocus)
def remove_item_triggered(self):
field, value, book_id = self.remove_item_action.data
if field:
self.remove_item.emit(book_id, field, value)
def context_action_triggered(self, which): def context_action_triggered(self, which):
f = getattr(self, '%s_action'%which).current_fmt f = getattr(self, '%s_action'%which).current_fmt
url = getattr(self, '%s_action'%which).current_url url = getattr(self, '%s_action'%which).current_url
@ -579,6 +600,7 @@ class BookDetails(QWidget): # {{{
view_specific_format = pyqtSignal(int, object) view_specific_format = pyqtSignal(int, object)
search_requested = pyqtSignal(object) search_requested = pyqtSignal(object)
remove_specific_format = pyqtSignal(int, object) remove_specific_format = pyqtSignal(int, object)
remove_metadata_item = pyqtSignal(int, object, object)
save_specific_format = pyqtSignal(int, object) save_specific_format = pyqtSignal(int, object)
restore_specific_format = pyqtSignal(int, object) restore_specific_format = pyqtSignal(int, object)
compare_specific_format = pyqtSignal(int, object) compare_specific_format = pyqtSignal(int, object)
@ -654,6 +676,7 @@ class BookDetails(QWidget): # {{{
self._layout.addWidget(self.book_info) self._layout.addWidget(self.book_info)
self.book_info.link_clicked.connect(self.handle_click) self.book_info.link_clicked.connect(self.handle_click)
self.book_info.remove_format.connect(self.remove_specific_format) self.book_info.remove_format.connect(self.remove_specific_format)
self.book_info.remove_item.connect(self.remove_metadata_item)
self.book_info.open_fmt_with.connect(self.open_fmt_with) self.book_info.open_fmt_with.connect(self.open_fmt_with)
self.book_info.save_format.connect(self.save_specific_format) self.book_info.save_format.connect(self.save_specific_format)
self.book_info.restore_format.connect(self.restore_specific_format) self.book_info.restore_format.connect(self.restore_specific_format)

View File

@ -503,6 +503,8 @@ class LayoutMixin(object): # {{{
self.book_details.search_requested.connect(self.search.set_search_string) self.book_details.search_requested.connect(self.search.set_search_string)
self.book_details.remove_specific_format.connect( self.book_details.remove_specific_format.connect(
self.iactions['Remove Books'].remove_format_by_id) self.iactions['Remove Books'].remove_format_by_id)
self.book_details.remove_metadata_item.connect(
self.iactions['Edit Metadata'].remove_metadata_item)
self.book_details.save_specific_format.connect( self.book_details.save_specific_format.connect(
self.iactions['Save To Disk'].save_library_format_by_ids) self.iactions['Save To Disk'].save_library_format_by_ids)
self.book_details.restore_specific_format.connect( self.book_details.restore_specific_format.connect(