Edit book: Allow drag and drop of image files/stylesheets into the editor to insert the appropriate <img> and <link> tags and add the files to the book automatically.

This commit is contained in:
Kovid Goyal 2017-02-21 12:44:00 +05:30
parent ec9e8e1c58
commit 02970401ea
3 changed files with 144 additions and 35 deletions

View File

@ -12,6 +12,7 @@ from future_builtins import map
from calibre.utils.config import JSONConfig from calibre.utils.config import JSONConfig
from calibre.spell.dictionary import Dictionaries, parse_lang_code from calibre.spell.dictionary import Dictionaries, parse_lang_code
CONTAINER_DND_MIMETYPE = 'application/x-calibre-container-name-list'
tprefs = JSONConfig('tweak_book_gui') tprefs = JSONConfig('tweak_book_gui')
d = tprefs.defaults d = tprefs.defaults
@ -83,6 +84,7 @@ ucase_map = {l:string.ascii_uppercase[i] for i, l in enumerate(string.ascii_lowe
def capitalize(x): def capitalize(x):
return ucase_map[x[0]] + x[1:] return ucase_map[x[0]] + x[1:]
_current_container = None _current_container = None
@ -102,6 +104,7 @@ class NonReplaceDict(dict):
raise ValueError('The key %s is already present' % k) raise ValueError('The key %s is already present' % k)
dict.__setitem__(self, k, v) dict.__setitem__(self, k, v)
actions = NonReplaceDict() actions = NonReplaceDict()
editors = NonReplaceDict() editors = NonReplaceDict()
toolbar_actions = NonReplaceDict() toolbar_actions = NonReplaceDict()

View File

@ -1,33 +1,46 @@
#!/usr/bin/env python2 #!/usr/bin/env python2
# vim:fileencoding=utf-8 # vim:fileencoding=utf-8
from __future__ import (unicode_literals, division, absolute_import, # License: GPLv3 Copyright: 2013, Kovid Goyal <kovid at kovidgoyal.net>
print_function) from __future__ import absolute_import, division, print_function, unicode_literals
__license__ = 'GPL v3' import importlib
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>' import os
import re
import re, importlib import textwrap
import textwrap, unicodedata import unicodedata
from future_builtins import map from future_builtins import map
import regex
from PyQt5.Qt import ( from PyQt5.Qt import (
QPlainTextEdit, QFontDatabase, QToolTip, QPalette, QFont, QKeySequence, QColor, QColorDialog, QFont, QFontDatabase, QKeySequence, QPainter, QPalette,
QTextEdit, QTextFormat, QWidget, QSize, QPainter, Qt, QRect, QColor, QPlainTextEdit, QRect, QSize, Qt, QTextEdit, QTextFormat, QTimer, QToolTip,
QColorDialog, QTimer, pyqtSignal) QWidget, pyqtSignal
)
import regex
from calibre import prepare_string_for_xml from calibre import prepare_string_for_xml
from calibre.gui2.tweak_book import tprefs, TOP, current_container from calibre.ebooks.oeb.base import OEB_DOCS, OEB_STYLES
from calibre.ebooks.oeb.polish.replace import get_recommended_folders
from calibre.ebooks.oeb.polish.utils import guess_type
from calibre.gui2.tweak_book import (
CONTAINER_DND_MIMETYPE, TOP, current_container, tprefs
)
from calibre.gui2.tweak_book.completion.popup import CompletionPopup from calibre.gui2.tweak_book.completion.popup import CompletionPopup
from calibre.gui2.tweak_book.editor import ( from calibre.gui2.tweak_book.editor import (
SYNTAX_PROPERTY, SPELL_PROPERTY, SPELL_LOCALE_PROPERTY, store_locale, LINK_PROPERTY) LINK_PROPERTY, SPELL_LOCALE_PROPERTY, SPELL_PROPERTY, SYNTAX_PROPERTY,
from calibre.gui2.tweak_book.editor.themes import get_theme, theme_color, theme_format store_locale
from calibre.gui2.tweak_book.editor.syntax.base import SyntaxHighlighter )
from calibre.gui2.tweak_book.editor.smarts import NullSmarts from calibre.gui2.tweak_book.editor.smarts import NullSmarts
from calibre.gui2.tweak_book.editor.snippets import SnippetManager from calibre.gui2.tweak_book.editor.snippets import SnippetManager
from calibre.gui2.tweak_book.widgets import PlainTextEdit, PARAGRAPH_SEPARATOR from calibre.gui2.tweak_book.editor.syntax.base import SyntaxHighlighter
from calibre.gui2.tweak_book.editor.themes import (
get_theme, theme_color, theme_format
)
from calibre.gui2.tweak_book.widgets import PARAGRAPH_SEPARATOR, PlainTextEdit
from calibre.spell.break_iterator import index_of from calibre.spell.break_iterator import index_of
from calibre.utils.icu import safe_chr, string_length, capitalize, upper, lower, swapcase from calibre.utils.icu import (
capitalize, lower, safe_chr, string_length, swapcase, upper
)
from calibre.utils.img import image_to_data
from calibre.utils.titlecase import titlecase from calibre.utils.titlecase import titlecase
@ -110,6 +123,83 @@ class TextEdit(PlainTextEdit):
self.blockCountChanged[int].connect(self.update_line_number_area_width) self.blockCountChanged[int].connect(self.update_line_number_area_width)
self.updateRequest.connect(self.update_line_number_area) self.updateRequest.connect(self.update_line_number_area)
def get_droppable_files(self, md):
def is_mt_ok(mt):
return self.syntax == 'html' and (
mt in OEB_DOCS or mt in OEB_STYLES or mt.startswith('image/')
)
if md.hasFormat(CONTAINER_DND_MIMETYPE):
for line in bytes(md.data(CONTAINER_DND_MIMETYPE)).decode('utf-8').splitlines():
mt = current_container().mime_map.get(line, 'application/octet-stream')
if is_mt_ok(mt):
yield line, mt, True
return
for qurl in md.urls():
if qurl.isLocalFile() and os.access(qurl.toLocalFile(), os.R_OK):
path = qurl.toLocalFile()
mt = guess_type(path)
if is_mt_ok(mt):
yield path, mt, False
def canInsertFromMimeData(self, md):
if md.hasText() or (md.hasHtml() and self.syntax == 'html') or md.hasImage():
return True
elif tuple(self.get_droppable_files(md)):
return True
return False
def insertFromMimeData(self, md):
files = tuple(self.get_droppable_files(md))
base = self.highlighter.doc_name or None
def get_name(name):
return get_recommended_folders(current_container(), (name,))[name] + '/' + name
def get_href(name):
return current_container().name_to_href(name, base)
def insert_text(text):
c = self.textCursor()
c.insertText(text)
self.setTextCursor(c)
def add_file(name, data, mt=None):
from calibre.gui2.tweak_book.boss import get_boss
name = current_container().add_file(name, data, media_type=mt, modify_name_if_needed=True)
get_boss().refresh_file_list()
return name
if files:
for path, mt, is_name in files:
if is_name:
name = path
else:
name = get_name(os.path.basename(path))
with lopen(path, 'rb') as f:
name = add_file(name, f.read(), mt)
href = get_href(name)
if mt.startswith('image/'):
self.insert_image(href)
elif mt in OEB_STYLES:
insert_text('<link href="{}" rel="stylesheet" type="text/css"/>'.format(href))
elif mt in OEB_DOCS:
self.insert_hyperlink(href, name)
return
if md.hasImage():
img = md.imageData()
if img.isValid():
data = image_to_data(img, fmt='PNG')
name = add_file(get_name('dropped_image.png', data))
self.insert_image(get_href(name))
return
if md.hasHtml():
insert_text(md.html())
return
if md.hasText():
return insert_text(md.text())
@dynamic_property @dynamic_property
def is_modified(self): def is_modified(self):
''' True if the document has been modified since it was loaded or since ''' True if the document has been modified since it was loaded or since

View File

@ -1,35 +1,40 @@
#!/usr/bin/env python2 #!/usr/bin/env python2
# vim:fileencoding=utf-8 # vim:fileencoding=utf-8
from __future__ import (unicode_literals, division, absolute_import, # License: GPLv3 Copyright: 2013, Kovid Goyal <kovid at kovidgoyal.net>
print_function) from __future__ import absolute_import, division, print_function, unicode_literals
__license__ = 'GPL v3' import os
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>' import posixpath
import os, posixpath
from binascii import hexlify from binascii import hexlify
from collections import OrderedDict, defaultdict, Counter from collections import Counter, OrderedDict, defaultdict
from functools import partial from functools import partial
import sip import sip
from PyQt5.Qt import ( from PyQt5.Qt import (
QWidget, QTreeWidget, QGridLayout, QSize, Qt, QTreeWidgetItem, QIcon, QFont, QCheckBox, QDialog, QDialogButtonBox, QFont, QFormLayout, QGridLayout, QIcon,
QStyledItemDelegate, QStyle, QPixmap, QPainter, pyqtSignal, QMenu, QTimer, QInputDialog, QLabel, QLineEdit, QListWidget, QListWidgetItem, QMenu, QPainter,
QDialogButtonBox, QDialog, QLabel, QLineEdit, QVBoxLayout, QScrollArea, QInputDialog, QPixmap, QRadioButton, QScrollArea, QSize, QSpinBox, QStyle, QStyledItemDelegate,
QRadioButton, QFormLayout, QSpinBox, QListWidget, QListWidgetItem, QCheckBox) Qt, QTimer, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget, pyqtSignal
)
from calibre import human_readable, sanitize_file_name_unicode, plugins from calibre import human_readable, plugins, sanitize_file_name_unicode
from calibre.ebooks.oeb.base import OEB_STYLES, OEB_DOCS from calibre.ebooks.oeb.base import OEB_DOCS, OEB_STYLES
from calibre.ebooks.oeb.polish.container import guess_type, OEB_FONTS from calibre.ebooks.oeb.polish.container import OEB_FONTS, guess_type
from calibre.ebooks.oeb.polish.replace import get_recommended_folders
from calibre.ebooks.oeb.polish.cover import ( from calibre.ebooks.oeb.polish.cover import (
get_cover_page_name, get_raster_cover_name, is_raster_image) get_cover_page_name, get_raster_cover_name, is_raster_image
from calibre.gui2 import error_dialog, choose_files, question_dialog, elided_text, choose_save_file )
from calibre.gui2.tweak_book import current_container, tprefs, editors from calibre.ebooks.oeb.polish.replace import get_recommended_folders
from calibre.gui2 import (
choose_files, choose_save_file, elided_text, error_dialog, question_dialog
)
from calibre.gui2.tweak_book import (
CONTAINER_DND_MIMETYPE, current_container, editors, tprefs
)
from calibre.gui2.tweak_book.editor import syntax_from_mime from calibre.gui2.tweak_book.editor import syntax_from_mime
from calibre.gui2.tweak_book.templates import template_for from calibre.gui2.tweak_book.templates import template_for
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key
TOP_ICON_SIZE = 24 TOP_ICON_SIZE = 24
NAME_ROLE = Qt.UserRole NAME_ROLE = Qt.UserRole
CATEGORY_ROLE = NAME_ROLE + 1 CATEGORY_ROLE = NAME_ROLE + 1
@ -210,6 +215,17 @@ class FileList(QTreeWidget):
}.iteritems()} }.iteritems()}
self.itemActivated.connect(self.item_double_clicked) self.itemActivated.connect(self.item_double_clicked)
def mimeTypes(self):
ans = QTreeWidget.mimeTypes(self)
ans.append(CONTAINER_DND_MIMETYPE)
return ans
def mimeData(self, indices):
ans = QTreeWidget.mimeData(self, indices)
names = (idx.data(0, NAME_ROLE) for idx in indices if idx.data(0, MIME_ROLE))
ans.setData(CONTAINER_DND_MIMETYPE, '\n'.join(filter(None, names)).encode('utf-8'))
return ans
@property @property
def current_name(self): def current_name(self):
ci = self.currentItem() ci = self.currentItem()