Support pasting image to upload in the composer
This commit is contained in:
parent
2449fd5f18
commit
2d623118b5
@ -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."""
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prevent alt/super+any key from typing text
|
Keys.onPressed: ev => {
|
||||||
Keys.onPressed: if (
|
// Prevent alt/super+any key from typing text
|
||||||
event.modifiers & Qt.AltModifier ||
|
if (
|
||||||
event.modifiers & Qt.MetaModifier
|
ev.modifiers & Qt.AltModifier ||
|
||||||
) event.accepted = true
|
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)
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {}
|
||||||
|
@ -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 => {
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user