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:
miruka 2020-07-19 03:02:14 -04:00
parent bf1e36031f
commit 26a4d76fc2
5 changed files with 240 additions and 3 deletions

View File

@ -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

View File

@ -9,5 +9,6 @@ Flickable {
ScrollBar.vertical: HScrollBar { ScrollBar.vertical: HScrollBar {
visible: parent.interactive visible: parent.interactive
z: 999
} }
} }

View File

@ -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

View File

@ -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:

View 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()
}
}
}
}
}