Add an image editor
This commit is contained in:
@@ -15,6 +15,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|||||||
include(FeatureSummary)
|
include(FeatureSummary)
|
||||||
include(ECMSetupVersion)
|
include(ECMSetupVersion)
|
||||||
include(KDEInstallDirs)
|
include(KDEInstallDirs)
|
||||||
|
include(ECMQMLModules)
|
||||||
include(KDEClangFormat)
|
include(KDEClangFormat)
|
||||||
include(KDECMakeSettings)
|
include(KDECMakeSettings)
|
||||||
include(KDECompilerSettings NO_POLICY_SCOPE)
|
include(KDECompilerSettings NO_POLICY_SCOPE)
|
||||||
@@ -53,6 +54,15 @@ set_package_properties(cmark PROPERTIES
|
|||||||
PURPOSE "Convert markdown to html"
|
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.desktop DESTINATION ${KDE_INSTALL_APPDIR})
|
||||||
install(FILES org.kde.neochat.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR})
|
install(FILES org.kde.neochat.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR})
|
||||||
install(FILES neochat.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/hicolor/scalable/apps)
|
install(FILES neochat.svg DESTINATION ${KDE_INSTALL_FULL_ICONDIR}/hicolor/scalable/apps)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import org.kde.kirigami 2.13 as Kirigami
|
|||||||
import NeoChat.Component 1.0
|
import NeoChat.Component 1.0
|
||||||
import NeoChat.Component.Emoji 1.0
|
import NeoChat.Component.Emoji 1.0
|
||||||
import NeoChat.Dialog 1.0
|
import NeoChat.Dialog 1.0
|
||||||
import NeoChat.Effect 1.0
|
import NeoChat.Page 1.0
|
||||||
|
|
||||||
import org.kde.neochat 1.0
|
import org.kde.neochat 1.0
|
||||||
|
|
||||||
@@ -188,6 +188,86 @@ ToolBar {
|
|||||||
visible: emojiPicker.visible || replyItem.visible || autoCompleteListView.visible
|
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 {
|
RowLayout {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
@@ -203,39 +283,6 @@ ToolBar {
|
|||||||
onClicked: clearReply()
|
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 {
|
TextArea {
|
||||||
id: inputField
|
id: inputField
|
||||||
|
|||||||
191
imports/NeoChat/Page/ImageEditorPage.qml
Normal file
191
imports/NeoChat/Page/ImageEditorPage.qml
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: (C) 2020 Carl Schwan <carl@carlschwan.eu>
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,4 +6,5 @@ RoomPage 1.0 RoomPage.qml
|
|||||||
JoinRoomPage 1.0 JoinRoomPage.qml
|
JoinRoomPage 1.0 JoinRoomPage.qml
|
||||||
InviteUserPage 1.0 InviteUserPage.qml
|
InviteUserPage 1.0 InviteUserPage.qml
|
||||||
SettingsPage 1.0 SettingsPage.qml
|
SettingsPage 1.0 SettingsPage.qml
|
||||||
|
ImageEditorPage 1.0 ImageEditorPage.qml
|
||||||
|
|
||||||
|
|||||||
@@ -121,9 +121,10 @@ Kirigami.ApplicationWindow {
|
|||||||
modal: !root.wideScreen
|
modal: !root.wideScreen
|
||||||
onEnabledChanged: drawerOpen = enabled && !modal
|
onEnabledChanged: drawerOpen = enabled && !modal
|
||||||
onModalChanged: drawerOpen = !modal
|
onModalChanged: drawerOpen = !modal
|
||||||
enabled: roomManager.hasOpenRoom
|
enabled: roomManager.hasOpenRoom && pageStack.layers.depth < 2 && pageStack.depth < 3
|
||||||
|
visible: enabled
|
||||||
room: roomManager.currentRoom
|
room: roomManager.currentRoom
|
||||||
handleVisible: enabled && pageStack.layers.depth < 2
|
handleVisible: enabled && pageStack.layers.depth < 2 && pageStack.depth < 3
|
||||||
}
|
}
|
||||||
|
|
||||||
globalDrawer: Kirigami.GlobalDrawer {
|
globalDrawer: Kirigami.GlobalDrawer {
|
||||||
|
|||||||
1
res.qrc
1
res.qrc
@@ -14,6 +14,7 @@
|
|||||||
<file>imports/NeoChat/Page/SettingsPage.qml</file>
|
<file>imports/NeoChat/Page/SettingsPage.qml</file>
|
||||||
<file>imports/NeoChat/Page/InvitationPage.qml</file>
|
<file>imports/NeoChat/Page/InvitationPage.qml</file>
|
||||||
<file>imports/NeoChat/Page/StartChatPage.qml</file>
|
<file>imports/NeoChat/Page/StartChatPage.qml</file>
|
||||||
|
<file>imports/NeoChat/Page/ImageEditorPage.qml</file>
|
||||||
<file>imports/NeoChat/Component/qmldir</file>
|
<file>imports/NeoChat/Component/qmldir</file>
|
||||||
<file>imports/NeoChat/Component/ChatTextInput.qml</file>
|
<file>imports/NeoChat/Component/ChatTextInput.qml</file>
|
||||||
<file>imports/NeoChat/Component/AutoMouseArea.qml</file>
|
<file>imports/NeoChat/Component/AutoMouseArea.qml</file>
|
||||||
|
|||||||
@@ -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)
|
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)
|
kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc)
|
||||||
|
|
||||||
|
if (KQuickImageEditor_FOUND)
|
||||||
|
target_compile_definitions(neochat PRIVATE HAS_KQUICKIMAGEEDITOR)
|
||||||
|
endif()
|
||||||
|
|
||||||
if(ANDROID)
|
if(ANDROID)
|
||||||
target_link_libraries(neochat PRIVATE Qt5::Svg OpenSSL::SSL)
|
target_link_libraries(neochat PRIVATE Qt5::Svg OpenSSL::SSL)
|
||||||
kirigami_package_breeze_icons(ICONS
|
kirigami_package_breeze_icons(ICONS
|
||||||
|
|||||||
Reference in New Issue
Block a user