From d00e122d88f50a29809fdbdad9be7d12813ad9fe Mon Sep 17 00:00:00 2001 From: James Graham Date: Sun, 9 Oct 2022 16:27:51 +0000 Subject: [PATCH] Rework fullscreen image As discussed in network/neochat#161, when clicking the image it now only covers the neochat window. A modal popup that covers the neochat window is now used. The app behind get dimmed. Left clicking anywhere closes the preview as well as the using the close button. Right clicking on the image itself still gives the image's context menu. Before ![fullscreenimage_before](/uploads/f7a64ab2f0b75405f3f0a16f32c029f3/fullscreenimage_before.png) After ![fullscreenimage_updated2](/uploads/8feb6c79891019203a6a0a8439c71b70/fullscreenimage_updated2.png) Latest ![fullscreenimage_updated_final](/uploads/61ca4c1251b914ae3a6bdd158f4dc396/fullscreenimage_updated_final.png) Closes network/neochat#161 --- imports/NeoChat/Component/FullScreenImage.qml | 327 +++++++++++++++--- .../Component/Timeline/ImageDelegate.qml | 23 +- src/CMakeLists.txt | 4 + 3 files changed, 292 insertions(+), 62 deletions(-) diff --git a/imports/NeoChat/Component/FullScreenImage.qml b/imports/NeoChat/Component/FullScreenImage.qml index 234ee62db..48cc9dcc2 100644 --- a/imports/NeoChat/Component/FullScreenImage.qml +++ b/imports/NeoChat/Component/FullScreenImage.qml @@ -3,10 +3,12 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import Qt.labs.platform 1.1 import org.kde.kirigami 2.15 as Kirigami -ApplicationWindow { +Popup { id: root property alias source: image.source @@ -16,74 +18,291 @@ ApplicationWindow { property int imageHeight: -1 property var modelData - flags: Qt.FramelessWindowHint | Qt.WA_TranslucentBackground + parent: Overlay.overlay + closePolicy: Popup.CloseOnEscape + width: parent.width + height: parent.height + modal: true + padding: 0 + background: null - title: i18n("Image View - %1", filename) + ColumnLayout { + anchors.fill: parent + spacing: Kirigami.Units.largeSpacing - Shortcut { - sequence: "Escape" - onActivated: root.destroy() - } + Control { + Layout.fillWidth: true - color: Kirigami.Theme.backgroundColor + contentItem: RowLayout { + spacing: Kirigami.Units.largeSpacing - background: AbstractButton { - onClicked: root.destroy() - } + Kirigami.Avatar { + id: avatar - BusyIndicator { - visible: image.status !== Image.Ready && root.blurhash === "" - anchors.centerIn: parent - running: visible - } + Layout.preferredWidth: Kirigami.Units.iconSizes.medium + Layout.preferredHeight: Kirigami.Units.iconSizes.medium - AnimatedImage { - id: image - anchors.centerIn: parent + name: modelData.author.name ?? modelData.author.displayName + source: modelData.author.avatarMediaId ? ("image://mxc/" + modelData.author.avatarMediaId) : "" + color: modelData.author.color + } + ColumnLayout { + Layout.fillWidth: true + spacing: 0 - width: Math.min(root.imageWidth !== -1 ? root.imageWidth : sourceSize.width, root.width) - height: Math.min(root.imageHeight !== -1 ? root.imageWidth : sourceSize.height, root.height) + Label { + id: nameLabel - fillMode: Image.PreserveAspectFit + text: modelData.author.displayName + textFormat: Text.PlainText + font.weight: Font.Bold + color: author.color + } + Label { + id: timeLabel - Image { - anchors.centerIn: parent - width: image.width - height: image.height - source: root.blurhash !== "" ? ("image://blurhash/" + root.blurhash) : "" - visible: root.blurhash !== "" && parent.status !== Image.Ready + text: time.toLocaleString(Qt.locale(), Locale.ShortFormat) + } + } + Label { + id: imageLabel + Layout.fillWidth: true + Layout.leftMargin: Kirigami.Units.largeSpacing + + text: modelData.display + font.weight: Font.Bold + elide: Text.ElideRight + } + ToolButton { + Layout.preferredWidth: Kirigami.Units.gridUnit * 2 + Layout.preferredHeight: Kirigami.Units.gridUnit * 2 + + text: i18n("Zoom in") + Accessible.name: text + icon.name: "zoom-in" + display: AbstractButton.IconOnly + onClicked: { + image.scaleFactor = image.scaleFactor + 0.25 + if (image.scaleFactor > 3) { + image.scaleFactor = 3 + } + } + + ToolTip.text: text + ToolTip.delay: Kirigami.Units.toolTipDelay + ToolTip.visible: hovered + } + ToolButton { + Layout.preferredWidth: Kirigami.Units.gridUnit * 2 + Layout.preferredHeight: Kirigami.Units.gridUnit * 2 + + text: i18n("Zoom out") + Accessible.name: text + icon.name: "zoom-out" + display: AbstractButton.IconOnly + onClicked: { + image.scaleFactor = image.scaleFactor - 0.25 + if (image.scaleFactor < 0.25) { + image.scaleFactor = 0.25 + } + } + + ToolTip.text: text + ToolTip.delay: Kirigami.Units.toolTipDelay + ToolTip.visible: hovered + } + ToolButton { + Layout.preferredWidth: Kirigami.Units.gridUnit * 2 + Layout.preferredHeight: Kirigami.Units.gridUnit * 2 + + text: i18n("Rotate left") + Accessible.name: text + icon.name: "image-rotate-left-symbolic" + display: AbstractButton.IconOnly + onClicked: image.rotationAngle = image.rotationAngle - 90 + + ToolTip.text: text + ToolTip.delay: Kirigami.Units.toolTipDelay + ToolTip.visible: hovered + } + ToolButton { + Layout.preferredWidth: Kirigami.Units.gridUnit * 2 + Layout.preferredHeight: Kirigami.Units.gridUnit * 2 + + text: i18n("Rotate right") + Accessible.name: text + icon.name: "image-rotate-right-symbolic" + display: AbstractButton.IconOnly + onClicked: image.rotationAngle = image.rotationAngle + 90 + + ToolTip.text: text + ToolTip.delay: Kirigami.Units.toolTipDelay + ToolTip.visible: hovered + } + ToolButton { + Layout.preferredWidth: Kirigami.Units.gridUnit * 2 + Layout.preferredHeight: Kirigami.Units.gridUnit * 2 + + text: i18n("Save as") + Accessible.name: text + icon.name: "document-save" + display: AbstractButton.IconOnly + onClicked: { + var dialog = saveAsDialog.createObject(ApplicationWindow.overlay) + dialog.open() + dialog.currentFile = dialog.folder + "/" + currentRoom.fileNameToDownload(eventId) + } + + ToolTip.text: text + ToolTip.delay: Kirigami.Units.toolTipDelay + ToolTip.visible: hovered + } + ToolButton { + Layout.preferredWidth: Kirigami.Units.gridUnit * 2 + Layout.preferredHeight: Kirigami.Units.gridUnit * 2 + + text: i18n("Close") + Accessible.name: text + icon.name: "dialog-close" + display: AbstractButton.IconOnly + onClicked: { + root.close() + } + + ToolTip.text: text + ToolTip.delay: Kirigami.Units.toolTipDelay + ToolTip.visible: hovered + } + } + + background: Rectangle { + color: Kirigami.Theme.alternateBackgroundColor + } + + Kirigami.Separator { + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + height: 1 + } } - TapHandler { - acceptedButtons: Qt.RightButton - onTapped: { - const contextMenu = fileDelegateContextMenu.createObject(parent, { - author: modelData.author, - message: modelData.message, - eventId: modelData.eventId, - source: modelData.source, - file: root.parent, - mimeType: modelData.mimeType, - progressInfo: modelData.progressInfo, - plainMessage: modelData.message, - }); - contextMenu.closeFullscreen.connect(root.destroy) - contextMenu.open(); + BusyIndicator { + Layout.fillWidth: true + visible: image.status !== Image.Ready && root.blurhash === "" + running: visible + } + // Provides container to fill the space that isn't taken up by the top controls and clips the image when zooming makes it larger than the available area. + Item { + id: imageContainer + Layout.fillWidth: true + Layout.fillHeight: true + Layout.leftMargin: Kirigami.Units.largeSpacing + Layout.rightMargin: Kirigami.Units.largeSpacing + Layout.bottomMargin: Kirigami.Units.largeSpacing + clip: true + + Image { + id: image + + property var scaleFactor: 1 + property int rotationAngle: 0 + property var rotationInsensitiveWidth: Math.min(root.imageWidth !== -1 ? root.imageWidth : sourceSize.width, imageContainer.width - Kirigami.Units.largeSpacing * 2) + property var rotationInsensitiveHeight: Math.min(root.imageHeight !== -1 ? root.imageHeight : sourceSize.height, imageContainer.height - Kirigami.Units.largeSpacing * 2) + + anchors.centerIn: parent + width: rotationAngle % 180 === 0 ? rotationInsensitiveWidth : rotationInsensitiveHeight + height: rotationAngle % 180 === 0 ? rotationInsensitiveHeight : rotationInsensitiveWidth + fillMode: Image.PreserveAspectFit + clip: true + + Behavior on width { + NumberAnimation {duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic} + } + Behavior on height { + NumberAnimation {duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic} + } + + Image { + anchors.centerIn: parent + width: image.width + height: image.height + source: root.blurhash !== "" ? ("image://blurhash/" + root.blurhash) : "" + visible: root.blurhash !== "" && parent.status !== Image.Ready + } + + transform: [ + Rotation { + origin.x: image.width / 2 + origin.y: image.height / 2 + angle: image.rotationAngle + + Behavior on angle { + RotationAnimation {duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic} + } + }, + Scale { + origin.x: image.width / 2 + origin.y: image.height / 2 + xScale: image.scaleFactor + yScale: image.scaleFactor + + Behavior on xScale { + NumberAnimation {duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic} + } + Behavior on yScale { + NumberAnimation {duration: Kirigami.Units.longDuration; easing.type: Easing.InOutCubic} + } + } + ] + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.RightButton + onClicked: { + const contextMenu = fileDelegateContextMenu.createObject(parent, { + author: modelData.author, + message: modelData.message, + eventId: modelData.eventId, + source: modelData.source, + file: root.parent, + mimeType: modelData.mimeType, + progressInfo: modelData.progressInfo, + plainMessage: modelData.message, + }); + contextMenu.closeFullscreen.connect(root.close) + contextMenu.open(); + } + } + } + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton + onClicked: { + root.close() + } } } } - Button { - anchors.top: parent.top - anchors.right: parent.right + Component { + id: saveAsDialog + FileDialog { + fileMode: FileDialog.SaveFile + folder: StandardPaths.writableLocation(StandardPaths.DownloadLocation) + onAccepted: { + if (!currentFile) { + return; + } + currentRoom.downloadFile(eventId, currentFile) + } + } + } - text: i18n("Close") - icon.name: "dialog-close" - display: AbstractButton.IconOnly - - width: Kirigami.Units.gridUnit * 2 - height: Kirigami.Units.gridUnit * 2 - - onClicked: root.destroy() + onClosed: { + image.scaleFactor = 1 + image.rotationAngle = 0 } } diff --git a/imports/NeoChat/Component/Timeline/ImageDelegate.qml b/imports/NeoChat/Component/Timeline/ImageDelegate.qml index c03493d2d..f77e9a8f5 100644 --- a/imports/NeoChat/Component/Timeline/ImageDelegate.qml +++ b/imports/NeoChat/Component/Timeline/ImageDelegate.qml @@ -6,6 +6,8 @@ import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 import Qt.labs.platform 1.1 +import org.kde.kirigami 2.15 as Kirigami + import org.kde.neochat 1.0 import NeoChat.Component 1.0 import NeoChat.Dialog 1.0 @@ -50,6 +52,7 @@ TimelineContainer { ToolTip.text: model.display ToolTip.visible: hoverHandler.hovered + ToolTip.delay: Kirigami.Units.toolTipDelay HoverHandler { id: hoverHandler @@ -89,17 +92,21 @@ TimelineContainer { acceptedButtons: Qt.LeftButton onLongPressed: openFileContext(model, parent) onTapped: { - fullScreenImage.createObject(parent, { - filename: eventId, - source: mediaUrl, - blurhash: model.content.info["xyz.amorgan.blurhash"], - imageWidth: content.info.w, - imageHeight: content.info.h, - modelData: model - }).showFullScreen(); + img.ToolTip.hide() + fullScreenImage.open() } } + FullScreenImage { + id: fullScreenImage + filename: eventId + source: mediaUrl + blurhash: model.content.info["xyz.amorgan.blurhash"] + imageWidth: content.info.w + imageHeight: content.info.h + modelData: model + } + function downloadAndOpen() { if (downloaded) { openSavedFile() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4b70f35ef..19e8a3b13 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -141,6 +141,10 @@ if(ANDROID) "preferences-system-users" "preferences-desktop-theme-global" "notifications" + "zoom-in" + "zoom-out" + "image-rotate-left-symbolic" + "image-rotate-right-symbolic" ) else() target_link_libraries(neochat PUBLIC Qt::Widgets KF5::KIOWidgets)