From 87833a8458b4a963d9749543aa1658b47afbe054 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Mon, 7 Dec 2020 09:58:03 +0000 Subject: [PATCH] Add an image editor --- CMakeLists.txt | 10 + imports/NeoChat/Component/ChatTextInput.qml | 115 ++++++++---- imports/NeoChat/Page/ImageEditorPage.qml | 191 ++++++++++++++++++++ imports/NeoChat/Page/qmldir | 1 + qml/main.qml | 5 +- res.qrc | 1 + src/CMakeLists.txt | 4 + 7 files changed, 291 insertions(+), 36 deletions(-) create mode 100644 imports/NeoChat/Page/ImageEditorPage.qml diff --git a/CMakeLists.txt b/CMakeLists.txt index a53b17d72..befcd07a0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,6 +15,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) include(FeatureSummary) include(ECMSetupVersion) include(KDEInstallDirs) +include(ECMQMLModules) include(KDEClangFormat) include(KDECMakeSettings) include(KDECompilerSettings NO_POLICY_SCOPE) @@ -53,6 +54,15 @@ set_package_properties(cmark PROPERTIES PURPOSE "Convert markdown to html" ) +ecm_find_qmlmodule(org.kde.kquickimageeditor 1.0) + +find_package(KQuickImageEditor COMPONENTS) +set_package_properties(KQuickImageEditor PROPERTIES + DESCRIPTION "Simple image editor for QtQuick applications" + URL "https://invent.kde.org/libraries/kquickimageeditor/" + PURPOSE "Add image editing capability to image attachments" +) + install(FILES org.kde.neochat.desktop DESTINATION ${KDE_INSTALL_APPDIR}) install(FILES org.kde.neochat.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR}) install(FILES neochat.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/hicolor/scalable/apps) diff --git a/imports/NeoChat/Component/ChatTextInput.qml b/imports/NeoChat/Component/ChatTextInput.qml index 0e7e4cee3..ce1ddd20d 100644 --- a/imports/NeoChat/Component/ChatTextInput.qml +++ b/imports/NeoChat/Component/ChatTextInput.qml @@ -12,7 +12,7 @@ import org.kde.kirigami 2.13 as Kirigami import NeoChat.Component 1.0 import NeoChat.Component.Emoji 1.0 import NeoChat.Dialog 1.0 -import NeoChat.Effect 1.0 +import NeoChat.Page 1.0 import org.kde.neochat 1.0 @@ -188,6 +188,86 @@ ToolBar { visible: emojiPicker.visible || replyItem.visible || autoCompleteListView.visible } + Image { + Layout.preferredHeight: Kirigami.Units.gridUnit * 10 + source: attachmentPath + visible: hasAttachment && (attachmentPath.toString().endsWith('.png') || attachmentPath.toString().endsWith('.jpg')) + fillMode: Image.PreserveAspectFit + Layout.preferredWidth: paintedWidth + RowLayout { + anchors.right: parent.right + Button { + visible: isImage + icon.name: "document-edit" + + // HACK: Use a component because an url doesn't work + Component { + id: imageEditorPage + ImageEditorPage { + imagePath: attachmentPath + } + } + onClicked: { + let imageEditor = applicationWindow().pageStack.layers.push(imageEditorPage, { + imagePath: attachmentPath + }); + imageEditor.newPathChanged.connect(function(newPath) { + applicationWindow().pageStack.layers.pop(); + attachmentPath = newPath; + }); + } + ToolTip { + text: i18n("Edit") + } + } + Button { + icon.name: "dialog-cancel" + onClicked: { + hasAttachment = false; + attachmentPath = ""; + } + ToolTip { + text: i18n("Cancel") + } + } + } + Rectangle { + color: rgba(255, 255, 255, 40) + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + implicitHeight: fileLabel.implicitHeight + + Label { + id: fileLabel + Layout.alignment: Qt.AlignVCenter + text: attachmentPath !== "" ? attachmentPath.toString().substring(attachmentPath.toString().lastIndexOf('/') + 1, attachmentPath.length) : "" + } + } + } + + RowLayout { + visible: hasAttachment && !(attachmentPath.toString().endsWith('.png') || attachmentPath.toString().endsWith('.jpg')) + ToolButton { + icon.name: "dialog-cancel" + onClicked: { + hasAttachment = false; + attachmentPath = ""; + } + } + + Label { + Layout.alignment: Qt.AlignVCenter + text: attachmentPath !== "" ? attachmentPath.toString().substring(attachmentPath.toString().lastIndexOf('/') + 1, attachmentPath.length) : "" + } + } + + Kirigami.Separator { + Layout.fillWidth: true + Layout.preferredHeight: 1 + visible: hasAttachment + } + RowLayout { Layout.fillWidth: true @@ -203,39 +283,6 @@ ToolBar { onClicked: clearReply() } - Control { - Layout.margins: 6 - Layout.preferredHeight: 36 - Layout.alignment: Qt.AlignVCenter - - visible: hasAttachment - - rightPadding: 8 - - contentItem: RowLayout { - spacing: 0 - - ToolButton { - Layout.preferredWidth: height - Layout.fillHeight: true - - id: cancelAttachmentButton - - icon.name: "dialog-cancel" - - onClicked: { - hasAttachment = false; - attachmentPath = ""; - } - } - - Label { - Layout.alignment: Qt.AlignVCenter - - text: attachmentPath !== "" ? attachmentPath.toString().substring(attachmentPath.toString().lastIndexOf('/') + 1, attachmentPath.length) : "" - } - } - } TextArea { id: inputField diff --git a/imports/NeoChat/Page/ImageEditorPage.qml b/imports/NeoChat/Page/ImageEditorPage.qml new file mode 100644 index 000000000..81129eab5 --- /dev/null +++ b/imports/NeoChat/Page/ImageEditorPage.qml @@ -0,0 +1,191 @@ +/* + * SPDX-FileCopyrightText: (C) 2020 Carl Schwan + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +import QtQuick 2.10 +import QtQuick.Controls 2.1 as QQC2 +import QtQuick.Layouts 1.12 +import org.kde.kirigami 2.12 as Kirigami +import QtQuick.Dialogs 1.2 +import org.kde.kquickimageeditor 1.0 as KQuickImageEditor +import QtGraphicalEffects 1.12 +import Qt.labs.platform 1.0 as Platform + +Kirigami.Page { + id: rootEditorView + + property bool resizing: false; + required property string imagePath + + signal newPathChanged(string newPath); + + title: i18n("Edit") + leftPadding: 0 + rightPadding: 0 + + + + function crop() { + const ratioX = editImage.paintedWidth / editImage.nativeWidth; + const ratioY = editImage.paintedHeight / editImage.nativeHeight; + rootEditorView.resizing = false + imageDoc.crop(resizeRectangle.insideX / ratioX, resizeRectangle.insideY / ratioY, resizeRectangle.insideWidth / ratioX, resizeRectangle.insideHeight / ratioY); + } + + actions { + left: Kirigami.Action { + id: undoAction + text: i18nc("@action:button Undo modification", "Undo") + iconName: "edit-undo" + onTriggered: imageDoc.undo(); + visible: imageDoc.edited + } + main: Kirigami.Action { + id: okAction + text: i18nc("@action:button Accept image modification", "Accept") + iconName: "dialog-ok" + onTriggered: { + let newPath = Platform.StandardPaths.writableLocation(Platform.StandardPaths.CacheLocation) + "/" + (new Date()).getTime() + "." + imagePath.split('.').pop(); + if (imageDoc.saveAs(newPath)) {; + newPathChanged(newPath); + } else { + msg.type = Kirigami.MessageType.Error + msg.text = i18n("Unable to save file. Check if you have the correct permission to edit the cache directory.") + msg.visible = true; + } + } + } + } + + + + contentItem: KQuickImageEditor.ImageItem { + id: editImage + fillMode: KQuickImageEditor.ImageItem.PreserveAspectFit + image: imageDoc.image + + Shortcut { + sequence: StandardKey.Undo + onActivated: undoAction.trigger(); + } + + Shortcut { + sequences: [StandardKey.Save, "Enter"] + onActivated: saveAction.trigger(); + } + + Shortcut { + sequence: StandardKey.SaveAs + onActivated: saveAsAction.trigger(); + } anchors.fill: parent + + FileDialog { + id: fileDialog + title: i18n("Save As") + folder: shortcuts.home + selectMultiple: false + selectExisting: false + onAccepted: { + fileDialog.close() + } + onRejected: { + fileDialog.close() + } + Component.onCompleted: visible = false + } + + KQuickImageEditor.ImageDocument { + id: imageDoc + path: rootEditorView.imagePath + } + } + + header: QQC2.ToolBar { + contentItem: Kirigami.ActionToolBar { + id: actionToolBar + display: QQC2.Button.TextBesideIcon + actions: [ + Kirigami.Action { + iconName: rootEditorView.resizing ? "dialog-cancel" : "transform-crop" + text: rootEditorView.resizing ? i18n("Cancel") : i18nc("@action:button Crop an image", "Crop"); + onTriggered: rootEditorView.resizing = !rootEditorView.resizing; + }, + Kirigami.Action { + iconName: "dialog-ok" + visible: rootEditorView.resizing + text: i18nc("@action:button Rotate an image to the right", "Crop"); + onTriggered: rootEditorView.crop(); + }, + Kirigami.Action { + iconName: "object-rotate-left" + text: i18nc("@action:button Rotate an image to the left", "Rotate left"); + onTriggered: imageDoc.rotate(-90); + visible: !rootEditorView.resizing + }, + Kirigami.Action { + iconName: "object-rotate-right" + text: i18nc("@action:button Rotate an image to the right", "Rotate right"); + onTriggered: imageDoc.rotate(90); + visible: !rootEditorView.resizing + }, + Kirigami.Action { + iconName: "object-flip-vertical" + text: i18nc("@action:button Mirror an image vertically", "Flip"); + onTriggered: imageDoc.mirror(false, true); + visible: !rootEditorView.resizing + }, + Kirigami.Action { + iconName: "object-flip-horizontal" + text: i18nc("@action:button Mirror an image horizontally", "Mirror"); + onTriggered: imageDoc.mirror(true, false); + visible: !rootEditorView.resizing + } + ] + } + } + + footer: Kirigami.InlineMessage { + id: msg + type: Kirigami.MessageType.Error + showCloseButton: true + visible: false + } + + KQuickImageEditor.ResizeRectangle { + id: resizeRectangle + + visible: rootEditorView.resizing + + width: editImage.paintedWidth + height: editImage.paintedHeight + x: 0 + y: editImage.verticalPadding + + insideX: 100 + insideY: 100 + insideWidth: 100 + insideHeight: 100 + + onAcceptSize: rootEditorView.crop(); + + //resizeHandle: KQuickImageEditor.BasicResizeHandle { } + + /*Rectangle { + radius: 2 + width: Kirigami.Units.gridUnit * 8 + height: Kirigami.Units.gridUnit * 3 + anchors.centerIn: parent + Kirigami.Theme.colorSet: Kirigami.Theme.View + color: Kirigami.Theme.backgroundColor + QQC2.Label { + anchors.centerIn: parent + text: "x: " + (resizeRectangle.x - rootEditorView.contentItem.width + editImage.paintedWidth) + + " y: " + (resizeRectangle.y - rootEditorView.contentItem.height + editImage.paintedHeight) + + "\nwidth: " + resizeRectangle.width + + " height: " + resizeRectangle.height + } + }*/ + } +} diff --git a/imports/NeoChat/Page/qmldir b/imports/NeoChat/Page/qmldir index 9d7c31b09..9c3539af4 100644 --- a/imports/NeoChat/Page/qmldir +++ b/imports/NeoChat/Page/qmldir @@ -6,4 +6,5 @@ RoomPage 1.0 RoomPage.qml JoinRoomPage 1.0 JoinRoomPage.qml InviteUserPage 1.0 InviteUserPage.qml SettingsPage 1.0 SettingsPage.qml +ImageEditorPage 1.0 ImageEditorPage.qml diff --git a/qml/main.qml b/qml/main.qml index d9eeb0416..334632a23 100644 --- a/qml/main.qml +++ b/qml/main.qml @@ -121,9 +121,10 @@ Kirigami.ApplicationWindow { modal: !root.wideScreen onEnabledChanged: drawerOpen = enabled && !modal onModalChanged: drawerOpen = !modal - enabled: roomManager.hasOpenRoom + enabled: roomManager.hasOpenRoom && pageStack.layers.depth < 2 && pageStack.depth < 3 + visible: enabled room: roomManager.currentRoom - handleVisible: enabled && pageStack.layers.depth < 2 + handleVisible: enabled && pageStack.layers.depth < 2 && pageStack.depth < 3 } globalDrawer: Kirigami.GlobalDrawer { diff --git a/res.qrc b/res.qrc index bb33381e9..07fceb223 100644 --- a/res.qrc +++ b/res.qrc @@ -14,6 +14,7 @@ imports/NeoChat/Page/SettingsPage.qml imports/NeoChat/Page/InvitationPage.qml imports/NeoChat/Page/StartChatPage.qml + imports/NeoChat/Page/ImageEditorPage.qml imports/NeoChat/Component/qmldir imports/NeoChat/Component/ChatTextInput.qml imports/NeoChat/Component/AutoMouseArea.qml diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 28fbb0901..fb98c95c8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -27,6 +27,10 @@ target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR}) target_link_libraries(neochat PRIVATE Qt5::Quick Qt5::Qml Qt5::Gui Qt5::Network Qt5::QuickControls2 KF5::I18n KF5::Kirigami2 KF5::Notifications KF5::ConfigCore KF5::ConfigGui KF5::CoreAddons Quotient cmark::cmark) kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc) +if (KQuickImageEditor_FOUND) + target_compile_definitions(neochat PRIVATE HAS_KQUICKIMAGEEDITOR) +endif() + if(ANDROID) target_link_libraries(neochat PRIVATE Qt5::Svg OpenSSL::SSL) kirigami_package_breeze_icons(ICONS