Compare commits

...

2 commits

Author SHA1 Message Date
Daniel O'Neill
0c45946a0d Merge remote-tracking branch 'refs/remotes/origin/main' 2025-12-29 08:59:08 -08:00
Daniel O'Neill
3ec7a71fa3 Use TapHandler/WheelHandler/HoverHandler/DragHandler instead of MouseArea
Disable mouse emulation to facilitate proper touchscreen input
2025-12-29 08:57:52 -08:00
7 changed files with 178 additions and 148 deletions

View file

@ -58,7 +58,7 @@ Rectangle {
}
function showGallery(index) {
imageMouseArea.snapping = true;
tapHandler.snapping = true;
baseMode = "Full";
extraZoomPercent = 100;
@ -68,7 +68,7 @@ Rectangle {
currentIndex = idx;
Qt.callLater( function() {
imageMouseArea.snapping = false;
tapHandler.snapping = false;
} );
}
@ -113,85 +113,90 @@ Rectangle {
}
}
// background click to close (outside mainImageFrame)
MouseArea {
id: imageMouseArea
anchors.fill: parent
hoverEnabled: true
TapHandler {
id: tapHandler
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
//preventStealing: false
//propagateComposedEvents: true
property bool insideMe: false
property bool snapping: false
onTapped: function(ev, button) {
if( Qt.MiddleButton === button ) {
mouse.accepted = true;
overlayRoot.toggleBaseOrReset();
return;
}
let item = contentRepeater.itemAt( overlayRoot.currentIndex );
if( !item || !item.clicked )
return;
console.log(`Sending click`);
item.clicked(ev);
}
onDoubleTapped: function(ev, button) {
ev.accepted = true;
let item = contentRepeater.itemAt( overlayRoot.currentIndex );
if( !item )
return;
console.log(`Sending doubleClick`);
item.doubleClicked(ev);
}
}
DragHandler {
id: dragHandler
target: null
acceptedButtons: Qt.LeftButton
property bool dragging: false
property real pressX: 0
property real pressY: 0
property int startIndex: 0
property real dragFactor: 2.0 // perceived speed
property real thresholdFactor: 0.15 // 15% of width to change
property bool snapping: false
property bool doubleClicking: false
Timer {
id: doubleClickTimer
interval: 150
repeat: false
property variant ev
onTriggered: {
let item = contentRepeater.itemAt( overlayRoot.currentIndex );
if( !item || !item.clicked )
return;
console.log(`Sending click`);
item.clicked(ev);
}
}
onDoubleClicked: function(ev) {
ev.accepted = true;
doubleClicking = true;
doubleClickTimer.stop();
let item = contentRepeater.itemAt( overlayRoot.currentIndex );
if( !item || !item.doubleClicked )
return;
console.log(`Sending doubleClick`);
item.doubleClicked(ev);
}
onPressed: function(mouse) {
if (Qt.LeftButton === mouse.button) {
mouse.accepted = true;
insideMe = true;
onActiveChanged: function() {
const mouse = dragHandler.centroid.position;
if( active ) {
dragging = true;
pressX = mouse.x;
pressY = mouse.y;
startIndex = overlayRoot.currentIndex;
} else if (Qt.MiddleButton === mouse.button) {
overlayRoot.toggleBaseOrReset();
mouse.accepted = true;
} else {
dragging = false;
const pageWidth = mainImageFrame.width;
if (pageWidth <= 0 || !overlayRoot.images || overlayRoot.images.length <= 0) {
overlayRoot.currentIndex = 0;
imageView.snap();
return;
}
const dx = (mouse.x - pressX) * dragFactor;
const threshold = pageWidth * thresholdFactor;
let newIndex = startIndex;
if (Math.abs(dx) >= threshold) {
if (dx < 0 && startIndex < images.length - 1)
newIndex = startIndex + 1;
else if (dx > 0 && startIndex > 0)
newIndex = startIndex - 1;
}
overlayRoot.currentIndex = newIndex;
imageView.snap();
}
}
onPositionChanged: function(mouse) {
if( Qt.LeftButton !== imageMouseArea.pressedButtons )
return;
onCentroidChanged: {
if( !dragging ) return;
const mouse = dragHandler.centroid.position;
const px = mouse.x - pressX;
const py = mouse.y - pressY;
if( !insideMe )
return;
if( !dragging ) {
if( Math.hypot(px, py) < 10 ) // 10px threshold
return;
else
dragging = true;
}
doubleClickTimer.stop();
mouse.accepted = true;
const pageWidth = mainImageFrame.width;
if (pageWidth <= 0 || !overlayRoot.images || overlayRoot.images.length <= 0)
return;
@ -209,65 +214,12 @@ Rectangle {
imageView.contentX = newX;
}
onReleased: function(mouse) {
if( Qt.LeftButton !== mouse.button )
return;
if( !insideMe && !dragging )
return false;
mouse.accepted = true;
if( !dragging ) {
mouse.accepted = true;
if( doubleClicking || doubleClickTimer.running ) {
console.log(`Stopping doubleClickTimer`);
doubleClicking = false;
doubleClickTimer.stop();
return;
}
console.log(`Starting doubleClickTimer`);
doubleClickTimer.ev = mouse;
doubleClickTimer.start();
return true;
}
dragging = false;
if( Math.abs(pressX - mouse.x) < 5 && Math.abs(pressY - mouse.y) < 5 )
return
const pageWidth = mainImageFrame.width;
if (pageWidth <= 0 || !overlayRoot.images || overlayRoot.images.length <= 0) {
overlayRoot.currentIndex = 0;
imageView.snap();
return;
}
const dx = (mouse.x - pressX) * dragFactor;
const threshold = pageWidth * thresholdFactor;
let newIndex = startIndex;
if (Math.abs(dx) >= threshold) {
if (dx < 0 && startIndex < images.length - 1)
newIndex = startIndex + 1;
else if (dx > 0 && startIndex > 0)
newIndex = startIndex - 1;
}
overlayRoot.currentIndex = newIndex;
imageView.snap();
mouse.accepted = true;
}
onCanceled: {
dragging = false;
insideMe = false;
imageView.snap();
}
}
WheelHandler {
onWheel: function(event) {
event.accepted = true;
const delta = event.angleDelta.y || event.pixelDelta.y;
@ -305,7 +257,7 @@ Rectangle {
x: 0 - contentX;
Behavior on x {
enabled: !imageMouseArea.dragging && !imageMouseArea.snapping
enabled: !tapHandler.dragging && !tapHandler.snapping
NumberAnimation {
duration: 150
easing.type: Easing.InOutQuad
@ -325,9 +277,9 @@ Rectangle {
if (pageWidth <= 0)
return;
if( instantly ) imageMouseArea.snapping = true;
if( instantly ) tapHandler.snapping = true;
imageView.contentX = overlayRoot.currentIndex * pageWidth;
if( instantly ) imageMouseArea.snapping = false;
if( instantly ) tapHandler.snapping = false;
}
Repeater {
@ -515,7 +467,7 @@ Rectangle {
preferredHighlightEnd: width * 2 / 3
highlightRangeMode: ListView.StrictlyEnforceRange
highlightFollowsCurrentItem: true
highlightMoveDuration: imageMouseArea.snapping ? 0 : 150
highlightMoveDuration: tapHandler.snapping ? 0 : 150
onWidthChanged: {
if (count > 0)
@ -523,7 +475,7 @@ Rectangle {
}
Behavior on contentX {
enabled: !imageMouseArea.snapping
enabled: !tapHandler.snapping
NumberAnimation {
duration: 180
easing.type: Easing.InOutQuad
@ -569,9 +521,10 @@ Rectangle {
//live: index === overlayRoot.currentIndex && visible
}
*/
MouseArea {
anchors.fill: parent
onClicked: {
TapHandler {
gesturePolicy: TapHandler.WithinBounds
onTapped: function(ev, btn) {
ev.accepted = true;
overlayRoot.currentIndex = index;
imageView.snap();
}
@ -643,9 +596,12 @@ Rectangle {
color: "white"
}
MouseArea {
anchors.fill: parent
onClicked: hide()
TapHandler {
gesturePolicy: TapHandler.WithinBounds
onTapped: function(ev, btn) {
ev.accepted = true;
hide();
}
}
}
}

View file

@ -17,9 +17,11 @@ ToolButton {
property int currentIndex: root.player[root.activeProperty]
onTracksChanged: currentIndex = root.player[root.activeProperty];
signal updated(int track)
Menu {
id: menu
y: root.height
y: 0 - height
Repeater {
model: root.tracks || []
@ -34,6 +36,7 @@ ToolButton {
onTriggered: {
root.currentIndex = trackIndex;
root.player[root.activeProperty] = trackIndex;
root.updated(trackIndex);
}
}
}

View file

@ -49,6 +49,7 @@ Item {
console.log(`Audio Tracks: ${JSON.stringify(videoMediaPlayer.audioTracks, null, 2)}`);
console.log(`Subtitle Tracks: ${JSON.stringify(videoMediaPlayer.subtitleTracks, null, 2)}`);
return; // skip thumbnails
Qt.callLater( function() {
console.log(`Calling grabToImage on ${previewItem} which is ${previewItem.implicitWidth}x${previewItem.implicitHeight}`);
previewItem.grabToImage( function(res) {
@ -112,6 +113,7 @@ Item {
onActivated: {
controlsBar.popup();
videoMediaPlayer.audioOutput.muted = !videoMediaPlayer.audioOutput.muted;
videoSessionManager.setMute(videoMediaPlayer.audioOutput.muted);
}
}
Shortcut {
@ -120,6 +122,7 @@ Item {
onActivated: {
controlsBar.popup();
videoMediaPlayer.loopRequested = !videoMediaPlayer.loopRequested;
videoSessionManager.setLoop(videoMediaPlayer.loopRequested);
}
}
Shortcut {
@ -244,7 +247,7 @@ Item {
}
}
}
/*
MouseArea {
id: controlsBarMouse
anchors.left: parent.left
@ -262,7 +265,7 @@ Item {
controlsCloseLater.interval = 2000;
controlsCloseLater.start();
}
*/
Rectangle {
id: controlsBar
anchors.left: parent.left
@ -273,9 +276,28 @@ Item {
height: 60
color: "#CC212121" // semi-opaque dark
onShouldBeVisibleChanged: {
console.log(`shouldBeVisible: ${shouldBeVisible}`);
}
HoverHandler {
id: controlsBarMouseHover
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onHoveredChanged: {
console.log(`controlsBarMouseHover: ${hovered}`);
controlsCloseLater.stop();
if( hovered ) {
controlsBar.shouldBeVisible = true;
} else {
controlsCloseLater.interval = 2000;
controlsCloseLater.start();
}
}
}
function popup(duration) {
if( !duration ) {
if( controlsBarMouse.containsMouse )
if( controlsBarMouseHover.hovered )
return;
duration = 1500;
}
@ -352,7 +374,10 @@ Item {
text: "\uf0e2"
checkable: true
checked: videoMediaPlayer.loopRequested
onClicked: videoMediaPlayer.loopRequested = !videoMediaPlayer.loopRequested
onClicked: {
videoMediaPlayer.loopRequested = !videoMediaPlayer.loopRequested;
videoSessionManager.setLoop(videoMediaPlayer.loopRequested);
}
}
VolumeButton {
@ -366,6 +391,9 @@ Item {
player: videoMediaPlayer
tracks: videoMediaPlayer.audioTracks
activeProperty: "activeAudioTrack"
onUpdated: function(v) {
videoSessionManager.setAudioTrack(v);
}
text: "\uf1ab"
}
@ -375,6 +403,9 @@ Item {
player: videoMediaPlayer
tracks: videoMediaPlayer.subtitleTracks
activeProperty: "activeSubtitleTrack"
onUpdated: function(v) {
videoSessionManager.setSubtitleTrack(v);
}
text: "\uf20a"
}
@ -387,5 +418,5 @@ Item {
}
} // RowLayout
} // Rectangle
} // MouseArea:constrolsBarMouse
// } // MouseArea:constrolsBarMouse
}

View file

@ -21,6 +21,40 @@ Item {
sessions[k].pause();
}
property bool m_muted: false
property bool m_looping: false
property int m_audioTrack: 0
property int m_subtitleTrack: 0
function setMute(onoff) {
m_muted = onoff;
const keys = Object.keys(sessions);
for( let k of keys )
sessions[k].audioOutput.muted = m_muted;
}
function setLoop(onoff) {
m_looping = onoff;
const keys = Object.keys(sessions);
for( let k of keys )
sessions[k].loopRequested = m_looping;
}
function setAudioTrack(idx) {
m_audioTrack = idx;
const keys = Object.keys(sessions);
for( let k of keys )
if( sessions[k].audioTracks.length < m_audioTrack )
sessions[k].activeAudioTrack = m_audioTrack;
}
function setSubtitleTrack(idx) {
m_subtitleTrack = idx;
const keys = Object.keys(sessions);
for( let k of keys )
if( sessions[k].subtitleTracks.length < m_subtitleTrack )
sessions[k].activeSubtitleTrack = m_subtitleTrack;
}
function ensureSession(messageId, url, videoOutput, callback) {
if( videoSessionComponent.status !== Component.Ready )
console.log(`videoSessionComponent not ready: ${videoSessionComponent.status} / ${videoSessionComponent.errorString()}`);

View file

@ -34,8 +34,10 @@ Control {
if (v < 0) v = 0
if (v > 1) v = 1
audioOutput.volume = v
if (v > 0 && audioOutput.muted)
audioOutput.muted = false
if (v > 0 && audioOutput.muted) {
audioOutput.muted = false;
videoSessionManager.setMute(audioOutput.muted);
}
popup.open();
if( !popupHover.containsMouse )
@ -51,14 +53,12 @@ Control {
text: root.volumeIcon()
focusPolicy: Qt.NoFocus
MouseArea {
anchors.fill: parent
TapHandler {
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
onClicked: function(ev) {
onTapped: function(ev, button) {
if (!root.audioOutput)
return
const button = ev.button;
if (button === Qt.MiddleButton) {
ev.accepted = true
root.audioOutput.toggleMuted();
@ -69,13 +69,14 @@ Control {
closeTimer.stop();
closeTimer.start();
}
return;
}
else if (button === Qt.LeftButton)
{
ev.accepted = true
popup.open()
}
ev.accepted = true
popup.open()
}
}
WheelHandler {
onWheel: function(ev) {
root.setVolumeFromWheel(ev.angleDelta.y)
ev.accepted = true
@ -169,6 +170,7 @@ Control {
root.audioOutput.muted = false;
else
root.audioOutput.muted = true;
videoSessionManager.setMute(audioOutput.muted);
root.audioOutput.volume = value
}

View file

@ -162,6 +162,7 @@ Window {
contentHeight: flow.height
contentWidth: flow.width
clip: true
interactive: !imageViewer.visible
GridLayout {
id: flow
@ -250,9 +251,10 @@ Window {
color: "transparent"
}
MouseArea {
anchors.fill: parent
onClicked: navigator.open(modelData);
TapHandler {
enabled: !imageViewer.visible
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad | PointerDevice.TouchScreen
onTapped: navigator.open(modelData);
}
}
}

View file

@ -24,6 +24,8 @@ int main(int argc, char *argv[])
app.setOrganizationDomain("oneill.app");
app.setDesktopFileName("Media.desktop");
QGuiApplication::setAttribute(Qt::AA_SynthesizeMouseForUnhandledTouchEvents, false);
QQmlApplicationEngine engine;
engine.rootContext()->setContextProperty("Clipboard", new Clipboard());
engine.rootContext()->setContextProperty("Settings", new Settings());