From 0cf4238d5e112fb2512c46575c60dede9633979a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 18 Dec 2013 22:32:42 +0530 Subject: [PATCH] Edit book: Add button to easily insert tag while editing HTML --- src/calibre/gui2/tweak_book/boss.py | 30 +- .../gui2/tweak_book/editor/insert_resource.py | 276 ++++++++++++++++++ src/calibre/gui2/tweak_book/editor/text.py | 22 ++ src/calibre/gui2/tweak_book/editor/widget.py | 8 + 4 files changed, 334 insertions(+), 2 deletions(-) create mode 100644 src/calibre/gui2/tweak_book/editor/insert_resource.py diff --git a/src/calibre/gui2/tweak_book/boss.py b/src/calibre/gui2/tweak_book/boss.py index f032a41d6c..b7601577d8 100644 --- a/src/calibre/gui2/tweak_book/boss.py +++ b/src/calibre/gui2/tweak_book/boss.py @@ -32,6 +32,7 @@ from calibre.gui2.tweak_book.save import SaveManager, save_container from calibre.gui2.tweak_book.preview import parse_worker, font_cache from calibre.gui2.tweak_book.toc import TOCEditor from calibre.gui2.tweak_book.editor import editor_from_syntax, syntax_from_mime +from calibre.gui2.tweak_book.editor.insert_resource import get_resource_data from calibre.gui2.tweak_book.preferences import Preferences def get_container(*args, **kwargs): @@ -217,9 +218,12 @@ class Boss(QObject): else: self.close_editor(name) - def apply_container_update_to_gui(self): + def refresh_file_list(self): container = current_container() self.gui.file_list.build(container) + + def apply_container_update_to_gui(self): + self.refresh_file_list() self.update_global_history_actions() self.update_editors_from_container() self.set_modified() @@ -445,8 +449,30 @@ class Boss(QObject): def editor_action(self, action): ed = self.gui.central.current_editor + for n, x in editors.iteritems(): + if x is ed: + edname = n + break if hasattr(ed, 'action_triggered'): - ed.action_triggered(action) + if action and action[0] == 'insert_resource': + rtype = action[1] + if rtype == 'image' and ed.syntax not in {'css', 'html'}: + return error_dialog(self.gui, _('Not supported'), _( + 'Inserting images is only supported for HTML and CSS files.'), show=True) + rdata = get_resource_data(rtype, self.gui) + if rdata is None: + return + if rtype == 'image': + chosen_name, chosen_image_is_external = rdata + if chosen_image_is_external: + with open(chosen_image_is_external[1], 'rb') as f: + current_container().add_file(chosen_image_is_external[0], f.read()) + self.refresh_file_list() + chosen_name = chosen_image_is_external[0] + href = current_container().name_to_href(chosen_name, edname) + ed.insert_image(href) + else: + ed.action_triggered(action) def show_find(self): self.gui.central.show_find() diff --git a/src/calibre/gui2/tweak_book/editor/insert_resource.py b/src/calibre/gui2/tweak_book/editor/insert_resource.py new file mode 100644 index 0000000000..35d9bfce49 --- /dev/null +++ b/src/calibre/gui2/tweak_book/editor/insert_resource.py @@ -0,0 +1,276 @@ +#!/usr/bin/env python +# vim:fileencoding=utf-8 +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2013, Kovid Goyal ' + +import sys, os + +from PyQt4.Qt import ( + QDialog, QGridLayout, QDialogButtonBox, QSize, QListView, QStyledItemDelegate, + QLabel, QPixmap, QApplication, QSizePolicy, QAbstractListModel, QVariant, + Qt, QRect, QPainter, QModelIndex, QSortFilterProxyModel, QLineEdit, + QToolButton, QIcon, QFormLayout) + +from calibre import fit_image +from calibre.constants import plugins +from calibre.gui2 import NONE, choose_files, error_dialog +from calibre.gui2.tweak_book import current_container, tprefs +from calibre.gui2.tweak_book.file_list import name_is_ok +from calibre.utils.icu import sort_key + +class Dialog(QDialog): + + def __init__(self, title, name, parent=None): + QDialog.__init__(self, parent) + self.setWindowTitle(title) + self.name = name + self.bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + self.bb.accepted.connect(self.accept) + self.bb.rejected.connect(self.reject) + + self.setup_ui() + + self.resize(self.sizeHint()) + geom = tprefs.get(name + '-geometry', None) + if geom is not None: + self.restoreGeometry(geom) + if hasattr(self, 'splitter'): + state = tprefs.get(name + '-splitter-state', None) + if state is not None: + self.splitter.restoreState(state) + + def accept(self): + tprefs.set(self.name + '-geometry', bytearray(self.saveGeometry())) + if hasattr(self, 'splitter'): + tprefs.set(self.name + '-splitter-state', bytearray(self.splitter.saveState())) + QDialog.accept(self) + + def reject(self): + tprefs.set(self.name + '-geometry', bytearray(self.saveGeometry())) + if hasattr(self, 'splitter'): + tprefs.set(self.name + '-splitter-state', bytearray(self.splitter.saveState())) + QDialog.reject(self) + +class ChooseName(Dialog): + + def __init__(self, candidate, parent=None): + self.candidate = candidate + self.filename = None + Dialog.__init__(self, _('Choose file name'), 'choose-file-name', parent=parent) + + def setup_ui(self): + self.l = l = QFormLayout(self) + self.setLayout(l) + + self.err_label = QLabel('') + self.name_edit = QLineEdit(self) + self.name_edit.textChanged.connect(self.verify) + self.name_edit.setText(self.candidate) + pos = self.candidate.rfind('.') + if pos > -1: + self.name_edit.setSelection(0, pos) + l.addRow(_('File &name:'), self.name_edit) + l.addRow(self.err_label) + l.addRow(self.bb) + + def show_error(self, msg): + self.err_label.setText('

