mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 18:54:09 -04:00
Edit book: Add button to easily insert <img> tag while editing HTML
This commit is contained in:
parent
d6e52d097e
commit
0cf4238d5e
@ -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()
|
||||
|
276
src/calibre/gui2/tweak_book/editor/insert_resource.py
Normal file
276
src/calibre/gui2/tweak_book/editor/insert_resource.py
Normal 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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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'):
|
||||
|
Loading…
x
Reference in New Issue
Block a user