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 # }}}