Implement basic image viewer popup
Current features: - Show scaled up thumbnail while the full image is loading - Click to alternate between scaling mode (or reset zoom if not 1x) - Click outside of image to close - Double click to toggle fullscreen - Middle click to open externally (also for thumbnail in timeline) - Right click anywhere to close - Ctrl+wheel to zoom - Click-drag to pan when image larger than window
This commit is contained in:
parent
bf1e36031f
commit
26a4d76fc2
8
TODO.md
8
TODO.md
|
@ -1,6 +1,14 @@
|
||||||
# TODO
|
# TODO
|
||||||
|
|
||||||
|
- Image viewer:
|
||||||
|
- open externally in context menu in timeline thumbnail
|
||||||
|
- fix: gif always closes?
|
||||||
|
- hflickable support kinetic scrolling disabler and speed/decel settings
|
||||||
|
- buttons
|
||||||
|
- keyboard controls
|
||||||
|
|
||||||
- Avatar tooltip can get displayed in front of presence menu
|
- Avatar tooltip can get displayed in front of presence menu
|
||||||
|
- Use loading cursorShape
|
||||||
|
|
||||||
- global presence control
|
- global presence control
|
||||||
|
|
||||||
|
|
|
@ -9,5 +9,6 @@ Flickable {
|
||||||
|
|
||||||
ScrollBar.vertical: HScrollBar {
|
ScrollBar.vertical: HScrollBar {
|
||||||
visible: parent.interactive
|
visible: parent.interactive
|
||||||
|
z: 999
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,15 +12,16 @@ HImage {
|
||||||
property bool thumbnail: true
|
property bool thumbnail: true
|
||||||
property var cryptDict: ({})
|
property var cryptDict: ({})
|
||||||
|
|
||||||
property bool show: false
|
|
||||||
property string cachedPath: ""
|
property string cachedPath: ""
|
||||||
|
property bool canUpdate: true
|
||||||
|
property bool show: ! canUpdate
|
||||||
|
|
||||||
property Future getFuture: null
|
property Future getFuture: null
|
||||||
|
|
||||||
readonly property bool isMxc: mxc.startsWith("mxc://")
|
readonly property bool isMxc: mxc.startsWith("mxc://")
|
||||||
|
|
||||||
function update() {
|
function update() {
|
||||||
if (! py) return // component was destroyed
|
if (! py || ! canUpdate) return // component was destroyed
|
||||||
|
|
||||||
const w = sourceSize.width || width
|
const w = sourceSize.width || width
|
||||||
const h = sourceSize.height || height
|
const h = sourceSize.height || height
|
||||||
|
|
|
@ -62,6 +62,38 @@ HMxcImage {
|
||||||
callback(toOpen)
|
callback(toOpen)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openUrlExternally() {
|
||||||
|
getOpenUrl(Qt.openUrlExternally)
|
||||||
|
}
|
||||||
|
|
||||||
|
function openImageViewer() {
|
||||||
|
utils.makePopup(
|
||||||
|
"Popups/ImageViewerPopup.qml",
|
||||||
|
{
|
||||||
|
thumbnailTitle: loader.thumbnailTitle,
|
||||||
|
thumbnailMxc: loader.thumbnailMxc,
|
||||||
|
thumbnailPath: image.cachedPath,
|
||||||
|
thumbnailCryptDict:
|
||||||
|
JSON.parse(loader.singleMediaInfo.thumbnail_crypt_dict),
|
||||||
|
|
||||||
|
fullTitle: loader.title,
|
||||||
|
// The thumbnail/cached path will be the full GIF
|
||||||
|
fullMxc: animated ? "" : loader.mediaUrl,
|
||||||
|
fullCryptDict:
|
||||||
|
JSON.parse(loader.singleMediaInfo.media_crypt_dict),
|
||||||
|
|
||||||
|
overallSize: Qt.size(
|
||||||
|
loader.singleMediaInfo.media_width ||
|
||||||
|
loader.singleMediaInfo.thumbnail_width,
|
||||||
|
|
||||||
|
loader.singleMediaInfo.media_height ||
|
||||||
|
loader.singleMediaInfo.thumbnail_height,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
obj => { obj.openExternallyRequested.connect(openUrlExternally) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
width: fitSize.width
|
width: fitSize.width
|
||||||
height: fitSize.height
|
height: fitSize.height
|
||||||
|
@ -81,14 +113,23 @@ HMxcImage {
|
||||||
)
|
)
|
||||||
|
|
||||||
TapHandler {
|
TapHandler {
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
acceptedModifiers: Qt.NoModifier
|
acceptedModifiers: Qt.NoModifier
|
||||||
onTapped:
|
onTapped:
|
||||||
eventList.selectedCount ?
|
eventList.selectedCount ?
|
||||||
eventDelegate.toggleChecked() : getOpenUrl(Qt.openUrlExternally)
|
eventDelegate.toggleChecked() :
|
||||||
|
openImageViewer()
|
||||||
|
|
||||||
gesturePolicy: TapHandler.ReleaseWithinBounds
|
gesturePolicy: TapHandler.ReleaseWithinBounds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TapHandler {
|
||||||
|
acceptedButtons: Qt.MiddleButton
|
||||||
|
acceptedModifiers: Qt.NoModifier
|
||||||
|
onTapped: getOpenUrl(Qt.openUrlExternally)
|
||||||
|
gesturePolicy: TapHandler.ReleaseWithinBounds
|
||||||
|
}
|
||||||
|
|
||||||
TapHandler {
|
TapHandler {
|
||||||
acceptedModifiers: Qt.ShiftModifier
|
acceptedModifiers: Qt.ShiftModifier
|
||||||
onTapped:
|
onTapped:
|
||||||
|
|
186
src/gui/Popups/ImageViewerPopup.qml
Normal file
186
src/gui/Popups/ImageViewerPopup.qml
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||||
|
|
||||||
|
import QtQuick 2.12
|
||||||
|
import QtQuick.Controls 2.12
|
||||||
|
import QtQuick.Window 2.12
|
||||||
|
import "../Base"
|
||||||
|
|
||||||
|
HPopup {
|
||||||
|
id: popup
|
||||||
|
|
||||||
|
property alias thumbnailTitle: thumbnail.title
|
||||||
|
property alias thumbnailMxc: thumbnail.mxc
|
||||||
|
property alias thumbnailPath: thumbnail.cachedPath
|
||||||
|
property alias thumbnailCryptDict: thumbnail.cryptDict
|
||||||
|
property alias fullTitle: full.title
|
||||||
|
property alias fullMxc: full.mxc
|
||||||
|
property alias fullCryptDict: full.cryptDict
|
||||||
|
property size overallSize
|
||||||
|
|
||||||
|
property bool alternateScaling: false
|
||||||
|
property bool activedFullScreen: false
|
||||||
|
|
||||||
|
readonly property bool imageLargerThanWindow:
|
||||||
|
overallSize.width > window.width || overallSize.height > window.height
|
||||||
|
|
||||||
|
readonly property bool imageEqualToWindow:
|
||||||
|
overallSize.width == window.width &&
|
||||||
|
overallSize.height == window.height
|
||||||
|
|
||||||
|
readonly property int paintedWidth:
|
||||||
|
full.status === Image.Ready?
|
||||||
|
full.paintedWidth :
|
||||||
|
thumbnail.paintedWidth
|
||||||
|
|
||||||
|
readonly property int paintedHeight:
|
||||||
|
full.status === Image.Ready ?
|
||||||
|
full.paintedHeight :
|
||||||
|
thumbnail.paintedHeight
|
||||||
|
|
||||||
|
signal openExternallyRequested()
|
||||||
|
|
||||||
|
|
||||||
|
margins: 0
|
||||||
|
background: null
|
||||||
|
|
||||||
|
onAboutToHide: if (activedFullScreen) window.showNormal()
|
||||||
|
|
||||||
|
HFlickable {
|
||||||
|
id: flickable
|
||||||
|
pressDelay: 30
|
||||||
|
implicitWidth: window.width
|
||||||
|
implicitHeight: window.height
|
||||||
|
contentWidth:
|
||||||
|
Math.max(window.width, popup.paintedWidth * thumbnail.scale)
|
||||||
|
contentHeight:
|
||||||
|
Math.max(window.height, popup.paintedHeight * thumbnail.scale)
|
||||||
|
|
||||||
|
ScrollBar.vertical: null
|
||||||
|
|
||||||
|
TapHandler {
|
||||||
|
onTapped: popup.close()
|
||||||
|
gesturePolicy: TapHandler.ReleaseWithinBounds
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
acceptedButtons: Qt.NoButton
|
||||||
|
onWheel: {
|
||||||
|
if (wheel.modifiers !== Qt.ControlModifier) {
|
||||||
|
wheel.accepted = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
wheel.accepted = true
|
||||||
|
const add = wheel.angleDelta.y / 120 / 5
|
||||||
|
thumbnail.scale = Math.max(
|
||||||
|
0.1, Math.min(10, thumbnail.scale + add),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HMxcImage {
|
||||||
|
id: thumbnail
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width:
|
||||||
|
popup.alternateScaling && popup.imageLargerThanWindow ?
|
||||||
|
popup.overallSize.width :
|
||||||
|
|
||||||
|
popup.alternateScaling ?
|
||||||
|
window.width :
|
||||||
|
|
||||||
|
Math.min(window.width, popup.overallSize.width)
|
||||||
|
|
||||||
|
height:
|
||||||
|
popup.alternateScaling && popup.imageLargerThanWindow ?
|
||||||
|
popup.overallSize.height :
|
||||||
|
|
||||||
|
popup.alternateScaling ?
|
||||||
|
window.height :
|
||||||
|
|
||||||
|
Math.min(window.height, popup.overallSize.height)
|
||||||
|
|
||||||
|
fillMode: HMxcImage.PreserveAspectFit
|
||||||
|
// Use only the cachedPath, don't waste time refetching thumb
|
||||||
|
canUpdate: false
|
||||||
|
|
||||||
|
Behavior on width {
|
||||||
|
HNumberAnimation { overshoot: popup.alternateScaling? 2 : 3 }
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on height {
|
||||||
|
HNumberAnimation { overshoot: popup.alternateScaling? 2 : 3 }
|
||||||
|
}
|
||||||
|
|
||||||
|
Binding on showProgressBar {
|
||||||
|
value: false
|
||||||
|
when: ! thumbnail.show
|
||||||
|
}
|
||||||
|
|
||||||
|
HNumberAnimation {
|
||||||
|
id: resetScaleAnimation
|
||||||
|
target: thumbnail
|
||||||
|
property: "scale"
|
||||||
|
from: thumbnail.scale
|
||||||
|
to: 1
|
||||||
|
overshoot: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
// Timer to not disappear before full image is done rendering
|
||||||
|
interval: 1000
|
||||||
|
running: full.status === HMxcImage.Ready
|
||||||
|
onTriggered: thumbnail.show = false
|
||||||
|
}
|
||||||
|
|
||||||
|
HMxcImage {
|
||||||
|
id: full
|
||||||
|
anchors.fill: parent
|
||||||
|
thumbnail: false
|
||||||
|
fillMode: parent.fillMode
|
||||||
|
// Image never loads at 0 opacity or with visible: false
|
||||||
|
opacity: status === HMxcImage.Ready ? 1 : 0.01
|
||||||
|
|
||||||
|
Behavior on opacity { HNumberAnimation {} }
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: popup.paintedWidth
|
||||||
|
height: popup.paintedHeight
|
||||||
|
|
||||||
|
TapHandler {
|
||||||
|
gesturePolicy: TapHandler.ReleaseWithinBounds
|
||||||
|
onTapped: {
|
||||||
|
thumbnail.scale === 1 ?
|
||||||
|
popup.alternateScaling = ! popup.alternateScaling :
|
||||||
|
resetScaleAnimation.start()
|
||||||
|
}
|
||||||
|
onDoubleTapped: {
|
||||||
|
if (window.visibility === Window.FullScreen) {
|
||||||
|
window.showNormal()
|
||||||
|
popup.activedFullScreen = false
|
||||||
|
popup.alternateScaling = false
|
||||||
|
} else {
|
||||||
|
window.showFullScreen()
|
||||||
|
popup.activedFullScreen = true
|
||||||
|
popup.alternateScaling = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TapHandler {
|
||||||
|
acceptedButtons: Qt.MiddleButton
|
||||||
|
gesturePolicy: TapHandler.ReleaseWithinBounds
|
||||||
|
onTapped: popup.openExternallyRequested()
|
||||||
|
}
|
||||||
|
|
||||||
|
TapHandler {
|
||||||
|
acceptedButtons: Qt.RightButton
|
||||||
|
gesturePolicy: TapHandler.ReleaseWithinBounds
|
||||||
|
onTapped: popup.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user