From 02970401ea57ae51962c462e02aacf355bc706c0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 21 Feb 2017 12:44:00 +0530 Subject: [PATCH] Edit book: Allow drag and drop of image files/stylesheets into the editor to insert the appropriate and tags and add the files to the book automatically. --- src/calibre/gui2/tweak_book/__init__.py | 3 + src/calibre/gui2/tweak_book/editor/text.py | 124 ++++++++++++++++++--- src/calibre/gui2/tweak_book/file_list.py | 52 ++++++--- 3 files changed, 144 insertions(+), 35 deletions(-) diff --git a/src/calibre/gui2/tweak_book/__init__.py b/src/calibre/gui2/tweak_book/__init__.py index f13b5b9342..7ce83810c6 100644 --- a/src/calibre/gui2/tweak_book/__init__.py +++ b/src/calibre/gui2/tweak_book/__init__.py @@ -12,6 +12,7 @@ from future_builtins import map from calibre.utils.config import JSONConfig from calibre.spell.dictionary import Dictionaries, parse_lang_code +CONTAINER_DND_MIMETYPE = 'application/x-calibre-container-name-list' tprefs = JSONConfig('tweak_book_gui') 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): return ucase_map[x[0]] + x[1:] + _current_container = None @@ -102,6 +104,7 @@ class NonReplaceDict(dict): raise ValueError('The key %s is already present' % k) dict.__setitem__(self, k, v) + actions = NonReplaceDict() editors = NonReplaceDict() toolbar_actions = NonReplaceDict() diff --git a/src/calibre/gui2/tweak_book/editor/text.py b/src/calibre/gui2/tweak_book/editor/text.py index ac00fe244b..44f55118b0 100644 --- a/src/calibre/gui2/tweak_book/editor/text.py +++ b/src/calibre/gui2/tweak_book/editor/text.py @@ -1,33 +1,46 @@ #!/usr/bin/env python2 # vim:fileencoding=utf-8 -from __future__ import (unicode_literals, division, absolute_import, - print_function) +# License: GPLv3 Copyright: 2013, Kovid Goyal +from __future__ import absolute_import, division, print_function, unicode_literals -__license__ = 'GPL v3' -__copyright__ = '2013, Kovid Goyal ' - -import re, importlib -import textwrap, unicodedata +import importlib +import os +import re +import textwrap +import unicodedata from future_builtins import map -import regex from PyQt5.Qt import ( - QPlainTextEdit, QFontDatabase, QToolTip, QPalette, QFont, QKeySequence, - QTextEdit, QTextFormat, QWidget, QSize, QPainter, Qt, QRect, QColor, - QColorDialog, QTimer, pyqtSignal) + QColor, QColorDialog, QFont, QFontDatabase, QKeySequence, QPainter, QPalette, + QPlainTextEdit, QRect, QSize, Qt, QTextEdit, QTextFormat, QTimer, QToolTip, + QWidget, pyqtSignal +) +import regex 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.editor import ( - SYNTAX_PROPERTY, SPELL_PROPERTY, SPELL_LOCALE_PROPERTY, store_locale, LINK_PROPERTY) -from calibre.gui2.tweak_book.editor.themes import get_theme, theme_color, theme_format -from calibre.gui2.tweak_book.editor.syntax.base import SyntaxHighlighter + LINK_PROPERTY, SPELL_LOCALE_PROPERTY, SPELL_PROPERTY, SYNTAX_PROPERTY, + store_locale +) from calibre.gui2.tweak_book.editor.smarts import NullSmarts 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.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 @@ -110,6 +123,83 @@ class TextEdit(PlainTextEdit): self.blockCountChanged[int].connect(self.update_line_number_area_width) 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(''.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 def is_modified(self): ''' True if the document has been modified since it was loaded or since diff --git a/src/calibre/gui2/tweak_book/file_list.py b/src/calibre/gui2/tweak_book/file_list.py index 2085d99fff..2f59c4338e 100644 --- a/src/calibre/gui2/tweak_book/file_list.py +++ b/src/calibre/gui2/tweak_book/file_list.py @@ -1,35 +1,40 @@ #!/usr/bin/env python2 # vim:fileencoding=utf-8 -from __future__ import (unicode_literals, division, absolute_import, - print_function) +# License: GPLv3 Copyright: 2013, Kovid Goyal +from __future__ import absolute_import, division, print_function, unicode_literals -__license__ = 'GPL v3' -__copyright__ = '2013, Kovid Goyal ' - -import os, posixpath +import os +import posixpath from binascii import hexlify -from collections import OrderedDict, defaultdict, Counter +from collections import Counter, OrderedDict, defaultdict from functools import partial import sip from PyQt5.Qt import ( - QWidget, QTreeWidget, QGridLayout, QSize, Qt, QTreeWidgetItem, QIcon, QFont, - QStyledItemDelegate, QStyle, QPixmap, QPainter, pyqtSignal, QMenu, QTimer, - QDialogButtonBox, QDialog, QLabel, QLineEdit, QVBoxLayout, QScrollArea, QInputDialog, - QRadioButton, QFormLayout, QSpinBox, QListWidget, QListWidgetItem, QCheckBox) + QCheckBox, QDialog, QDialogButtonBox, QFont, QFormLayout, QGridLayout, QIcon, + QInputDialog, QLabel, QLineEdit, QListWidget, QListWidgetItem, QMenu, QPainter, + QPixmap, QRadioButton, QScrollArea, QSize, QSpinBox, QStyle, QStyledItemDelegate, + Qt, QTimer, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget, pyqtSignal +) -from calibre import human_readable, sanitize_file_name_unicode, plugins -from calibre.ebooks.oeb.base import OEB_STYLES, OEB_DOCS -from calibre.ebooks.oeb.polish.container import guess_type, OEB_FONTS -from calibre.ebooks.oeb.polish.replace import get_recommended_folders +from calibre import human_readable, plugins, sanitize_file_name_unicode +from calibre.ebooks.oeb.base import OEB_DOCS, OEB_STYLES +from calibre.ebooks.oeb.polish.container import OEB_FONTS, guess_type from calibre.ebooks.oeb.polish.cover import ( - 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 + get_cover_page_name, get_raster_cover_name, is_raster_image +) +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.templates import template_for from calibre.utils.icu import sort_key + TOP_ICON_SIZE = 24 NAME_ROLE = Qt.UserRole CATEGORY_ROLE = NAME_ROLE + 1 @@ -210,6 +215,17 @@ class FileList(QTreeWidget): }.iteritems()} 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 def current_name(self): ci = self.currentItem()