Edit book: Add button to easily insert <img> tag while editing HTML

This commit is contained in:
Kovid Goyal 2013-12-18 22:32:42 +05:30
parent d6e52d097e
commit 0cf4238d5e
4 changed files with 334 additions and 2 deletions

View File

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

View File

@ -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 <kovid at kovidgoyal.net>'
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('<p style="color:red">' + 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)

View File

@ -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 = '<img alt="{0}" src="%s" />'.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)

View File

@ -36,6 +36,9 @@ def register_text_editor_actions(reg):
'format-text-background-color', (), _('Change background color of text'))
ac.setToolTip(_('<h3>Background Color</h3>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(_('<h3>Insert image</h3>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'):