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
|
||||
|
||||
- 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
|
||||
- Use loading cursorShape
|
||||
|
||||
- global presence control
|
||||
|
||||
|
|
|
@ -9,5 +9,6 @@ Flickable {
|
|||
|
||||
ScrollBar.vertical: HScrollBar {
|
||||
visible: parent.interactive
|
||||
z: 999
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,15 +12,16 @@ HImage {
|
|||
property bool thumbnail: true
|
||||
property var cryptDict: ({})
|
||||
|
||||
property bool show: false
|
||||
property string cachedPath: ""
|
||||
property bool canUpdate: true
|
||||
property bool show: ! canUpdate
|
||||
|
||||
property Future getFuture: null
|
||||
|
||||
readonly property bool isMxc: mxc.startsWith("mxc://")
|
||||
|
||||
function update() {
|
||||
if (! py) return // component was destroyed
|
||||
if (! py || ! canUpdate) return // component was destroyed
|
||||
|
||||
const w = sourceSize.width || width
|
||||
const h = sourceSize.height || height
|
||||
|
|
|
@ -62,6 +62,38 @@ HMxcImage {
|
|||
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
|
||||
height: fitSize.height
|
||||
|
@ -81,14 +113,23 @@ HMxcImage {
|
|||
)
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
acceptedModifiers: Qt.NoModifier
|
||||
onTapped:
|
||||
eventList.selectedCount ?
|
||||
eventDelegate.toggleChecked() : getOpenUrl(Qt.openUrlExternally)
|
||||
eventDelegate.toggleChecked() :
|
||||
openImageViewer()
|
||||
|
||||
gesturePolicy: TapHandler.ReleaseWithinBounds
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.MiddleButton
|
||||
acceptedModifiers: Qt.NoModifier
|
||||
onTapped: getOpenUrl(Qt.openUrlExternally)
|
||||
gesturePolicy: TapHandler.ReleaseWithinBounds
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedModifiers: Qt.ShiftModifier
|
||||
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