2017-07-18 13:09:12 +05:30

1177 lines
49 KiB
Python

#!/usr/bin/env python2
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os, errno
from datetime import datetime
from functools import partial
from PyQt5.Qt import (Qt, QVBoxLayout, QHBoxLayout, QWidget, QPushButton,
QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, QFont, QCoreApplication,
QTabWidget, QIcon, QToolButton, QSplitter, QGroupBox, QSpacerItem, QInputDialog,
QSizePolicy, QFrame, QSize, QKeySequence, QMenu, QShortcut, QDialog)
from calibre.constants import isosx
from calibre.ebooks.metadata import authors_to_string, string_to_authors
from calibre.gui2 import error_dialog, gprefs, pixmap_to_data
from calibre.gui2.metadata.basic_widgets import (TitleEdit, AuthorsEdit,
AuthorSortEdit, TitleSortEdit, SeriesEdit, SeriesIndexEdit, IdentifiersEdit,
RatingEdit, PublisherEdit, TagsEdit, FormatsManager, Cover, CommentsEdit,
BuddyLabel, DateEdit, PubdateEdit, LanguagesEdit, RightClickButton)
from calibre.gui2.metadata.single_download import FullFetch
from calibre.gui2.custom_column_widgets import populate_metadata_page, Comments
from calibre.utils.config import tweaks
from calibre.ebooks.metadata.book.base import Metadata
from calibre.utils.localization import canonicalize_lang
from calibre.utils.date import local_tz
from calibre.library.comments import merge_comments as merge_two_comments
BASE_TITLE = _('Edit Metadata')
fetched_fields = ('title', 'title_sort', 'authors', 'author_sort', 'series',
'series_index', 'languages', 'publisher', 'tags', 'rating',
'comments', 'pubdate')
class ScrollArea(QScrollArea):
def __init__(self, widget=None, parent=None):
QScrollArea.__init__(self, parent)
self.setFrameShape(self.NoFrame)
self.setWidgetResizable(True)
if widget is not None:
self.setWidget(widget)
class MetadataSingleDialogBase(QDialog):
view_format = pyqtSignal(object, object)
cc_two_column = tweaks['metadata_single_use_2_cols_for_custom_fields']
one_line_comments_toolbar = False
use_toolbutton_for_config_metadata = True
def __init__(self, db, parent=None, editing_multiple=False):
self.db = db
self.changed = set()
self.books_to_refresh = set()
self.rows_to_refresh = set()
self.metadata_before_fetch = None
self.editing_multiple = editing_multiple
self.comments_edit_state_at_apply = {}
QDialog.__init__(self, parent)
self.setupUi()
def setupUi(self, *args): # {{{
self.download_shortcut = QShortcut(self)
self.download_shortcut.setKey(QKeySequence('Ctrl+D',
QKeySequence.PortableText))
p = self.parent()
if hasattr(p, 'keyboard'):
kname = u'Interface Action: Edit Metadata (Edit Metadata) : menu action : download'
sc = p.keyboard.keys_map.get(kname, None)
if sc:
self.download_shortcut.setKey(sc[0])
self.swap_title_author_shortcut = s = QShortcut(self)
s.setKey(QKeySequence('Alt+Down', QKeySequence.PortableText))
self.button_box = bb = QDialogButtonBox(self)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
self.next_button = QPushButton(QIcon(I('forward.png')), _('&Next'),
self)
self.next_button.setShortcut(QKeySequence('Alt+Right'))
self.next_button.clicked.connect(self.next_clicked)
self.prev_button = QPushButton(QIcon(I('back.png')), _('&Previous'),
self)
self.prev_button.setShortcut(QKeySequence('Alt+Left'))
self.button_box.addButton(self.prev_button, bb.ActionRole)
self.button_box.addButton(self.next_button, bb.ActionRole)
self.prev_button.clicked.connect(self.prev_clicked)
bb.setStandardButtons(bb.Ok|bb.Cancel)
bb.button(bb.Ok).setDefault(True)
self.central_widget = QTabWidget(self)
self.l = QVBoxLayout(self)
self.setLayout(self.l)
self.l.addWidget(self.central_widget)
ll = self.button_box_layout = QHBoxLayout()
self.l.addLayout(ll)
ll.addSpacing(10)
ll.addWidget(self.button_box)
self.setWindowIcon(QIcon(I('edit_input.png')))
self.setWindowTitle(BASE_TITLE)
self.create_basic_metadata_widgets()
if len(self.db.custom_column_label_map):
self.create_custom_metadata_widgets()
self.comments_edit_state_at_apply = {self.comments:None}
self.do_layout()
geom = gprefs.get('metasingle_window_geometry3', None)
if geom is not None:
self.restoreGeometry(bytes(geom))
else:
self.resize(self.sizeHint())
# }}}
def sizeHint(self):
desktop = QCoreApplication.instance().desktop()
geom = desktop.availableGeometry(self)
nh, nw = max(300, geom.height()-50), max(400, geom.width()-70)
return QSize(nw, nh)
def create_basic_metadata_widgets(self): # {{{
self.basic_metadata_widgets = []
self.languages = LanguagesEdit(self)
self.basic_metadata_widgets.append(self.languages)
self.title = TitleEdit(self)
self.title.textChanged.connect(self.update_window_title)
self.deduce_title_sort_button = QToolButton(self)
self.deduce_title_sort_button.setToolTip(
_('Automatically create the title sort entry based on the current '
'title entry.\nUsing this button to create title sort will '
'change title sort from red to green.'))
self.deduce_title_sort_button.setWhatsThis(
self.deduce_title_sort_button.toolTip())
self.title_sort = TitleSortEdit(self, self.title,
self.deduce_title_sort_button, self.languages)
self.basic_metadata_widgets.extend([self.title, self.title_sort])
self.deduce_author_sort_button = b = RightClickButton(self)
b.setToolTip('<p>' +
_('Automatically create the author sort entry based on the current '
'author entry. Using this button to create author sort will '
'change author sort from red to green. There is a menu of '
'functions available under this button. Click and hold '
'on the button to see it.') + '</p>')
if isosx:
# Workaround for https://bugreports.qt-project.org/browse/QTBUG-41017
class Menu(QMenu):
def mouseReleaseEvent(self, ev):
ac = self.actionAt(ev.pos())
if ac is not None:
ac.trigger()
return QMenu.mouseReleaseEvent(self, ev)
b.m = m = Menu()
else:
b.m = m = QMenu()
ac = m.addAction(QIcon(I('forward.png')), _('Set author sort from author'))
ac2 = m.addAction(QIcon(I('back.png')), _('Set author from author sort'))
ac3 = m.addAction(QIcon(I('user_profile.png')), _('Manage authors'))
ac4 = m.addAction(QIcon(I('next.png')),
_('Copy author to author sort'))
ac5 = m.addAction(QIcon(I('previous.png')),
_('Copy author sort to author'))
b.setMenu(m)
self.authors = AuthorsEdit(self, ac3)
self.author_sort = AuthorSortEdit(self, self.authors, b, self.db, ac,
ac2, ac4, ac5)
self.basic_metadata_widgets.extend([self.authors, self.author_sort])
self.swap_title_author_button = QToolButton(self)
self.swap_title_author_button.setIcon(QIcon(I('swap.png')))
self.swap_title_author_button.setToolTip(_(
'Swap the author and title') + ' [%s]' % self.swap_title_author_shortcut.key().toString(QKeySequence.NativeText))
self.swap_title_author_button.clicked.connect(self.swap_title_author)
self.swap_title_author_shortcut.activated.connect(self.swap_title_author_button.click)
self.manage_authors_button = QToolButton(self)
self.manage_authors_button.setIcon(QIcon(I('user_profile.png')))
self.manage_authors_button.setToolTip('<p>' + _(
'Manage authors. Use to rename authors and correct '
'individual author\'s sort values') + '</p>')
self.manage_authors_button.clicked.connect(self.authors.manage_authors)
self.series = SeriesEdit(self)
self.clear_series_button = QToolButton(self)
self.clear_series_button.setToolTip(
_('Clear series'))
self.clear_series_button.clicked.connect(self.series.clear)
self.series_index = SeriesIndexEdit(self, self.series)
self.basic_metadata_widgets.extend([self.series, self.series_index])
self.formats_manager = FormatsManager(self, self.copy_fmt)
# We want formats changes to be committed before title/author, as
# otherwise we could have data loss if the title/author changed and the
# user was trying to add an extra file from the old books directory.
self.basic_metadata_widgets.insert(0, self.formats_manager)
self.formats_manager.metadata_from_format_button.clicked.connect(
self.metadata_from_format)
self.formats_manager.cover_from_format_button.clicked.connect(
self.cover_from_format)
self.cover = Cover(self)
self.cover.download_cover.connect(self.download_cover)
self.basic_metadata_widgets.append(self.cover)
self.comments = CommentsEdit(self, self.one_line_comments_toolbar)
self.basic_metadata_widgets.append(self.comments)
self.rating = RatingEdit(self)
self.clear_ratings_button = QToolButton(self)
self.clear_ratings_button.setToolTip(_('Clear rating'))
self.clear_ratings_button.setIcon(QIcon(I('trash.png')))
self.clear_ratings_button.clicked.connect(self.rating.zero)
self.basic_metadata_widgets.append(self.rating)
self.tags = TagsEdit(self)
self.tags_editor_button = QToolButton(self)
self.tags_editor_button.setToolTip(_('Open Tag editor'))
self.tags_editor_button.setIcon(QIcon(I('chapters.png')))
self.tags_editor_button.clicked.connect(self.tags_editor)
self.clear_tags_button = QToolButton(self)
self.clear_tags_button.setToolTip(_('Clear all tags'))
self.clear_tags_button.setIcon(QIcon(I('trash.png')))
self.clear_tags_button.clicked.connect(self.tags.clear)
self.basic_metadata_widgets.append(self.tags)
self.identifiers = IdentifiersEdit(self)
self.basic_metadata_widgets.append(self.identifiers)
self.clear_identifiers_button = QToolButton(self)
self.clear_identifiers_button.setIcon(QIcon(I('trash.png')))
self.clear_identifiers_button.setToolTip(_('Clear Ids'))
self.clear_identifiers_button.clicked.connect(self.identifiers.clear)
self.paste_isbn_button = b = RightClickButton(self)
b.setToolTip('<p>' +
_('Paste the contents of the clipboard into the '
'identifiers prefixed with isbn:. Or right click, '
'to choose a different prefix.') + '</p>')
b.setIcon(QIcon(I('edit-paste.png')))
b.clicked.connect(self.identifiers.paste_identifier)
b.setPopupMode(b.DelayedPopup)
b.setMenu(QMenu())
self.update_paste_identifiers_menu()
self.publisher = PublisherEdit(self)
self.basic_metadata_widgets.append(self.publisher)
self.timestamp = DateEdit(self)
self.pubdate = PubdateEdit(self)
self.basic_metadata_widgets.extend([self.timestamp, self.pubdate])
self.fetch_metadata_button = b = RightClickButton(self)
# The following rigmarole is needed so that Qt gives the button the
# same height as the other buttons in the dialog. There is no way to
# center the text in a QToolButton with an icon, so we cant just set an
# icon
b.setIcon(QIcon(I('download-metadata.png')))
b.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
b.setMinimumHeight(b.sizeHint().height())
b.setIcon(QIcon())
b.setText(_('&Download metadata')), b.setPopupMode(b.DelayedPopup)
b.setToolTip(_('Download metadata for this book [%s]') % self.download_shortcut.key().toString(QKeySequence.NativeText))
b.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed))
self.fetch_metadata_button.clicked.connect(self.fetch_metadata)
self.fetch_metadata_menu = m = QMenu(self.fetch_metadata_button)
m.addAction(QIcon(I('edit-undo.png')), _('Undo last metadata download'), self.undo_fetch_metadata)
self.fetch_metadata_button.setMenu(m)
self.download_shortcut.activated.connect(self.fetch_metadata_button.click)
font = self.fmb_font = QFont()
font.setBold(True)
self.fetch_metadata_button.setFont(font)
if self.use_toolbutton_for_config_metadata:
self.config_metadata_button = QToolButton(self)
self.config_metadata_button.setIcon(QIcon(I('config.png')))
else:
self.config_metadata_button = QPushButton(self)
self.config_metadata_button.setText(_('Configure download metadata'))
self.config_metadata_button.setIcon(QIcon(I('config.png')))
self.config_metadata_button.clicked.connect(self.configure_metadata)
self.config_metadata_button.setToolTip(
_('Change how calibre downloads metadata'))
# }}}
def update_paste_identifiers_menu(self):
m = self.paste_isbn_button.menu()
m.clear()
m.addAction(_('Edit list of prefixes'), self.edit_prefix_list)
m.addSeparator()
for prefix in gprefs['paste_isbn_prefixes'][1:]:
m.addAction(prefix, partial(self.identifiers.paste_prefix, prefix))
def edit_prefix_list(self):
prefixes, ok = QInputDialog.getMultiLineText(
self, _('Edit prefixes'), _('Enter prefixes, one on a line. The first prefix becomes the default.'),
'\n'.join(list(map(type(u''), gprefs['paste_isbn_prefixes']))))
if ok:
gprefs['paste_isbn_prefixes'] = list(filter(None, (x.strip() for x in prefixes.splitlines()))) or gprefs.defaults['paste_isbn_prefixes']
self.update_paste_identifiers_menu()
def create_custom_metadata_widgets(self): # {{{
self.custom_metadata_widgets_parent = w = QWidget(self)
layout = QGridLayout()
w.setLayout(layout)
self.custom_metadata_widgets, self.__cc_spacers = \
populate_metadata_page(layout, self.db, None, parent=w, bulk=False,
two_column=self.cc_two_column)
self.__custom_col_layouts = [layout]
for widget in self.custom_metadata_widgets:
if isinstance(widget, Comments):
self.comments_edit_state_at_apply[widget] = None
# }}}
def set_custom_metadata_tab_order(self, before=None, after=None): # {{{
sto = QWidget.setTabOrder
if getattr(self, 'custom_metadata_widgets', []):
ans = self.custom_metadata_widgets
for i in range(len(ans)-1):
if before is not None and i == 0:
pass
if len(ans[i+1].widgets) == 2:
sto(ans[i].widgets[-1], ans[i+1].widgets[1])
else:
sto(ans[i].widgets[-1], ans[i+1].widgets[0])
for c in range(2, len(ans[i].widgets), 2):
sto(ans[i].widgets[c-1], ans[i].widgets[c+1])
if after is not None:
pass
# }}}
def do_view_format(self, path, fmt):
if path:
self.view_format.emit(None, path)
else:
self.view_format.emit(self.book_id, fmt)
def copy_fmt(self, fmt, f):
self.db.copy_format_to(self.book_id, fmt, f, index_is_id=True)
def do_layout(self):
raise NotImplementedError()
def __call__(self, id_):
self.book_id = id_
self.books_to_refresh = set([])
self.metadata_before_fetch = None
for widget in self.basic_metadata_widgets:
widget.initialize(self.db, id_)
for widget in getattr(self, 'custom_metadata_widgets', []):
widget.initialize(id_)
if callable(self.set_current_callback):
self.set_current_callback(id_)
# Commented out as it doesn't play nice with Next, Prev buttons
# self.fetch_metadata_button.setFocus(Qt.OtherFocusReason)
# Miscellaneous interaction methods {{{
def update_window_title(self, *args):
title = self.title.current_val
if len(title) > 50:
title = title[:50] + u'\u2026'
self.setWindowTitle(BASE_TITLE + ' - ' +
title + ' - ' +
_(' [%(num)d of %(tot)d]')%dict(num=self.current_row+1,
tot=len(self.row_list)))
def swap_title_author(self, *args):
title = self.title.current_val
self.title.current_val = authors_to_string(self.authors.current_val)
self.authors.current_val = string_to_authors(title)
self.title_sort.auto_generate()
self.author_sort.auto_generate()
def tags_editor(self, *args):
self.tags.edit(self.db, self.book_id)
def metadata_from_format(self, *args):
mi, ext = self.formats_manager.get_selected_format_metadata(self.db,
self.book_id)
if mi is not None:
self.update_from_mi(mi)
def get_pdf_cover(self):
pdfpath = self.formats_manager.get_format_path(self.db, self.book_id,
'pdf')
from calibre.gui2.metadata.pdf_covers import PDFCovers
d = PDFCovers(pdfpath, parent=self)
if d.exec_() == d.Accepted:
cpath = d.cover_path
if cpath:
with open(cpath, 'rb') as f:
self.update_cover(f.read(), 'PDF')
d.cleanup()
def cover_from_format(self, *args):
ext = self.formats_manager.get_selected_format()
if ext is None:
return
if ext == 'pdf':
return self.get_pdf_cover()
try:
mi, ext = self.formats_manager.get_selected_format_metadata(self.db,
self.book_id)
except (IOError, OSError) as err:
if getattr(err, 'errno', None) == errno.EACCES: # Permission denied
import traceback
fname = err.filename if err.filename else 'file'
error_dialog(self, _('Permission denied'),
_('Could not open %s. Is it being used by another'
' program?')%fname, det_msg=traceback.format_exc(),
show=True)
return
raise
if mi is None:
return
cdata = None
if mi.cover and os.access(mi.cover, os.R_OK):
cdata = open(mi.cover).read()
elif mi.cover_data[1] is not None:
cdata = mi.cover_data[1]
if cdata is None:
error_dialog(self, _('Could not read cover'),
_('Could not read cover from %s format')%ext).exec_()
return
self.update_cover(cdata, ext)
def update_cover(self, cdata, fmt):
orig = self.cover.current_val
self.cover.current_val = cdata
if self.cover.current_val is None:
self.cover.current_val = orig
return error_dialog(self, _('Could not read cover'),
_('The cover in the %s format is invalid')%fmt,
show=True)
return
def update_from_mi(self, mi, update_sorts=True, merge_tags=True, merge_comments=False):
fw = self.focusWidget()
if not mi.is_null('title'):
self.title.set_value(mi.title)
if update_sorts:
self.title_sort.auto_generate()
if not mi.is_null('authors'):
self.authors.set_value(mi.authors)
if not mi.is_null('author_sort'):
self.author_sort.set_value(mi.author_sort)
elif update_sorts and not mi.is_null('authors'):
self.author_sort.auto_generate()
if not mi.is_null('rating'):
self.rating.set_value(mi.rating * 2)
if not mi.is_null('publisher'):
self.publisher.set_value(mi.publisher)
if not mi.is_null('tags'):
old_tags = self.tags.current_val
tags = mi.tags if mi.tags else []
if old_tags and merge_tags:
ltags, lotags = {t.lower() for t in tags}, {t.lower() for t in
old_tags}
tags = [t for t in tags if t.lower() in ltags-lotags] + old_tags
self.tags.set_value(tags)
if not mi.is_null('identifiers'):
current = self.identifiers.current_val
current.update(mi.identifiers)
self.identifiers.set_value(current)
if not mi.is_null('pubdate'):
self.pubdate.set_value(mi.pubdate)
if not mi.is_null('series') and mi.series.strip():
self.series.set_value(mi.series)
if mi.series_index is not None:
self.series_index.reset_original()
self.series_index.set_value(float(mi.series_index))
if not mi.is_null('languages'):
langs = [canonicalize_lang(x) for x in mi.languages]
langs = [x for x in langs if x is not None]
if langs:
self.languages.set_value(langs)
if mi.comments and mi.comments.strip():
val = mi.comments
if val and merge_comments:
cval = self.comments.current_val
if cval:
val = merge_two_comments(cval, val)
self.comments.set_value(val)
if fw is not None:
fw.setFocus(Qt.OtherFocusReason)
def fetch_metadata(self, *args):
from calibre.ebooks.metadata.sources.update import update_sources
update_sources()
d = FullFetch(self.cover.pixmap(), self)
ret = d.start(title=self.title.current_val, authors=self.authors.current_val,
identifiers=self.identifiers.current_val)
if ret == d.Accepted:
self.metadata_before_fetch = {f:getattr(self, f).current_val for f in fetched_fields}
from calibre.ebooks.metadata.sources.prefs import msprefs
mi = d.book
dummy = Metadata(_('Unknown'))
for f in msprefs['ignore_fields']:
if ':' not in f:
setattr(mi, f, getattr(dummy, f))
if mi is not None:
pd = mi.pubdate
if pd is not None:
# Put the downloaded published date into the local timezone
# as we discard time info and the date is timezone
# invariant. This prevents the as_local_timezone() call in
# update_from_mi from changing the pubdate
mi.pubdate = datetime(pd.year, pd.month, pd.day,
tzinfo=local_tz)
self.update_from_mi(mi, merge_comments=msprefs['append_comments'])
if d.cover_pixmap is not None:
self.metadata_before_fetch['cover'] = self.cover.current_val
self.cover.current_val = pixmap_to_data(d.cover_pixmap)
def undo_fetch_metadata(self):
if self.metadata_before_fetch is None:
return error_dialog(self, _('No downloaded metadata'), _(
'There is no downloaded metadata to undo'), show=True)
for field, val in self.metadata_before_fetch.iteritems():
getattr(self, field).current_val = val
self.metadata_before_fetch = None
def configure_metadata(self):
from calibre.gui2.preferences import show_config_widget
gui = self.parent()
show_config_widget('Sharing', 'Metadata download', parent=self,
gui=gui, never_shutdown=True)
def download_cover(self, *args):
from calibre.ebooks.metadata.sources.update import update_sources
update_sources()
from calibre.gui2.metadata.single_download import CoverFetch
d = CoverFetch(self.cover.pixmap(), self)
ret = d.start(self.title.current_val, self.authors.current_val,
self.identifiers.current_val)
if ret == d.Accepted:
if d.cover_pixmap is not None:
self.cover.current_val = pixmap_to_data(d.cover_pixmap)
# }}}
def to_book_metadata(self):
mi = Metadata(_('Unknown'))
if self.db is None:
return mi
mi.set_all_user_metadata(self.db.field_metadata.custom_field_metadata())
for widget in self.basic_metadata_widgets:
widget.apply_to_metadata(mi)
for widget in getattr(self, 'custom_metadata_widgets', []):
widget.apply_to_metadata(mi)
return mi
def apply_changes(self):
self.changed.add(self.book_id)
if self.db is None:
# break_cycles has already been called, don't know why this should
# happen but a user reported it
return True
self.comments_edit_state_at_apply = {w:w.tab for w in self.comments_edit_state_at_apply}
for widget in self.basic_metadata_widgets:
try:
if hasattr(widget, 'validate_for_commit'):
title, msg, det_msg = widget.validate_for_commit()
if title is not None:
error_dialog(self, title, msg, det_msg=det_msg, show=True)
return False
widget.commit(self.db, self.book_id)
self.books_to_refresh |= getattr(widget, 'books_to_refresh', set())
except (IOError, OSError) as err:
if getattr(err, 'errno', None) == errno.EACCES: # Permission denied
import traceback
fname = getattr(err, 'filename', None)
p = 'Locked file: %s\n\n'%fname if fname else ''
error_dialog(self, _('Permission denied'),
_('Could not change the on disk location of this'
' book. Is it open in another program?'),
det_msg=p+traceback.format_exc(), show=True)
return False
raise
for widget in getattr(self, 'custom_metadata_widgets', []):
self.books_to_refresh |= widget.commit(self.book_id)
self.db.commit()
rows = self.db.refresh_ids(list(self.books_to_refresh))
if rows:
self.rows_to_refresh |= set(rows)
return True
def accept(self):
self.save_state()
if not self.apply_changes():
return
if self.editing_multiple and self.current_row != len(self.row_list) - 1:
num = len(self.row_list) - 1 - self.current_row
from calibre.gui2 import question_dialog
pm = ngettext('There is another book to edit in this set.',
'There are still {} more books to edit in this set.', num).format(num)
if not question_dialog(
self, _('Are you sure?'), pm + _(
' Are you sure you want to stop? Use the "Next" button'
' instead of the "OK" button to move through books in the set.'),
yes_text=_('&Stop editing'), no_text=_('&Continue editing'),
yes_icon='dot_red.png', no_icon='dot_green.png',
default_yes=False, skip_dialog_name='edit-metadata-single-confirm-ok-on-multiple'):
return self.do_one(delta=1, apply_changes=False)
QDialog.accept(self)
def reject(self):
self.save_state()
QDialog.reject(self)
def save_state(self):
try:
gprefs['metasingle_window_geometry3'] = bytearray(self.saveGeometry())
except:
# Weird failure, see https://bugs.launchpad.net/bugs/995271
import traceback
traceback.print_exc()
# Dialog use methods {{{
def start(self, row_list, current_row, view_slot=None,
set_current_callback=None):
self.row_list = row_list
self.current_row = current_row
if view_slot is not None:
self.view_format.connect(view_slot)
self.set_current_callback = set_current_callback
self.do_one(apply_changes=False)
ret = self.exec_()
self.break_cycles()
return ret
def next_clicked(self):
if not self.apply_changes():
return
self.do_one(delta=1, apply_changes=False)
def prev_clicked(self):
if not self.apply_changes():
return
self.do_one(delta=-1, apply_changes=False)
def do_one(self, delta=0, apply_changes=True):
if apply_changes:
self.apply_changes()
self.current_row += delta
self.update_window_title()
prev = next_ = None
if self.current_row > 0:
prev = self.db.title(self.row_list[self.current_row-1])
if self.current_row < len(self.row_list) - 1:
next_ = self.db.title(self.row_list[self.current_row+1])
if next_ is not None:
tip = (_('Save changes and edit the metadata of %s')+
' [Alt+Right]')%next_
self.next_button.setToolTip(tip)
self.next_button.setEnabled(next_ is not None)
if prev is not None:
tip = (_('Save changes and edit the metadata of %s')+
' [Alt+Left]')%prev
self.prev_button.setToolTip(tip)
self.prev_button.setEnabled(prev is not None)
self.button_box.button(self.button_box.Ok).setDefault(True)
self.button_box.button(self.button_box.Ok).setFocus(Qt.OtherFocusReason)
self(self.db.id(self.row_list[self.current_row]))
for w, state in self.comments_edit_state_at_apply.iteritems():
if state == 'code':
w.tab = 'code'
def break_cycles(self):
# Break any reference cycles that could prevent python
# from garbage collecting this dialog
self.set_current_callback = self.db = None
self.metadata_before_fetch = None
def disconnect(signal):
try:
signal.disconnect()
except:
pass # Fails if view format was never connected
disconnect(self.view_format)
for b in ('next_button', 'prev_button'):
x = getattr(self, b, None)
if x is not None:
disconnect(x.clicked)
for widget in self.basic_metadata_widgets:
bc = getattr(widget, 'break_cycles', None)
if bc is not None and callable(bc):
bc()
for widget in getattr(self, 'custom_metadata_widgets', []):
widget.break_cycles()
# }}}
class Splitter(QSplitter):
frame_resized = pyqtSignal(object)
def resizeEvent(self, ev):
self.frame_resized.emit(ev)
return QSplitter.resizeEvent(self, ev)
class MetadataSingleDialog(MetadataSingleDialogBase): # {{{
def do_layout(self):
if len(self.db.custom_column_label_map) == 0:
self.central_widget.tabBar().setVisible(False)
self.central_widget.clear()
self.tabs = []
self.labels = []
self.tabs.append(QWidget(self))
self.central_widget.addTab(ScrollArea(self.tabs[0], self), _("&Basic metadata"))
self.tabs[0].l = l = QVBoxLayout()
self.tabs[0].tl = tl = QGridLayout()
self.tabs[0].setLayout(l)
w = getattr(self, 'custom_metadata_widgets_parent', None)
if w is not None:
self.tabs.append(w)
self.central_widget.addTab(ScrollArea(w, self), _('&Custom metadata'))
l.addLayout(tl)
l.addItem(QSpacerItem(10, 15, QSizePolicy.Expanding,
QSizePolicy.Fixed))
sto = QWidget.setTabOrder
sto(self.button_box, self.fetch_metadata_button)
sto(self.fetch_metadata_button, self.config_metadata_button)
sto(self.config_metadata_button, self.title)
def create_row(row, one, two, three, col=1, icon='forward.png'):
ql = BuddyLabel(one)
tl.addWidget(ql, row, col+0, 1, 1)
self.labels.append(ql)
tl.addWidget(one, row, col+1, 1, 1)
if two is not None:
tl.addWidget(two, row, col+2, 1, 1)
two.setIcon(QIcon(I(icon)))
ql = BuddyLabel(three)
tl.addWidget(ql, row, col+3, 1, 1)
self.labels.append(ql)
tl.addWidget(three, row, col+4, 1, 1)
sto(one, two)
sto(two, three)
tl.addWidget(self.swap_title_author_button, 0, 0, 1, 1)
tl.addWidget(self.manage_authors_button, 1, 0, 1, 1)
create_row(0, self.title, self.deduce_title_sort_button, self.title_sort)
sto(self.title_sort, self.authors)
create_row(1, self.authors, self.deduce_author_sort_button, self.author_sort)
sto(self.author_sort, self.series)
create_row(2, self.series, self.clear_series_button,
self.series_index, icon='trash.png')
sto(self.series_index, self.swap_title_author_button)
sto(self.swap_title_author_button, self.manage_authors_button)
tl.addWidget(self.formats_manager, 0, 6, 3, 1)
self.splitter = Splitter(Qt.Horizontal, self)
self.splitter.addWidget(self.cover)
self.splitter.frame_resized.connect(self.cover.frame_resized)
l.addWidget(self.splitter)
self.tabs[0].gb = gb = QGroupBox(_('Change cover'), self)
gb.l = l = QGridLayout()
gb.setLayout(l)
sto(self.manage_authors_button, self.cover.buttons[0])
for i, b in enumerate(self.cover.buttons[:3]):
l.addWidget(b, 0, i, 1, 1)
sto(b, self.cover.buttons[i+1])
gb.hl = QHBoxLayout()
for b in self.cover.buttons[3:]:
gb.hl.addWidget(b)
sto(self.cover.buttons[-2], self.cover.buttons[-1])
l.addLayout(gb.hl, 1, 0, 1, 3)
self.tabs[0].middle = w = QWidget(self)
w.l = l = QGridLayout()
w.setLayout(w.l)
self.splitter.addWidget(w)
def create_row2(row, widget, button=None, front_button=None):
row += 1
ql = BuddyLabel(widget)
if front_button:
ltl = QHBoxLayout()
ltl.addWidget(front_button)
ltl.addWidget(ql)
l.addLayout(ltl, row, 0, 1, 1)
else:
l.addWidget(ql, row, 0, 1, 1)
l.addWidget(widget, row, 1, 1, 2 if button is None else 1)
if button is not None:
l.addWidget(button, row, 2, 1, 1)
if button is not None:
sto(widget, button)
l.addWidget(gb, 0, 0, 1, 3)
self.tabs[0].spc_one = QSpacerItem(10, 10, QSizePolicy.Expanding,
QSizePolicy.Expanding)
l.addItem(self.tabs[0].spc_one, 1, 0, 1, 3)
sto(self.cover.buttons[-1], self.rating)
create_row2(1, self.rating, self.clear_ratings_button)
sto(self.rating, self.clear_ratings_button)
sto(self.clear_ratings_button, self.tags_editor_button)
sto(self.tags_editor_button, self.tags)
create_row2(2, self.tags, self.clear_tags_button, front_button=self.tags_editor_button)
sto(self.clear_tags_button, self.paste_isbn_button)
sto(self.paste_isbn_button, self.identifiers)
create_row2(3, self.identifiers, self.clear_identifiers_button,
front_button=self.paste_isbn_button)
sto(self.clear_identifiers_button, self.timestamp)
create_row2(4, self.timestamp, self.timestamp.clear_button)
sto(self.timestamp.clear_button, self.pubdate)
create_row2(5, self.pubdate, self.pubdate.clear_button)
sto(self.pubdate.clear_button, self.publisher)
create_row2(6, self.publisher, self.publisher.clear_button)
sto(self.publisher.clear_button, self.languages)
create_row2(7, self.languages)
self.tabs[0].spc_two = QSpacerItem(10, 10, QSizePolicy.Expanding,
QSizePolicy.Expanding)
l.addItem(self.tabs[0].spc_two, 9, 0, 1, 3)
l.addWidget(self.fetch_metadata_button, 10, 0, 1, 2)
l.addWidget(self.config_metadata_button, 10, 2, 1, 1)
self.tabs[0].gb2 = gb = QGroupBox(_('Comments'), self)
gb.l = l = QVBoxLayout()
gb.setLayout(l)
l.addWidget(self.comments)
self.splitter.addWidget(gb)
self.set_custom_metadata_tab_order()
# }}}
class DragTrackingWidget(QWidget): # {{{
def __init__(self, parent, on_drag_enter):
QWidget.__init__(self, parent)
self.on_drag_enter = on_drag_enter
def dragEnterEvent(self, ev):
self.on_drag_enter.emit()
# }}}
class MetadataSingleDialogAlt1(MetadataSingleDialogBase): # {{{
cc_two_column = False
one_line_comments_toolbar = True
use_toolbutton_for_config_metadata = False
on_drag_enter = pyqtSignal()
def handle_drag_enter(self):
self.central_widget.setCurrentIndex(1)
def do_layout(self):
self.central_widget.clear()
self.tabs = []
self.labels = []
sto = QWidget.setTabOrder
self.on_drag_enter.connect(self.handle_drag_enter)
self.tabs.append(DragTrackingWidget(self, self.on_drag_enter))
self.central_widget.addTab(ScrollArea(self.tabs[0], self), _("&Metadata"))
self.tabs[0].l = QGridLayout()
self.tabs[0].setLayout(self.tabs[0].l)
self.tabs.append(QWidget(self))
self.central_widget.addTab(ScrollArea(self.tabs[1], self), _("&Cover and formats"))
self.tabs[1].l = QGridLayout()
self.tabs[1].setLayout(self.tabs[1].l)
# accept drop events so we can automatically switch to the second tab to
# drop covers and formats
self.tabs[0].setAcceptDrops(True)
# Tab 0
tab0 = self.tabs[0]
tl = QGridLayout()
gb = QGroupBox(_('&Basic metadata'), self.tabs[0])
self.tabs[0].l.addWidget(gb, 0, 0, 1, 1)
gb.setLayout(tl)
self.button_box_layout.insertWidget(1, self.fetch_metadata_button)
self.button_box_layout.insertWidget(2, self.config_metadata_button)
sto(self.button_box, self.fetch_metadata_button)
sto(self.fetch_metadata_button, self.config_metadata_button)
sto(self.config_metadata_button, self.title)
def create_row(row, widget, tab_to, button=None, icon=None, span=1):
ql = BuddyLabel(widget)
tl.addWidget(ql, row, 1, 1, 1)
tl.addWidget(widget, row, 2, 1, 1)
if button is not None:
tl.addWidget(button, row, 3, span, 1)
if icon is not None:
button.setIcon(QIcon(I(icon)))
if tab_to is not None:
if button is not None:
sto(widget, button)
sto(button, tab_to)
else:
sto(widget, tab_to)
tl.addWidget(self.swap_title_author_button, 0, 0, 2, 1)
tl.addWidget(self.manage_authors_button, 2, 0, 1, 1)
tl.addWidget(self.paste_isbn_button, 12, 0, 1, 1)
tl.addWidget(self.tags_editor_button, 6, 0, 1, 1)
create_row(0, self.title, self.title_sort,
button=self.deduce_title_sort_button, span=2,
icon='auto_author_sort.png')
create_row(1, self.title_sort, self.authors)
create_row(2, self.authors, self.author_sort,
button=self.deduce_author_sort_button,
span=2, icon='auto_author_sort.png')
create_row(3, self.author_sort, self.series)
create_row(4, self.series, self.series_index,
button=self.clear_series_button, icon='trash.png')
create_row(5, self.series_index, self.tags)
create_row(6, self.tags, self.rating, button=self.clear_tags_button)
create_row(7, self.rating, self.pubdate, button=self.clear_ratings_button)
create_row(8, self.pubdate, self.publisher,
button=self.pubdate.clear_button, icon='trash.png')
create_row(9, self.publisher, self.languages, button=self.publisher.clear_button, icon='trash.png')
create_row(10, self.languages, self.timestamp)
create_row(11, self.timestamp, self.identifiers,
button=self.timestamp.clear_button, icon='trash.png')
create_row(12, self.identifiers, self.comments,
button=self.clear_identifiers_button, icon='trash.png')
sto(self.clear_identifiers_button, self.swap_title_author_button)
sto(self.swap_title_author_button, self.manage_authors_button)
sto(self.manage_authors_button, self.tags_editor_button)
sto(self.tags_editor_button, self.paste_isbn_button)
tl.addItem(QSpacerItem(1, 1, QSizePolicy.Fixed, QSizePolicy.Expanding),
13, 1, 1 ,1)
w = getattr(self, 'custom_metadata_widgets_parent', None)
if w is not None:
gb = QGroupBox(_('C&ustom metadata'), tab0)
gbl = QVBoxLayout()
gb.setLayout(gbl)
sr = QScrollArea(tab0)
sr.setWidgetResizable(True)
sr.setFrameStyle(QFrame.NoFrame)
sr.setWidget(w)
gbl.addWidget(sr)
self.tabs[0].l.addWidget(gb, 0, 1, 1, 1)
sto(self.identifiers, gb)
w = QGroupBox(_('&Comments'), tab0)
sp = QSizePolicy()
sp.setVerticalStretch(10)
sp.setHorizontalPolicy(QSizePolicy.Expanding)
sp.setVerticalPolicy(QSizePolicy.Expanding)
w.setSizePolicy(sp)
l = QHBoxLayout()
w.setLayout(l)
l.addWidget(self.comments)
tab0.l.addWidget(w, 1, 0, 1, 2)
# Tab 1
tab1 = self.tabs[1]
wsp = QWidget(tab1)
wgl = QVBoxLayout()
wsp.setLayout(wgl)
# right-hand side of splitter
gb = QGroupBox(_('Change cover'), tab1)
l = QGridLayout()
gb.setLayout(l)
for i, b in enumerate(self.cover.buttons[:3]):
l.addWidget(b, 0, i, 1, 1)
sto(b, self.cover.buttons[i+1])
hl = QHBoxLayout()
for b in self.cover.buttons[3:]:
hl.addWidget(b)
sto(self.cover.buttons[-2], self.cover.buttons[-1])
l.addLayout(hl, 1, 0, 1, 3)
wgl.addWidget(gb)
wgl.addItem(QSpacerItem(10, 10, QSizePolicy.Expanding,
QSizePolicy.Expanding))
wgl.addItem(QSpacerItem(10, 10, QSizePolicy.Expanding,
QSizePolicy.Expanding))
wgl.addWidget(self.formats_manager)
self.splitter = QSplitter(Qt.Horizontal, tab1)
tab1.l.addWidget(self.splitter)
self.splitter.addWidget(self.cover)
self.splitter.addWidget(wsp)
self.formats_manager.formats.setMaximumWidth(10000)
self.formats_manager.formats.setIconSize(QSize(64, 64))
# }}}
class MetadataSingleDialogAlt2(MetadataSingleDialogBase): # {{{
cc_two_column = False
one_line_comments_toolbar = True
use_toolbutton_for_config_metadata = False
def do_layout(self):
self.central_widget.clear()
self.labels = []
sto = QWidget.setTabOrder
self.central_widget.tabBar().setVisible(False)
tab0 = QWidget(self)
self.central_widget.addTab(ScrollArea(tab0, self), _("&Metadata"))
l = QGridLayout()
tab0.setLayout(l)
# Basic metadata in col 0
tl = QGridLayout()
gb = QGroupBox(_('Basic metadata'), tab0)
l.addWidget(gb, 0, 0, 1, 1)
gb.setLayout(tl)
self.button_box_layout.insertWidget(1, self.fetch_metadata_button)
self.button_box_layout.insertWidget(2, self.config_metadata_button)
sto(self.button_box, self.fetch_metadata_button)
sto(self.fetch_metadata_button, self.config_metadata_button)
sto(self.config_metadata_button, self.title)
def create_row(row, widget, tab_to, button=None, icon=None, span=1):
ql = BuddyLabel(widget)
tl.addWidget(ql, row, 1, 1, 1)
tl.addWidget(widget, row, 2, 1, 1)
if button is not None:
tl.addWidget(button, row, 3, span, 1)
if icon is not None:
button.setIcon(QIcon(I(icon)))
if tab_to is not None:
if button is not None:
sto(widget, button)
sto(button, tab_to)
else:
sto(widget, tab_to)
tl.addWidget(self.swap_title_author_button, 0, 0, 2, 1)
tl.addWidget(self.manage_authors_button, 2, 0, 2, 1)
tl.addWidget(self.paste_isbn_button, 12, 0, 1, 1)
tl.addWidget(self.tags_editor_button, 6, 0, 1, 1)
create_row(0, self.title, self.title_sort,
button=self.deduce_title_sort_button, span=2,
icon='auto_author_sort.png')
create_row(1, self.title_sort, self.authors)
create_row(2, self.authors, self.author_sort,
button=self.deduce_author_sort_button,
span=2, icon='auto_author_sort.png')
create_row(3, self.author_sort, self.series)
create_row(4, self.series, self.series_index,
button=self.clear_series_button, icon='trash.png')
create_row(5, self.series_index, self.tags)
create_row(6, self.tags, self.rating, button=self.clear_tags_button)
create_row(7, self.rating, self.pubdate, button=self.clear_ratings_button)
create_row(8, self.pubdate, self.publisher,
button=self.pubdate.clear_button, icon='trash.png')
create_row(9, self.publisher, self.languages,
button=self.publisher.clear_button, icon='trash.png')
create_row(10, self.languages, self.timestamp)
create_row(11, self.timestamp, self.identifiers,
button=self.timestamp.clear_button, icon='trash.png')
create_row(12, self.identifiers, self.comments,
button=self.clear_identifiers_button, icon='trash.png')
sto(self.clear_identifiers_button, self.swap_title_author_button)
sto(self.swap_title_author_button, self.manage_authors_button)
sto(self.manage_authors_button, self.tags_editor_button)
sto(self.tags_editor_button, self.paste_isbn_button)
tl.addItem(QSpacerItem(1, 1, QSizePolicy.Fixed, QSizePolicy.Expanding),
13, 1, 1 ,1)
# Custom metadata in col 1
w = getattr(self, 'custom_metadata_widgets_parent', None)
if w is not None:
gb = QGroupBox(_('Custom metadata'), tab0)
gbl = QVBoxLayout()
gb.setLayout(gbl)
sr = QScrollArea(gb)
sr.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
sr.setWidgetResizable(True)
sr.setFrameStyle(QFrame.NoFrame)
sr.setWidget(w)
gbl.addWidget(sr)
l.addWidget(gb, 0, 1, 1, 1)
sp = QSizePolicy()
sp.setVerticalStretch(10)
sp.setHorizontalPolicy(QSizePolicy.Minimum)
sp.setVerticalPolicy(QSizePolicy.Expanding)
gb.setSizePolicy(sp)
self.set_custom_metadata_tab_order()
# comments span col 0 & 1
w = QGroupBox(_('Comments'), tab0)
sp = QSizePolicy()
sp.setVerticalStretch(10)
sp.setHorizontalPolicy(QSizePolicy.Expanding)
sp.setVerticalPolicy(QSizePolicy.Expanding)
w.setSizePolicy(sp)
lb = QHBoxLayout()
w.setLayout(lb)
lb.addWidget(self.comments)
l.addWidget(w, 1, 0, 1, 2)
# Cover & formats in col 3
gb = QGroupBox(_('Cover'), tab0)
lb = QGridLayout()
gb.setLayout(lb)
lb.addWidget(self.cover, 0, 0, 1, 3, alignment=Qt.AlignCenter)
sto(self.manage_authors_button, self.cover.buttons[0])
for i, b in enumerate(self.cover.buttons[:3]):
lb.addWidget(b, 1, i, 1, 1)
sto(b, self.cover.buttons[i+1])
hl = QHBoxLayout()
for b in self.cover.buttons[3:]:
hl.addWidget(b)
sto(self.cover.buttons[-2], self.cover.buttons[-1])
lb.addLayout(hl, 2, 0, 1, 3)
l.addWidget(gb, 0, 2, 1, 1)
l.addWidget(self.formats_manager, 1, 2, 1, 1)
sto(self.cover.buttons[-1], self.formats_manager)
self.formats_manager.formats.setMaximumWidth(10000)
self.formats_manager.formats.setIconSize(QSize(32, 32))
# }}}
editors = {'default': MetadataSingleDialog, 'alt1': MetadataSingleDialogAlt1,
'alt2': MetadataSingleDialogAlt2}
def edit_metadata(db, row_list, current_row, parent=None, view_slot=None,
set_current_callback=None, editing_multiple=False):
cls = gprefs.get('edit_metadata_single_layout', '')
if cls not in editors:
cls = 'default'
d = editors[cls](db, parent, editing_multiple=editing_multiple)
try:
d.start(row_list, current_row, view_slot=view_slot,
set_current_callback=set_current_callback)
return d.changed, d.rows_to_refresh
finally:
# possible workaround for bug reports of occasional ghost edit metadata dialog on windows
d.deleteLater()
if __name__ == '__main__':
from calibre.gui2 import Application as QApplication
app = QApplication([])
from calibre.library import db
db = db()
row_list = list(range(len(db.data)))
edit_metadata(db, row_list, 0)