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'
|
__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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user