From df1c8b7e56f9471e6dbbfd1f6dd3535366ff4d6b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 1 Dec 2013 14:35:24 +0530 Subject: [PATCH] Integrate the new Tweak Book tool into the main calibre gui The old Tweak Book tool has become "Unpack Book" --- resources/images/unpack-book.png | Bin 0 -> 8770 bytes src/calibre/customize/builtins.py | 7 +- src/calibre/db/cache.py | 4 +- src/calibre/gui2/actions/tweak_epub.py | 341 +++++------------------ src/calibre/gui2/actions/unpack_book.py | 348 ++++++++++++++++++++++++ src/calibre/gui2/tweak_book/__init__.py | 2 + src/calibre/utils/ipc/worker.py | 4 +- 7 files changed, 425 insertions(+), 281 deletions(-) create mode 100644 resources/images/unpack-book.png create mode 100644 src/calibre/gui2/actions/unpack_book.py diff --git a/resources/images/unpack-book.png b/resources/images/unpack-book.png new file mode 100644 index 0000000000000000000000000000000000000000..c7540bb5cfdf508df54370538f0e6cb93749599a GIT binary patch literal 8770 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 1) + def accept(self): + from calibre.gui2.tweak_book import tprefs + tprefs['choose_tweak_fmt'] = self.rem.isChecked() + QDialog.accept(self) - self.help_label = QLabel(_('''\ -

About Tweak Book

-

Tweak Book allows you to fine tune the appearance of an ebook by - making small changes to its internals. In order to use Tweak Book, - you need to know a little bit about HTML and CSS, technologies that - are used in ebooks. Follow the steps:

-
-
    -
  1. Click "Explode Book": This will "explode" the book into its - individual internal components.
  2. -
  3. Right click on any individual file and select "Open with..." to - edit it in your favorite text editor.
  4. -
  5. When you are done Tweaking: close the file browser window - and the editor windows you used to make your tweaks. Then click - the "Rebuild Book" button, to update the book in your calibre - library.
  6. -
''')) - self.help_label.setWordWrap(True) - self._fr = QFrame() - self._fr.setFrameShape(QFrame.VLine) - g.addWidget(self._fr) - g.addWidget(self.help_label) - - self._b = b = QGridLayout() - left, top, right, bottom = b.getContentsMargins() - top += top - b.setContentsMargins(left, top, right, bottom) - l.addLayout(b, stretch=10) - - self.explode_button = QPushButton(QIcon(I('wizard.png')), _('&Explode Book')) - self.preview_button = QPushButton(QIcon(I('view.png')), _('&Preview Book')) - self.cancel_button = QPushButton(QIcon(I('window-close.png')), _('&Cancel')) - self.rebuild_button = QPushButton(QIcon(I('exec.png')), _('&Rebuild Book')) - - self.explode_button.setToolTip( - _('Explode the book to edit its components')) - self.preview_button.setToolTip( - _('Preview the result of your tweaks')) - self.cancel_button.setToolTip( - _('Abort without saving any changes')) - self.rebuild_button.setToolTip( - _('Save your changes and update the book in the calibre library')) - - a = b.addWidget - a(self.explode_button, 0, 0, 1, 1) - a(self.preview_button, 0, 1, 1, 1) - a(self.cancel_button, 1, 0, 1, 1) - a(self.rebuild_button, 1, 1, 1, 1) - - for x in ('explode', 'preview', 'cancel', 'rebuild'): - getattr(self, x+'_button').clicked.connect(getattr(self, x)) - - self.msg = QLabel('dummy', self) - self.msg.setVisible(False) - self.msg.setStyleSheet(''' - QLabel { - text-align: center; - background-color: white; - color: black; - border-width: 1px; - border-style: solid; - border-radius: 20px; - font-size: x-large; - font-weight: bold; - } - ''') - - self.resize(self.sizeHint() + QSize(40, 10)) - # }}} - - def show_msg(self, msg): - self.msg.setText(msg) - self.msg.resize(self.size() - QSize(50, 25)) - self.msg.move((self.width() - self.msg.width())//2, - (self.height() - self.msg.height())//2) - self.msg.setVisible(True) - - def hide_msg(self): - self.msg.setVisible(False) - - def explode(self): - self.show_msg(_('Exploding, please wait...')) - if len(self.fmt_choice_buttons) > 1: - gprefs.set('last_tweak_format', self.current_format.upper()) - QTimer.singleShot(5, self.do_explode) - - def ask_question(self, msg): - return question_dialog(self, _('Are you sure?'), msg) - - def do_explode(self): - from calibre.ebooks.tweak import get_tools, Error, WorkerError - tdir = PersistentTemporaryDirectory('_tweak_explode') - self._cleanup_dirs.append(tdir) - det_msg = None - try: - src = self.db.format(self.book_id, self.current_format, - index_is_id=True, as_path=True) - self._cleanup_files.append(src) - exploder = get_tools(self.current_format)[0] - opf = exploder(src, tdir, question=self.ask_question) - except WorkerError as e: - det_msg = e.orig_tb - except Error as e: - return error_dialog(self, _('Failed to unpack'), - (_('Could not explode the %s file.')%self.current_format) + ' ' - + as_unicode(e), show=True) - except: - import traceback - det_msg = traceback.format_exc() - finally: - self.hide_msg() - - if det_msg is not None: - return error_dialog(self, _('Failed to unpack'), - _('Could not explode the %s file. Click "Show Details" for ' - 'more information.')%self.current_format, det_msg=det_msg, - show=True) - - if opf is None: - # The question was answered with No - return - - self._exploded = tdir - self.explode_button.setEnabled(False) - self.preview_button.setEnabled(True) - self.rebuild_button.setEnabled(True) - open_local_file(tdir) - - def rebuild_it(self): - from calibre.ebooks.tweak import get_tools, WorkerError - src_dir = self._exploded - det_msg = None - of = PersistentTemporaryFile('_tweak_rebuild.'+self.current_format.lower()) - of.close() - of = of.name - self._cleanup_files.append(of) - try: - rebuilder = get_tools(self.current_format)[1] - rebuilder(src_dir, of) - except WorkerError as e: - det_msg = e.orig_tb - except: - import traceback - det_msg = traceback.format_exc() - finally: - self.hide_msg() - - if det_msg is not None: - error_dialog(self, _('Failed to rebuild file'), - _('Failed to rebuild %s. For more information, click ' - '"Show details".')%self.current_format, - det_msg=det_msg, show=True) - return None - - return of - - def preview(self): - self.show_msg(_('Rebuilding, please wait...')) - QTimer.singleShot(5, self.do_preview) - - def do_preview(self): - rebuilt = self.rebuild_it() - if rebuilt is not None: - self.parent().iactions['View']._view_file(rebuilt) - - def rebuild(self): - self.show_msg(_('Rebuilding, please wait...')) - QTimer.singleShot(5, self.do_rebuild) - - def do_rebuild(self): - rebuilt = self.rebuild_it() - if rebuilt is not None: - fmt = os.path.splitext(rebuilt)[1][1:].upper() - with open(rebuilt, 'rb') as f: - self.db.add_format(self.book_id, fmt, f, index_is_id=True) - self.accept() - - def cancel(self): - self.reject() - - def cleanup(self): - if isosx and self._exploded: - try: - import appscript - self.finder = appscript.app('Finder') - self.finder.Finder_windows[os.path.basename(self._exploded)].close() - except: - pass - - for f in self._cleanup_files: - try: - os.remove(f) - except: - pass - - for d in self._cleanup_dirs: - try: - shutil.rmtree(d) - except: - pass - - @property - def db(self): - return self.db_ref() - - @property - def current_format(self): - for b in self.fmt_choice_buttons: - if b.isChecked(): - return unicode(b.text()) class TweakEpubAction(InterfaceAction): name = 'Tweak ePub' - action_spec = (_('Tweak Book'), 'tweak.png', - _('Make small changes to ePub, HTMLZ or AZW3 format books'), - _('T')) + action_spec = (_('Tweak Book'), 'tweak.png', _('Edit eBooks'), _('T')) dont_add_to = frozenset(['context-menu-device']) action_type = 'current' @@ -324,25 +90,46 @@ class TweakEpubAction(InterfaceAction): def tweak_book(self): row = self.gui.library_view.currentIndex() if not row.isValid(): - return error_dialog(self.gui, _('Cannot tweak Book'), + return error_dialog(self.gui, _('Cannot Tweak Book'), _('No book selected'), show=True) book_id = self.gui.library_view.model().id(row) self.do_tweak(book_id) def do_tweak(self, book_id): + from calibre.ebooks.oeb.polish.main import SUPPORTED db = self.gui.library_view.model().db fmts = db.formats(book_id, index_is_id=True) or '' - fmts = [x.lower().strip() for x in fmts.split(',')] - tweakable_fmts = set(fmts).intersection({'epub', 'htmlz', 'azw3', - 'mobi', 'azw'}) + fmts = [x.upper().strip() for x in fmts.split(',')] + tweakable_fmts = set(fmts).intersection(SUPPORTED) if not tweakable_fmts: return error_dialog(self.gui, _('Cannot Tweak Book'), - _('The book must be in ePub, HTMLZ or AZW3 formats to tweak.' - '\n\nFirst convert the book to one of these formats.'), + _('The book must be in the %s formats to tweak.' + '\n\nFirst convert the book to one of these formats.') % (_(' or '.join(SUPPORTED))), show=True) - dlg = TweakBook(self.gui, book_id, tweakable_fmts, db) - dlg.exec_() - dlg.cleanup() - + if len(tweakable_fmts) > 1: + from calibre.gui2.tweak_book import tprefs + if tprefs['choose_tweak_fmt']: + d = Choose(sorted(tweakable_fmts, key=tprefs.defaults['tweak_fmt_order'].index), self.gui) + if d.exec_() != d.Accepted: + return + tweakable_fmts = {d.fmt} + else: + fmts = [f for f in tprefs['tweak_fmt_order'] if f in tweakable_fmts] + if not fmts: + fmts = [f for f in tprefs.defaults['tweak_fmt_order'] if f in tweakable_fmts] + tweakable_fmts = {fmts[0]} + fmt = tuple(tweakable_fmts)[0] + path = db.new_api.format_abspath(book_id, fmt) + if path is None: + return error_dialog(self.gui, _('File missing'), _( + 'The %s format is missing from the calibre library. You should run' + ' library maintenance.') % fmt, show=True) + tweak = 'ebook-tweak' + self.gui.setCursor(Qt.BusyCursor) + try: + self.gui.job_manager.launch_gui_app(tweak, kwargs=dict(args=[tweak, path])) + time.sleep(2) + finally: + self.gui.unsetCursor() diff --git a/src/calibre/gui2/actions/unpack_book.py b/src/calibre/gui2/actions/unpack_book.py new file mode 100644 index 0000000000..d204554211 --- /dev/null +++ b/src/calibre/gui2/actions/unpack_book.py @@ -0,0 +1,348 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +__license__ = 'GPL v3' +__copyright__ = '2010, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import os, weakref, shutil + +from PyQt4.Qt import (QDialog, QVBoxLayout, QHBoxLayout, QRadioButton, QFrame, + QPushButton, QLabel, QGroupBox, QGridLayout, QIcon, QSize, QTimer) + +from calibre import as_unicode +from calibre.constants import isosx +from calibre.gui2 import error_dialog, question_dialog, open_local_file, gprefs +from calibre.gui2.actions import InterfaceAction +from calibre.ptempfile import (PersistentTemporaryDirectory, + PersistentTemporaryFile) +from calibre.utils.config import prefs, tweaks + +class UnpackBook(QDialog): + + def __init__(self, parent, book_id, fmts, db): + QDialog.__init__(self, parent) + self.setWindowIcon(QIcon(I('unpack-book.png'))) + self.book_id, self.fmts, self.db_ref = book_id, fmts, weakref.ref(db) + self._exploded = None + self._cleanup_dirs = [] + self._cleanup_files = [] + + self.setup_ui() + self.setWindowTitle(_('Unpack Book') + ' - ' + db.title(book_id, + index_is_id=True)) + + button = self.fmt_choice_buttons[0] + button_map = {unicode(x.text()):x for x in self.fmt_choice_buttons} + of = prefs['output_format'].upper() + df = tweaks.get('default_tweak_format', None) + lf = gprefs.get('last_tweak_format', None) + if df and df.lower() == 'remember' and lf in button_map: + button = button_map[lf] + elif df and df.upper() in button_map: + button = button_map[df.upper()] + elif of in button_map: + button = button_map[of] + button.setChecked(True) + + self.init_state() + for button in self.fmt_choice_buttons: + button.toggled.connect(self.init_state) + + def init_state(self, *args): + self._exploded = None + self.preview_button.setEnabled(False) + self.rebuild_button.setEnabled(False) + self.explode_button.setEnabled(True) + + def setup_ui(self): # {{{ + self._g = g = QHBoxLayout(self) + self.setLayout(g) + self._l = l = QVBoxLayout() + g.addLayout(l) + + fmts = sorted(x.upper() for x in self.fmts) + self.fmt_choice_box = QGroupBox(_('Choose the format to unpack:'), self) + self._fl = fl = QHBoxLayout() + self.fmt_choice_box.setLayout(self._fl) + self.fmt_choice_buttons = [QRadioButton(y, self) for y in fmts] + for x in self.fmt_choice_buttons: + fl.addWidget(x, stretch=10 if x is self.fmt_choice_buttons[-1] else + 0) + l.addWidget(self.fmt_choice_box) + self.fmt_choice_box.setVisible(len(fmts) > 1) + + self.help_label = QLabel(_('''\ +

