From 4c158cb44b5638ecbe4c8c3debad554412292640 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 22 Dec 2013 13:58:35 +0530 Subject: [PATCH] Edit book: Allow linking of stylesheets into HTML files automatically. Right click ont he selected HTML files in the File browser and choose "Link stylesheets" to have the tags for the shhets automatically inserted. --- manual/edit.rst | 6 +++ src/calibre/ebooks/oeb/polish/utils.py | 25 ++++++++++++ src/calibre/gui2/tweak_book/boss.py | 18 +++++++-- src/calibre/gui2/tweak_book/file_list.py | 48 +++++++++++++++++++++--- 4 files changed, 89 insertions(+), 8 deletions(-) diff --git a/manual/edit.rst b/manual/edit.rst index 5ad66e7452..de9a5e6416 100644 --- a/manual/edit.rst +++ b/manual/edit.rst @@ -171,6 +171,12 @@ book by clicking :guilabel:`File->New file`. This lets you either import a file by clicking the :guilabel:`Import resource file` button or create a new blank html file or stylesheet by simply entering the file name into the box for the new file. +Linking stylesheets to HTML files efficiently +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +As a convenience, you can select multiple HTML files in the Files Browser, +right click and choose Link stylesheets to have |app| automatically insert the + tags for those stylesheets into all the selected HTML files. Search & Replace ------------------- diff --git a/src/calibre/ebooks/oeb/polish/utils.py b/src/calibre/ebooks/oeb/polish/utils.py index 8a72228382..33b5b4b5ad 100644 --- a/src/calibre/ebooks/oeb/polish/utils.py +++ b/src/calibre/ebooks/oeb/polish/utils.py @@ -37,3 +37,28 @@ class CommentFinder(object): q = bisect(self.starts, offset) - 1 return q >= 0 and self.starts[q] <= offset <= self.ends[q] +def link_stylesheets(container, names, sheets, mtype='text/css'): + from calibre.ebooks.oeb.base import XPath, XHTML + changed_names = set() + snames = set(sheets) + lp = XPath('//h:link[@href]') + hp = XPath('//h:head') + for name in names: + root = container.parsed(name) + existing = {container.href_to_name(l.get('href'), name) for l in lp(root) if (l.get('type', mtype) or mtype) == mtype} + extra = snames - existing + if extra: + changed_names.add(name) + try: + parent = hp(root)[0] + except (TypeError, IndexError): + parent = XHTML('head') + container.insert_into_xml(root, parent, index=0) + for sheet in sheets: + if sheet in extra: + container.insert_into_xml( + parent, parent.makeelement(XHTML('link'), rel='stylesheet', type=mtype, + href=container.name_to_href(sheet, name))) + container.dirty(name) + + return changed_names diff --git a/src/calibre/gui2/tweak_book/boss.py b/src/calibre/gui2/tweak_book/boss.py index 6749df0c60..7d9caa5d19 100644 --- a/src/calibre/gui2/tweak_book/boss.py +++ b/src/calibre/gui2/tweak_book/boss.py @@ -24,6 +24,7 @@ from calibre.ebooks.oeb.polish.pretty import fix_all_html, pretty_all from calibre.ebooks.oeb.polish.replace import rename_files, replace_file from calibre.ebooks.oeb.polish.split import split, merge, AbortError from calibre.ebooks.oeb.polish.toc import remove_names_from_toc, find_existing_toc +from calibre.ebooks.oeb.polish.utils import link_stylesheets from calibre.gui2 import error_dialog, choose_files, question_dialog, info_dialog, choose_save_file from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.tweak_book import set_current_container, current_container, tprefs, actions, editors @@ -96,6 +97,7 @@ class Boss(QObject): fl.mark_requested.connect(self.mark_requested) fl.export_requested.connect(self.export_requested) fl.replace_requested.connect(self.replace_requested) + fl.link_stylesheets_requested.connect(self.link_stylesheets_requested) self.gui.central.current_editor_changed.connect(self.apply_current_editor_state) self.gui.central.close_requested.connect(self.editor_close_requested) self.gui.central.search_panel.search_triggered.connect(self.search) @@ -228,12 +230,13 @@ class Boss(QObject): self.gui.file_list.request_edit(ef) self.gui.toc_view.update_if_visible() - def update_editors_from_container(self, container=None): + def update_editors_from_container(self, container=None, names=None): c = container or current_container() for name, ed in tuple(editors.iteritems()): if c.has_name(name): - ed.replace_data(c.raw_data(name)) - ed.is_synced_to_container = True + if names is None or name in names: + ed.replace_data(c.raw_data(name)) + ed.is_synced_to_container = True else: self.close_editor(name) @@ -830,6 +833,15 @@ class Boss(QObject): if master in editors: self.show_editor(master) + @in_thread_job + def link_stylesheets_requested(self, names, sheets): + self.commit_all_editors_to_container() + self.add_savepoint(_('Link stylesheets')) + changed_names = link_stylesheets(current_container(), names, sheets) + if changed_names: + self.update_editors_from_container(names=changed_names) + self.set_modified() + @in_thread_job def export_requested(self, name, path): if name in editors and not editors[name].is_synced_to_container: diff --git a/src/calibre/gui2/tweak_book/file_list.py b/src/calibre/gui2/tweak_book/file_list.py index 5ee296390d..5318f13da8 100644 --- a/src/calibre/gui2/tweak_book/file_list.py +++ b/src/calibre/gui2/tweak_book/file_list.py @@ -16,7 +16,7 @@ from PyQt4.Qt import ( QWidget, QTreeWidget, QGridLayout, QSize, Qt, QTreeWidgetItem, QIcon, QFont, QStyledItemDelegate, QStyle, QPixmap, QPainter, pyqtSignal, QMenu, QTimer, QDialogButtonBox, QDialog, QLabel, QLineEdit, QVBoxLayout, QScrollArea, - QRadioButton, QFormLayout, QSpinBox) + QRadioButton, QFormLayout, QSpinBox, QListWidget, QListWidgetItem) from calibre import human_readable, sanitize_file_name_unicode from calibre.ebooks.oeb.base import OEB_STYLES, OEB_DOCS @@ -120,6 +120,7 @@ class FileList(QTreeWidget): mark_requested = pyqtSignal(object, object) export_requested = pyqtSignal(object, object) replace_requested = pyqtSignal(object, object, object, object) + link_stylesheets_requested = pyqtSignal(object, object) def __init__(self, parent=None): QTreeWidget.__init__(self, parent) @@ -412,6 +413,9 @@ class FileList(QTreeWidget): for items in selected_map.itervalues(): items.sort(key=self.index_of_name) + if selected_map['text']: + m.addAction(QIcon(I('format-text-color.png')), _('Link &stylesheets...'), partial(self.link_stylesheets, selected_map['text'])) + if len(selected_map['text']) > 1: m.addAction(QIcon(I('merge.png')), _('&Merge selected text files'), partial(self.start_merge, 'text', selected_map['text'])) if len(selected_map['styles']) > 1: @@ -611,6 +615,40 @@ class FileList(QTreeWidget): nname = nname + '.' + ext.lower() self.replace_requested.emit(name, path, nname, force_mt) + def link_stylesheets(self, names): + s = self.categories['styles'] + sheets = [unicode(s.child(i).data(0, NAME_ROLE).toString()) for i in xrange(s.childCount())] + if not sheets: + return error_dialog(self, _('No stylesheets'), _( + 'This book currently has no stylesheets. You must first create a stylesheet' + ' before linking it.'), show=True) + d = QDialog(self) + d.l = l = QVBoxLayout(d) + d.setLayout(l) + d.setWindowTitle(_('Choose stylesheets')) + d.la = la = QLabel(_('Choose the stylesheets to link. Drag and drop to re-arrange')) + + la.setWordWrap(True) + l.addWidget(la) + d.s = s = QListWidget(d) + l.addWidget(s) + s.setDragEnabled(True) + s.setDropIndicatorShown(True) + s.setDragDropMode(self.InternalMove) + s.setAutoScroll(True) + s.setDefaultDropAction(Qt.MoveAction) + for name in sheets: + i = QListWidgetItem(name, s) + flags = Qt.ItemIsEnabled | Qt.ItemIsUserCheckable | Qt.ItemIsDragEnabled | Qt.ItemIsSelectable + i.setFlags(flags) + i.setCheckState(Qt.Checked) + d.bb = bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + bb.accepted.connect(d.accept), bb.rejected.connect(d.reject) + l.addWidget(bb) + if d.exec_() == d.Accepted: + sheets = [unicode(s.item(i).text()) for i in xrange(s.count()) if s.item(i).checkState() == Qt.Checked] + if sheets: + self.link_stylesheets_requested.emit(names, sheets) class NewFileDialog(QDialog): # {{{ @@ -729,6 +767,7 @@ class FileListWidget(QWidget): mark_requested = pyqtSignal(object, object) export_requested = pyqtSignal(object, object) replace_requested = pyqtSignal(object, object, object, object) + link_stylesheets_requested = pyqtSignal(object, object) def __init__(self, parent=None): QWidget.__init__(self, parent) @@ -736,10 +775,9 @@ class FileListWidget(QWidget): self.file_list = FileList(self) self.layout().addWidget(self.file_list) self.layout().setContentsMargins(0, 0, 0, 0) - for x in ('delete_requested', 'reorder_spine', 'rename_requested', - 'edit_file', 'merge_requested', 'mark_requested', - 'export_requested', 'replace_requested', 'bulk_rename_requested'): - getattr(self.file_list, x).connect(getattr(self, x)) + for k, o in vars(self.__class__).iteritems(): + if isinstance(o, pyqtSignal) and hasattr(self.file_list, k): + getattr(self.file_list, k).connect(getattr(self, k)) for x in ('delete_done', 'select_name', 'request_edit', 'mark_name_as_current', 'clear_currently_edited_name'): setattr(self, x, getattr(self.file_list, x))