diff --git a/src/calibre/db/notes/connect.py b/src/calibre/db/notes/connect.py
index ce5241ad73..9ffef1df1e 100644
--- a/src/calibre/db/notes/connect.py
+++ b/src/calibre/db/notes/connect.py
@@ -32,6 +32,7 @@ copy_marked_up_text = cmt()
SEP = b'\0\x1c\0'
DOC_NAME = 'doc.html'
METADATA_EXT = '.metadata'
+RESOURCE_URL_SCHEME = 'calres'
def hash_data(data: bytes) -> str:
diff --git a/src/calibre/gui2/comments_editor.py b/src/calibre/gui2/comments_editor.py
index bffd614504..de29ace1a8 100644
--- a/src/calibre/gui2/comments_editor.py
+++ b/src/calibre/gui2/comments_editor.py
@@ -346,6 +346,7 @@ class EditorWidget(QTextEdit, LineEditECM): # {{{
r('background', 'format-fill-color', _('Background color'))
r('insert_link', 'insert-link', _('Insert link') if self.insert_images_separately else _('Insert link or image'),
shortcut=QKeySequence('Ctrl+l', QKeySequence.SequenceFormat.PortableText))
+ r('insert_image', 'view-image', _('Insert image'), shortcut=QKeySequence('Ctrl+p', QKeySequence.SequenceFormat.PortableText))
r('insert_hr', 'format-text-hr', _('Insert separator'),)
r('clear', 'trash', _('Clear'))
@@ -648,6 +649,14 @@ class EditorWidget(QTextEdit, LineEditECM): # {{{
c.movePosition(QTextCursor.MoveOperation.EndOfBlock, QTextCursor.MoveMode.MoveAnchor)
c.insertHtml('
')
+ def do_insert_image(self):
+ from calibre.gui2 import choose_images
+ files = choose_images(self, 'choose-image-for-comments-editor', _('Choose image'), formats='png jpeg jpg gif svg webp'.split())
+ if files:
+ self.focus_self()
+ with self.editing_cursor() as c:
+ c.insertImage(files[0])
+
def do_insert_link(self, *args):
link, name, is_image = self.ask_link()
if not link:
@@ -907,6 +916,8 @@ class EditorWidget(QTextEdit, LineEditECM): # {{{
menu.addMenu(am)
am.addAction(self.action_block_style)
am.addAction(self.action_insert_link)
+ if self.insert_images_separately:
+ am.addAction(self.action_insert_image)
am.addAction(self.action_background)
am.addAction(self.action_color)
menu.addAction(_('Smarten punctuation'), parent.smarten_punctuation)
@@ -1206,6 +1217,8 @@ class Editor(QWidget): # {{{
self.toolbar.add_action(self.editor.action_block_style, popup_mode=QToolButton.ToolButtonPopupMode.InstantPopup)
self.toolbar.add_action(self.editor.action_insert_link)
+ if self.editor.insert_images_separately:
+ self.toolbar.add_action(self.editor.action_insert_image)
self.toolbar.add_action(self.editor.action_insert_hr)
self.toolbar.add_separator()
diff --git a/src/calibre/gui2/dialogs/edit_category_notes.py b/src/calibre/gui2/dialogs/edit_category_notes.py
index 10f420c6fd..3556cc196d 100644
--- a/src/calibre/gui2/dialogs/edit_category_notes.py
+++ b/src/calibre/gui2/dialogs/edit_category_notes.py
@@ -3,13 +3,20 @@
import os
from qt.core import (
- QDialog, QFormLayout, QIcon, QLineEdit, QSize, Qt, QVBoxLayout, QWidget, pyqtSlot,
+ QButtonGroup, QByteArray, QDialog, QFormLayout, QHBoxLayout, QIcon, QLabel,
+ QLineEdit, QPixmap, QPushButton, QRadioButton, QSize, Qt, QTextFrameFormat,
+ QTextImageFormat, QVBoxLayout, QWidget, pyqtSlot,
)
+from typing import NamedTuple
-from calibre.gui2 import Application
+from calibre.db.notes.connect import RESOURCE_URL_SCHEME, hash_data
+from calibre.gui2 import Application, choose_images, error_dialog
from calibre.gui2.comments_editor import Editor, EditorWidget
+from calibre.gui2.widgets import ImageView
from calibre.gui2.widgets2 import Dialog
+IMAGE_EXTENSIONS = 'png', 'jpeg', 'jpg', 'gif', 'svg', 'webp'
+
class AskLink(Dialog): # {{{
@@ -47,18 +54,136 @@ class AskLink(Dialog): # {{{
# }}}
+# Images {{{
+class ImageResource(NamedTuple):
+ name: str
+ digest: str
+ path: str = ''
+ data: bytes = b''
+ from_db: bool = False
+
+
+class AskImage(Dialog):
+
+ def __init__(self, local_images, db, parent=None):
+ self.local_images = local_images
+ self.db = db
+ self.current_digest = ''
+ super().__init__(_('Insert image'), 'insert-image-for-notes', parent=parent)
+ self.setWindowIcon(QIcon.ic('view-image.png'))
+
+ def setup_ui(self):
+ self.v = v = QVBoxLayout(self)
+ self.h = h = QHBoxLayout()
+ v.addLayout(h)
+ v.addWidget(self.bb)
+
+ self.image_preview = ip = ImageView(self, 'insert-image-for-notes-preview', True)
+ ip.cover_changed.connect(self.image_pasted_or_dropped)
+ h.addWidget(ip)
+
+ self.vr = vr = QVBoxLayout()
+ h.addLayout(vr)
+
+ self.la = la = QLabel(_('Choose an image:'))
+ vr.addWidget(la)
+
+ self.name_edit = ne = QLineEdit(self)
+ ne.setPlaceholderText(_('Filename for the image'))
+ vr.addWidget(ne)
+
+ self.la2 = la = QLabel(_('Place image:'))
+ vr.addWidget(la)
+ self.hr = hr = QHBoxLayout()
+ vr.addLayout(hr)
+ self.image_layout_group = bg = QButtonGroup(self)
+ self.float_left = r = QRadioButton(_('Float &left'))
+ bg.addButton(r), hr.addWidget(r)
+ self.inline = r = QRadioButton(_('Inline'))
+ bg.addButton(r), hr.addWidget(r)
+ self.float_right = r = QRadioButton(_('Float &right'))
+ bg.addButton(r), hr.addWidget(r)
+ self.inline.setChecked(True)
+
+ self.hb = hb = QHBoxLayout()
+ vr.addLayout(hb)
+ self.add_file_button = b = QPushButton(QIcon.ic('document_open.png'), _('Choose image &file'), self)
+ b.clicked.connect(self.add_file)
+ hb.addWidget(b)
+ self.existing_button = b = QPushButton(QIcon.ic('view-image.png'), _('Browse &existing'), self)
+ b.clicked.connect(self.browse_existing)
+ hb.addWidget(b)
+ self.paste_button = b = QPushButton(QIcon.ic('edit-paste.png'), _('&Paste from clipboard'), self)
+ b.clicked.connect(self.paste_image)
+ hb.addWidget(b)
+
+ vr.addStretch(10)
+
+ def image_pasted_or_dropped(self, cover_data):
+ digest = hash_data(cover_data)
+ if digest in self.local_images:
+ ir = self.local_images[digest]
+ else:
+ self.local_images[digest] = ir = ImageResource('unnamed.png', digest, data=cover_data)
+ self.name_edit.setText(ir.name)
+ self.current_digest = digest
+
+ def browse_existing(self):
+ raise NotImplementedError('TODO: Implement me')
+
+ def add_file(self):
+ files = choose_images(self, 'choose-image-for-notes', _('Choose image'), formats=IMAGE_EXTENSIONS)
+ if files:
+ with open(files[0], 'rb') as f:
+ data = f.read()
+ digest = hash_data(data)
+ p = QPixmap()
+ if not p.loadFromData(data) or p.isNull():
+ return error_dialog(self, _('Bad image'), _(
+ 'Failed to render the image in {}').format(files[0]), show=True)
+ ir = ImageResource(os.path.basename(files[0]), digest, path=files[0])
+ self.local_images[digest] = ir
+ self.image_preview.set_pixmap(p)
+ self.name_edit.setText(ir.name)
+ self.current_digest = digest
+
+ def paste_image(self):
+ if not self.image_preview.paste_from_clipboard():
+ return error_dialog(self, _('Could not paste'), _(
+ 'No image is present int he system clipboard'), show=True)
+
+ @property
+ def image_layout(self) -> 'QTextFrameFormat.Position':
+ b = self.image_layout_group.checkedButton()
+ if b is self.inline:
+ return QTextFrameFormat.Position.InFlow
+ if b is self.float_left:
+ return QTextFrameFormat.Position.FloatLeft
+ return QTextFrameFormat.Position.FloatRight
+# }}}
+
+
+
class NoteEditorWidget(EditorWidget):
- load_resource = None
insert_images_separately = True
-
- def __init__(self, *args, **kw):
- super().__init__(*args, **kw)
+ db = field = item_id = item_val = None
+ images = None
@pyqtSlot(int, 'QUrl', result='QVariant')
def loadResource(self, rtype, qurl):
- if self.load_resource is not None:
- return self.load_resource(rtype, qurl)
+ if self.db is None or self.images is None:
+ return
+ if qurl.scheme() != RESOURCE_URL_SCHEME:
+ return
+ digest = qurl.path()[1:]
+ ir = self.images.get(digest)
+ if ir is not None:
+ if ir.data:
+ return QByteArray(ir.data)
+ if ir.path:
+ with open(ir.path, 'rb') as f:
+ return QByteArray(f.read())
def get_html_callback(self, root):
self.searchable_text = ''
@@ -72,6 +197,16 @@ class NoteEditorWidget(EditorWidget):
return d.url, d.link_name, False
return '', '', False
+ def do_insert_image(self):
+ d = AskImage(self.images, self.db)
+ if d.exec() == QDialog.DialogCode.Accepted and d.current_digest:
+ ir = self.images[d.current_digest]
+ self.focus_self()
+ with self.editing_cursor() as c:
+ fmt = QTextImageFormat()
+ fmt.setName(RESOURCE_URL_SCHEME + ':///' + ir.digest)
+ c.insertImage(fmt, d.image_layout)
+
class NoteEditor(Editor):
@@ -87,23 +222,21 @@ class EditNoteWidget(QWidget):
def __init__(self, db, field, item_id, item_val, parent=None):
super().__init__(parent)
- self.db, self.field, self.item_id, self.item_val = db, field, item_id, item_val
self.l = l = QVBoxLayout(self)
l.setContentsMargins(0, 0, 0, 0)
self.editor = e = NoteEditor(self, toolbar_prefs_name='edit-notes-for-category-ce')
- e.editor.load_resource = self.load_resource
+ e.editor.db, e.editor.field, e.editor.item_id, e.editor.item_val = db, field, item_id, item_val
+ e.editor.images = {}
l.addWidget(e)
- e.html = self.db.notes_for(field, item_id) or ''
-
- def load_resource(self, resource_type, qurl):
- pass
+ e.html = db.notes_for(field, item_id) or ''
def sizeHint(self):
return QSize(800, 600)
def commit(self):
doc, searchable_text, resources = self.editor.get_doc()
- self.db.set_notes_for(self.field, self.item_id, doc, searchable_text, resources, remove_unused_resources=True)
+ s = self.editor.editor
+ s.db.set_notes_for(s.field, s.item_id, doc, searchable_text, resources)
return True
@@ -130,9 +263,22 @@ class EditNoteDialog(Dialog):
super().accept()
-if __name__ == '__main__':
+def develop_edit_note():
from calibre.library import db as dbc
app = Application([])
d = EditNoteDialog('authors', 1, dbc(os.path.expanduser('~/test library')))
d.exec()
del d, app
+
+
+def develop_ask_image():
+ app = Application([])
+ from calibre.library import db as dbc
+ d = AskImage({},dbc(os.path.expanduser('~/test library')))
+ d.exec()
+ del d, app
+
+
+if __name__ == '__main__':
+ develop_edit_note()
+ # develop_ask_image()
diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py
index 3724271843..04857e377a 100644
--- a/src/calibre/gui2/widgets.py
+++ b/src/calibre/gui2/widgets.py
@@ -301,6 +301,8 @@ class ImageDropMixin: # {{{
self.set_pixmap(pmap)
self.cover_changed.emit(
pixmap_to_data(pmap, format='PNG'))
+ return True
+ return False
# }}}