← Back to team overview

ubuntu-touch-coreapps-reviewers team mailing list archive

[Merge] lp:~mcintire-evan/music-app/songs-fastscroll into lp:music-app

 

Evan McIntire has proposed merging lp:~mcintire-evan/music-app/songs-fastscroll into lp:music-app.

Requested reviews:
  Music App Developers (music-app-dev)

For more details, see:
https://code.launchpad.net/~mcintire-evan/music-app/songs-fastscroll/+merge/281471

Add fastscroll to the song list
-- 
Your team Music App Developers is requested to review the proposed merge of lp:~mcintire-evan/music-app/songs-fastscroll into lp:music-app.
=== added file 'app/components/FastScroll.qml'
--- app/components/FastScroll.qml	1970-01-01 00:00:00 +0000
+++ app/components/FastScroll.qml	2016-01-02 17:05:37 +0000
@@ -0,0 +1,316 @@
+/****************************************************************************
+**
+** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies).
+** Copyright (C) 2014 Canonical Ltda
+** All rights reserved.
+** Contact: Nokia Corporation (qt-info@xxxxxxxxx)
+**
+** This file is part of the Qt Components project.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** You may use this file under the terms of the BSD license as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+**   * Redistributions of source code must retain the above copyright
+**     notice, this list of conditions and the following disclaimer.
+**   * Redistributions in binary form must reproduce the above copyright
+**     notice, this list of conditions and the following disclaimer in
+**     the documentation and/or other materials provided with the
+**     distribution.
+**   * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor
+**     the names of its contributors may be used to endorse or promote
+**     products derived from this software without specific prior written
+**     permission.
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+// FastScroll.qml
+import QtQuick 2.4
+import Ubuntu.Components 1.3
+import "../logic/FastScroll.js" as Sections
+
+Item {
+    id: root
+
+    property ListView listView
+    property int pinSize: units.gu(2)
+
+    readonly property var letters: ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "#"]
+    readonly property alias fastScrolling: internal.fastScrolling
+    readonly property bool showing: (rail.opacity !== 0.0)
+    readonly property double minimumHeight: rail.height
+
+    width: units.gu(7)
+    height: rail.height
+    visible: enabled
+
+    onListViewChanged: {
+        if (listView && listView.model) {
+            internal.initDirtyObserver();
+        } else if (listView) {
+            listView.modelChanged.connect(function() {
+                if (listView.model) {
+                    internal.initDirtyObserver();
+                }
+            });
+        }
+    }
+
+
+    Rectangle {
+        id: magnified
+
+        color: Theme.palette.normal.foreground
+        radius: height * 0.3
+        height: pinSize * 2
+        width: height
+        opacity: internal.fastScrolling && root.enabled ? 1.0 : 0.0
+        x: -cursor.width - units.gu(3)
+        y: {
+            if (internal.currentItem) {
+                var itemCenterY = rail.y + internal.currentItem.y + (internal.currentItem.height / 2)
+                return (itemCenterY - (magnified.height / 2))
+            } else {
+                return 0
+            }
+        }
+
+        Label {
+            color: Theme.palette.normal.foregroundText
+            anchors.fill: parent
+            horizontalAlignment: Text.AlignHCenter
+            verticalAlignment: Text.AlignVCenter
+            text: internal.desireSection
+            fontSize: "small"
+        }
+
+        Behavior on opacity {
+            UbuntuNumberAnimation {}
+        }
+    }
+
+    Rectangle {
+        id: cursor
+
+        property bool showLabel: false
+        property string currentSectionName: ""
+
+        radius: pinSize * 0.3
+        height: pinSize
+        width: height
+        color: Theme.palette.normal.foreground
+        opacity: rail.opacity
+        x: rail.x
+        y: {
+            if (internal.currentItem) {
+                var itemCenterY = rail.y + internal.currentItem.y + (internal.currentItem.height / 2)
+                return (itemCenterY - (cursor.height / 2))
+            } else {
+                return 0
+            }
+        }
+        Behavior on y {
+            enabled: !internal.fastScrolling
+            UbuntuNumberAnimation { }
+        }
+    }
+
+    Column {
+        id: rail
+
+        property bool isVisible: root.enabled &&
+                                 (listView.flicking || dragArea.pressed)
+        anchors {
+            right: parent.right
+            rightMargin: units.gu(2)
+            left: parent.left
+            leftMargin: units.gu(2)
+            top: parent.top
+        }
+        height: childrenRect.height
+        opacity: 0.0
+        onIsVisibleChanged: {
+            if (isVisible) {
+                rail.opacity = 1.0
+                hideTimer.stop()
+            } else if (!root.enabled) {
+                rail.opacity = 0.0
+            } else {
+                hideTimer.restart()
+            }
+        }
+
+        Behavior on opacity {
+            UbuntuNumberAnimation { }
+        }
+
+        Repeater {
+            id: sectionsRepeater
+
+            model: root.letters
+            Label {
+                id: lbl
+
+                anchors.left: parent.left
+                height: pinSize
+                width: pinSize
+                verticalAlignment: Text.AlignVCenter
+                horizontalAlignment: Text.AlignHCenter
+                text: modelData
+                fontSize: "x-small"
+                color: cursor.y === y ? Theme.palette.normal.foregroundText : Theme.palette.selected.backgroundText
+                opacity: !internal.modelDirty && Sections.contains(text) ? 1.0 : 0.5
+            }
+        }
+
+        Timer {
+            id: hideTimer
+
+            running: false
+            interval: 2000
+            onTriggered: rail.opacity = 0.0
+        }
+    }
+
+    MouseArea {
+        id: dragArea
+
+        anchors {
+            left: parent.left
+            right: parent.right
+        }
+        y: rail.y
+        height: rail.height
+        visible: rail.opacity == 1.0
+
+        preventStealing: true
+        onPressed: {
+            internal.adjustContentPosition(mouseY)
+            dragginTimer.start()
+        }
+
+        onReleased: {
+            dragginTimer.stop()
+            internal.desireSection = ""
+            internal.fastScrolling = false
+        }
+
+        onPositionChanged: internal.adjustContentPosition(mouseY)
+
+        Timer {
+            id: dragginTimer
+
+            running: false
+            interval: 150
+            onTriggered: {
+                internal.fastScrolling = true
+            }
+        }
+    }
+
+    Timer {
+        id: dirtyTimer
+        interval: 500
+        running: false
+        onTriggered: {
+            Sections.initSectionData(listView);
+            internal.modelDirty = false;
+        }
+    }
+
+    Timer {
+        id: timerScroll
+
+        running: false
+        interval: 10
+        onTriggered: {
+            if (internal.desireSection != internal.currentSection) {
+                var idx = Sections.getIndexFor(internal.desireSection)
+                if (idx !== -1) {
+                    listView.cancelFlick()
+                    listView.positionViewAtIndex(idx, ListView.Beginning)
+                }
+            }
+        }
+    }
+
+    QtObject {
+        id: internal
+
+        property string currentSection: listView.currentSection
+        property string desireSection: ""
+        property string targetSection: fastScrolling ? desireSection : currentSection
+        property int oldY: 0
+        property bool modelDirty: false
+        property bool down: true
+        property bool fastScrolling: false
+        property var currentItem: null
+
+        onTargetSectionChanged: moveIndicator(targetSection)
+
+        function initDirtyObserver() {
+            Sections.initialize(listView);
+            function dirtyObserver() {
+                if (!internal.modelDirty) {
+                    internal.modelDirty = true;
+                    dirtyTimer.running = true;
+                }
+            }
+
+            if (listView.model.countChanged)
+                listView.model.countChanged.connect(dirtyObserver);
+
+            if (listView.model.itemsChanged)
+                listView.model.itemsChanged.connect(dirtyObserver);
+
+            if (listView.model.itemsInserted)
+                listView.model.itemsInserted.connect(dirtyObserver);
+
+            if (listView.model.itemsMoved)
+                listView.model.itemsMoved.connect(dirtyObserver);
+
+            if (listView.model.itemsRemoved)
+                listView.model.itemsRemoved.connect(dirtyObserver);
+        }
+
+        function adjustContentPosition(mouseY) {
+            var child = rail.childAt(rail.width / 2, mouseY)
+            if (!child || child.text === "") {
+                return
+            }
+            var section = child.text
+            if (internal.desireSection !== section) {
+                internal.desireSection = section
+                moveIndicator(section)
+                if (dragArea.pressed) {
+                    timerScroll.restart()
+                }
+            }
+        }
+
+        function moveIndicator(section)
+        {
+            var index = root.letters.indexOf(section)
+            if (index != -1) {
+                currentItem = sectionsRepeater.itemAt(index)
+            }
+        }
+    }
+}
+
+

=== added file 'app/logic/FastScroll.js'
--- app/logic/FastScroll.js	1970-01-01 00:00:00 +0000
+++ app/logic/FastScroll.js	2016-01-02 17:05:37 +0000
@@ -0,0 +1,131 @@
+/****************************************************************************
+**
+** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies).
+** Copyright (C) 2014 Canonical Ltda
+** All rights reserved.
+** Contact: Nokia Corporation (qt-info@xxxxxxxxx)
+**
+** This file is part of the Qt Components project.
+**
+** $QT_BEGIN_LICENSE:BSD$
+** You may use this file under the terms of the BSD license as follows:
+**
+** "Redistribution and use in source and binary forms, with or without
+** modification, are permitted provided that the following conditions are
+** met:
+**   * Redistributions of source code must retain the above copyright
+**     notice, this list of conditions and the following disclaimer.
+**   * Redistributions in binary form must reproduce the above copyright
+**     notice, this list of conditions and the following disclaimer in
+**     the documentation and/or other materials provided with the
+**     distribution.
+**   * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor
+**     the names of its contributors may be used to endorse or promote
+**     products derived from this software without specific prior written
+**     permission.
+**
+** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+// FastScroll.js - this is just SectionScroller.js with a fix for
+// section.criteria == ViewSection.FirstCharacter
+var sectionData = [];
+var _sections = [];
+
+function initialize(list) {
+    initSectionData(list);
+}
+
+function contains(name) {
+    return (_sections.indexOf(name) > -1)
+}
+
+function initSectionData(list) {
+    if (!list || !list.model) return;
+    sectionData = [];
+    _sections = [];
+    var current = "",
+        prop = list.section.property,
+        sectionText;
+
+    if (list.section.criteria === ViewSection.FullString) {
+        for (var i = 0, count = list.model.count; i < count; i++) {
+            sectionText = list.getSectionText(i)
+            if (sectionText !== current) {
+                current = sectionText;
+                _sections.push(current);
+                sectionData.push({ index: i, header: current });
+            }
+        }
+    } else if (list.section.criteria === ViewSection.FirstCharacter) {
+        for (var i = 0, count = list.model.count; i < count; i++) {
+            sectionText = list.getSectionText(i).substring(0, 1)
+            if (sectionText !== current) {
+                current = sectionText
+                _sections.push(sectionText);
+                sectionData.push({ index: i, header: current });
+            }
+        }
+    }
+}
+
+function getSectionPositionString(name) {
+    var val = _sections.indexOf(name);
+    return val === 0 ? "first" :
+           val === _sections.length - 1 ? "last" : false;
+}
+
+function getAt(pos) {
+    return _sections[pos] ? _sections[pos] : "";
+}
+
+function getRelativeSections(current) {
+    var val = _sections.indexOf(current),
+        sect = [],
+        sl = _sections.length;
+
+    val = val < 1 ? 1 : val >= sl-1 ? sl-2 : val;
+    sect = [getAt(val - 1), getAt(val), getAt(val + 1)];
+
+    return sect;
+}
+
+function getClosestSection(pos, down) {
+    var tmp = (_sections.length) * pos;
+    var val = Math.ceil(tmp) // TODO: better algorithm
+    val = val < 2 ? 1 : val;
+    return _sections[val-1];
+}
+
+function getNextSection(current) {
+    var val = _sections.indexOf(current);
+    return (val > -1 ? _sections[(val < _sections.length - 1 ? val + 1 : val)] : _sections[0]) || "";
+}
+
+function getPreviousSection(current) {
+    var val = _sections.indexOf(current);
+    return (val > -1 ? _sections[(val > 0 ? val - 1 : val)] : _sections[0]) || "";
+}
+
+function getIndexFor(sectionName) {
+    var data = sectionData[_sections.indexOf(sectionName)]
+    if (data) {
+        var val = data.index;
+        return val === 0 || val > 0 ? val : -1;
+    } else {
+        return -1
+    }
+}
+

=== modified file 'app/ui/Songs.qml'
--- app/ui/Songs.qml	2015-10-18 18:16:05 +0000
+++ app/ui/Songs.qml	2016-01-02 17:05:37 +0000
@@ -19,6 +19,7 @@
 
 import QtQuick 2.4
 import Ubuntu.Components 1.3
+import Ubuntu.Components.ListItems 0.1 as ListItem
 import Ubuntu.MediaScanner 0.1
 import Ubuntu.Thumbnailer 0.1
 import QtMultimedia 5.0
@@ -30,7 +31,6 @@
 import "../components/HeadState"
 import "../components/ListItemActions"
 
-
 MusicPage {
     id: songsPage
     objectName: "songsPage"
@@ -64,7 +64,13 @@
             bottomMargin: units.gu(2)
             fill: parent
             topMargin: units.gu(2)
+            rightMargin: fastScroll.showing ? fastScroll.width - units.gu(1)
+                                            : 0
         }
+
+        clip: true
+        currentIndex: -1
+
         objectName: "trackstab-listview"
         model: SortFilterModel {
             id: songsModelFilter
@@ -90,6 +96,16 @@
             }
         }
 
+        section.property: "title"
+        section.criteria: ViewSection.FirstCharacter
+        section.delegate: ListItem.Standard {
+            text: section
+        }
+
+        function getSectionText(index) {
+            return model.get(index).title.substring(0,1)
+        }
+
         delegate: MusicListItem {
             id: track
             objectName: "tracksPageListItem" + index
@@ -126,5 +142,15 @@
             }
         }
     }
+
+    FastScroll {
+        id: fastScroll
+
+        listView: tracklist
+        anchors {
+            right: parent.right
+            verticalCenter: tracklist.verticalCenter
+        }
+    }
 }
 


Follow ups