diff --git a/imports/Spectral/Dialog/qmldir b/imports/Spectral/Dialog/qmldir index df7f062..51a130a 100644 --- a/imports/Spectral/Dialog/qmldir +++ b/imports/Spectral/Dialog/qmldir @@ -11,3 +11,4 @@ FontFamilyDialog 2.0 FontFamilyDialog.qml AccountDetailDialog 2.0 AccountDetailDialog.qml OpenFileDialog 2.0 OpenFileDialog.qml OpenFolderDialog 2.0 OpenFolderDialog.qml +ImageClipboardDialog 2.0 ImageClipboardDialog.qml diff --git a/imports/Spectral/Panel/RoomListPanel.qml b/imports/Spectral/Panel/RoomListPanel.qml index a6125e7..254078a 100644 --- a/imports/Spectral/Panel/RoomListPanel.qml +++ b/imports/Spectral/Panel/RoomListPanel.qml @@ -31,7 +31,7 @@ Item { connection: root.connection - onNewMessage: if (!window.active && MSettings.showNotification) spectralController.postNotification(roomId, eventId, roomName, senderName, text, icon) + onNewMessage: if (!window.active && MSettings.showNotification) notificationsManager.postNotification(roomId, eventId, roomName, senderName, text, icon) } SortFilterProxyModel { diff --git a/imports/Spectral/Panel/RoomPanel.qml b/imports/Spectral/Panel/RoomPanel.qml index 185e4bd..7cb1c48 100644 --- a/imports/Spectral/Panel/RoomPanel.qml +++ b/imports/Spectral/Panel/RoomPanel.qml @@ -3,10 +3,12 @@ import QtQuick.Controls 2.12 import QtQuick.Layouts 1.12 import QtQuick.Controls.Material 2.12 import Qt.labs.qmlmodels 1.0 +import Qt.labs.platform 1.0 import Spectral.Component 2.0 import Spectral.Component.Emoji 2.0 import Spectral.Component.Timeline 2.0 +import Spectral.Dialog 2.0 import Spectral.Effect 2.0 import Spectral 0.1 @@ -31,10 +33,113 @@ Item { onDropped: { if (!drop.hasUrls) return - currentRoom.uploadFile(drop.urls[0]) + roomPanelInput.attach(drop.urls[0]) } } + ImageClipboard { + id: imageClipboard + } + + Popup { + anchors.centerIn: parent + + id: attachDialog + + padding: 16 + + contentItem: RowLayout { + Control { + Layout.preferredWidth: 160 + Layout.fillHeight: true + + padding: 16 + + contentItem: ColumnLayout { + spacing: 16 + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + + icon: "\ue2c8" + font.pixelSize: 64 + color: MPalette.lighter + } + + Label { + Layout.alignment: Qt.AlignHCenter + + text: "Choose local file" + } + } + + background: RippleEffect { + onClicked: { + attachDialog.close() + + var fileDialog = openFileDialog.createObject(ApplicationWindow.overlay) + + fileDialog.chosen.connect(function(path) { + if (!path) return + + roomPanelInput.attach(path) + }) + + fileDialog.open() + } + } + } + + Rectangle { + Layout.preferredWidth: 1 + Layout.fillHeight: true + + color: MPalette.banner + } + + Control { + Layout.preferredWidth: 160 + Layout.fillHeight: true + + padding: 16 + + contentItem: ColumnLayout { + spacing: 16 + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + + icon: "\ue410" + font.pixelSize: 64 + color: MPalette.lighter + } + + Label { + Layout.alignment: Qt.AlignHCenter + + text: "Clipboard image" + color: MPalette.foreground + } + } + + background: RippleEffect { + onClicked: { + var localPath = StandardPaths.writableLocation(StandardPaths.CacheLocation) + "/screenshots/" + (new Date()).getTime() + ".png" + imageClipboard.saveImage(localPath) + roomPanelInput.attach(localPath) + attachDialog.close() + } + } + } + } + } + + Component { + id: openFileDialog + + OpenFileDialog {} + } + Column { anchors.centerIn: parent diff --git a/imports/Spectral/Panel/RoomPanelInput.qml b/imports/Spectral/Panel/RoomPanelInput.qml index 6dc897d..41bbd72 100644 --- a/imports/Spectral/Panel/RoomPanelInput.qml +++ b/imports/Spectral/Panel/RoomPanelInput.qml @@ -5,6 +5,7 @@ import QtQuick.Controls.Material 2.12 import Spectral.Component 2.0 import Spectral.Component.Emoji 2.0 +import Spectral.Dialog 2.0 import Spectral.Effect 2.0 import Spectral.Setting 0.1 @@ -21,6 +22,9 @@ Control { property int autoCompleteBeginPosition property int autoCompleteEndPosition + property bool hasAttachment: false + property url attachmentPath + id: root padding: 0 @@ -171,13 +175,13 @@ Control { Layout.alignment: Qt.AlignBottom id: uploadButton - visible: !isReply + visible: !isReply && !hasAttachment contentItem: MaterialIcon { icon: "\ue226" } - onClicked: currentRoom.chooseAndUploadFile() + onClicked: attachDialog.open() BusyIndicator { anchors.fill: parent @@ -202,6 +206,51 @@ Control { onClicked: clearReply() } + Control { + Layout.margins: 6 + Layout.preferredHeight: 36 + Layout.alignment: Qt.AlignVCenter + + visible: hasAttachment + + rightPadding: 8 + + background: Rectangle { + color: MPalette.accent + radius: height / 2 + antialiasing: true + } + + contentItem: RowLayout { + spacing: 0 + + ToolButton { + Layout.preferredWidth: height + Layout.fillHeight: true + + id: cancelAttachmentButton + + contentItem: MaterialIcon { + icon: "\ue5cd" + color: "white" + font.pixelSize: 18 + } + + onClicked: { + hasAttachment = false + attachmentPath = "" + } + } + + Label { + Layout.alignment: Qt.AlignVCenter + + text: attachmentPath != "" ? attachmentPath.toString().substring(attachmentPath.toString().lastIndexOf('/') + 1, attachmentPath.length) : "" + color: "white" + } + } + } + TextArea { property real progress: 0 @@ -307,6 +356,12 @@ Control { if (text.trim().length === 0) { return } if(!currentRoom) { return } + if (hasAttachment) { + currentRoom.uploadFile(attachmentPath, text) + clearAttachment() + return + } + var PREFIX_ME = '/me ' var PREFIX_NOTICE = '/notice ' var PREFIX_RAINBOW = '/rainbow ' @@ -372,6 +427,10 @@ Control { } } + ImageClipboard { + id: imageClipboard + } + function insert(str) { inputField.insert(inputField.cursorPosition, str) } @@ -396,4 +455,14 @@ Control { autoCompleteListView.visible = false emojiPicker.visible = false } + + function attach(localPath) { + hasAttachment = true + attachmentPath = localPath + } + + function clearAttachment() { + hasAttachment = false + attachmentPath = "" + } } diff --git a/qml/main.qml b/qml/main.qml index 350b241..e28743d 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -56,12 +56,17 @@ ApplicationWindow { quitOnLastWindowClosed: !MSettings.showTray + onErrorOccured: errorControl.show(error + ": " + detail, 3000) + } + + NotificationsManager { + id: notificationsManager + onNotificationClicked: { roomListForm.enteredRoom = spectralController.connection.room(roomId) roomForm.goToEvent(eventId) showWindow() } - onErrorOccured: errorControl.show(error + ": " + detail, 3000) } Shortcut { diff --git a/spectral.pro b/spectral.pro index fe7103f..91e9392 100644 --- a/spectral.pro +++ b/spectral.pro @@ -42,7 +42,9 @@ HEADERS += \ include/hoedown/escape.h \ include/hoedown/html.h \ include/hoedown/stack.h \ - include/hoedown/version.h + include/hoedown/version.h \ + src/imageclipboard.h \ + src/matriximageprovider.h SOURCES += \ include/hoedown/autolink.c \ @@ -53,7 +55,9 @@ SOURCES += \ include/hoedown/html_blocks.c \ include/hoedown/html_smartypants.c \ include/hoedown/stack.c \ - include/hoedown/version.c + include/hoedown/version.c \ + src/imageclipboard.cpp \ + src/matriximageprovider.cpp # The following define makes your compiler emit warnings if you use # any feature of Qt which as been marked deprecated (the exact warnings @@ -117,7 +121,6 @@ mac { HEADERS += \ src/controller.h \ src/roomlistmodel.h \ - src/imageprovider.h \ src/messageeventmodel.h \ src/emojimodel.h \ src/spectralroom.h \ @@ -130,7 +133,6 @@ HEADERS += \ SOURCES += src/main.cpp \ src/controller.cpp \ src/roomlistmodel.cpp \ - src/imageprovider.cpp \ src/messageeventmodel.cpp \ src/emojimodel.cpp \ src/spectralroom.cpp \ diff --git a/src/controller.cpp b/src/controller.cpp index d305f38..4f52ebd 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -31,13 +31,9 @@ #include #include -Controller::Controller(QObject* parent) - : QObject(parent), notificationsManager(this) { +Controller::Controller(QObject* parent) : QObject(parent) { QApplication::setQuitOnLastWindowClosed(false); - connect(¬ificationsManager, &NotificationsManager::notificationClicked, - this, &Controller::notificationClicked); - Connection::setRoomType(); Connection::setUserType(); @@ -232,10 +228,6 @@ void Controller::createDirectChat(Connection* c, const QString& userID) { }); } -void Controller::copyToClipboard(const QString& text) { - m_clipboard->setText(text); -} - void Controller::playAudio(QUrl localFile) { QMediaPlayer* player = new QMediaPlayer; player->setMedia(localFile); @@ -243,16 +235,6 @@ void Controller::playAudio(QUrl localFile) { connect(player, &QMediaPlayer::stateChanged, [=] { player->deleteLater(); }); } -void Controller::postNotification(const QString& roomId, - const QString& eventId, - const QString& roomName, - const QString& senderName, - const QString& text, - const QImage& icon) { - notificationsManager.postNotification(roomId, eventId, roomName, senderName, - text, icon); -} - int Controller::dpi() { return SettingsGroup("Interface").value("dpi", 100).toInt(); } diff --git a/src/controller.h b/src/controller.h index 2e73c21..cb25b1b 100644 --- a/src/controller.h +++ b/src/controller.h @@ -67,8 +67,6 @@ class Controller : public QObject { } private: - QClipboard* m_clipboard = QApplication::clipboard(); - NotificationsManager notificationsManager; QVector m_connections; QPointer m_connection; @@ -99,14 +97,7 @@ class Controller : public QObject { void joinRoom(Connection* c, const QString& alias); void createRoom(Connection* c, const QString& name, const QString& topic); void createDirectChat(Connection* c, const QString& userID); - void copyToClipboard(const QString& text); void playAudio(QUrl localFile); - void postNotification(const QString& roomId, - const QString& eventId, - const QString& roomName, - const QString& senderName, - const QString& text, - const QImage& icon); }; #endif // CONTROLLER_H diff --git a/src/imageclipboard.cpp b/src/imageclipboard.cpp new file mode 100644 index 0000000..deb00ed --- /dev/null +++ b/src/imageclipboard.cpp @@ -0,0 +1,26 @@ +#include "imageclipboard.h" + +#include +#include + +ImageClipboard::ImageClipboard(QObject* parent) + : QObject(parent), m_clipboard(QGuiApplication::clipboard()) { + connect(m_clipboard, &QClipboard::changed, this, + &ImageClipboard::imageChanged); +} + +bool ImageClipboard::hasImage() { + return !image().isNull(); +} + +QImage ImageClipboard::image() { + return m_clipboard->image(); +} + +void ImageClipboard::saveImage(const QUrl& localPath) { + auto i = image(); + + if (i.isNull()) return; + + i.save(localPath.toString()); +} diff --git a/src/imageclipboard.h b/src/imageclipboard.h new file mode 100644 index 0000000..d548c7e --- /dev/null +++ b/src/imageclipboard.h @@ -0,0 +1,29 @@ +#ifndef IMAGECLIPBOARD_H +#define IMAGECLIPBOARD_H + +#include +#include +#include + +class ImageClipboard : public QObject { + Q_OBJECT + Q_PROPERTY(bool hasImage READ hasImage NOTIFY imageChanged) + Q_PROPERTY(QImage image READ image NOTIFY imageChanged) + + public: + explicit ImageClipboard(QObject* parent = nullptr); + + bool hasImage(); + QImage image(); + + private: + QClipboard* m_clipboard; + + signals: + void imageChanged(); + + public slots: + void saveImage(const QUrl& localPath); +}; + +#endif // IMAGECLIPBOARD_H diff --git a/src/main.cpp b/src/main.cpp index 60727f3..b6ee77f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -8,8 +8,10 @@ #include "accountlistmodel.h" #include "controller.h" #include "emojimodel.h" -#include "imageprovider.h" +#include "imageclipboard.h" +#include "matriximageprovider.h" #include "messageeventmodel.h" +#include "notifications/manager.h" #include "room.h" #include "roomlistmodel.h" #include "spectralroom.h" @@ -55,6 +57,9 @@ int main(int argc, char* argv[]) { qmlRegisterType("Spectral", 0, 1, "UserListModel"); qmlRegisterType("Spectral", 0, 1, "MessageEventModel"); qmlRegisterType("Spectral", 0, 1, "EmojiModel"); + qmlRegisterType("Spectral", 0, 1, + "NotificationsManager"); + qmlRegisterType("Spectral", 0, 1, "ImageClipboard"); qmlRegisterUncreatableType("Spectral", 0, 1, "RoomMessageEvent", "ENUM"); qmlRegisterUncreatableType("Spectral", 0, 1, "RoomType", "ENUM"); @@ -72,9 +77,10 @@ int main(int argc, char* argv[]) { QQmlApplicationEngine engine; engine.addImportPath("qrc:/imports"); - ImageProvider* m_provider = new ImageProvider(); - engine.rootContext()->setContextProperty("imageProvider", m_provider); - engine.addImageProvider(QLatin1String("mxc"), m_provider); + MatrixImageProvider* matrixImageProvider = new MatrixImageProvider(); + engine.rootContext()->setContextProperty("imageProvider", + matrixImageProvider); + engine.addImageProvider(QLatin1String("mxc"), matrixImageProvider); engine.load(QUrl(QStringLiteral("qrc:/qml/main.qml"))); if (engine.rootObjects().isEmpty()) diff --git a/src/imageprovider.cpp b/src/matriximageprovider.cpp similarity index 97% rename from src/imageprovider.cpp rename to src/matriximageprovider.cpp index be98de2..13c4ca5 100644 --- a/src/imageprovider.cpp +++ b/src/matriximageprovider.cpp @@ -1,4 +1,4 @@ -#include "imageprovider.h" +#include "matriximageprovider.h" #include #include @@ -108,7 +108,7 @@ void ThumbnailResponse::cancel() { Qt::QueuedConnection); } -QQuickImageResponse* ImageProvider::requestImageResponse( +QQuickImageResponse* MatrixImageProvider::requestImageResponse( const QString& id, const QSize& requestedSize) { return new ThumbnailResponse(m_connection.load(), id, requestedSize); diff --git a/src/imageprovider.h b/src/matriximageprovider.h similarity index 87% rename from src/imageprovider.h rename to src/matriximageprovider.h index ed31e94..980ed3f 100644 --- a/src/imageprovider.h +++ b/src/matriximageprovider.h @@ -1,5 +1,5 @@ -#ifndef IMAGEPROVIDER_H -#define IMAGEPROVIDER_H +#ifndef MatrixImageProvider_H +#define MatrixImageProvider_H #pragma once #include @@ -43,12 +43,12 @@ class ThumbnailResponse : public QQuickImageResponse { void cancel() override; }; -class ImageProvider : public QObject, public QQuickAsyncImageProvider { +class MatrixImageProvider : public QObject, public QQuickAsyncImageProvider { Q_OBJECT Q_PROPERTY(QMatrixClient::Connection* connection READ connection WRITE setConnection NOTIFY connectionChanged) public: - explicit ImageProvider() = default; + explicit MatrixImageProvider() = default; QQuickImageResponse* requestImageResponse( const QString& id, @@ -67,4 +67,4 @@ class ImageProvider : public QObject, public QQuickAsyncImageProvider { QAtomicPointer m_connection; }; -#endif // IMAGEPROVIDER_H +#endif // MatrixImageProvider_H diff --git a/src/notifications/manager.h b/src/notifications/manager.h index 0115713..5e7888f 100644 --- a/src/notifications/manager.h +++ b/src/notifications/manager.h @@ -19,11 +19,7 @@ struct roomEventId { class NotificationsManager : public QObject { Q_OBJECT public: - NotificationsManager(QObject *parent = nullptr); - - void postNotification(const QString &roomId, const QString &eventId, - const QString &roomName, const QString &senderName, - const QString &text, const QImage &icon); + NotificationsManager(QObject* parent = nullptr); signals: void notificationClicked(const QString roomId, const QString eventId); @@ -31,7 +27,8 @@ class NotificationsManager : public QObject { private: #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) QDBusInterface dbus; - uint showNotification(const QString summary, const QString text, + uint showNotification(const QString summary, + const QString text, const QImage image); #endif @@ -43,9 +40,16 @@ class NotificationsManager : public QObject { public slots: void actionInvoked(uint id, QString action); void notificationClosed(uint id, uint reason); + + void postNotification(const QString& roomId, + const QString& eventId, + const QString& roomName, + const QString& senderName, + const QString& text, + const QImage& icon); }; #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) -QDBusArgument &operator<<(QDBusArgument &arg, const QImage &image); -const QDBusArgument &operator>>(const QDBusArgument &arg, QImage &); +QDBusArgument& operator<<(QDBusArgument& arg, const QImage& image); +const QDBusArgument& operator>>(const QDBusArgument& arg, QImage&); #endif diff --git a/src/spectralroom.cpp b/src/spectralroom.cpp index 63b3f33..ddbdb95 100644 --- a/src/spectralroom.cpp +++ b/src/spectralroom.cpp @@ -44,18 +44,11 @@ inline QSize getImageSize(const QUrl& imageUrl) { return reader.size(); } -void SpectralRoom::chooseAndUploadFile() { - auto localFile = QFileDialog::getOpenFileUrl(Q_NULLPTR, tr("Save File as")); - if (!localFile.isEmpty()) { - uploadFile(localFile); - } -} - -void SpectralRoom::uploadFile(const QUrl& url) { +void SpectralRoom::uploadFile(const QUrl& url, const QString& body) { if (url.isEmpty()) return; - QString txnID = postFile(url.fileName(), url, false); + QString txnID = postFile(body.isEmpty() ? url.fileName() : body, url, false); setHasFileUploading(true); connect(this, &Room::fileTransferCompleted, [=](QString id, QUrl localFile, QUrl mxcUrl) { diff --git a/src/spectralroom.h b/src/spectralroom.h index 72cc487..10d7d37 100644 --- a/src/spectralroom.h +++ b/src/spectralroom.h @@ -254,8 +254,7 @@ class SpectralRoom : public Room { void fileUploadingProgressChanged(); public slots: - void chooseAndUploadFile(); - void uploadFile(const QUrl& url); + void uploadFile(const QUrl& url, const QString& body = ""); void acceptInvitation(); void forget(); void sendTypingNotification(bool isTyping);