diff --git a/src/qml/MediaTrackMetadataView.qml b/src/qml/MediaTrackMetadataView.qml index 85f34e35..e8fb5955 100644 --- a/src/qml/MediaTrackMetadataView.qml +++ b/src/qml/MediaTrackMetadataView.qml @@ -1,243 +1,243 @@ /* * Copyright 2017 Alexander Stippich * Copyright 2018 Matthieu Gallien * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ import QtQuick 2.7 import QtQuick.Controls 2.2 import QtQuick.Window 2.2 import QtQml.Models 2.2 import QtQuick.Layouts 1.2 import QtGraphicalEffects 1.0 import org.kde.elisa 1.0 Window { id: trackMetadata property int databaseId: 0 property url fileName signal rejected() LayoutMirroring.enabled: Qt.application.layoutDirection == Qt.RightToLeft LayoutMirroring.childrenInherit: true title: i18nc("Window title for track metadata", "View Details") TrackMetadataModel { id: realModel } modality: Qt.NonModal flags: Qt.Dialog | Qt.WindowCloseButtonHint color: myPalette.window minimumHeight: elisaTheme.coverImageSize * 1.8 minimumWidth: elisaTheme.coverImageSize * 2.8 ColumnLayout { anchors.fill: parent anchors.margins: elisaTheme.layoutVerticalMargin spacing: elisaTheme.layoutVerticalMargin RowLayout { id: metadataView Layout.fillHeight: true Layout.fillWidth: true spacing: 0 Image { - source: (realModel.coverUrl != "" ? realModel.coverUrl : elisaTheme.tracksIcon) + source: (realModel.coverUrl !== "" ? realModel.coverUrl : elisaTheme.tracksIcon) sourceSize.width: elisaTheme.coverImageSize sourceSize.height: elisaTheme.coverImageSize fillMode: Image.PreserveAspectFit Layout.alignment: Qt.AlignTop | Qt.AlignHCenter Layout.preferredHeight: elisaTheme.coverImageSize Layout.preferredWidth: elisaTheme.coverImageSize Layout.minimumHeight: elisaTheme.coverImageSize Layout.minimumWidth: elisaTheme.coverImageSize Layout.maximumHeight: elisaTheme.coverImageSize Layout.maximumWidth: elisaTheme.coverImageSize } ListView { id: trackData Layout.fillWidth: true Layout.fillHeight: true focus: true ScrollBar.vertical: ScrollBar { id: scrollBar } boundsBehavior: Flickable.StopAtBounds clip: true ScrollHelper { id: scrollHelper flickable: trackData anchors.fill: trackData } model: realModel delegate: metadataDelegate } } RowLayout { Layout.alignment: Qt.AlignLeft | Qt.AlignBottom Layout.topMargin: elisaTheme.layoutVerticalMargin Layout.bottomMargin: elisaTheme.layoutVerticalMargin spacing: elisaTheme.layoutHorizontalMargin Image { Layout.preferredWidth: fileNameLabel.height Layout.preferredHeight: fileNameLabel.height sourceSize.width: fileNameLabel.height sourceSize.height: fileNameLabel.height source: elisaTheme.folderIcon } LabelWithToolTip { id: fileNameLabel Layout.fillWidth: true text: realModel.fileUrl elide: Text.ElideRight } } DialogButtonBox { id: buttons Layout.fillWidth: true Layout.minimumHeight: implicitHeight standardButtons: DialogButtonBox.Close alignment: Qt.AlignRight onRejected: trackMetadata.rejected() } } Component { id: metadataDelegate RowLayout { id: delegateRow spacing: 0 width: scrollBar.visible ? trackData.width - scrollBar.width : trackData.width Label { id: metaDataLabels text: model.name color: myPalette.text horizontalAlignment: Text.AlignRight Layout.preferredWidth: 0.8 * elisaTheme.coverImageSize Layout.rightMargin: !LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 Layout.leftMargin: LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 } Loader { active: model.type === TrackMetadataModel.TextEntry || model.type === TrackMetadataModel.IntegerEntry Layout.fillWidth: true sourceComponent: LabelWithToolTip { text: model.display horizontalAlignment: Text.AlignLeft elide: Text.ElideRight anchors.fill: parent } } Loader { active: model.type === TrackMetadataModel.DateEntry Layout.fillWidth: true sourceComponent: LabelWithToolTip { text: rawDate.toLocaleDateString() horizontalAlignment: Text.AlignLeft elide: Text.ElideRight anchors.fill: parent property date rawDate: new Date(model.display) } } Loader { active: model.type === TrackMetadataModel.RatingEntry Layout.fillWidth: true sourceComponent: RatingStar { starRating: model.display starSize: elisaTheme.ratingStarSize readOnly: true anchors { left: parent.left top: parent.top bottom: parent.bottom } } } } } Connections { target: elisa onMusicManagerChanged: { if (databaseId !== 0) { realModel.initializeByTrackId(elisa.musicManager, databaseId) } else { realModel.initializeByTrackFileName(elisa.musicManager, fileName) } } } Component.onCompleted: { if (elisa.musicManager) { if (databaseId !== 0) { realModel.initializeByTrackId(elisa.musicManager, databaseId) } else { realModel.initializeByTrackFileName(elisa.musicManager, fileName) } } } } diff --git a/src/qml/PassiveNotification.qml b/src/qml/PassiveNotification.qml index 55111124..e7ad42fc 100644 --- a/src/qml/PassiveNotification.qml +++ b/src/qml/PassiveNotification.qml @@ -1,152 +1,152 @@ /* * Copyright 2015 Marco Martin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Library General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import QtQuick 2.5 import QtQuick.Controls 2.2 import QtQuick.Layouts 1.2 import QtGraphicalEffects 1.0 MouseArea { id: root z: 9999999 width: background.width height: background.height opacity: 0 enabled: appearAnimation.appear anchors { horizontalCenter: parent.horizontalCenter bottom: parent.bottom bottomMargin: 3 * 4 } function showNotification(message, timeout, actionText, callBack) { if (!message) { return; } appearAnimation.running = false; appearAnimation.appear = true; appearAnimation.running = true; - if (timeout == "short") { + if (timeout === "short") { timer.interval = 1000; - } else if (timeout == "long") { + } else if (timeout === "long") { timer.interval = 4500; } else if (timeout > 0) { timer.interval = timeout; } else { timer.interval = 4500; } messageLabel.text = message ? message : ""; actionButton.text = actionText ? actionText : ""; actionButton.callBack = callBack ? callBack : ""; timer.restart(); } function hideNotification() { appearAnimation.running = false; appearAnimation.appear = false; appearAnimation.running = true; } onClicked: { appearAnimation.appear = false; appearAnimation.running = true; } transform: Translate { id: transform y: root.height } Timer { id: timer interval: 4000 onTriggered: { appearAnimation.appear = false; appearAnimation.running = true; } } ParallelAnimation { id: appearAnimation property bool appear: true NumberAnimation { target: root properties: "opacity" to: appearAnimation.appear ? 1 : 0 duration: 1000 easing.type: Easing.InOutQuad } NumberAnimation { target: transform properties: "y" to: appearAnimation.appear ? 0 : background.height duration: 1000 easing.type: appearAnimation.appear ? Easing.OutQuad : Easing.InQuad } } Item { id: background width: backgroundRect.width + 3 height: backgroundRect.height + 3 Rectangle { id: backgroundRect anchors.centerIn: parent radius: 5 color: myPalette.button opacity: 0.6 width: mainLayout.width + Math.round((height - mainLayout.height)) height: Math.max(mainLayout.height + 5*2, 3*2) } RowLayout { id: mainLayout anchors.centerIn: parent Label { id: messageLabel Layout.maximumWidth: Math.min(root.parent.width - 20*2, implicitWidth) elide: Text.ElideRight wrapMode: Text.WordWrap maximumLineCount: 4 color: myPalette.buttonText } Button { id: actionButton property var callBack visible: text != "" onClicked: { appearAnimation.appear = false; appearAnimation.running = true; if (callBack) { callBack(); } } } } layer.enabled: true layer.effect: DropShadow { horizontalOffset: 0 verticalOffset: 0 radius: 3 samples: 32 color: Qt.rgba(0, 0, 0, 0.5) } } } diff --git a/src/qml/PlayListEntry.qml b/src/qml/PlayListEntry.qml index 802dfa5b..72d10649 100644 --- a/src/qml/PlayListEntry.qml +++ b/src/qml/PlayListEntry.qml @@ -1,478 +1,478 @@ /* * Copyright 2016-2017 Matthieu Gallien * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ import QtQuick 2.7 import QtQuick.Layouts 1.2 import QtQuick.Controls 2.3 import QtQuick.Controls 1.4 as Controls1 import QtQuick.Window 2.2 import QtGraphicalEffects 1.0 import org.kde.elisa 1.0 FocusScope { id: playListEntry property var index property bool isSingleDiscAlbum property int isPlaying property bool isSelected property bool isValid property bool isAlternateColor property bool containsMouse property int databaseId: 0 property string title property string artist property string album property string albumArtist property string duration property url fileName property url imageUrl property int trackNumber property int discNumber property int rating property bool hasValidDiscNumber: true property int scrollBarWidth property bool noBackground: false signal startPlayback() signal pausePlayback() signal removeFromPlaylist(var trackIndex) signal switchToTrack(var trackIndex) height: elisaTheme.playListDelegateHeight Controls1.Action { id: removeFromPlayList text: i18nc("Remove current track from play list", "Remove") iconName: "error" onTriggered: { playListEntry.removeFromPlaylist(playListEntry.index) } } Controls1.Action { id: playNow text: i18nc("Play now current track from play list", "Play Now") iconName: "media-playback-start" enabled: !(isPlaying === MediaPlayList.IsPlaying) && isValid onTriggered: { if (isPlaying === MediaPlayList.NotPlaying) { playListEntry.switchToTrack(playListEntry.index) } playListEntry.startPlayback() } } Controls1.Action { id: pauseNow text: i18nc("Pause current track from play list", "Pause") iconName: "media-playback-pause" - enabled: isPlaying == MediaPlayList.IsPlaying && isValid + enabled: isPlaying === MediaPlayList.IsPlaying && isValid onTriggered: playListEntry.pausePlayback() } Controls1.Action { id: showInfo text: i18nc("Show track metadata", "View Details") iconName: "help-about" enabled: isValid onTriggered: { if (metadataLoader.active === false) { metadataLoader.active = true } else { metadataLoader.item.close(); metadataLoader.active = false } } } Loader { id: metadataLoader active: false onLoaded: item.show() sourceComponent: MediaTrackMetadataView { databaseId: playListEntry.databaseId fileName: playListEntry.fileName onRejected: metadataLoader.active = false; } } Rectangle { id: entryBackground anchors.fill: parent anchors.rightMargin: LayoutMirroring.enabled ? scrollBarWidth : 0 color: (isAlternateColor ? myPalette.alternateBase : myPalette.base) height: elisaTheme.playListDelegateHeight focus: true ColumnLayout { spacing: 0 anchors.fill: parent Item { Layout.fillWidth: true Layout.fillHeight: true RowLayout { id: trackRow anchors.fill: parent spacing: elisaTheme.layoutHorizontalMargin / 4 Item { id: playIconItem implicitHeight: elisaTheme.smallDelegateToolButtonSize implicitWidth: elisaTheme.smallDelegateToolButtonSize Layout.maximumWidth: elisaTheme.smallDelegateToolButtonSize Layout.maximumHeight: elisaTheme.smallDelegateToolButtonSize Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter Layout.leftMargin: !LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin / 2 : 0 Layout.rightMargin: LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin / 2 : 0 Image { id: playIcon anchors.fill: parent opacity: 0 source: (isPlaying === MediaPlayList.IsPlaying ? Qt.resolvedUrl(elisaTheme.playingIndicatorIcon) : Qt.resolvedUrl(elisaTheme.pausedIndicatorIcon)) width: parent.height * 1. height: parent.height * 1. sourceSize.width: parent.height * 1. sourceSize.height: parent.height * 1. fillMode: Image.PreserveAspectFit mirror: LayoutMirroring.enabled visible: opacity > 0.0 } } Item { id: fakeDiscNumberItem visible: isValid && (!hasValidDiscNumber || isSingleDiscAlbum) Layout.preferredWidth: fakeDiscNumberSize.width + (elisaTheme.layoutHorizontalMargin / 4) Layout.minimumWidth: fakeDiscNumberSize.width + (elisaTheme.layoutHorizontalMargin / 4) Layout.maximumWidth: fakeDiscNumberSize.width + (elisaTheme.layoutHorizontalMargin / 4) TextMetrics { id: fakeDiscNumberSize text: '/9' } } Label { id: trackNumberLabel horizontalAlignment: Text.AlignRight text: trackNumber !== 0 && trackNumber !== -1 ? Number(trackNumber).toLocaleString(Qt.locale(), 'f', 0) : '' font.weight: (isPlaying ? Font.Bold : Font.Light) color: myPalette.text Layout.alignment: Qt.AlignVCenter | Qt.AlignRight visible: isValid Layout.preferredWidth: (trackNumberSize.width > realTrackNumberSize.width ? trackNumberSize.width : realTrackNumberSize.width) Layout.minimumWidth: (trackNumberSize.width > realTrackNumberSize.width ? trackNumberSize.width : realTrackNumberSize.width) Layout.maximumWidth: (trackNumberSize.width > realTrackNumberSize.width ? trackNumberSize.width : realTrackNumberSize.width) Layout.rightMargin: !LayoutMirroring.enabled ? (discNumber !== 0 && !isSingleDiscAlbum ? 0 : elisaTheme.layoutHorizontalMargin / 2) : 0 Layout.leftMargin: LayoutMirroring.enabled ? (discNumber !== 0 && !isSingleDiscAlbum ? 0 : elisaTheme.layoutHorizontalMargin / 2) : 0 TextMetrics { id: trackNumberSize text: (99).toLocaleString(Qt.locale(), 'f', 0) } TextMetrics { id: realTrackNumberSize text: Number(trackNumber).toLocaleString(Qt.locale(), 'f', 0) } } Label { horizontalAlignment: Text.AlignCenter text: '/' visible: isValid && discNumber !== 0 && !isSingleDiscAlbum font.weight: (isPlaying ? Font.Bold : Font.Light) color: myPalette.text Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter Layout.preferredWidth: numberSeparatorSize.width Layout.minimumWidth: numberSeparatorSize.width Layout.maximumWidth: numberSeparatorSize.width TextMetrics { id: numberSeparatorSize text: '/' } } Label { horizontalAlignment: Text.AlignRight font.weight: (isPlaying ? Font.Bold : Font.Light) color: myPalette.text text: Number(discNumber).toLocaleString(Qt.locale(), 'f', 0) visible: isValid && discNumber !== 0 && !isSingleDiscAlbum Layout.alignment: Qt.AlignVCenter | Qt.AlignRight Layout.preferredWidth: (discNumberSize.width > realDiscNumberSize.width ? discNumberSize.width : realDiscNumberSize.width) Layout.minimumWidth: (discNumberSize.width > realDiscNumberSize.width ? discNumberSize.width : realDiscNumberSize.width) Layout.maximumWidth: (discNumberSize.width > realDiscNumberSize.width ? discNumberSize.width : realDiscNumberSize.width) Layout.rightMargin: !LayoutMirroring.enabled ? (elisaTheme.layoutHorizontalMargin / 2) : 0 Layout.leftMargin: LayoutMirroring.enabled ? (elisaTheme.layoutHorizontalMargin / 2) : 0 TextMetrics { id: discNumberSize text: '9' } TextMetrics { id: realDiscNumberSize text: Number(discNumber).toLocaleString(Qt.locale(), 'f', 0) } } LabelWithToolTip { id: mainCompactLabel text: title font.weight: (isPlaying ? Font.Bold : Font.Normal) color: myPalette.text Layout.maximumWidth: mainCompactLabel.implicitWidth + 1 Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft visible: isValid elide: Text.ElideRight horizontalAlignment: Text.AlignLeft } LabelWithToolTip { id: mainInvalidCompactLabel text: title font.weight: Font.Normal color: myPalette.text Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft visible: !isValid elide: Text.ElideRight } Item { Layout.fillWidth: true Layout.preferredWidth: 0 } Controls1.ToolButton { id: infoButton objectName: 'infoButton' implicitHeight: elisaTheme.smallDelegateToolButtonSize implicitWidth: elisaTheme.smallDelegateToolButtonSize opacity: 0 visible: opacity > 0.1 action: showInfo Layout.alignment: Qt.AlignVCenter | Qt.AlignRight } Controls1.ToolButton { id: playPauseButton objectName: 'playPauseButton' implicitHeight: elisaTheme.smallDelegateToolButtonSize implicitWidth: elisaTheme.smallDelegateToolButtonSize opacity: 0 scale: LayoutMirroring.enabled ? -1 : 1 // We can mirror the symmetrical pause icon visible: opacity > 0.1 action: !(isPlaying === MediaPlayList.IsPlaying) ? playNow : pauseNow Layout.alignment: Qt.AlignVCenter | Qt.AlignRight } Item { implicitHeight: elisaTheme.smallDelegateToolButtonSize implicitWidth: elisaTheme.smallDelegateToolButtonSize Layout.maximumWidth: elisaTheme.smallDelegateToolButtonSize Layout.maximumHeight: elisaTheme.smallDelegateToolButtonSize Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter Controls1.ToolButton { id: removeButton objectName: 'removeButton' anchors.fill: parent opacity: 0 visible: opacity > 0.1 action: removeFromPlayList } } RatingStar { id: ratingWidget starRating: rating starSize: elisaTheme.ratingStarSize } TextMetrics { id: durationTextMetrics text: i18nc("This is used to preserve a fixed width for the duration text.", "00:00") } LabelWithToolTip { id: durationLabel text: duration font.weight: (isPlaying ? Font.Bold : Font.Normal) color: myPalette.text Layout.alignment: Qt.AlignVCenter | Qt.AlignRight Layout.preferredWidth: durationTextMetrics.width + 1 Layout.leftMargin: elisaTheme.layoutHorizontalMargin / 2 Layout.rightMargin: elisaTheme.layoutHorizontalMargin / 2 horizontalAlignment: Text.AlignRight } } } } } states: [ State { name: 'notSelected' when: !containsMouse && (!playListEntry.activeFocus || !isSelected) PropertyChanges { target: removeButton opacity: 0 } PropertyChanges { target: infoButton opacity: 0 } PropertyChanges { target: playPauseButton opacity: 0 } PropertyChanges { target: playIcon opacity: (isPlaying === MediaPlayList.IsPlaying || isPlaying === MediaPlayList.IsPaused ? 1.0 : 0.0) } PropertyChanges { target: entryBackground color: (isAlternateColor ? myPalette.alternateBase : myPalette.base) } PropertyChanges { target: ratingWidget hoverWidgetOpacity: 0.0 } }, State { name: 'hoveredOrSelected' when: containsMouse || (playListEntry.activeFocus && isSelected) PropertyChanges { target: removeButton opacity: 1 } PropertyChanges { target: playPauseButton opacity: 1 } PropertyChanges { target: infoButton opacity: 1 } PropertyChanges { target: playIcon opacity: (isPlaying === MediaPlayList.IsPlaying || isPlaying === MediaPlayList.IsPaused ? 1.0 : 0.0) } PropertyChanges { target: entryBackground color: myPalette.mid } PropertyChanges { target: ratingWidget hoverWidgetOpacity: 1.0 } } ] transitions: Transition { ParallelAnimation { NumberAnimation { properties: "opacity, hoverWidgetOpacity" easing.type: Easing.InOutQuad duration: 250 } ColorAnimation { properties: "color" duration: 250 } } } } diff --git a/src/qml/ScrollHelper.qml b/src/qml/ScrollHelper.qml index 1a6be531..2666438a 100644 --- a/src/qml/ScrollHelper.qml +++ b/src/qml/ScrollHelper.qml @@ -1,117 +1,117 @@ /* * Copyright (C) 2016 Michael Bohlender, * Copyright (C) 2017 Christian Mollekopf, * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import QtQuick 2.7 import QtQuick.Controls 2.2 /* * The MouseArea + interactive: false + maximumFlickVelocity are required * to fix scrolling for desktop systems where we don't want flicking behaviour. * * See also: * ScrollView.qml in qtquickcontrols * qquickwheelarea.cpp in qtquickcontrols */ MouseArea { id: root propagateComposedEvents: true property Flickable flickable //Place the mouse area under the flickable z: -1 onFlickableChanged: { flickable.interactive = false flickable.maximumFlickVelocity = 100000 flickable.boundsBehavior = Flickable.StopAtBounds root.parent = flickable } function scrollByPixelDelta(flickableItem, pixelDelta) { if (!pixelDelta) { return flickableItem.contentY; } var minYExtent = flickableItem.originY + flickableItem.topMargin; var maxYExtent = (flickableItem.contentHeight + flickableItem.bottomMargin + flickableItem.originY) - flickableItem.height; if (typeof(flickableItem.headerItem) !== "undefined" && flickableItem.headerItem) { minYExtent += flickableItem.headerItem.height } //Avoid overscrolling return Math.max(minYExtent, Math.min(maxYExtent, flickableItem.contentY - pixelDelta)); } function calculateNewPosition(flickableItem, wheel) { //Nothing to scroll if (flickableItem.contentHeight < flickableItem.height) { return flickableItem.contentY; } //Ignore 0 events (happens at least with Christians trackpad) - if (wheel.pixelDelta.y == 0 && wheel.angleDelta.y == 0) { + if (wheel.pixelDelta.y === 0 && wheel.angleDelta.y === 0) { return flickableItem.contentY; } //pixelDelta seems to be the same as angleDelta/8 var pixelDelta = 0 //The pixelDelta is a smaller number if both are provided, so pixelDelta can be 0 while angleDelta is still something. So we check the angleDelta if (wheel.angleDelta.y) { var wheelScrollLines = 3 //Default value of QApplication wheelScrollLines property var pixelPerLine = 20 //Default value in Qt, originally comes from QTextEdit var ticks = (wheel.angleDelta.y / 8) / 15.0 //Divide by 8 gives us pixels typically come in 15pixel steps. pixelDelta = ticks * pixelPerLine * wheelScrollLines } else { pixelDelta = wheel.pixelDelta.y } return scrollByPixelDelta(flickableItem, pixelDelta); } function scrollDown() { flickable.flick(0, 0); flickable.contentY = scrollByPixelDelta(flickable, -60); //3 lines * 20 pixels } function scrollUp() { flickable.flick(0, 0); flickable.contentY = scrollByPixelDelta(flickable, 60); //3 lines * 20 pixels } onWheel: { var newPos = calculateNewPosition(flickable, wheel); // console.warn("Delta: ", wheel.pixelDelta.y); // console.warn("Old position: ", flickable.contentY); // console.warn("New position: ", newPos); // Show the scrollbars flickable.flick(0, 0); flickable.contentY = newPos; cancelFlickStateTimer.start() } Timer { id: cancelFlickStateTimer //How long the scrollbar will remain visible interval: 500 // Hide the scrollbars onTriggered: flickable.cancelFlick(); } }