From 2d623118b54d2f5a9fb6def3742afed4c4164dd4 Mon Sep 17 00:00:00 2001 From: miruka Date: Wed, 15 Jul 2020 15:10:34 -0400 Subject: [PATCH] Support pasting image to upload in the composer --- src/backend/matrix_client.py | 16 ++++++++- src/clipboard.h | 36 +++++++++++++++++++-- src/gui/Base/HTextArea.qml | 32 ++++++++++++++---- src/gui/Base/HTextContextMenu.qml | 13 ++++++-- src/gui/Pages/Chat/Composer/MessageArea.qml | 6 ++++ src/main.cpp | 1 + 6 files changed, 93 insertions(+), 11 deletions(-) diff --git a/src/backend/matrix_client.py b/src/backend/matrix_client.py index 154a727f..eccb7a42 100644 --- a/src/backend/matrix_client.py +++ b/src/backend/matrix_client.py @@ -16,6 +16,7 @@ from copy import deepcopy from datetime import datetime, timedelta from functools import partial from pathlib import Path +from tempfile import NamedTemporaryFile from typing import ( TYPE_CHECKING, Any, ClassVar, Dict, List, NamedTuple, Optional, Set, Tuple, Type, Union, @@ -23,6 +24,7 @@ from typing import ( from urllib.parse import urlparse from uuid import UUID, uuid4 +import aiofiles import cairosvg from PIL import Image as PILImage from pymediainfo import MediaInfo @@ -40,7 +42,7 @@ from .errors import ( from .html_markdown import HTML_PROCESSOR as HTML from .media_cache import Media, Thumbnail from .models.items import ( - Account, Event, Member, Presence, Room, Upload, UploadStatus, ZERO_DATE, + ZERO_DATE, Account, Event, Member, Presence, Room, Upload, UploadStatus, ) from .models.model_store import ModelStore from .nio_callbacks import NioCallbacks @@ -557,6 +559,18 @@ class MatrixClient(nio.AsyncClient): self.upload_tasks[uuid].cancel() + async def send_clipboard_image(self, room_id: str, image: bytes) -> None: + """Send a clipboard image passed from QML as a `m.image` message.""" + + prefix = datetime.now().strftime("%Y%m%d-%H%M%S.") + + with NamedTemporaryFile(prefix=prefix, suffix=".png") as temp: + async with aiofiles.open(temp.name, "wb") as file: + await file.write(image) + + await self.send_file(room_id, temp.name) + + async def send_file(self, room_id: str, path: Union[Path, str]) -> None: """Send a `m.file`, `m.image`, `m.audio` or `m.video` message.""" diff --git a/src/clipboard.h b/src/clipboard.h index 372974be..fcb2fe14 100644 --- a/src/clipboard.h +++ b/src/clipboard.h @@ -6,14 +6,22 @@ #ifndef CLIPBOARD_H #define CLIPBOARD_H -#include +#include +#include #include +#include +#include +#include +#include #include class Clipboard : public QObject { Q_OBJECT Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged) + Q_PROPERTY(QByteArray image READ image WRITE setImage NOTIFY imageChanged) + Q_PROPERTY(bool hasImage READ hasImage NOTIFY hasImageChanged) + Q_PROPERTY(QString selection READ selection WRITE setSelection NOTIFY selectionChanged) @@ -22,7 +30,7 @@ class Clipboard : public QObject { public: explicit Clipboard(QObject *parent = nullptr) : QObject(parent) { connect(this->clipboard, &QClipboard::dataChanged, - this, &Clipboard::textChanged); + this, &Clipboard::mainClipboardChanged); connect(this->clipboard, &QClipboard::selectionChanged, this, &Clipboard::selectionChanged); @@ -38,6 +46,23 @@ public: this->clipboard->setText(text, QClipboard::Clipboard); } + QByteArray image() const { + QByteArray byteArray; + QBuffer buffer(&byteArray); + buffer.open(QIODevice::WriteOnly); + this->clipboard->image(QClipboard::Clipboard).save(&buffer, "PNG"); + buffer.close(); + return byteArray; + } + + void setImage(const QByteArray &image) const { // TODO + Q_UNUSED(image) + } + + bool hasImage() const { + return this->clipboard->mimeData()->hasImage(); + } + // X11 select-middle-click-paste clipboard QString selection() const { @@ -58,10 +83,17 @@ public: signals: void textChanged(); + void imageChanged(); + void hasImageChanged(); void selectionChanged(); private: QClipboard *clipboard = QGuiApplication::clipboard(); + + void mainClipboardChanged() { + this->hasImage() ? this->imageChanged() : this->textChanged(); + this->hasImageChanged(); + }; }; #endif diff --git a/src/gui/Base/HTextArea.qml b/src/gui/Base/HTextArea.qml index daac70e1..f71a8768 100644 --- a/src/gui/Base/HTextArea.qml +++ b/src/gui/Base/HTextArea.qml @@ -2,6 +2,7 @@ import QtQuick 2.12 import QtQuick.Controls 2.12 +import Clipboard 0.1 TextArea { id: textArea @@ -26,6 +27,10 @@ TextArea { property string previousDefaultText: "" // private + property bool enableCustomImagePaste: false + + signal customImagePaste() + function reset() { clear(); text = Qt.binding(() => defaultText || "") } function insertAtCursor(text) { insert(cursorPosition, text) } @@ -85,11 +90,22 @@ TextArea { previousDefaultText = defaultText } - // Prevent alt/super+any key from typing text - Keys.onPressed: if ( - event.modifiers & Qt.AltModifier || - event.modifiers & Qt.MetaModifier - ) event.accepted = true + Keys.onPressed: ev => { + // Prevent alt/super+any key from typing text + if ( + ev.modifiers & Qt.AltModifier || + ev.modifiers & Qt.MetaModifier + ) ev.accepted = true + + if ( + ev.matches(StandardKey.Paste) && + textArea.enableCustomImagePaste && + Clipboard.hasImage + ) { + ev.accepted = true + textArea.customImagePaste() + } + } Keys.onMenuPressed: contextMenu.spawn(false) @@ -159,5 +175,9 @@ TextArea { onLongPressed: contextMenu.spawn() } - HTextContextMenu { id: contextMenu } + HTextContextMenu { + id: contextMenu + enableCustomImagePaste: textArea.enableCustomImagePaste + onCustomImagePaste: print("foo") || textArea.customImagePaste() + } } diff --git a/src/gui/Base/HTextContextMenu.qml b/src/gui/Base/HTextContextMenu.qml index 3e378bb7..2930d962 100644 --- a/src/gui/Base/HTextContextMenu.qml +++ b/src/gui/Base/HTextContextMenu.qml @@ -1,12 +1,18 @@ // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 +import Clipboard 0.1 HMenu { + id: menu + property Item control: parent // HTextField or HTextArea + property bool enableCustomImagePaste: false property bool hadPersistentSelection: false // TODO: use a Qt 5.15 Binding + signal customImagePaste() + function spawn(atMousePosition=true) { hadPersistentSelection = control.persistentSelection control.persistentSelection = true @@ -55,10 +61,13 @@ HMenu { } HMenuItem { + property bool pasteImage: + menu.enableCustomImagePaste && Clipboard.hasImage + icon.name: "paste-text" text: qsTr("Paste") - enabled: control.canPaste - onTriggered: control.paste() + enabled: control.canPaste || pasteImage + onTriggered: pasteImage ? menu.customImagePaste() : control.paste() } HMenuSeparator {} diff --git a/src/gui/Pages/Chat/Composer/MessageArea.qml b/src/gui/Pages/Chat/Composer/MessageArea.qml index 6975ee8e..2df54c80 100644 --- a/src/gui/Pages/Chat/Composer/MessageArea.qml +++ b/src/gui/Pages/Chat/Composer/MessageArea.qml @@ -1,6 +1,7 @@ // SPDX-License-Identifier: LGPL-3.0-or-later import QtQuick 2.12 +import Clipboard 0.1 import "../../.." import "../../../Base" @@ -93,6 +94,7 @@ HTextArea { enabled: chat.roomInfo.can_send_messages disabledText: qsTr("You do not have permission to post in this room") placeholderText: qsTr("Type a message...") + enableCustomImagePaste: true backgroundColor: "transparent" focusedBorderColor: "transparent" @@ -159,6 +161,10 @@ HTextArea { } } + onCustomImagePaste: py.callClientCoro( + writingUserId, "send_clipboard_image", [chat.roomId, Clipboard.image], + ) + Keys.onEscapePressed: clearReplyTo() Keys.onReturnPressed: ev => { diff --git a/src/main.cpp b/src/main.cpp index 6f2ca7fb..7612edf3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,6 +3,7 @@ // This file creates the application, registers custom objects for QML // and launches Window.qml (the root component). +#include // must be first include to avoid clipboard.h errors #include #include #include