Support pasting image to upload in the composer

This commit is contained in:
miruka 2020-07-15 15:10:34 -04:00
parent 2449fd5f18
commit 2d623118b5
6 changed files with 93 additions and 11 deletions

View File

@ -16,6 +16,7 @@ from copy import deepcopy
from datetime import datetime, timedelta from datetime import datetime, timedelta
from functools import partial from functools import partial
from pathlib import Path from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import ( from typing import (
TYPE_CHECKING, Any, ClassVar, Dict, List, NamedTuple, Optional, Set, Tuple, TYPE_CHECKING, Any, ClassVar, Dict, List, NamedTuple, Optional, Set, Tuple,
Type, Union, Type, Union,
@ -23,6 +24,7 @@ from typing import (
from urllib.parse import urlparse from urllib.parse import urlparse
from uuid import UUID, uuid4 from uuid import UUID, uuid4
import aiofiles
import cairosvg import cairosvg
from PIL import Image as PILImage from PIL import Image as PILImage
from pymediainfo import MediaInfo from pymediainfo import MediaInfo
@ -40,7 +42,7 @@ from .errors import (
from .html_markdown import HTML_PROCESSOR as HTML from .html_markdown import HTML_PROCESSOR as HTML
from .media_cache import Media, Thumbnail from .media_cache import Media, Thumbnail
from .models.items import ( 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 .models.model_store import ModelStore
from .nio_callbacks import NioCallbacks from .nio_callbacks import NioCallbacks
@ -557,6 +559,18 @@ class MatrixClient(nio.AsyncClient):
self.upload_tasks[uuid].cancel() 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: 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.""" """Send a `m.file`, `m.image`, `m.audio` or `m.video` message."""

View File

@ -6,14 +6,22 @@
#ifndef CLIPBOARD_H #ifndef CLIPBOARD_H
#define CLIPBOARD_H #define CLIPBOARD_H
#include <QGuiApplication> #include <QBuffer>
#include <QByteArray>
#include <QClipboard> #include <QClipboard>
#include <QGuiApplication>
#include <QIODevice>
#include <QImage>
#include <QMimeData>
#include <QObject> #include <QObject>
class Clipboard : public QObject { class Clipboard : public QObject {
Q_OBJECT Q_OBJECT
Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged) 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 Q_PROPERTY(QString selection READ selection WRITE setSelection
NOTIFY selectionChanged) NOTIFY selectionChanged)
@ -22,7 +30,7 @@ class Clipboard : public QObject {
public: public:
explicit Clipboard(QObject *parent = nullptr) : QObject(parent) { explicit Clipboard(QObject *parent = nullptr) : QObject(parent) {
connect(this->clipboard, &QClipboard::dataChanged, connect(this->clipboard, &QClipboard::dataChanged,
this, &Clipboard::textChanged); this, &Clipboard::mainClipboardChanged);
connect(this->clipboard, &QClipboard::selectionChanged, connect(this->clipboard, &QClipboard::selectionChanged,
this, &Clipboard::selectionChanged); this, &Clipboard::selectionChanged);
@ -38,6 +46,23 @@ public:
this->clipboard->setText(text, QClipboard::Clipboard); 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 // X11 select-middle-click-paste clipboard
QString selection() const { QString selection() const {
@ -58,10 +83,17 @@ public:
signals: signals:
void textChanged(); void textChanged();
void imageChanged();
void hasImageChanged();
void selectionChanged(); void selectionChanged();
private: private:
QClipboard *clipboard = QGuiApplication::clipboard(); QClipboard *clipboard = QGuiApplication::clipboard();
void mainClipboardChanged() {
this->hasImage() ? this->imageChanged() : this->textChanged();
this->hasImageChanged();
};
}; };
#endif #endif

View File

@ -2,6 +2,7 @@
import QtQuick 2.12 import QtQuick 2.12
import QtQuick.Controls 2.12 import QtQuick.Controls 2.12
import Clipboard 0.1
TextArea { TextArea {
id: textArea id: textArea
@ -26,6 +27,10 @@ TextArea {
property string previousDefaultText: "" // private property string previousDefaultText: "" // private
property bool enableCustomImagePaste: false
signal customImagePaste()
function reset() { clear(); text = Qt.binding(() => defaultText || "") } function reset() { clear(); text = Qt.binding(() => defaultText || "") }
function insertAtCursor(text) { insert(cursorPosition, text) } function insertAtCursor(text) { insert(cursorPosition, text) }
@ -85,11 +90,22 @@ TextArea {
previousDefaultText = defaultText previousDefaultText = defaultText
} }
Keys.onPressed: ev => {
// Prevent alt/super+any key from typing text // Prevent alt/super+any key from typing text
Keys.onPressed: if ( if (
event.modifiers & Qt.AltModifier || ev.modifiers & Qt.AltModifier ||
event.modifiers & Qt.MetaModifier ev.modifiers & Qt.MetaModifier
) event.accepted = true ) ev.accepted = true
if (
ev.matches(StandardKey.Paste) &&
textArea.enableCustomImagePaste &&
Clipboard.hasImage
) {
ev.accepted = true
textArea.customImagePaste()
}
}
Keys.onMenuPressed: contextMenu.spawn(false) Keys.onMenuPressed: contextMenu.spawn(false)
@ -159,5 +175,9 @@ TextArea {
onLongPressed: contextMenu.spawn() onLongPressed: contextMenu.spawn()
} }
HTextContextMenu { id: contextMenu } HTextContextMenu {
id: contextMenu
enableCustomImagePaste: textArea.enableCustomImagePaste
onCustomImagePaste: print("foo") || textArea.customImagePaste()
}
} }

View File

@ -1,12 +1,18 @@
// SPDX-License-Identifier: LGPL-3.0-or-later // SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12 import QtQuick 2.12
import Clipboard 0.1
HMenu { HMenu {
id: menu
property Item control: parent // HTextField or HTextArea property Item control: parent // HTextField or HTextArea
property bool enableCustomImagePaste: false
property bool hadPersistentSelection: false // TODO: use a Qt 5.15 Binding property bool hadPersistentSelection: false // TODO: use a Qt 5.15 Binding
signal customImagePaste()
function spawn(atMousePosition=true) { function spawn(atMousePosition=true) {
hadPersistentSelection = control.persistentSelection hadPersistentSelection = control.persistentSelection
control.persistentSelection = true control.persistentSelection = true
@ -55,10 +61,13 @@ HMenu {
} }
HMenuItem { HMenuItem {
property bool pasteImage:
menu.enableCustomImagePaste && Clipboard.hasImage
icon.name: "paste-text" icon.name: "paste-text"
text: qsTr("Paste") text: qsTr("Paste")
enabled: control.canPaste enabled: control.canPaste || pasteImage
onTriggered: control.paste() onTriggered: pasteImage ? menu.customImagePaste() : control.paste()
} }
HMenuSeparator {} HMenuSeparator {}

View File

@ -1,6 +1,7 @@
// SPDX-License-Identifier: LGPL-3.0-or-later // SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12 import QtQuick 2.12
import Clipboard 0.1
import "../../.." import "../../.."
import "../../../Base" import "../../../Base"
@ -93,6 +94,7 @@ HTextArea {
enabled: chat.roomInfo.can_send_messages enabled: chat.roomInfo.can_send_messages
disabledText: qsTr("You do not have permission to post in this room") disabledText: qsTr("You do not have permission to post in this room")
placeholderText: qsTr("Type a message...") placeholderText: qsTr("Type a message...")
enableCustomImagePaste: true
backgroundColor: "transparent" backgroundColor: "transparent"
focusedBorderColor: "transparent" focusedBorderColor: "transparent"
@ -159,6 +161,10 @@ HTextArea {
} }
} }
onCustomImagePaste: py.callClientCoro(
writingUserId, "send_clipboard_image", [chat.roomId, Clipboard.image],
)
Keys.onEscapePressed: clearReplyTo() Keys.onEscapePressed: clearReplyTo()
Keys.onReturnPressed: ev => { Keys.onReturnPressed: ev => {

View File

@ -3,6 +3,7 @@
// This file creates the application, registers custom objects for QML // This file creates the application, registers custom objects for QML
// and launches Window.qml (the root component). // and launches Window.qml (the root component).
#include <QDataStream> // must be first include to avoid clipboard.h errors
#include <QApplication> #include <QApplication>
#include <QQmlEngine> #include <QQmlEngine>
#include <QQmlContext> #include <QQmlContext>