mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
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:
parent
8e77a2469c
commit
d13c4c2b94
@ -6,7 +6,7 @@ from __future__ import (unicode_literals, division, absolute_import,
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
|
||||
import os
|
||||
import os, cPickle
|
||||
from functools import partial
|
||||
from binascii import hexlify
|
||||
|
||||
@ -50,6 +50,9 @@ def search_href(search_term, value):
|
||||
search = '%s:"=%s"' % (search_term, value.replace('"', '\\"'))
|
||||
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'):
|
||||
if field_list is None:
|
||||
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))))
|
||||
elif field == '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]
|
||||
links = u', '.join(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':
|
||||
if not mi.publisher:
|
||||
continue
|
||||
val = '<a href="%s" title="%s">%s</a>' % (
|
||||
search_href('publisher', mi.publisher), _('Click to see books with {0}: {1}').format(metadata['name'], a(mi.publisher)), p(mi.publisher))
|
||||
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)),
|
||||
a(item_data('publisher', mi.publisher, mi.id)), p(mi.publisher))
|
||||
ans.append((field, row % (name, val)))
|
||||
else:
|
||||
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
|
||||
series = getattr(mi, field)
|
||||
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(
|
||||
sidx=fmt_sidx(sidx, use_roman=use_roman_numbers), cls="series_name",
|
||||
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')))
|
||||
elif metadata['datatype'] == 'datetime':
|
||||
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)
|
||||
if field == 'tags':
|
||||
all_vals = sorted(all_vals, key=sort_key)
|
||||
links = ['<a href="%s" title="%s">%s</a>' % (
|
||||
search_href(st, x), _('Click to see books with {0}: {1}').format(metadata['name'], a(x)), p(x))
|
||||
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)), a(item_data(field, x, mi.id)), p(x))
|
||||
for x in all_vals]
|
||||
val = metadata['is_multiple']['list_to_ui'].join(links)
|
||||
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]
|
||||
except Exception:
|
||||
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)))
|
||||
|
||||
@ -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]
|
||||
# print '\n'.join(ans)
|
||||
return u'<table class="fields">%s</table>'%(u'\n'.join(ans)), comment_fields
|
||||
|
||||
|
||||
|
@ -322,18 +322,22 @@ class EditMetadataAction(InterfaceAction):
|
||||
m.refresh_rows(rows_to_refresh)
|
||||
|
||||
if changed:
|
||||
m.refresh_ids(list(changed))
|
||||
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()
|
||||
self.refresh_books_after_metadata_edit(changed, previous)
|
||||
if self.gui.library_view.alternate_views.current_view is view:
|
||||
if hasattr(view, 'restore_hpos'):
|
||||
view.restore_hpos(hpos)
|
||||
else:
|
||||
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):
|
||||
from calibre.gui2.metadata.single import edit_metadata
|
||||
db = self.gui.library_view.model().db
|
||||
@ -418,8 +422,8 @@ class EditMetadataAction(InterfaceAction):
|
||||
show=True)
|
||||
if len(rows) > 5:
|
||||
if not confirm('<p>'+_('You are about to merge more than 5 books. '
|
||||
'Are you <b>sure</b> you want to proceed?')
|
||||
+'</p>', 'merge_too_many_books', self.gui):
|
||||
'Are you <b>sure</b> you want to proceed?') +
|
||||
'</p>', 'merge_too_many_books', self.gui):
|
||||
return
|
||||
|
||||
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> '
|
||||
'The second and subsequently selected books will not '
|
||||
'be deleted or changed.<br><br>'
|
||||
'Please confirm you want to proceed.')%title
|
||||
+'</p>', 'merge_books_safe', self.gui):
|
||||
'Please confirm you want to proceed.')%title +
|
||||
'</p>', 'merge_books_safe', self.gui):
|
||||
return
|
||||
self.add_formats(dest_id, self.formats_for_books(rows))
|
||||
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 '
|
||||
'and any duplicate formats in the second and subsequently selected books '
|
||||
'will be permanently <b>deleted</b> from your calibre library.<br><br> '
|
||||
'Are you <b>sure</b> you want to proceed?')%title
|
||||
+'</p>', 'merge_only_formats', self.gui):
|
||||
'Are you <b>sure</b> you want to proceed?')%title +
|
||||
'</p>', 'merge_only_formats', self.gui):
|
||||
return
|
||||
self.add_formats(dest_id, self.formats_for_books(rows))
|
||||
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 '
|
||||
'and any duplicate formats in the second and subsequently selected books '
|
||||
'will be permanently <b>deleted</b> from your calibre library.<br><br> '
|
||||
'Are you <b>sure</b> you want to proceed?')%title
|
||||
+'</p>', 'merge_books', self.gui):
|
||||
'Are you <b>sure</b> you want to proceed?')%title +
|
||||
'</p>', 'merge_books', self.gui):
|
||||
return
|
||||
self.add_formats(dest_id, self.formats_for_books(rows))
|
||||
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)
|
||||
|
@ -5,6 +5,7 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import cPickle
|
||||
from binascii import unhexlify
|
||||
from functools import partial
|
||||
|
||||
@ -164,6 +165,7 @@ def details_context_menu_event(view, ev, book_info): # {{{
|
||||
menu.addAction(ac)
|
||||
else:
|
||||
el = r.linkElement()
|
||||
data = el.attribute('data-item')
|
||||
author = el.toPlainText() if unicode(el.attribute('calibre-data')) == u'authors' else None
|
||||
if not url.startswith('search:'):
|
||||
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.setText(_('Manage %s') % author)
|
||||
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:
|
||||
menu.exec_(ev.globalPos())
|
||||
@ -385,6 +397,7 @@ class BookInfo(QWebView):
|
||||
|
||||
link_clicked = pyqtSignal(object)
|
||||
remove_format = pyqtSignal(int, object)
|
||||
remove_item = pyqtSignal(int, object, object)
|
||||
save_format = pyqtSignal(int, object)
|
||||
restore_format = pyqtSignal(int, object)
|
||||
compare_format = pyqtSignal(int, object)
|
||||
@ -415,8 +428,16 @@ class BookInfo(QWebView):
|
||||
ac.current_url = None
|
||||
ac.triggered.connect(getattr(self, '%s_triggerred'%x))
|
||||
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)
|
||||
|
||||
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):
|
||||
f = getattr(self, '%s_action'%which).current_fmt
|
||||
url = getattr(self, '%s_action'%which).current_url
|
||||
@ -579,6 +600,7 @@ class BookDetails(QWidget): # {{{
|
||||
view_specific_format = pyqtSignal(int, object)
|
||||
search_requested = pyqtSignal(object)
|
||||
remove_specific_format = pyqtSignal(int, object)
|
||||
remove_metadata_item = pyqtSignal(int, object, object)
|
||||
save_specific_format = pyqtSignal(int, object)
|
||||
restore_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.book_info.link_clicked.connect(self.handle_click)
|
||||
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.save_format.connect(self.save_specific_format)
|
||||
self.book_info.restore_format.connect(self.restore_specific_format)
|
||||
|
@ -503,6 +503,8 @@ class LayoutMixin(object): # {{{
|
||||
self.book_details.search_requested.connect(self.search.set_search_string)
|
||||
self.book_details.remove_specific_format.connect(
|
||||
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.iactions['Save To Disk'].save_library_format_by_ids)
|
||||
self.book_details.restore_specific_format.connect(
|
||||
|
Loading…
x
Reference in New Issue
Block a user