← Back to team overview

ubuntu-touch-coreapps-reviewers team mailing list archive

[Merge] lp:~ahayzen/music-app/convergence-tabs-with-sidebar-01 into lp:music-app

 

Andrew Hayzen has proposed merging lp:~ahayzen/music-app/convergence-tabs-with-sidebar-01 into lp:music-app.

Commit message:
* Implement convergent mode with now playing and queue as a sidebar

Requested reviews:
  Victor Thompson (vthompson)
Related bugs:
  Bug #1253761 in Ubuntu Music App: "Implement initial conditional layout framework"
  https://bugs.launchpad.net/music-app/+bug/1253761
  Bug #1320252 in Ubuntu Music App: "[Tablet]Cannot Go Into Landscape"
  https://bugs.launchpad.net/music-app/+bug/1320252

For more details, see:
https://code.launchpad.net/~ahayzen/music-app/convergence-tabs-with-sidebar-01/+merge/286127

* Implement convergent mode with now playing and queue as a sidebar

Known Issues:
* When the app is starting and is wide enough for wideAspect, a black box appears where the async loader ends up (this happens for all apps seems to be they are launched in portrait and resized too late) [bug 1548096]
-- 
Your team Music App Developers is subscribed to branch lp:music-app.
=== modified file 'app/components/BlurredBackground.qml'
--- app/components/BlurredBackground.qml	2016-01-16 01:30:31 +0000
+++ app/components/BlurredBackground.qml	2016-03-04 03:23:11 +0000
@@ -26,13 +26,14 @@
     width: parent.width
 
     property string art
+    property string color: "black"
 
     // dark layer
     Rectangle {
         anchors {
             fill: parent
         }
-        color: "black" 
+        color: parent.color
     }
 
     // the album art

=== modified file 'app/components/Flickables/MusicGridView.qml'
--- app/components/Flickables/MusicGridView.qml	2016-01-12 00:30:08 +0000
+++ app/components/Flickables/MusicGridView.qml	2016-03-04 03:23:11 +0000
@@ -25,16 +25,6 @@
         fill: parent
         leftMargin: units.gu(1)
         rightMargin: units.gu(1)
-        // FIXME: workaround until pad.lv/1531016 (gridview juddery) is fixed
-        // due to anchors.fill: parent not being used when the header is locked
-        // an extra margin is needed
-        topMargin: {
-            if (parent.head.locked) {
-                units.gu(6.125) * 2 + units.gu(2)  // FIXME: 6.125 is header.height
-            } else {
-                units.gu(6.125) + units.gu(2)  // FIXME: 6.125 is header.height
-            }
-        }
     }
     cellHeight: cellSize + heightOffset
     cellWidth: cellSize + widthOffset

=== modified file 'app/components/HeadState/MultiSelectHeadState.qml'
--- app/components/HeadState/MultiSelectHeadState.qml	2016-01-16 01:30:31 +0000
+++ app/components/HeadState/MultiSelectHeadState.qml	2016-03-04 03:23:11 +0000
@@ -20,89 +20,101 @@
 import Ubuntu.Components 1.3
 import "../Flickables"
 
