diff --git a/manual/gui.rst b/manual/gui.rst index 98954ebabd..b7cd4230bf 100755 --- a/manual/gui.rst +++ b/manual/gui.rst @@ -531,6 +531,8 @@ Calibre has several keyboard shortcuts to save you time and mouse movement. Thes - Get Books * - :kbd:`I` - Show book details + * - :kbd:`K` + - Edit Table of Contents * - :kbd:`M` - Merge selected records * - :kbd:`Alt+M` diff --git a/resources/images/toc.png b/resources/images/toc.png new file mode 100644 index 0000000000..67252a4ae6 Binary files /dev/null and b/resources/images/toc.png differ diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index cf12f42d34..8e3b3c59b1 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -789,6 +789,11 @@ class ActionPolish(InterfaceActionBase): actual_plugin = 'calibre.gui2.actions.polish:PolishAction' description = _('Fine tune your ebooks') +class ActionEditToC(InterfaceActionBase): + name = 'Edit ToC' + actual_plugin = 'calibre.gui2.actions.toc_edit:ToCEditAction' + description = _('Edit the Table of Contents in your books') + class ActionDelete(InterfaceActionBase): name = 'Remove Books' actual_plugin = 'calibre.gui2.actions.delete:DeleteAction' @@ -929,7 +934,7 @@ plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog, ActionSendToDevice, ActionHelp, ActionPreferences, ActionSimilarBooks, ActionAddToLibrary, ActionEditCollections, ActionChooseLibrary, ActionCopyToLibrary, ActionTweakEpub, ActionNextMatch, ActionStore, - ActionPluginUpdater, ActionPickRandom] + ActionPluginUpdater, ActionPickRandom, ActionEditToC] # }}} diff --git a/src/calibre/gui2/actions/toc_edit.py b/src/calibre/gui2/actions/toc_edit.py new file mode 100644 index 0000000000..b22fdaba21 --- /dev/null +++ b/src/calibre/gui2/actions/toc_edit.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2013, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +from collections import OrderedDict + +from PyQt4.Qt import (QTimer, QDialog, QGridLayout, QCheckBox, QLabel, + QDialogButtonBox, QIcon) + +from calibre.gui2 import error_dialog +from calibre.gui2.actions import InterfaceAction + +SUPPORTED = {'EPUB', 'AZW3'} + +class ChooseFormat(QDialog): # {{{ + + def __init__(self, formats, parent=None): + QDialog.__init__(self, parent) + self.setWindowTitle(_('Choose format to edit')) + self.setWindowIcon(QIcon(I('dialog_question.png'))) + l = self.l = QGridLayout() + self.setLayout(l) + la = self.la = QLabel(_('Choose which format you want to edit:')) + formats = sorted(formats) + l.addWidget(la, 0, 0, 1, -1) + self.buttons = [] + for i, f in enumerate(formats): + b = QCheckBox('&' + f, self) + l.addWidget(b, 1, i) + self.buttons.append(b) + if i == 0: + b.setChecked(True) + bb = self.bb = QDialogButtonBox( + QDialogButtonBox.Ok|QDialogButtonBox.Cancel) + bb.addButton(_('&All formats'), + bb.ActionRole).clicked.connect(self.do_all) + bb.accepted.connect(self.accept) + bb.rejected.connect(self.reject) + l.addWidget(bb, l.rowCount(), 0, 1, -1) + self.resize(self.sizeHint()) + + def do_all(self): + for b in self.buttons: + b.setChecked(True) + self.accept() + + @property + def formats(self): + for b in self.buttons: + if b.isChecked(): + yield unicode(b.text())[1:] +# }}} + +class ToCEditAction(InterfaceAction): + + name = 'Edit ToC' + action_spec = (_('Edit ToC'), 'toc.png', + _('Edit the Table of Contents in your books'), _('K')) + dont_add_to = frozenset(['context-menu-device']) + action_type = 'current' + accepts_drops = True + + def accept_enter_event(self, event, mime_data): + if mime_data.hasFormat("application/calibre+from_library"): + return True + return False + + def accept_drag_move_event(self, event, mime_data): + if mime_data.hasFormat("application/calibre+from_library"): + return True + return False + + def drop_event(self, event, mime_data): + mime = 'application/calibre+from_library' + if mime_data.hasFormat(mime): + self.dropped_ids = tuple(map(int, str(mime_data.data(mime)).split())) + QTimer.singleShot(1, self.do_drop) + return True + return False + + def do_drop(self): + book_id_map = self.get_supported_books(self.dropped_ids) + del self.dropped_ids + if book_id_map: + self.do_edit(book_id_map) + + def genesis(self): + self.qaction.triggered.connect(self.edit_books) + + def get_supported_books(self, book_ids): + db = self.gui.library_view.model().db + supported = set(SUPPORTED) + ans = [(x, set( (db.formats(x, index_is_id=True) or '').split(',') ) + .intersection(supported)) for x in book_ids] + ans = [x for x in ans if x[1]] + if not ans: + error_dialog(self.gui, _('Cannot edit ToC'), + _('Editing Table of Contents is only supported for books in the %s' + ' formats. Convert to one of those formats before polishing.') + %_(' or ').join(sorted(supported)), show=True) + ans = OrderedDict(ans) + return ans + + def get_books_for_editing(self): + rows = [r.row() for r in + self.gui.library_view.selectionModel().selectedRows()] + if not rows or len(rows) == 0: + d = error_dialog(self.gui, _('Cannot edit ToC'), + _('No books selected')) + d.exec_() + return None + db = self.gui.current_db + ans = (db.id(r) for r in rows) + return self.get_supported_books(ans) + + def do_edit(self, book_id_map): + for book_id, fmts in book_id_map.iteritems(): + if len(fmts) > 1: + d = ChooseFormat(fmts, self.gui) + if d.exec_() != d.Accepted: + return + fmts = d.formats + for fmt in fmts: + self.do_one(book_id, fmt) + + def do_one(self, book_id, fmt): + from calibre.gui2.toc.main import TOCEditor + db = self.gui.current_db + path = db.format(book_id, fmt, index_is_id=True, as_path=True) + title = db.title(book_id, index_is_id=True) + ' [%s]'%fmt + d = TOCEditor(path, title=title, parent=self.gui) + d.start() + if d.exec_() == d.Accepted: + with open(path, 'rb') as f: + db.add_format(book_id, fmt, f, index_is_id=True) + + def edit_books(self): + book_id_map = self.get_books_for_editing() + if not book_id_map: + return + self.do_edit(book_id_map) + + diff --git a/src/calibre/gui2/toc/location.py b/src/calibre/gui2/toc/location.py index 31e651b88f..ec10ec7c5f 100644 --- a/src/calibre/gui2/toc/location.py +++ b/src/calibre/gui2/toc/location.py @@ -37,6 +37,7 @@ class Page(QWebPage): # {{{ def javaScriptAlert(self, frame, msg): self.log(unicode(msg)) + @pyqtSlot(result=bool) def shouldInterruptJavaScript(self): return True diff --git a/src/calibre/gui2/toc/main.py b/src/calibre/gui2/toc/main.py index 2db710eff1..876b6d9947 100644 --- a/src/calibre/gui2/toc/main.py +++ b/src/calibre/gui2/toc/main.py @@ -425,6 +425,7 @@ class TOCEditor(QDialog): # {{{ def __init__(self, pathtobook, title=None, parent=None): QDialog.__init__(self, parent) self.pathtobook = pathtobook + self.working = True t = title or os.path.basename(pathtobook) self.setWindowTitle(_('Edit the ToC in %s')%t) @@ -444,6 +445,7 @@ class TOCEditor(QDialog): # {{{ pi.startAnimation() ll.addWidget(pi, alignment=Qt.AlignHCenter|Qt.AlignCenter) la = self.la = QLabel(_('Loading %s, please wait...')%t) + la.setWordWrap(True) la.setStyleSheet('QLabel { font-size: 20pt }') ll.addWidget(la, alignment=Qt.AlignHCenter|Qt.AlignTop) self.toc_view = TOCView(self) @@ -460,7 +462,6 @@ class TOCEditor(QDialog): # {{{ self.explode_done.connect(self.read_toc, type=Qt.QueuedConnection) self.resize(950, 630) - self.working = True def add_new_item(self, item, where): self.item_edit(item, where) @@ -491,6 +492,7 @@ class TOCEditor(QDialog): # {{{ def explode(self): self.ebook = get_container(self.pathtobook, log=self.log) if self.working: + self.working = False self.explode_done.emit() def read_toc(self):