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
This commit is contained in:
James Graham
2022-10-09 16:27:51 +00:00
parent a761d36abd
commit d00e122d88
3 changed files with 292 additions and 62 deletions

View File

@@ -3,10 +3,12 @@
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Controls 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 import org.kde.kirigami 2.15 as Kirigami
ApplicationWindow { Popup {
id: root id: root
property alias source: image.source property alias source: image.source
@@ -16,35 +18,212 @@ ApplicationWindow {
property int imageHeight: -1 property int imageHeight: -1
property var modelData 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 { Control {
sequence: "Escape" Layout.fillWidth: true
onActivated: root.destroy()
contentItem: RowLayout {
spacing: Kirigami.Units.largeSpacing
Kirigami.Avatar {
id: avatar
Layout.preferredWidth: Kirigami.Units.iconSizes.medium
Layout.preferredHeight: Kirigami.Units.iconSizes.medium
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
Label {
id: nameLabel
text: modelData.author.displayName
textFormat: Text.PlainText
font.weight: Font.Bold
color: author.color
}
Label {
id: timeLabel
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
}
} }
color: Kirigami.Theme.backgroundColor ToolTip.text: text
ToolTip.delay: Kirigami.Units.toolTipDelay
ToolTip.visible: hovered
}
ToolButton {
Layout.preferredWidth: Kirigami.Units.gridUnit * 2
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
background: AbstractButton { text: i18n("Zoom out")
onClicked: root.destroy() 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
}
} }
BusyIndicator { BusyIndicator {
Layout.fillWidth: true
visible: image.status !== Image.Ready && root.blurhash === "" visible: image.status !== Image.Ready && root.blurhash === ""
anchors.centerIn: parent
running: visible 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
AnimatedImage { Image {
id: 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 anchors.centerIn: parent
width: rotationAngle % 180 === 0 ? rotationInsensitiveWidth : rotationInsensitiveHeight
width: Math.min(root.imageWidth !== -1 ? root.imageWidth : sourceSize.width, root.width) height: rotationAngle % 180 === 0 ? rotationInsensitiveHeight : rotationInsensitiveWidth
height: Math.min(root.imageHeight !== -1 ? root.imageWidth : sourceSize.height, root.height)
fillMode: Image.PreserveAspectFit 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 { Image {
anchors.centerIn: parent anchors.centerIn: parent
@@ -54,9 +233,35 @@ ApplicationWindow {
visible: root.blurhash !== "" && parent.status !== Image.Ready visible: root.blurhash !== "" && parent.status !== Image.Ready
} }
TapHandler { 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 acceptedButtons: Qt.RightButton
onTapped: { onClicked: {
const contextMenu = fileDelegateContextMenu.createObject(parent, { const contextMenu = fileDelegateContextMenu.createObject(parent, {
author: modelData.author, author: modelData.author,
message: modelData.message, message: modelData.message,
@@ -67,23 +272,37 @@ ApplicationWindow {
progressInfo: modelData.progressInfo, progressInfo: modelData.progressInfo,
plainMessage: modelData.message, plainMessage: modelData.message,
}); });
contextMenu.closeFullscreen.connect(root.destroy) contextMenu.closeFullscreen.connect(root.close)
contextMenu.open(); contextMenu.open();
} }
} }
} }
MouseArea {
Button { anchors.fill: parent
anchors.top: parent.top acceptedButtons: Qt.LeftButton
anchors.right: parent.right onClicked: {
root.close()
text: i18n("Close") }
icon.name: "dialog-close" }
display: AbstractButton.IconOnly }
}
width: Kirigami.Units.gridUnit * 2
height: Kirigami.Units.gridUnit * 2 Component {
id: saveAsDialog
onClicked: root.destroy() FileDialog {
fileMode: FileDialog.SaveFile
folder: StandardPaths.writableLocation(StandardPaths.DownloadLocation)
onAccepted: {
if (!currentFile) {
return;
}
currentRoom.downloadFile(eventId, currentFile)
}
}
}
onClosed: {
image.scaleFactor = 1
image.rotationAngle = 0
} }
} }

View File

@@ -6,6 +6,8 @@ import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15 import QtQuick.Layouts 1.15
import Qt.labs.platform 1.1 import Qt.labs.platform 1.1
import org.kde.kirigami 2.15 as Kirigami
import org.kde.neochat 1.0 import org.kde.neochat 1.0
import NeoChat.Component 1.0 import NeoChat.Component 1.0
import NeoChat.Dialog 1.0 import NeoChat.Dialog 1.0
@@ -50,6 +52,7 @@ TimelineContainer {
ToolTip.text: model.display ToolTip.text: model.display
ToolTip.visible: hoverHandler.hovered ToolTip.visible: hoverHandler.hovered
ToolTip.delay: Kirigami.Units.toolTipDelay
HoverHandler { HoverHandler {
id: hoverHandler id: hoverHandler
@@ -89,17 +92,21 @@ TimelineContainer {
acceptedButtons: Qt.LeftButton acceptedButtons: Qt.LeftButton
onLongPressed: openFileContext(model, parent) onLongPressed: openFileContext(model, parent)
onTapped: { onTapped: {
fullScreenImage.createObject(parent, { img.ToolTip.hide()
filename: eventId, fullScreenImage.open()
source: mediaUrl,
blurhash: model.content.info["xyz.amorgan.blurhash"],
imageWidth: content.info.w,
imageHeight: content.info.h,
modelData: model
}).showFullScreen();
} }
} }
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() { function downloadAndOpen() {
if (downloaded) { if (downloaded) {
openSavedFile() openSavedFile()

View File

@@ -141,6 +141,10 @@ if(ANDROID)
"preferences-system-users" "preferences-system-users"
"preferences-desktop-theme-global" "preferences-desktop-theme-global"
"notifications" "notifications"
"zoom-in"
"zoom-out"
"image-rotate-left-symbolic"
"image-rotate-right-symbolic"
) )
else() else()
target_link_libraries(neochat PUBLIC Qt::Widgets KF5::KIOWidgets) target_link_libraries(neochat PUBLIC Qt::Widgets KF5::KIOWidgets)