-PageHeadState {
-    id: selectionState
-    actions: [
-        Action {
-            iconName: "select"
-            text: i18n.tr("Select All")
-            onTriggered: {
-                if (listview.getSelectedIndices().length === listview.model.count) {
-                    listview.clearSelection()
-                } else {
-                    listview.selectAll()
-                }
-            }
-        },
-        Action {
-            enabled: listview !== null ? listview.getSelectedIndices().length > 0 : false
-            iconName: "add-to-playlist"
-            text: i18n.tr("Add to playlist")
-            onTriggered: {
-                var items = []
-                var indicies = listview.getSelectedIndices();
-
-                for (var i=0; i < indicies.length; i++) {
-                    items.push(makeDict(listview.model.get(indicies[i], listview.model.RoleModelData)));
-                }
-
-                mainPageStack.push(Qt.resolvedUrl("../../ui/AddToPlaylist.qml"),
-                                   {"chosenElements": items})
-
-                listview.closeSelection()
-            }
-        },
-        Action {
-            enabled: listview !== null ? listview.getSelectedIndices().length > 0 : false
-            iconName: "add"
-            text: i18n.tr("Add to queue")
-            visible: addToQueue
-
-            onTriggered: {
-                var items = [];
-                var indicies = listview.getSelectedIndices();
-
-                for (var i=0; i < indicies.length; i++) {
-                    items.push(Qt.resolvedUrl(listview.model.get(indicies[i], listview.model.RoleModelData).filename));
-                }
-
-                player.mediaPlayer.playlist.addItems(items);
-
-                listview.closeSelection()
-            }
-        },
-        Action {
-            enabled: listview !== null ? listview.getSelectedIndices().length > 0 : false
-            iconName: "delete"
-            text: i18n.tr("Delete")
-            visible: removable
-
-            onTriggered: {
-                removed(listview.getSelectedIndices())
-
-                listview.closeSelection()
-            }
-        }
-
-    ]
-    backAction: Action {
-        text: i18n.tr("Cancel selection")
-        iconName: "back"
-        onTriggered: listview.closeSelection()
-    }
-    head: thisPage.head
+State {
     name: "selection"
 
-    PropertyChanges {
-        target: thisPage.head
-        backAction: selectionState.backAction
-        actions: selectionState.actions
-    }
-
     property bool addToQueue: true
     property MultiSelectListView listview
     property bool removable: false
-    property Page thisPage
+    property PageHeader thisHeader: PageHeader {
+        id: selectionState
+        flickable: thisPage.flickable
+        leadingActionBar {
+            actions: [
+                Action {
+                    text: i18n.tr("Cancel selection")
+                    iconName: "back"
+                    onTriggered: listview.closeSelection()
+                }
+            ]
+        }
+        title: thisPage.title
+        trailingActionBar {
+            actions: [
+                Action {
+                    iconName: "select"
+                    text: i18n.tr("Select All")
+                    onTriggered: {
+                        if (listview.getSelectedIndices().length === listview.model.count) {
+                            listview.clearSelection()
+                        } else {
+                            listview.selectAll()
+                        }
+                    }
+                },
+                Action {
+                    enabled: listview !== null ? listview.getSelectedIndices().length > 0 : false
+                    iconName: "add-to-playlist"
+                    text: i18n.tr("Add to playlist")
+                    onTriggered: {
+                        var items = []
+                        var indicies = listview.getSelectedIndices();
+
+                        for (var i=0; i < indicies.length; i++) {
+                            items.push(makeDict(listview.model.get(indicies[i], listview.model.RoleModelData)));
+                        }
+
+                        mainPageStack.push(Qt.resolvedUrl("../../ui/AddToPlaylist.qml"),
+                                           {"chosenElements": items})
+
+                        listview.closeSelection()
+                    }
+                },
+                Action {
+                    enabled: listview !== null ? listview.getSelectedIndices().length > 0 : false
+                    iconName: "add"
+                    text: i18n.tr("Add to queue")
+                    visible: addToQueue
+
+                    onTriggered: {
+                        var items = [];
+                        var indicies = listview.getSelectedIndices();
+
+                        for (var i=0; i < indicies.length; i++) {
+                            items.push(Qt.resolvedUrl(listview.model.get(indicies[i], listview.model.RoleModelData).filename));
+                        }
+
+                        player.mediaPlayer.playlist.addItems(items);
+
+                        listview.closeSelection()
+                    }
+                },
+                Action {
+                    enabled: listview !== null ? listview.getSelectedIndices().length > 0 : false
+                    iconName: "delete"
+                    text: i18n.tr("Delete")
+                    visible: removable
+
+                    onTriggered: {
+                        removed(listview.getSelectedIndices())
+
+                        listview.closeSelection()
+                    }
+                }
+            ]
+        }
+        visible: thisPage.state === "selection"
+
+        StyleHints {
+            backgroundColor: mainView.headerColor
+        }
+    }
+    property Item thisPage
 
     signal removed(var selectedIndices)
+
+    PropertyChanges {
+        target: thisPage
+        header: thisHeader
+    }
 }

=== modified file 'app/components/HeadState/PlaylistsHeadState.qml'
--- app/components/HeadState/PlaylistsHeadState.qml	2015-10-23 03:08:43 +0000
+++ app/components/HeadState/PlaylistsHeadState.qml	2016-03-04 03:23:11 +0000
@@ -20,28 +20,61 @@
 import Ubuntu.Components 1.3
 import Ubuntu.Components.Popups 1.3
 
-
-PageHeadState {
+State {
     name: "default"
-    head: thisPage.head
-    actions: [
-        Action {
-            id: newPlaylistAction
-            objectName: "newPlaylistButton"
-            iconName: "add"
-            onTriggered: {
-                customdebug("New playlist.")
-                thisPage.currentDialog = PopupUtils.open(Qt.resolvedUrl("../Dialog/NewPlaylistDialog.qml"), mainView)
-            }
-        },
-        Action {
-            id: searchAction
-            iconName: "search"
-            onTriggered: thisPage.state = "search"
-        }
-    ]
 
     property alias newPlaylistEnabled: newPlaylistAction.enabled
     property alias searchEnabled: searchAction.enabled
-    property Page thisPage
+    property PageHeader thisHeader: PageHeader {
+        id: headerState
+        flickable: thisPage.flickable
+        leadingActionBar {
+            actions: {
+                if (mainPageStack.currentPage === tabs) {
+                    tabs.tabActions
+                } else if (mainPageStack.depth > 1) {
+                    backActionComponent
+                }
+            }
+        }
+        title: thisPage.title
+        trailingActionBar {
+            actions: [
+                Action {
+                    id: newPlaylistAction
+                    objectName: "newPlaylistButton"
+                    iconName: "add"
+                    onTriggered: {
+                        customdebug("New playlist.")
+                        thisPage.currentDialog = PopupUtils.open(Qt.resolvedUrl("../Dialog/NewPlaylistDialog.qml"), mainView)
+                    }
+                },
+                Action {
+                    id: searchAction
+                    iconName: "search"
+                    onTriggered: {
+                        thisPage.state = "search";
+                        thisPage.header.contents.forceActiveFocus();
+                    }
+                }
+            ]
+        }
+        visible: thisPage.state === "default"
+
+        Action {
+            id: backActionComponent
+            iconName: "back"
+            onTriggered: mainPageStack.pop()
+        }
+
+        StyleHints {
+            backgroundColor: mainView.headerColor
+        }
+    }
+    property Item thisPage
+
+    PropertyChanges {
+        target: thisPage
+        header: thisHeader
+    }
 }

=== modified file 'app/components/HeadState/SearchHeadState.qml'
--- app/components/HeadState/SearchHeadState.qml	2015-11-17 01:59:32 +0000
+++ app/components/HeadState/SearchHeadState.qml	2016-03-04 03:23:11 +0000
@@ -19,61 +19,78 @@
 import QtQuick 2.4
 import Ubuntu.Components 1.3
 
-PageHeadState {
-    id: headerState
+State {
     name: "search"
-    head: thisPage.head
-    backAction: Action {
-        id: leaveSearchAction
-        text: "back"
-        iconName: "back"
-        onTriggered: thisPage.state = "default"
-    }
-    contents: TextField {
-        id: searchField
-        anchors {
-            left: parent ? parent.left : undefined
-            right: parent ? parent.right : undefined
-            rightMargin: units.gu(2)
-        }
-        color: styleMusic.common.black
-        hasClearButton: true
-        inputMethodHints: Qt.ImhNoPredictiveText
-        placeholderText: i18n.tr("Search music")
+
+    property PageHeader thisHeader: PageHeader {
+        id: headerState
+        contents: TextField {
+            id: searchField
+            anchors {
+                left: parent ? parent.left : undefined
+                right: parent ? parent.right : undefined
+                verticalCenter: parent ? parent.verticalCenter : undefined
+            }
+            color: styleMusic.common.black
+            focus: true
+            hasClearButton: true
+            inputMethodHints: Qt.ImhNoPredictiveText
+            placeholderText: i18n.tr("Search music")
+
+            // Use the page onVisible as the text field goes visible=false when switching states
+            // This is used when popping from the pageStack and returning back to a page with search
+            Connections {
+                target: thisPage
+
+                onStateChanged: {  // ensure the search is reset (eg pressing Esc)
+                    if (state === "default") {
+                        searchField.text = ""
+                    }
+
+                    // FIXME: Workaround for pad.lv/1514143 (keyboard show/hide on view moving)
+                    // by locking the header and forcing a topMargin of page to the header height
+                    thisPage.head.locked = state === headerState.name;
+                    //thisPage.anchors.topMargin = state === headerState.name ? units.gu(6.125) : 0  // FIXME: 6.125 is header.height
+                }
+
+                onVisibleChanged: {
+                    // clear when the page becomes visible not invisible
+                    // if invisible is used the delegates can be destroyed which
+                    // have created the pushed component
+                    if (visible) {
+                        thisPage.state = "default"
+                    }
+                }
+            }
+        }
+        flickable: thisPage.flickable
+        leadingActionBar {
+            actions: [
+                Action {
+                    id: leaveSearchAction
+                    text: "back"
+                    iconName: "back"
+                    onTriggered: thisPage.state = "default"
+                }
+            ]
+        }
+        visible: thisPage.state === "search"
 
         onVisibleChanged: {
             if (visible) {
-                forceActiveFocus()
+                searchField.forceActiveFocus()
             }
         }
 
-        // Use the page onVisible as the text field goes visible=false when switching states
-        // This is used when popping from the pageStack and returning back to a page with search
-        Connections {
-            target: thisPage
-
-            onStateChanged: {  // ensure the search is reset (eg pressing Esc)
-                if (state === "default") {
-                    searchField.text = ""
-                }
-
-                // FIXME: Workaround for pad.lv/1514143 (keyboard show/hide on view moving)
-                // by locking the header and forcing a topMargin of page to the header height
-                headerState.head.locked = state === headerState.name;
-                thisPage.anchors.topMargin = state === headerState.name ? units.gu(6.125) : 0  // FIXME: 6.125 is header.height
-            }
-
-            onVisibleChanged: {
-                // clear when the page becomes visible not invisible
-                // if invisible is used the delegates can be destroyed which
-                // have created the pushed component
-                if (visible) {
-                    thisPage.state = "default"
-                }
-            }
+        StyleHints {
+            backgroundColor: mainView.headerColor
         }
     }
-
-    property Page thisPage
+    property Item thisPage
     property alias query: searchField.text
+
+    PropertyChanges {
+        target: thisPage
+        header: thisHeader
+    }
 }

=== modified file 'app/components/HeadState/SearchableHeadState.qml'
--- app/components/HeadState/SearchableHeadState.qml	2015-08-12 23:36:44 +0000
+++ app/components/HeadState/SearchableHeadState.qml	2016-03-04 03:23:11 +0000
@@ -20,15 +20,51 @@
 import Ubuntu.Components 1.3
 
 
-PageHeadState {
+State {
     name: "default"
-    head: thisPage.head
-    actions: Action {
-        id: searchAction
-        iconName: "search"
-        onTriggered: thisPage.state = "search"
-    }
 
     property alias searchEnabled: searchAction.enabled
-    property Page thisPage
+    property PageHeader thisHeader: PageHeader {
+        id: headerState
+        flickable: thisPage.flickable
+        leadingActionBar {
+            actions: {
+                if (mainPageStack.currentPage === tabs) {
+                    tabs.tabActions
+                } else if (mainPageStack.depth > 1) {
+                    backActionComponent
+                }
+            }
+        }
+        title: thisPage.title
+        trailingActionBar {
+            actions: [
+                Action {
+                    id: searchAction
+                    iconName: "search"
+                    onTriggered: {
+                        thisPage.state = "search";
+                        thisPage.header.contents.forceActiveFocus();
+                    }
+                }
+            ]
+        }
+        visible: thisPage.state === "default"
+
+        Action {
+            id: backActionComponent
+            iconName: "back"
+            onTriggered: mainPageStack.pop()
+        }
+
+        StyleHints {
+            backgroundColor: mainView.headerColor
+        }
+    }
+    property Item thisPage
+
+    PropertyChanges {
+        target: thisPage
+        header: thisHeader
+    }
 }

=== modified file 'app/components/MusicPage.qml'
--- app/components/MusicPage.qml	2015-10-23 03:08:43 +0000
+++ app/components/MusicPage.qml	2016-03-04 03:23:11 +0000
@@ -29,6 +29,35 @@
         bottomMargin: musicToolbar.visible ? musicToolbar.height : 0
         fill: parent
     }
+    head {  // hide default header
+        locked: true
+        visible: false
+    }
+    header: PageHeader {
+        id: pageHeader
+        title: thisPage.title
+        leadingActionBar {
+            actions: {
+                if (mainPageStack.currentPage === tabs) {
+                    tabs.tabActions
+                } else if (mainPageStack.depth > 1) {
+                    backActionComponent
+                } else {
+                    null
+                }
+            }
+        }
+
+        StyleHints {
+            backgroundColor: mainView.headerColor
+        }
+
+        Action {
+            id: backActionComponent
+            iconName: "back"
+            onTriggered: mainPageStack.pop()
+        }
+    }
 
     property Dialog currentDialog
     property bool searchable: false

=== modified file 'app/components/NowPlayingFullView.qml'
--- app/components/NowPlayingFullView.qml	2016-01-16 01:30:31 +0000
+++ app/components/NowPlayingFullView.qml	2016-03-04 03:23:11 +0000
@@ -29,6 +29,9 @@
         fill: parent
     }
 
+    property string backgroundColor: styleMusic.common.black
+    property bool sidebar: false
+
     BlurredBackground {
         id: blurredBackground
         anchors {
@@ -37,7 +40,8 @@
             top: parent.top
         }
         art: albumImage.firstSource
-        height: parent.height - units.gu(7)
+        color: backgroundColor
+        height: parent.height - (sidebar ? units.gu(7) + nowPlayingWideAspectLabelsBackground.height  : units.gu(7))
 
         Item {
             id: albumImageContainer
@@ -56,70 +60,6 @@
             }
         }
 
-        Rectangle {
-            id: nowPlayingWideAspectLabelsBackground
-            anchors.bottom: parent.bottom
-            color: styleMusic.common.black
-            height: nowPlayingWideAspectTitle.lineCount === 1 ? units.gu(10) : units.gu(13)
-            opacity: 0.8
-            width: parent.width
-        }
-
-        /* Column for labels in wideAspect */
-        Column {
-            id: nowPlayingWideAspectLabels
-            spacing: units.gu(1)
-            anchors {
-                left: parent.left
-                leftMargin: units.gu(2)
-                right: parent.right
-                rightMargin: units.gu(2)
-                top: nowPlayingWideAspectLabelsBackground.top
-                topMargin: nowPlayingWideAspectTitle.lineCount === 1 ? units.gu(2) : units.gu(1.5)
-            }
-
-            /* Title of track */
-            Label {
-                id: nowPlayingWideAspectTitle
-                anchors {
-                    left: parent.left
-                    leftMargin: units.gu(1)
-                    right: parent.right
-                    rightMargin: units.gu(1)
-                }
-                color: styleMusic.playerControls.labelColor
-                elide: Text.ElideRight
-                fontSize: "x-large"
-                maximumLineCount: 2
-                objectName: "playercontroltitle"
-                text: {
-                    if (player.mediaPlayer.playlist.empty) {
-                        ""
-                    } else if (player.currentMeta.title === "") {
-                        player.mediaPlayer.playlist.currentSource
-                    } else {
-                        player.currentMeta.title
-                    }
-                }
-                wrapMode: Text.WordWrap
-            }
-
-            /* Artist of track */
-            Label {
-                id: nowPlayingWideAspectArtist
-                anchors {
-                    left: parent.left
-                    leftMargin: units.gu(1)
-                    right: parent.right
-                    rightMargin: units.gu(1)
-                }
-                color: styleMusic.nowPlaying.labelSecondaryColor
-                elide: Text.ElideRight
-                fontSize: "small"
-                text: player.mediaPlayer.playlist.empty ? "" : player.currentMeta.author
-            }
-        }
-
         /* Detect cover art swipe */
         MouseArea {
             anchors.fill: parent
@@ -142,6 +82,74 @@
         }
     }
 
+    Rectangle {
+        id: nowPlayingWideAspectLabelsBackground
+        anchors {
+            bottom: sidebar ? undefined : blurredBackground.bottom
+            left: parent.left
+            right: parent.right
+            top: sidebar ? blurredBackground.bottom : undefined
+        }
+        color: backgroundColor
+        height: nowPlayingWideAspectTitle.lineCount === 1 ? units.gu(10) : units.gu(13)
+        opacity: sidebar ? 1.0 : 0.8
+    }
+
+    /* Column for labels in wideAspect */
+    Column {
+        id: nowPlayingWideAspectLabels
+        spacing: units.gu(1)
+        anchors {
+            left: parent.left
+            leftMargin: units.gu(2)
+            right: parent.right
+            rightMargin: units.gu(2)
+            top: nowPlayingWideAspectLabelsBackground.top
+            topMargin: nowPlayingWideAspectTitle.lineCount === 1 ? units.gu(2) : units.gu(1.5)
+        }
+
+        /* Title of track */
+        Label {
+            id: nowPlayingWideAspectTitle
+            anchors {
+                left: parent.left
+                leftMargin: units.gu(1)
+                right: parent.right
+                rightMargin: units.gu(1)
+            }
+            color: styleMusic.playerControls.labelColor
+            elide: Text.ElideRight
+            fontSize: "x-large"
+            maximumLineCount: 2
+            objectName: "playercontroltitle"
+            text: {
+                if (player.mediaPlayer.playlist.empty) {
+                    ""
+                } else if (player.currentMeta.title === "") {
+                    player.mediaPlayer.playlist.currentSource
+                } else {
+                    player.currentMeta.title
+                }
+            }
+            wrapMode: Text.WordWrap
+        }
+
+        /* Artist of track */
+        Label {
+            id: nowPlayingWideAspectArtist
+            anchors {
+                left: parent.left
+                leftMargin: units.gu(1)
+                right: parent.right
+                rightMargin: units.gu(1)
+            }
+            color: styleMusic.nowPlaying.labelSecondaryColor
+            elide: Text.ElideRight
+            fontSize: "small"
+            text: player.mediaPlayer.playlist.empty ? "" : player.currentMeta.author
+        }
+    }
+
     /* Background for progress bar component */
     Rectangle {
         id: musicToolbarFullProgressBackground
@@ -149,9 +157,9 @@
             bottom: parent.bottom
             left: parent.left
             right: parent.right
-            top: blurredBackground.bottom
+            top: sidebar ? nowPlayingWideAspectLabelsBackground.bottom : blurredBackground.bottom
         }
-        color: styleMusic.common.black
+        color: backgroundColor
     }
 
     /* Progress bar component */
@@ -161,7 +169,7 @@
         anchors.leftMargin: units.gu(3)
         anchors.right: parent.right
         anchors.rightMargin: units.gu(3)
-        anchors.top: blurredBackground.bottom
+        anchors.top: sidebar ? nowPlayingWideAspectLabelsBackground.bottom : blurredBackground.bottom
         anchors.topMargin: units.gu(1)
         height: units.gu(3)
         width: parent.width

=== added file 'app/components/NowPlayingSidebar.qml'
--- app/components/NowPlayingSidebar.qml	1970-01-01 00:00:00 +0000
+++ app/components/NowPlayingSidebar.qml	2016-03-04 03:23:11 +0000
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2013, 2014, 2015, 2016
+ *      Andrew Hayzen <ahayzen@xxxxxxxxx>
+ *      Daniel Holm <d.holmen@xxxxxxxxx>
+ *      Victor Thompson <victor.thompson@xxxxxxxxx>
+ *
+ * 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; version 3.
+ *
+ * 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, see <http://www.gnu.org/licenses/>.
+ */
+
+import QtQuick 2.4
+import Ubuntu.Components 1.3
+
+import "HeadState"
+
+Rectangle {
+    id: nowPlayingSidebar
+    anchors {
+        fill: parent
+    }
+    color: "#2c2c34"
+    state: queue.state === "multiselectable" ? "selection" : "default"
+    states: [
+        QueueHeadState {
+            thisHeader {
+                leadingActionBar {
+                    actions: []  // hide tab bar
+                }
+                z: 100  // put on top of content
+            }
+            thisPage: nowPlayingSidebar
+        },
+        MultiSelectHeadState {
+            addToQueue: false
+            listview: queue
+            removable: true
+            thisHeader {
+                z: 100  // put on top of content
+            }
+            thisPage: nowPlayingSidebar
+
+            onRemoved: {
+                // Remove the tracks from the queue
+                // Use slice() to copy the list
+                // so that the indexes don't change as they are removed
+                player.mediaPlayer.playlist.removeItemsWrapper(selectedIndices.slice());
+            }
+        }
+    ]
+    property alias flickable: queue  // fake normal Page
+    property Item header: PageHeader {
+        id: pageHeader
+        leadingActionBar {
+            actions: nowPlayingSidebar.head.backAction
+        }
+        flickable: queue
+        trailingActionBar {
+            actions: nowPlayingSidebar.head.actions
+        }
+        z: 100  // put on top of content
+
+        StyleHints {
+            backgroundColor: mainView.headerColor
+        }
+    }
+    property Item previousHeader: null
+    property string title: ""  // fake normal Page
+
+    onHeaderChanged: {  // Copy what SDK does to parent header correctly
+        if (previousHeader) {
+            previousHeader.parent = null
+        }
+
+        header.parent = nowPlayingSidebar
+        previousHeader = header;
+    }
+
+    Loader {
+        anchors {
+            left: parent.left
+            right: parent.right
+            top: parent.top
+        }
+        height: units.gu(6.125)
+        sourceComponent: header
+    }
+
+    Queue {
+        id: queue
+        anchors {
+            bottomMargin: 0
+            topMargin: 0
+        }
+        clip: true
+        isSidebar: true
+        header: Column {
+            id: sidebarColumn
+            anchors {
+                left: parent.left
+                right: parent.right
+            }
+
+            NowPlayingFullView {
+                anchors {
+                    fill: undefined
+                }
+                backgroundColor: "#2c2c34"
+                clip: true
+                height: units.gu(47)
+                sidebar: true
+                width: parent.width
+            }
+
+            NowPlayingToolbar {
+                anchors {
+                    fill: undefined
+                }
+                bottomProgressHint: false
+                color: "#2c2c34"
+                height: itemSize + 2 * spacing + units.gu(2)
+                width: parent.width
+            }
+        }
+    }
+}

=== modified file 'app/components/NowPlayingToolbar.qml'
--- app/components/NowPlayingToolbar.qml	2016-01-16 01:30:31 +0000
+++ app/components/NowPlayingToolbar.qml	2016-03-04 03:23:11 +0000
@@ -30,19 +30,23 @@
     }
     color: styleMusic.common.black
 
+    property alias bottomProgressHint: playerControlsProgressBar.visible
+    property int itemSize: units.gu(6)
+    property int spacing: units.gu(1)
+
     /* Repeat button */
     MouseArea {
         id: nowPlayingRepeatButton
         anchors.right: nowPlayingPreviousButton.left
-        anchors.rightMargin: units.gu(1)
+        anchors.rightMargin: spacing
         anchors.verticalCenter: nowPlayingPlayButton.verticalCenter
-        height: units.gu(6)
+        height: itemSize
         width: height
         onClicked: player.repeat = !player.repeat
 
         Icon {
             id: repeatIcon
-            height: units.gu(3)
+            height: itemSize / 2
             width: height
             anchors.verticalCenter: parent.verticalCenter
             anchors.horizontalCenter: parent.horizontalCenter
@@ -58,15 +62,15 @@
         id: nowPlayingPreviousButton
         enabled: player.mediaPlayer.playlist.canGoPrevious
         anchors.right: nowPlayingPlayButton.left
-        anchors.rightMargin: units.gu(1)
+        anchors.rightMargin: spacing
         anchors.verticalCenter: nowPlayingPlayButton.verticalCenter
-        height: units.gu(6)
+        height: itemSize
         width: height
         onClicked: player.mediaPlayer.playlist.previousWrapper()
 
         Icon {
             id: nowPlayingPreviousIndicator
-            height: units.gu(3)
+            height: itemSize / 2
             width: height
             anchors.verticalCenter: parent.verticalCenter
             anchors.horizontalCenter: parent.horizontalCenter
@@ -81,13 +85,13 @@
     MouseArea {
         id: nowPlayingPlayButton
         anchors.centerIn: parent
-        height: units.gu(10)
+        height: itemSize + 2 * spacing
         width: height
         onClicked: player.mediaPlayer.toggle()
 
         Icon {
             id: nowPlayingPlayIndicator
-            height: units.gu(6)
+            height: itemSize
             width: height
             anchors.verticalCenter: parent.verticalCenter
             anchors.horizontalCenter: parent.horizontalCenter
@@ -101,16 +105,16 @@
     MouseArea {
         id: nowPlayingNextButton
         anchors.left: nowPlayingPlayButton.right
-        anchors.leftMargin: units.gu(1)
+        anchors.leftMargin: spacing
         anchors.verticalCenter: nowPlayingPlayButton.verticalCenter
         enabled: player.mediaPlayer.playlist.canGoNext
-        height: units.gu(6)
+        height: itemSize
         width: height
         onClicked: player.mediaPlayer.playlist.nextWrapper()
 
         Icon {
             id: nowPlayingNextIndicator
-            height: units.gu(3)
+            height: itemSize / 2
             width: height
             anchors.verticalCenter: parent.verticalCenter
             anchors.horizontalCenter: parent.horizontalCenter
@@ -125,15 +129,15 @@
     MouseArea {
         id: nowPlayingShuffleButton
         anchors.left: nowPlayingNextButton.right
-        anchors.leftMargin: units.gu(1)
+        anchors.leftMargin: spacing
         anchors.verticalCenter: nowPlayingPlayButton.verticalCenter
-        height: units.gu(6)
+        height: itemSize
         width: height
         onClicked: player.shuffle = !player.shuffle
 
         Icon {
             id: shuffleIcon
-            height: units.gu(3)
+            height: itemSize / 2
             width: height
             anchors.verticalCenter: parent.verticalCenter
             anchors.horizontalCenter: parent.horizontalCenter

=== modified file 'app/components/Queue.qml'
--- app/components/Queue.qml	2016-01-12 11:44:16 +0000
+++ app/components/Queue.qml	2016-03-04 03:23:11 +0000
@@ -29,7 +29,9 @@
 MultiSelectListView {
     id: queueList
     anchors {
+        bottomMargin: units.gu(1)
         fill: parent
+        topMargin: units.gu(1)
     }
     autoModelMove: false  // ensures we use moveItem() not move() in onReorder
     footer: Item {
@@ -38,11 +40,13 @@
     model: player.mediaPlayer.playlist
     objectName: "nowPlayingqueueList"
 
+    property bool isSidebar: false
+
     onCountChanged: customdebug("Queue: Now has: " + queueList.count + " tracks")
 
     delegate: MusicListItem {
         id: queueListItem
-        color: player.mediaPlayer.playlist.currentIndex === index ? "#2c2c34" : styleMusic.mainView.backgroundColor
+        color: player.mediaPlayer.playlist.currentIndex === index ? (isSidebar ? "#3d3d45" : "#2c2c34") : (isSidebar ? "#2c2c34" : styleMusic.mainView.backgroundColor)
         height: units.gu(7)
         leadingActions: ListItemActions {
             actions: [

=== modified file 'app/music-app.qml'
--- app/music-app.qml	2016-01-28 01:43:54 +0000
+++ app/music-app.qml	2016-03-04 03:23:11 +0000
@@ -115,7 +115,7 @@
                 break;
             case Qt.Key_J:  //      Ctrl+J      Jump to playing song
                 tabs.pushNowPlaying()
-                mainPageStack.currentPage.isListView = true
+                mainPageStack.currentPage.setListView(true)
                 break;
             case Qt.Key_N:  //      Ctrl+N      Show Now playing
                 tabs.pushNowPlaying()
@@ -282,7 +282,7 @@
 
     signal listItemSwiping(int i)
 
-    property bool wideAspect: width >= units.gu(70) && loadedUI
+    property bool wideAspect: width >= units.gu(95) && loadedUI
     property bool loadedUI: false  // property to detect if the UI has finished
 
     // FUNCTIONS
@@ -567,23 +567,16 @@
         }
     }
 
-    Loader {
-        id: musicToolbar
-        anchors {
-            bottom: parent.bottom
-            left: parent.left
-            right: parent.right
-        }
-        asynchronous: true
-        source: "components/MusicToolbar.qml"
-        visible: (mainPageStack.currentPage.showToolbar || mainPageStack.currentPage.showToolbar === undefined) &&
-                 !firstRun &&
-                 !noMusic
-        z: 200  // put on top of everything else
-    }
-
     PageStack {
         id: mainPageStack
+        anchors {
+            bottom: parent.bottom
+            fill: undefined
+            left: parent.left
+            right: nowPlayingSidebarLoader.left
+            top: parent.top
+        }
+        clip: true  // otherwise listitems actions overflow
 
         // Properties storing the current page info
         property Page currentMusicPage: null  // currentPage can be Tabs
@@ -644,6 +637,39 @@
             }
 
             property Tab lastTab: selectedTab
+            property list<Action> tabActions: [
+                Action {
+                    enabled: recentTabRepeater.count > 0
+                    text: enabled ? recentTabRepeater.itemAt(0).title : ""
+                    visible: enabled
+
+                    onTriggered: {
+                        if (enabled) {
+                            tabs.selectedTabIndex = recentTabRepeater.itemAt(0).index
+                        }
+                    }
+                },
+                Action {
+                    text: artistsTab.title
+                    onTriggered: tabs.selectedTabIndex = artistsTab.index
+                },
+                Action {
+                    text: albumsTab.title
+                    onTriggered: tabs.selectedTabIndex = albumsTab.index
+                },
+                Action {
+                    text: genresTab.title
+                    onTriggered: tabs.selectedTabIndex = genresTab.index
+                },
+                Action {
+                    text: songsTab.title
+                    onTriggered: tabs.selectedTabIndex = songsTab.index
+                },
+                Action {
+                    text: playlistsTab.title
+                    onTriggered: tabs.selectedTabIndex = playlistsTab.index
+                }
+            ]
 
             onSelectedTabChanged: {
                 // pause loading of the models in the old tab
@@ -844,18 +870,70 @@
 
             function pushNowPlaying()
             {
-                // only push if on a different page
-                if (mainPageStack.currentPage.title !== i18n.tr("Now playing")) {
-                    mainPageStack.push(Qt.resolvedUrl("ui/NowPlaying.qml"), {})
-                }
+                if (!wideAspect) {
+                    // only push if on a different page
+                    if (mainPageStack.currentPage.title !== i18n.tr("Now playing")) {
+                        mainPageStack.push(Qt.resolvedUrl("ui/NowPlaying.qml"), {})
+                    }
 
-                if (mainPageStack.currentPage.isListView === true) {
-                    mainPageStack.currentPage.isListView = false;  // ensure full view
+                    if (mainPageStack.currentPage.isListView === true) {
+                        mainPageStack.currentPage.setListView(false);  // ensure full view
+                    }
                 }
             }
         } // end of tabs
     }
 
+    //
+    // Components that are ontop of the PageStack
+    //
+
+    Loader {
+        id: nowPlayingSidebarLoader
+        active: shown || anchors.leftMargin < 0
+        anchors {  // start offscreen
+            bottom: parent.bottom
+            left: parent.right
+            leftMargin: shown && status === Loader.Ready ? -width : 0
+            top: parent.top
+        }
+        asynchronous: true
+        source: "components/NowPlayingSidebar.qml"
+        visible: width > 0
+        width: units.gu(40)
+
+        property bool shown: loadedUI && wideAspect && player.mediaPlayer.playlist.itemCount > 0
+
+        Behavior on anchors.leftMargin {
+            NumberAnimation {
+
+            }
+        }
+    }
+
+    Loader {
+        id: musicToolbar
+        active: !wideAspect || anchors.topMargin < 0
+        anchors {
+            left: parent.left
+            right: parent.right
+            top: parent.bottom
+            topMargin: !wideAspect && status === Loader.Ready ? -height : 0
+        }
+        asynchronous: true
+        source: "components/MusicToolbar.qml"
+        visible: (mainPageStack.currentPage && (mainPageStack.currentPage.showToolbar || mainPageStack.currentPage.showToolbar === undefined)) &&
+                 !firstRun &&
+                 !noMusic &&
+                 anchors.topMargin < 0
+
+        Behavior on anchors.topMargin {
+            NumberAnimation {
+
+            }
+        }
+    }
+
     LoadingSpinnerComponent {
         id: loading
     }

=== modified file 'app/ui/AddToPlaylist.qml'
--- app/ui/AddToPlaylist.qml	2016-02-28 01:51:17 +0000
+++ app/ui/AddToPlaylist.qml	2016-03-04 03:23:11 +0000
@@ -59,10 +59,13 @@
 
     // FIXME: workaround for pad.lv/1531016 (gridview juddery)
     anchors {
+        bottom: parent.bottom
+        left: parent.left
         fill: undefined
+        top: parent.top
     }
-    height: mainView.height + musicToolbar.height
-    width: mainView.width
+    height: mainPageStack.height + musicToolbar.height
+    width: mainPageStack.width
 
     property var chosenElements: []
 

=== modified file 'app/ui/Albums.qml'
--- app/ui/Albums.qml	2016-01-12 00:30:08 +0000
+++ app/ui/Albums.qml	2016-03-04 03:23:11 +0000
@@ -46,10 +46,13 @@
 
     // FIXME: workaround for pad.lv/1531016 (gridview juddery)
     anchors {
+        bottom: parent.bottom
+        left: parent.left
         fill: undefined
+        top: parent.top
     }
-    height: mainView.height
-    width: mainView.width
+    height: mainPageStack.height
+    width: mainPageStack.width
 
     // Hack for autopilot otherwise Albums appears as MusicPage
     // due to bug 1341671 it is required that there is a property so that

=== modified file 'app/ui/ArtistView.qml'
--- app/ui/ArtistView.qml	2016-01-12 00:30:08 +0000
+++ app/ui/ArtistView.qml	2016-03-04 03:23:11 +0000
@@ -38,10 +38,13 @@
 
     // FIXME: workaround for pad.lv/1531016 (gridview juddery)
     anchors {
+        bottom: parent.bottom
+        left: parent.left
         fill: undefined
+        top: parent.top
     }
-    height: mainView.height
-    width: mainView.width
+    height: mainPageStack.height
+    width: mainPageStack.width
 
     MusicGridView {
         id: artistAlbumView

=== modified file 'app/ui/Artists.qml'
--- app/ui/Artists.qml	2016-01-30 23:58:32 +0000
+++ app/ui/Artists.qml	2016-03-04 03:23:11 +0000
@@ -50,10 +50,13 @@
 
     // FIXME: workaround for pad.lv/1531016 (gridview juddery)
     anchors {
+        bottom: parent.bottom
+        left: parent.left
         fill: undefined
+        top: parent.top
     }
-    height: mainView.height
-    width: mainView.width
+    height: mainPageStack.height
+    width: mainPageStack.width
 
     // Hack for autopilot otherwise Artists appears as MusicPage
     // due to bug 1341671 it is required that there is a property so that

=== modified file 'app/ui/Genres.qml'
--- app/ui/Genres.qml	2016-01-12 00:30:08 +0000
+++ app/ui/Genres.qml	2016-03-04 03:23:11 +0000
@@ -46,10 +46,13 @@
 
     // FIXME: workaround for pad.lv/1531016 (gridview juddery)
     anchors {
+        bottom: parent.bottom
+        left: parent.left
         fill: undefined
+        top: parent.top
     }
-    height: mainView.height
-    width: mainView.width
+    height: mainPageStack.height
+    width: mainPageStack.width
 
     // Hack for autopilot otherwise Albums appears as MusicPage
     // due to bug 1341671 it is required that there is a property so that

=== modified file 'app/ui/NowPlaying.qml'
--- app/ui/NowPlaying.qml	2016-01-16 01:30:31 +0000
+++ app/ui/NowPlaying.qml	2016-03-04 03:23:11 +0000
@@ -30,8 +30,40 @@
     flickable: isListView ? queueListLoader.item : null  // Ensures that the header is shown in fullview
     objectName: "nowPlayingPage"
     showToolbar: false
+    state: isListView && queueListLoader.item.state === "multiselectable" ? "selection" : "default"
+    states: [
+        QueueHeadState {
+            thisHeader {
+                sections {
+                    model: defaultStateSections.model
+                    selectedIndex: defaultStateSections.selectedIndex
+                    onSelectedIndexChanged: isListView = !isListView
+                }
+            }
+            thisPage: nowPlaying
+        },
+        MultiSelectHeadState {
+            addToQueue: false
+            listview: queueListLoader.item
+            removable: true
+            thisHeader {
+                sections {
+                    model: defaultStateSections.model
+                    selectedIndex: defaultStateSections.selectedIndex
+                    onSelectedIndexChanged: isListView = !isListView
+                }
+            }
+            thisPage: nowPlaying
+
+            onRemoved: {
+                // Remove the tracks from the queue
+                // Use slice() to copy the list
+                // so that the indexes don't change as they are removed
+                player.mediaPlayer.playlist.removeItemsWrapper(selectedIndices.slice());
+            }
+        }
+    ]
     title: nowPlayingTitle
-    visible: false
 
     property bool isListView: false
     // TRANSLATORS: this appears in the header with limited space (around 20 characters)
@@ -59,6 +91,26 @@
         }
     }
 
+    onVisibleChanged: {
+        if (wideAspect) {
+            popWaitTimer.start()
+        }
+    }
+
+    Timer {  // FIXME: workaround for when entering wideAspect coming back from a stacked page (AddToPlaylist) and the page being deleted breaks the stacked page
+        id: popWaitTimer
+        interval: 250
+        onTriggered: mainPageStack.popPage(nowPlaying);
+    }
+
+    PageHeadSections {
+        id: defaultStateSections
+        model: [fullViewTitle, queueTitle]
+
+        // Set at startup to avoid binding loop
+        Component.onCompleted: selectedIndex = isListView ? 1 : 0
+    }
+
     // Ensure that the listview has loaded before attempting to positionAt
     function ensureListViewLoaded() {
         if (queueListLoader.item.count === player.mediaPlayer.playlist.itemCount) {
@@ -77,82 +129,16 @@
         queueListLoader.item.positionViewAtIndex(index, ListView.Center);
     }
 
-    PageHeadSections {
-        id: defaultStateSections
-        model: [fullViewTitle, queueTitle]
-        selectedIndex: isListView
-    }
-
-    head {
-        sections {
-            model: defaultStateSections.model
-            selectedIndex: defaultStateSections.selectedIndex
-            onSelectedIndexChanged: isListView = !isListView
-        }
-    }
-
-    state: isListView && queueListLoader.item.state === "multiselectable" ? "selection" : "default"
-    states: [
-        PageHeadState {
-            id: defaultState
-
-            name: "default"
-            actions: [
-                Action {
-                    enabled: !player.mediaPlayer.playlist.empty
-                    iconName: "add-to-playlist"
-                    // TRANSLATORS: this action appears in the overflow drawer with limited space (around 18 characters)
-                    text: i18n.tr("Add to playlist")
-                    visible: !isListView
-
-                    onTriggered: {
-                        var items = []
-
-                        items.push(makeDict(player.metaForSource(player.mediaPlayer.playlist.currentItemSource)));
-
-                        mainPageStack.push(Qt.resolvedUrl("AddToPlaylist.qml"),
-                                           {"chosenElements": items})
-                    }
-                },
-                Action {
-                    enabled: !player.mediaPlayer.playlist.empty
-                    iconName: "delete"
-                    objectName: "clearQueue"
-                    // TRANSLATORS: this action appears in the overflow drawer with limited space (around 18 characters)
-                    text: i18n.tr("Clear queue")
-                    visible: isListView
-
-                    onTriggered: player.mediaPlayer.playlist.clearWrapper()
-                }
-            ]
-            PropertyChanges {
-                target: nowPlaying.head
-                backAction: defaultState.backAction
-                actions: defaultState.actions
-            }
-        },
-        MultiSelectHeadState {
-            addToQueue: false
-            listview: queueListLoader.item
-            removable: true
-            thisPage: nowPlaying
-
-            onRemoved: {
-                // Remove the tracks from the queue
-                // Use slice() to copy the list
-                // so that the indexes don't change as they are removed
-                player.mediaPlayer.playlist.removeItemsWrapper(selectedIndices.slice());
-            }
-        }
-    ]
+    function setListView(listView) {
+        defaultStateSections.selectedIndex = listView ? 1 : 0;
+    }
 
     Loader {
         anchors {
             bottom: nowPlayingToolbarLoader.top
             left: parent.left
             right: parent.right
-            top: parent.top
-            topMargin: headerHeight
+            top: nowPlaying.header.bottom
         }
 
         property real headerHeight: units.gu(10.125) // FIXME: 10.125 is the header.height with the page sections
@@ -165,11 +151,9 @@
         id: queueListLoader
         anchors {
             bottom: nowPlayingToolbarLoader.top
-            bottomMargin: units.gu(2)
             left: parent.left
             right: parent.right
-            top: parent.top
-            topMargin: units.gu(2)
+            top: parent.top  // Don't use header.bottom otherwise flickery
         }
         asynchronous: true
         source: "../components/Queue.qml"
@@ -188,6 +172,16 @@
     }
 
     Connections {
+        target: mainView
+        onWideAspectChanged: {
+            // Do not pop if not visible (eg on AddToPlaylist)
+            if (wideAspect && nowPlaying.visible) {
+                mainPageStack.popPage(nowPlaying);
+            }
+        }
+    }
+
+    Connections {
         target: player.mediaPlayer.playlist
         onEmptyChanged: {
             if (player.mediaPlayer.playlist.empty) {

=== modified file 'app/ui/Playlists.qml'
--- app/ui/Playlists.qml	2016-01-29 02:20:55 +0000
+++ app/ui/Playlists.qml	2016-03-04 03:23:11 +0000
@@ -51,10 +51,13 @@
 
     // FIXME: workaround for pad.lv/1531016 (gridview juddery)
     anchors {
+        bottom: parent.bottom
+        left: parent.left
         fill: undefined
+        top: parent.top
     }
-    height: mainView.height
-    width: mainView.width
+    height: mainPageStack.height
+    width: mainPageStack.width
 
     property bool changed: false
     property bool childrenChanged: false

=== modified file 'app/ui/Recent.qml'
--- app/ui/Recent.qml	2016-01-12 01:06:16 +0000
+++ app/ui/Recent.qml	2016-03-04 03:23:11 +0000
@@ -31,14 +31,52 @@
 MusicPage {
     id: recentPage
     objectName: "recentPage"
+    header: PageHeader {
+        flickable: recentPage.flickable
+        leadingActionBar {
+            actions: {
+                if (mainPageStack.currentPage === tabs) {
+                    tabs.tabActions
+                } else if (mainPageStack.depth > 1) {
+                    backActionComponent
+                }
+            }
+        }
+        title: recentPage.title
+        trailingActionBar {
+            actions: [
+                Action {
+                    enabled: recentModel.model.count > 0
+                    iconName: "delete"
+                    onTriggered: {
+                        Library.clearRecentHistory()
+                        recentModel.filterRecent()
+                    }
+                }
+            ]
+        }
+
+        Action {
+            id: backActionComponent
+            iconName: "back"
+            onTriggered: mainPageStack.pop()
+        }
+
+        StyleHints {
+            backgroundColor: mainView.headerColor
+        }
+    }
     title: i18n.tr("Recent")
 
     // FIXME: workaround for pad.lv/1531016 (gridview juddery)
     anchors {
+        bottom: parent.bottom
+        left: parent.left
         fill: undefined
+        top: parent.top
     }
-    height: mainView.height
-    width: mainView.width
+    height: mainPageStack.height
+    width: mainPageStack.width
 
     property bool changed: false
     property bool childrenChanged: false
@@ -56,19 +94,6 @@
         onTriggered: recentModel.filterRecent()
     }
 
-    head {
-        actions: [
-            Action {
-                enabled: recentModel.model.count > 0
-                iconName: "delete"
-                onTriggered: {
-                    Library.clearRecentHistory()
-                    recentModel.filterRecent()
-                }
-            }
-        ]
-    }
-
     MusicGridView {
         id: recentGridView
         itemWidth: units.gu(15)

=== modified file 'debian/changelog'
--- debian/changelog	2016-02-28 01:51:17 +0000
+++ debian/changelog	2016-03-04 03:23:11 +0000
@@ -11,6 +11,7 @@
 
   [ Andrew Hayzen ]
   * Fix so that a press and hold cannot disable selection in the ContentHubExport.qml (LP: #1538838)
+  * Implement convergent mode with now playing and queue as a sidebar (LP: #1253761)
 
  -- Victor <victor@victor-virtual-machine>  Wed, 27 Jan 2016 19:40:55 -0600
 


Follow ups