' + msg) + return False + + def verify(self): + return name_is_ok(unicode(self.name_edit.text()), self.show_error) + + def accept(self): + if not self.verify(): + return error_dialog(self, _('No name specified'), _( + 'You must specify a file name for the new file, with an extension.'), show=True) + n = unicode(self.name_edit.text()).replace('\\', '/') + name, ext = n.rpartition('.')[0::2] + self.filename = name + '.' + ext.lower() + super(ChooseName, self).accept() + +class ImageDelegate(QStyledItemDelegate): + + MARGIN = 4 + + def __init__(self, parent): + super(ImageDelegate, self).__init__(parent) + self.set_dimensions() + self.cover_cache = {} + + def set_dimensions(self): + width, height = 120, 160 + self.cover_size = QSize(width, height) + f = self.parent().font() + sz = f.pixelSize() + if sz < 5: + sz = f.pointSize() * self.parent().logicalDpiY() / 72.0 + self.title_height = max(25, sz + 10) + self.item_size = self.cover_size + QSize(2 * self.MARGIN, (2 * self.MARGIN) + self.title_height) + self.calculate_spacing() + + def calculate_spacing(self): + self.spacing = max(10, min(50, int(0.1 * self.item_size.width()))) + + def sizeHint(self, option, index): + return self.item_size + + def paint(self, painter, option, index): + QStyledItemDelegate.paint(self, painter, option, QModelIndex()) # draw the hover and selection highlights + name = unicode(index.data(Qt.DisplayRole).toString()) + cover = self.cover_cache.get(name, None) + if cover is None: + cover = self.cover_cache[name] = QPixmap() + try: + raw = current_container().raw_data(name, decode=False) + except: + pass + else: + cover.loadFromData(raw) + if not cover.isNull(): + scaled, width, height = fit_image(cover.width(), cover.height(), self.cover_size.width(), self.cover_size.height()) + if scaled: + cover = self.cover_cache[name] = cover.scaled(width, height, transformMode=Qt.SmoothTransformation) + + painter.save() + try: + rect = option.rect + rect.adjust(self.MARGIN, self.MARGIN, -self.MARGIN, -self.MARGIN) + trect = QRect(rect) + rect.setBottom(rect.bottom() - self.title_height) + if not cover.isNull(): + dx = max(0, int((rect.width() - cover.width())/2.0)) + dy = max(0, rect.height() - cover.height()) + rect.adjust(dx, dy, -dx, 0) + painter.drawPixmap(rect, cover) + rect = trect + rect.setTop(rect.bottom() - self.title_height + 5) + painter.setRenderHint(QPainter.TextAntialiasing, True) + metrics = painter.fontMetrics() + painter.drawText(rect, Qt.AlignCenter|Qt.TextSingleLine, + metrics.elidedText(name, Qt.ElideLeft, rect.width())) + finally: + painter.restore() + +class Images(QAbstractListModel): + + def __init__(self, parent): + QAbstractListModel.__init__(self, parent) + self.icon_size = parent.iconSize() + c = current_container() + self.image_names = [] + for name in sorted(c.mime_map, key=sort_key): + if c.mime_map[name].startswith('image/'): + self.image_names.append(name) + self.image_cache = {} + + def rowCount(self, *args): + return len(self.image_names) + + def data(self, index, role): + try: + name = self.image_names[index.row()] + except IndexError: + return NONE + if role in (Qt.DisplayRole, Qt.ToolTipRole): + return QVariant(name) + return NONE + +class InsertImage(Dialog): + + def __init__(self, parent=None): + Dialog.__init__(self, _('Choose an image'), 'insert-image-dialog', parent) + self.chosen_image = None + self.chosen_image_is_external = False + + def sizeHint(self): + return QSize(800, 600) + + def setup_ui(self): + self.l = l = QGridLayout(self) + self.setLayout(l) + + self.la1 = la = QLabel(_('&Existing images in the book')) + la.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + l.addWidget(la, 0, 0, 1, 2) + + self.view = v = QListView(self) + v.setViewMode(v.IconMode) + v.setFlow(v.LeftToRight) + v.setSpacing(4) + v.setResizeMode(v.Adjust) + v.setUniformItemSizes(True) + pi = plugins['progress_indicator'][0] + if hasattr(pi, 'set_no_activate_on_click'): + pi.set_no_activate_on_click(v) + v.activated.connect(self.activated) + v.doubleClicked.connect(self.activated) + self.d = ImageDelegate(v) + v.setItemDelegate(self.d) + self.model = Images(self.view) + self.fm = fm = QSortFilterProxyModel(self.view) + fm.setSourceModel(self.model) + fm.setFilterCaseSensitivity(False) + v.setModel(fm) + l.addWidget(v, 1, 0, 1, 2) + la.setBuddy(v) + + self.filter = f = QLineEdit(self) + f.setPlaceholderText(_('Search for image by file name')) + l.addWidget(f, 2, 0) + self.cb = b = QToolButton(self) + b.setIcon(QIcon(I('clear_left.png'))) + b.clicked.connect(f.clear) + l.addWidget(b, 2, 1) + f.textChanged.connect(self.filter_changed) + + l.addWidget(self.bb, 3, 0, 1, 2) + b = self.import_button = self.bb.addButton(_('&Import image'), self.bb.ActionRole) + b.clicked.connect(self.import_image) + b.setIcon(QIcon(I('view-image.png'))) + b.setToolTip(_('Import an image from elsewhere in your computer')) + + def import_image(self): + path = choose_files(self, 'tweak-book-choose-image-for-import', _('Choose Image'), + filters=[(_('Images'), ('jpg', 'jpeg', 'png', 'gif', 'svg'))], all_files=True, select_only_single_file=True) + if path: + path = path[0] + basename = os.path.basename(path) + n, e = basename.rpartition('.')[0::2] + basename = n + '.' + e.lower() + d = ChooseName(basename, self) + if d.exec_() == d.Accepted and d.filename: + self.accept() + self.chosen_image_is_external = (d.filename, path) + + def activated(self, index): + self.chosen_image_is_external = False + self.accept() + + def accept(self): + self.chosen_image = unicode(self.view.currentIndex().data().toString()) + super(InsertImage, self).accept() + + def filter_changed(self, *args): + f = unicode(self.filter.text()) + self.fm.setFilterFixedString(f) + +def get_resource_data(rtype, parent): + if rtype == 'image': + d = InsertImage(parent) + if d.exec_() == d.Accepted: + return d.chosen_image, d.chosen_image_is_external + +if __name__ == '__main__': + app = QApplication([]) # noqa + from calibre.gui2.tweak_book import set_current_container + from calibre.gui2.tweak_book.boss import get_container + set_current_container(get_container(sys.argv[-1])) + + d = InsertImage() + if d.exec_() == d.Accepted: + print (d.chosen_image, d.chosen_image_is_external) + diff --git a/src/calibre/gui2/tweak_book/editor/text.py b/src/calibre/gui2/tweak_book/editor/text.py index 478f8648fa..b8bcb869ba 100644 --- a/src/calibre/gui2/tweak_book/editor/text.py +++ b/src/calibre/gui2/tweak_book/editor/text.py @@ -15,6 +15,7 @@ from PyQt4.Qt import ( QTextEdit, QTextFormat, QWidget, QSize, QPainter, Qt, QRect, pyqtSlot, QApplication, QMimeData, QColor, QColorDialog) +from calibre import prepare_string_for_xml from calibre.gui2.tweak_book import tprefs, TOP from calibre.gui2.tweak_book.editor import SYNTAX_PROPERTY from calibre.gui2.tweak_book.editor.themes import THEMES, default_theme, theme_color @@ -519,3 +520,24 @@ class TextEdit(QPlainTextEdit): c.setPosition(c.position() - len(suffix)) self.setTextCursor(c) + def insert_image(self, href): + c = self.textCursor() + template, alt = 'url(%s)', '' + left = min(c.position(), c.anchor) + if self.syntax == 'html': + left, right = self.get_range_inside_tag() + c.setPosition(left) + c.setPosition(right, c.KeepAnchor) + alt = _('Image') + template = '{0}'.format(alt) + href = prepare_string_for_xml(href, True) + text = template % href + c.insertText(text) + if self.syntax == 'html': + c.setPosition(left + 10) + c.setPosition(c.position() + len(alt), c.KeepAnchor) + else: + c.setPosition(left) + c.setPosition(left + len(text), c.KeepAnchor) + self.setTextCursor(c) + diff --git a/src/calibre/gui2/tweak_book/editor/widget.py b/src/calibre/gui2/tweak_book/editor/widget.py index 8201a0083e..423fd86539 100644 --- a/src/calibre/gui2/tweak_book/editor/widget.py +++ b/src/calibre/gui2/tweak_book/editor/widget.py @@ -36,6 +36,9 @@ def register_text_editor_actions(reg): 'format-text-background-color', (), _('Change background color of text')) ac.setToolTip(_('

Background Color

Change the background color of the selected text')) + ac = reg('view-image', _('&Insert image'), ('insert_resource', 'image'), 'insert-image', (), _('Insert an image into the text')) + ac.setToolTip(_('

Insert image

Insert an image into the text')) + class Editor(QMainWindow): has_line_numbers = True @@ -110,6 +113,9 @@ class Editor(QMainWindow): func = getattr(self.editor, action) func(*args) + def insert_image(self, href): + self.editor.insert_image(href) + def undo(self): self.editor.undo() @@ -162,6 +168,8 @@ class Editor(QMainWindow): b.addAction(actions['fix-html-current']) if self.syntax in {'xml', 'html', 'css'}: b.addAction(actions['pretty-current']) + if self.syntax in {'html', 'css'}: + b.addAction(actions['insert-image']) if self.syntax == 'html': self.format_bar = b = self.addToolBar(_('Format text')) for x in ('bold', 'italic', 'underline', 'strikethrough', 'subscript', 'superscript', 'color', 'background-color'):