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))