Support pasting image to upload in the composer
This commit is contained in:
		@@ -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."""
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,14 +6,22 @@
 | 
			
		||||
#ifndef CLIPBOARD_H
 | 
			
		||||
#define CLIPBOARD_H
 | 
			
		||||
 | 
			
		||||
#include <QGuiApplication>
 | 
			
		||||
#include <QBuffer>
 | 
			
		||||
#include <QByteArray>
 | 
			
		||||
#include <QClipboard>
 | 
			
		||||
#include <QGuiApplication>
 | 
			
		||||
#include <QIODevice>
 | 
			
		||||
#include <QImage>
 | 
			
		||||
#include <QMimeData>
 | 
			
		||||
#include <QObject>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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 {}
 | 
			
		||||
 
 | 
			
		||||
@@ -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 => {
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@
 | 
			
		||||
// This file creates the application, registers custom objects for QML
 | 
			
		||||
// and launches Window.qml (the root component).
 | 
			
		||||
 | 
			
		||||
#include <QDataStream>  // must be first include to avoid clipboard.h errors
 | 
			
		||||
#include <QApplication>
 | 
			
		||||
#include <QQmlEngine>
 | 
			
		||||
#include <QQmlContext>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user