About Unpack Book

+

Unpack Book allows you to fine tune the appearance of an ebook by + making small changes to its internals. In order to use Unpack Book, + you need to know a little bit about HTML and CSS, technologies that + are used in ebooks. Follow the steps:

+
+
    +
  1. Click "Explode Book": This will "explode" the book into its + individual internal components.
  2. +
  3. Right click on any individual file and select "Open with..." to + edit it in your favorite text editor.
  4. +
  5. When you are done: close the file browser window + and the editor windows you used to make your tweaks. Then click + the "Rebuild Book" button, to update the book in your calibre + library.
  6. +
''')) + self.help_label.setWordWrap(True) + self._fr = QFrame() + self._fr.setFrameShape(QFrame.VLine) + g.addWidget(self._fr) + g.addWidget(self.help_label) + + self._b = b = QGridLayout() + left, top, right, bottom = b.getContentsMargins() + top += top + b.setContentsMargins(left, top, right, bottom) + l.addLayout(b, stretch=10) + + self.explode_button = QPushButton(QIcon(I('wizard.png')), _('&Explode Book')) + self.preview_button = QPushButton(QIcon(I('view.png')), _('&Preview Book')) + self.cancel_button = QPushButton(QIcon(I('window-close.png')), _('&Cancel')) + self.rebuild_button = QPushButton(QIcon(I('exec.png')), _('&Rebuild Book')) + + self.explode_button.setToolTip( + _('Explode the book to edit its components')) + self.preview_button.setToolTip( + _('Preview the result of your changes')) + self.cancel_button.setToolTip( + _('Abort without saving any changes')) + self.rebuild_button.setToolTip( + _('Save your changes and update the book in the calibre library')) + + a = b.addWidget + a(self.explode_button, 0, 0, 1, 1) + a(self.preview_button, 0, 1, 1, 1) + a(self.cancel_button, 1, 0, 1, 1) + a(self.rebuild_button, 1, 1, 1, 1) + + for x in ('explode', 'preview', 'cancel', 'rebuild'): + getattr(self, x+'_button').clicked.connect(getattr(self, x)) + + self.msg = QLabel('dummy', self) + self.msg.setVisible(False) + self.msg.setStyleSheet(''' + QLabel { + text-align: center; + background-color: white; + color: black; + border-width: 1px; + border-style: solid; + border-radius: 20px; + font-size: x-large; + font-weight: bold; + } + ''') + + self.resize(self.sizeHint() + QSize(40, 10)) + # }}} + + def show_msg(self, msg): + self.msg.setText(msg) + self.msg.resize(self.size() - QSize(50, 25)) + self.msg.move((self.width() - self.msg.width())//2, + (self.height() - self.msg.height())//2) + self.msg.setVisible(True) + + def hide_msg(self): + self.msg.setVisible(False) + + def explode(self): + self.show_msg(_('Exploding, please wait...')) + if len(self.fmt_choice_buttons) > 1: + gprefs.set('last_tweak_format', self.current_format.upper()) + QTimer.singleShot(5, self.do_explode) + + def ask_question(self, msg): + return question_dialog(self, _('Are you sure?'), msg) + + def do_explode(self): + from calibre.ebooks.tweak import get_tools, Error, WorkerError + tdir = PersistentTemporaryDirectory('_tweak_explode') + self._cleanup_dirs.append(tdir) + det_msg = None + try: + src = self.db.format(self.book_id, self.current_format, + index_is_id=True, as_path=True) + self._cleanup_files.append(src) + exploder = get_tools(self.current_format)[0] + opf = exploder(src, tdir, question=self.ask_question) + except WorkerError as e: + det_msg = e.orig_tb + except Error as e: + return error_dialog(self, _('Failed to unpack'), + (_('Could not explode the %s file.')%self.current_format) + ' ' + + as_unicode(e), show=True) + except: + import traceback + det_msg = traceback.format_exc() + finally: + self.hide_msg() + + if det_msg is not None: + return error_dialog(self, _('Failed to unpack'), + _('Could not explode the %s file. Click "Show Details" for ' + 'more information.')%self.current_format, det_msg=det_msg, + show=True) + + if opf is None: + # The question was answered with No + return + + self._exploded = tdir + self.explode_button.setEnabled(False) + self.preview_button.setEnabled(True) + self.rebuild_button.setEnabled(True) + open_local_file(tdir) + + def rebuild_it(self): + from calibre.ebooks.tweak import get_tools, WorkerError + src_dir = self._exploded + det_msg = None + of = PersistentTemporaryFile('_tweak_rebuild.'+self.current_format.lower()) + of.close() + of = of.name + self._cleanup_files.append(of) + try: + rebuilder = get_tools(self.current_format)[1] + rebuilder(src_dir, of) + except WorkerError as e: + det_msg = e.orig_tb + except: + import traceback + det_msg = traceback.format_exc() + finally: + self.hide_msg() + + if det_msg is not None: + error_dialog(self, _('Failed to rebuild file'), + _('Failed to rebuild %s. For more information, click ' + '"Show details".')%self.current_format, + det_msg=det_msg, show=True) + return None + + return of + + def preview(self): + self.show_msg(_('Rebuilding, please wait...')) + QTimer.singleShot(5, self.do_preview) + + def do_preview(self): + rebuilt = self.rebuild_it() + if rebuilt is not None: + self.parent().iactions['View']._view_file(rebuilt) + + def rebuild(self): + self.show_msg(_('Rebuilding, please wait...')) + QTimer.singleShot(5, self.do_rebuild) + + def do_rebuild(self): + rebuilt = self.rebuild_it() + if rebuilt is not None: + fmt = os.path.splitext(rebuilt)[1][1:].upper() + with open(rebuilt, 'rb') as f: + self.db.add_format(self.book_id, fmt, f, index_is_id=True) + self.accept() + + def cancel(self): + self.reject() + + def cleanup(self): + if isosx and self._exploded: + try: + import appscript + self.finder = appscript.app('Finder') + self.finder.Finder_windows[os.path.basename(self._exploded)].close() + except: + pass + + for f in self._cleanup_files: + try: + os.remove(f) + except: + pass + + for d in self._cleanup_dirs: + try: + shutil.rmtree(d) + except: + pass + + @property + def db(self): + return self.db_ref() + + @property + def current_format(self): + for b in self.fmt_choice_buttons: + if b.isChecked(): + return unicode(b.text()) + +class UnpackBookAction(InterfaceAction): + + name = 'Unpack Book' + action_spec = (_('Unpack Book'), 'unpack-book.png', + _('Unpack books in the EPUB, AZW3, HTMLZ formats into their individual components'), None) + 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_ids = self.dropped_ids + del self.dropped_ids + if book_ids: + self.do_tweak(book_ids[0]) + + def genesis(self): + self.qaction.triggered.connect(self.tweak_book) + + def tweak_book(self): + row = self.gui.library_view.currentIndex() + if not row.isValid(): + return error_dialog(self.gui, _('Cannot unpack Book'), + _('No book selected'), show=True) + + book_id = self.gui.library_view.model().id(row) + self.do_tweak(book_id) + + def do_tweak(self, book_id): + db = self.gui.library_view.model().db + fmts = db.formats(book_id, index_is_id=True) or '' + fmts = [x.lower().strip() for x in fmts.split(',')] + tweakable_fmts = set(fmts).intersection({'epub', 'htmlz', 'azw3', + 'mobi', 'azw'}) + if not tweakable_fmts: + return error_dialog(self.gui, _('Cannot unpack Book'), + _('The book must be in ePub, HTMLZ or AZW3 formats to unpack.' + '\n\nFirst convert the book to one of these formats.'), + show=True) + dlg = UnpackBook(self.gui, book_id, tweakable_fmts, db) + dlg.exec_() + dlg.cleanup() + + + diff --git a/src/calibre/gui2/tweak_book/__init__.py b/src/calibre/gui2/tweak_book/__init__.py index 498986ee25..0bd840b508 100644 --- a/src/calibre/gui2/tweak_book/__init__.py +++ b/src/calibre/gui2/tweak_book/__init__.py @@ -14,6 +14,8 @@ tprefs.defaults['editor_font_family'] = None tprefs.defaults['editor_font_size'] = 12 tprefs.defaults['editor_line_wrap'] = True tprefs.defaults['preview_refresh_time'] = 2 +tprefs.defaults['choose_tweak_fmt'] = True +tprefs.defaults['tweak_fmt_order'] = ['EPUB', 'AZW3'] _current_container = None diff --git a/src/calibre/utils/ipc/worker.py b/src/calibre/utils/ipc/worker.py index 0cebdfee07..e8a257da4d 100644 --- a/src/calibre/utils/ipc/worker.py +++ b/src/calibre/utils/ipc/worker.py @@ -25,6 +25,9 @@ PARALLEL_FUNCS = { 'ebook-viewer' : ('calibre.gui2.viewer.main', 'main', None), + 'ebook-tweak' : + ('calibre.gui2.tweak_book.main', 'main', None), + 'render_pages' : ('calibre.ebooks.comic.input', 'render_pages', 'notification'), @@ -197,6 +200,5 @@ def main(): return 0 - if __name__ == '__main__': sys.exit(main())