diff --git a/src/calibre/gui2/actions/edit_metadata.py b/src/calibre/gui2/actions/edit_metadata.py
index 41645f11f2..79c4fbceff 100644
--- a/src/calibre/gui2/actions/edit_metadata.py
+++ b/src/calibre/gui2/actions/edit_metadata.py
@@ -480,6 +480,33 @@ class EditMetadataAction(InterfaceAction):
self.gui.library_view.select_rows(book_ids)
# Merge books {{{
+
+ def confirm_large_merge(self, num):
+ if num < 5:
+ return True
+ return confirm('
'+_(
+ 'You are about to merge very many ({}) books. '
+ 'Are you sure you want to proceed?').format(num) + '
',
+ 'merge_too_many_books', self.gui)
+
+ def books_dropped(self, merge_map):
+ for dest_id, src_ids in merge_map.iteritems():
+ if not self.confirm_large_merge(len(src_ids) + 1):
+ continue
+ from calibre.gui2.dialogs.confirm_merge import merge_drop
+ merge_metadata, merge_formats, delete_books = merge_drop(dest_id, src_ids, self.gui)
+ if merge_metadata is None:
+ return
+ if merge_formats:
+ self.add_formats(dest_id, self.formats_for_ids(list(src_ids)))
+ if merge_metadata:
+ self.merge_metadata(dest_id, src_ids)
+ if delete_books:
+ self.delete_books_after_merge(src_ids)
+ # leave the selection highlight on the target book
+ row = self.gui.library_view.ids_to_rows([dest_id])[dest_id]
+ self.gui.library_view.set_current_row(row)
+
def merge_books(self, safe_merge=False, merge_only_formats=False):
'''
Merge selected books in library.
@@ -495,12 +522,8 @@ class EditMetadataAction(InterfaceAction):
return error_dialog(self.gui, _('Cannot merge books'),
_('At least two books must be selected for merging'),
show=True)
- if len(rows) > 5:
- if not confirm(''+_(
- 'You are about to merge more than 5 books. '
- 'Are you sure you want to proceed?') + '
',
- 'merge_too_many_books', self.gui):
- return
+ if not self.confirm_large_merge(len(rows)):
+ return
dest_id, src_ids = self.books_to_merge(rows)
mi = self.gui.current_db.new_api.get_proxy_metadata(dest_id)
@@ -566,10 +589,10 @@ class EditMetadataAction(InterfaceAction):
self.gui.library_view.model().db.add_format(dest_id, fmt, f, index_is_id=True,
notify=False, replace=replace)
- def formats_for_books(self, rows):
+ def formats_for_ids(self, ids):
m = self.gui.library_view.model()
ans = []
- for id_ in map(m.id, rows):
+ for id_ in ids:
dbfmts = m.db.formats(id_, index_is_id=True)
if dbfmts:
for fmt in dbfmts.split(','):
@@ -581,6 +604,10 @@ class EditMetadataAction(InterfaceAction):
continue
return ans
+ def formats_for_books(self, rows):
+ m = self.gui.library_view.model()
+ return self.formats_for_ids(map(m.id, rows))
+
def books_to_merge(self, rows):
src_ids = []
m = self.gui.library_view.model()
diff --git a/src/calibre/gui2/dialogs/confirm_merge.py b/src/calibre/gui2/dialogs/confirm_merge.py
index 6984c57ba1..cd1a77d82a 100644
--- a/src/calibre/gui2/dialogs/confirm_merge.py
+++ b/src/calibre/gui2/dialogs/confirm_merge.py
@@ -6,19 +6,48 @@ from __future__ import (unicode_literals, division, absolute_import,
__license__ = 'GPL v3'
__copyright__ = '2015, Kovid Goyal '
+from functools import partial
from PyQt5.Qt import (
- QVBoxLayout, QSplitter, QWidget, QLabel, QCheckBox, QTextBrowser
+ QVBoxLayout, QSplitter, QWidget, QLabel, QCheckBox, QTextBrowser, Qt,
)
from calibre.ebooks.metadata import authors_to_string
from calibre.ebooks.metadata.book.base import field_metadata
-from calibre.gui2 import dynamic
+from calibre.gui2 import dynamic, gprefs
from calibre.gui2.widgets2 import Dialog
from calibre.gui2.dialogs.confirm_delete import confirm_config_name
from calibre.utils.config import tweaks
from calibre.utils.date import format_date
+class Target(QTextBrowser):
+
+ def __init__(self, mi, parent=None):
+ QTextBrowser.__init__(self, parent)
+ series = ''
+ fm = field_metadata
+ if mi.series:
+ series = _('{num} of {series}').format(num=mi.format_series_index(), series='%s' % mi.series)
+ self.setHtml('''
+{mb}
+{title} - {authors}
+
+{fm[timestamp][name]}: | {date} |
+{fm[pubdate][name]}: | {published} |
+{fm[formats][name]}: | {formats} |
+{fm[series][name]}: | {series} |
+
+ '''.format(
+ mb=_('Target book'),
+ title=mi.title,
+ authors=authors_to_string(mi.authors),
+ date=format_date(mi.timestamp, tweaks['gui_timestamp_display_format']), fm=fm,
+ published=(format_date(mi.pubdate, tweaks['gui_pubdate_display_format']) if mi.pubdate else ''),
+ formats=', '.join(mi.formats or ()),
+ series=series
+ ))
+
+
class ConfirmMerge(Dialog):
def __init__(self, msg, name, parent, mi):
@@ -44,29 +73,7 @@ class ConfirmMerge(Dialog):
c.stateChanged.connect(self.toggle)
l.addWidget(c)
- self.right = r = QTextBrowser(self)
- series = ''
- mi, fm = self.mi, field_metadata
- if mi.series:
- series = _('{num} of {series}').format(num=mi.format_series_index(), series='%s' % mi.series)
- r.setHtml('''
-{mb}
-{title} - {authors}
-
-{fm[timestamp][name]}: | {date} |
-{fm[pubdate][name]}: | {published} |
-{fm[formats][name]}: | {formats} |
-{fm[series][name]}: | {series} |
-
- '''.format(
- mb=_('Target book'),
- title=mi.title,
- authors=authors_to_string(mi.authors),
- date=format_date(mi.timestamp, tweaks['gui_timestamp_display_format']), fm=fm,
- published=(format_date(mi.pubdate, tweaks['gui_pubdate_display_format']) if mi.pubdate else ''),
- formats=', '.join(mi.formats or ()),
- series=series
- ))
+ self.right = r = Target(self.mi, self)
s.addWidget(r)
def toggle(self):
@@ -84,3 +91,103 @@ def confirm_merge(msg, name, parent, mi):
return True
d = ConfirmMerge(msg, name, parent, mi)
return d.exec_() == d.Accepted
+
+
+class ChooseMerge(Dialog):
+
+ def __init__(self, dest_id, src_ids, gui):
+ self.dest_id, self.src_ids = dest_id, src_ids
+ self.mi = gui.current_db.new_api.get_metadata(dest_id)
+ Dialog.__init__(self, _('Merge books'), 'choose-merge-dialog', parent=gui)
+
+ def setup_ui(self):
+ self.l = l = QVBoxLayout(self)
+ self.splitter = s = QSplitter(self)
+ s.setChildrenCollapsible(False)
+ l.addWidget(s), l.addWidget(self.bb)
+ self.bb.setStandardButtons(self.bb.Yes | self.bb.No)
+ self.left = w = QWidget(self)
+ s.addWidget(w)
+ w.l = l = QVBoxLayout(w)
+ l.setContentsMargins(0, 0, 0, 0)
+
+ def cb(name, text, tt=''):
+ ans = QCheckBox(text)
+ l.addWidget(ans)
+ prefs_key = ans.prefs_key = 'choose-merge-cb-' + name
+ ans.setChecked(gprefs.get(prefs_key, True))
+ ans.stateChanged.connect(partial(self.state_changed, ans), type=Qt.QueuedConnection)
+ if tt:
+ ans.setToolTip(tt)
+ setattr(self, name, ans)
+ return ans
+
+ cb('merge_metadata', _('Merge metadata'), _(
+ 'Merge the metadata of the selected books into the target book'))
+ cb('merge_formats', _('Merge formats'), _(
+ 'Merge the book files of the selected books into the target book'))
+ cb('delete_books', _('Delete merged books'), _(
+ 'Delete the selected books after merging'))
+ l.addStretch(10)
+ self.msg = la = QLabel(self)
+ la.setWordWrap(True)
+ l.addWidget(la)
+ self.update_msg()
+
+ self.right = r = Target(self.mi, self)
+ s.addWidget(r)
+
+ def state_changed(self, cb, state):
+ mm = self.merge_metadata.isChecked()
+ mf = self.merge_formats.isChecked()
+ if not mm and not mf:
+ (self.merge_metadata if cb is self.merge_formats else self.merge_formats).setChecked(True)
+ gprefs[cb.prefs_key] = cb.isChecked()
+ self.update_msg()
+
+ def update_msg(self):
+ mm = self.merge_metadata.isChecked()
+ mf = self.merge_formats.isChecked()
+ rm = self.delete_books.isChecked()
+ msg = ''
+ if mm and mf:
+ msg += _(
+ 'Book formats and metadata from the selected books'
+ ' will be merged into the target book ({title}).')
+ elif mf:
+ msg += _('Book formats from the selected books '
+ 'will be merged into to the target book ({title}).'
+ ' Metadata in the target book will not be changed.')
+ elif mm:
+ msg += _('Metadata from the selected books '
+ 'will be merged into to the target book ({title}).'
+ ' Formats will not be merged.')
+ msg += '
'
+ msg += _('All book formats of the first selected book will be kept.') + '
'
+ if rm:
+ msg += _('After being merged, the selected books will be deleted.')
+ if mf:
+ msg += '
' + _(
+ 'Any duplicate formats in the selected books '
+ 'will be permanently deleted from your calibre library.')
+ else:
+ if mf:
+ msg += _(
+ 'Any formats not in the target book will be added to it from the selected books.')
+ if not msg.endswith('
'):
+ msg += '
'
+
+ msg += _('Are you sure you want to proceed?') + '
'
+ msg = msg.format(title=self.mi.title)
+ self.msg.setText(msg)
+
+ @property
+ def merge_type(self):
+ return self.merge_metadata.isChecked(), self.merge_formats.isChecked(), self.delete_books.isChecked()
+
+
+def merge_drop(dest_id, src_ids, gui):
+ d = ChooseMerge(dest_id, src_ids, gui)
+ if d.exec_() != d.Accepted:
+ return None, None, None
+ return d.merge_type
diff --git a/src/calibre/gui2/init.py b/src/calibre/gui2/init.py
index ad2f5bd397..6b0694abf8 100644
--- a/src/calibre/gui2/init.py
+++ b/src/calibre/gui2/init.py
@@ -41,6 +41,7 @@ class LibraryViewMixin(object): # {{{
def init_library_view_mixin(self, db):
self.library_view.files_dropped.connect(self.iactions['Add Books'].files_dropped, type=Qt.QueuedConnection)
+ self.library_view.books_dropped.connect(self.iactions['Edit Metadata'].books_dropped, type=Qt.QueuedConnection)
self.library_view.add_column_signal.connect(partial(self.iactions['Preferences'].do_config,
initial_plugin=('Interface', 'Custom Columns')),
type=Qt.QueuedConnection)
diff --git a/src/calibre/gui2/library/alternate_views.py b/src/calibre/gui2/library/alternate_views.py
index 6427026459..83f45a0136 100644
--- a/src/calibre/gui2/library/alternate_views.py
+++ b/src/calibre/gui2/library/alternate_views.py
@@ -206,12 +206,24 @@ def dragEnterEvent(self, event):
int(event.possibleActions() & Qt.MoveAction) == 0:
return
paths = self.paths_from_event(event)
+ md = event.mimeData()
- if paths:
+ if paths or md.hasFormat('application/calibre+from_library'):
event.acceptProposedAction()
def dropEvent(self, event):
+ md = event.mimeData()
+ if md.hasFormat('application/calibre+from_library'):
+ ids = set(map(int, bytes(md.data('application/calibre+from_library')).decode('utf-8').split(' ')))
+ row = self.indexAt(event.pos()).row()
+ if row > -1 and ids:
+ book_id = self.model().id(row)
+ if book_id:
+ self.books_dropped.emit({book_id: ids})
+ event.setDropAction(Qt.CopyAction)
+ event.accept()
+ return
paths = self.paths_from_event(event)
event.setDropAction(Qt.CopyAction)
event.accept()
@@ -291,6 +303,7 @@ class AlternateViews(object):
view.selectionModel().currentChanged.connect(self.slave_current_changed)
view.selectionModel().selectionChanged.connect(self.slave_selection_changed)
view.files_dropped.connect(self.main_view.files_dropped)
+ view.books_dropped.connect(self.main_view.books_dropped)
def show_view(self, key=None):
view = self.views[key]
@@ -663,6 +676,7 @@ class GridView(QListView):
update_item = pyqtSignal(object)
files_dropped = pyqtSignal(object)
+ books_dropped = pyqtSignal(object)
def __init__(self, parent):
QListView.__init__(self, parent)
diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py
index 2a02e4a108..464be2081b 100644
--- a/src/calibre/gui2/library/views.py
+++ b/src/calibre/gui2/library/views.py
@@ -204,6 +204,7 @@ class PreserveViewState(object): # {{{
class BooksView(QTableView): # {{{
files_dropped = pyqtSignal(object)
+ books_dropped = pyqtSignal(object)
add_column_signal = pyqtSignal()
is_library_view = True