← Back to team overview

ubuntu-touch-coreapps-reviewers team mailing list archive

[Merge] lp:~ahayzen/ubuntu-weather-app/reboot-location-remove-reorder-multi into lp:ubuntu-weather-app/reboot

 

Andrew Hayzen has proposed merging lp:~ahayzen/ubuntu-weather-app/reboot-location-remove-reorder-multi into lp:ubuntu-weather-app/reboot.

Commit message:
* Added multiselect/swipe delete and reorder support to locations list
* Added a few FIXMEs to fix in future mps

Requested reviews:
  Ubuntu Weather Developers (ubuntu-weather-dev)

For more details, see:
https://code.launchpad.net/~ahayzen/ubuntu-weather-app/reboot-location-remove-reorder-multi/+merge/251638

* Added multiselect/swipe delete and reorder support to locations list
* Added a few FIXMEs to fix in future mps

Note the multiselect/reorder/swipe/head state etc code has been pulled/ported from lp:music-app/refactor only known limitation is that the reorder does not scroll the listview (but works fine within the current view :) ).

I've also added a few FIXMEs to things that are slow/causing issues that we need to fix in future mps, eg the selected item being lost when the locationsList is refreshed.
-- 
Your team Ubuntu Weather Developers is requested to review the proposed merge of lp:~ahayzen/ubuntu-weather-app/reboot-location-remove-reorder-multi into lp:ubuntu-weather-app/reboot.
=== modified file 'app/components/CMakeLists.txt'
--- app/components/CMakeLists.txt	2015-01-23 23:15:52 +0000
+++ app/components/CMakeLists.txt	2015-03-03 18:40:55 +0000
@@ -1,3 +1,5 @@
+add_subdirectory(ListItemActions)
+
 file(GLOB COMPONENTS_QML_JS_FILES *.qml *.js)
 
 add_custom_target(ubuntu-weather-app_components_QMlFiles ALL SOURCES ${COMPONENTS_QML_JS_FILES})

=== added directory 'app/components/ListItemActions'
=== added file 'app/components/ListItemActions/CMakeLists.txt'
--- app/components/ListItemActions/CMakeLists.txt	1970-01-01 00:00:00 +0000
+++ app/components/ListItemActions/CMakeLists.txt	2015-03-03 18:40:55 +0000
@@ -0,0 +1,5 @@
+file(GLOB LISTITEMACTIONS_QML_JS_FILES *.qml *.js)
+
+add_custom_target(ubuntu-weather-app_listitemactions_QMlFiles ALL SOURCES ${LISTITEMACTIONS_QML_JS_FILES})
+
+install(FILES ${LISTITEMACTIONS_QML_JS_FILES} DESTINATION ${UBUNTU-WEATHER_APP_DIR}/components/ListItemActions)

=== added file 'app/components/ListItemActions/CheckBox.qml'
--- app/components/ListItemActions/CheckBox.qml	1970-01-01 00:00:00 +0000
+++ app/components/ListItemActions/CheckBox.qml	2015-03-03 18:40:55 +0000
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2012-2014 Canonical, Ltd.
+ *
+ * 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.2
+import Ubuntu.Components 1.1
+
+CheckBox {
+    checked: root.selected
+    width: implicitWidth
+    // disable item mouse area to avoid conflicts with parent mouse area
+    __mouseArea.enabled: false
+}

=== added file 'app/components/ListItemActions/Remove.qml'
--- app/components/ListItemActions/Remove.qml	1970-01-01 00:00:00 +0000
+++ app/components/ListItemActions/Remove.qml	2015-03-03 18:40:55 +0000
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2014 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.3
+import Ubuntu.Components 1.1
+
+Action {
+    id: removeAction
+    iconName: "delete"
+    objectName: "swipeDeleteAction"
+    text: i18n.tr("Remove")
+}

=== added file 'app/components/ListItemReorderComponent.qml'
--- app/components/ListItemReorderComponent.qml	1970-01-01 00:00:00 +0000
+++ app/components/ListItemReorderComponent.qml	2015-03-03 18:40:55 +0000
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2013, 2014, 2015
+ *      Andrew Hayzen <ahayzen@xxxxxxxxx>
+ *      Nekhelesh Ramananthan <krnekhelesh@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.3
+import Ubuntu.Components 1.1
+
+
+Item {
+    id: actionReorder
+    width: units.gu(4)
+
+    Icon {
+        anchors {
+            horizontalCenter: parent.horizontalCenter
+            verticalCenter: parent.verticalCenter
+        }
+        name: "navigation-menu"  // TODO: use proper image
+        height: width
+        width: units.gu(3)
+    }
+
+    MouseArea {
+        id: actionReorderMouseArea
+        anchors {
+            fill: parent
+        }
+        property int startY: 0
+        property int startContentY: 0
+
+        onPressed: {
+            root.parent.parent.interactive = false;  // stop scrolling of listview
+            startY = root.y;
+            startContentY = root.parent.parent.contentY;
+            root.z += 10;  // force ontop of other elements
+
+            console.debug("Reorder listitem pressed", root.y)
+        }
+        onMouseYChanged: root.y += mouse.y - (root.height / 2);
+        onReleased: {
+            console.debug("Reorder diff by position", getDiff());
+
+            var diff = getDiff();
+
+            // Remove the height of the actual item if moved down
+            if (diff > 0) {
+                diff -= 1;
+            }
+
+            root.parent.parent.interactive = true;  // reenable scrolling
+
+            if (diff === 0) {
+                // Nothing has changed so reset the item
+                // z index is restored after animation
+                resetListItemYAnimation.start();
+            }
+            else {
+                var newIndex = index + diff;
+
+                if (newIndex < 0) {
+                    newIndex = 0;
+                }
+                else if (newIndex > root.parent.parent.count - 1) {
+                    newIndex = root.parent.parent.count - 1;
+                }
+
+                root.z -= 10;  // restore z index
+                reorder(index, newIndex)
+            }
+        }
+
+        function getDiff() {
+            // Get the amount of items that have been passed over (by centre)
+            return Math.round((((root.y - startY) + (root.parent.parent.contentY - startContentY)) / root.height) + 0.5);
+        }
+    }
+
+    SequentialAnimation {
+        id: resetListItemYAnimation
+        UbuntuNumberAnimation {
+            target: root;
+            property: "y";
+            to: actionReorderMouseArea.startY
+        }
+        ScriptAction {
+            script: {
+                root.z -= 10;  // restore z index
+            }
+        }
+    }
+}

=== added file 'app/components/ListItemWithActions.qml'
--- app/components/ListItemWithActions.qml	1970-01-01 00:00:00 +0000
+++ app/components/ListItemWithActions.qml	2015-03-03 18:40:55 +0000
@@ -0,0 +1,496 @@
+/*
+ * Copyright (C) 2012-2015 Canonical, Ltd.
+ *
+ * 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.3
+import Ubuntu.Components 1.1
+import Ubuntu.Components.ListItems 1.0 as ListItem
+
+
+Item {
+    id: root
+    width: parent.width
+
+    property Action leftSideAction: null
+    property list<Action> rightSideActions
+    property double defaultHeight: units.gu(8)
+    property bool locked: false
+    property Action activeAction: null
+    property var activeItem: null
+    property bool triggerActionOnMouseRelease: false
+    property color color: Theme.palette.normal.background
+    property color selectedColor: "#E6E6E6"
+    property bool selected: false
+    property bool selectionMode: false
+    property alias internalAnchors: mainContents.anchors
+    default property alias contents: mainContents.children
+
+    readonly property double actionWidth: units.gu(4)
+    readonly property double leftActionWidth: units.gu(10)
+    readonly property double actionThreshold: actionWidth * 0.4
+    readonly property double threshold: 0.4
+    readonly property string swipeState: main.x == 0 ? "Normal" : main.x > 0 ? "LeftToRight" : "RightToLeft"
+    readonly property alias swipping: mainItemMoving.running
+    readonly property bool _showActions: mouseArea.pressed || swipeState != "Normal" || swipping
+
+    property alias _main: main  // CUSTOM
+    property alias pressed: mouseArea.pressed  // CUSTOM
+
+    /* internal */
+    property var _visibleRightSideActions: filterVisibleActions(rightSideActions)
+
+    signal itemClicked(var mouse)
+    signal itemPressAndHold(var mouse)
+
+    function returnToBoundsRTL(direction)
+    {
+        var actionFullWidth = actionWidth + units.gu(2)
+
+        // go back to normal state if swipping reverse
+        if (direction === "LTR") {
+            updatePosition(0)
+            return
+        } else if (!triggerActionOnMouseRelease) {
+            updatePosition(-rightActionsView.width + units.gu(2))
+            return
+        }
+
+        var xOffset = Math.abs(main.x)
+        var index = Math.min(Math.floor(xOffset / actionFullWidth), _visibleRightSideActions.length)
+        var newX = 0
+
+        if (index === _visibleRightSideActions.length) {
+            newX = -(rightActionsView.width - units.gu(2))
+        } else if (index >= 1) {
+            newX = -(actionFullWidth * index)
+        }
+
+        updatePosition(newX)
+    }
+
+    function returnToBoundsLTR(direction)
+    {
+        var finalX = leftActionWidth
+        if ((direction === "RTL") || (main.x <= (finalX * root.threshold)))
+            finalX = 0
+        updatePosition(finalX)
+    }
+
+    function returnToBounds(direction)
+    {
+        if (main.x < 0) {
+            returnToBoundsRTL(direction)
+        } else if (main.x > 0) {
+            returnToBoundsLTR(direction)
+        } else {
+            updatePosition(0)
+        }
+    }
+
+    function contains(item, point, marginX)
+    {
+        var itemStartX = item.x - marginX
+        var itemEndX = item.x + item.width + marginX
+        return (point.x >= itemStartX) && (point.x <= itemEndX) &&
+               (point.y >= item.y) && (point.y <= (item.y + item.height));
+    }
+
+    function getActionAt(point)
+    {
+        if (leftSideAction && contains(leftActionViewLoader.item, point, 0)) {
+            return leftSideAction
+        } else if (contains(rightActionsView, point, 0)) {
+            var newPoint = root.mapToItem(rightActionsView, point.x, point.y)
+            for (var i = 0; i < rightActionsRepeater.count; i++) {
+                var child = rightActionsRepeater.itemAt(i)
+                if (contains(child, newPoint, units.gu(1))) {
+                    return i
+                }
+            }
+        }
+        return -1
+    }
+
+    function updateActiveAction()
+    {
+        if (triggerActionOnMouseRelease &&
+            (main.x <= -(root.actionWidth + units.gu(2))) &&
+            (main.x > -(rightActionsView.width - units.gu(2)))) {
+            var actionFullWidth = actionWidth + units.gu(2)
+            var xOffset = Math.abs(main.x)
+            var index = Math.min(Math.floor(xOffset / actionFullWidth), _visibleRightSideActions.length)
+            index = index - 1
+            if (index > -1) {
+                root.activeItem = rightActionsRepeater.itemAt(index)
+                root.activeAction = root._visibleRightSideActions[index]
+            }
+        } else {
+            root.activeAction = null
+        }
+    }
+
+    function resetSwipe()
+    {
+        updatePosition(0)
+    }
+
+    function filterVisibleActions(actions)
+    {
+        var visibleActions = []
+        for(var i = 0; i < actions.length; i++) {
+            var action = actions[i]
+            if (action.visible) {
+                visibleActions.push(action)
+            }
+        }
+        return visibleActions
+    }
+
+    function updatePosition(pos)
+    {
+        if (!root.triggerActionOnMouseRelease && (pos !== 0)) {
+            mouseArea.state = pos > 0 ? "RightToLeft" : "LeftToRight"
+        } else {
+            mouseArea.state = ""
+        }
+        main.x = pos
+    }
+
+    // CUSTOM remove animation
+    SequentialAnimation {
+        id: removeAnimation
+
+        property var action
+
+        UbuntuNumberAnimation {
+            target: root
+            duration: UbuntuAnimation.BriskDuration
+            property: "height";
+            to: 0
+        }
+        ScriptAction {
+            script: removeAnimation.action.trigger()
+        }
+    }
+
+    states: [
+        State {
+            name: "select"
+            when: selectionMode || selected
+            PropertyChanges {
+                target: selectionIcon
+                source: Qt.resolvedUrl("ListItemActions/CheckBox.qml")
+                anchors.leftMargin: units.gu(2)
+            }
+            PropertyChanges {
+                target: root
+                locked: true
+            }
+            PropertyChanges {
+                target: main
+                x: 0
+            }
+        }
+    ]
+
+    height: defaultHeight
+    //clip: height !== defaultHeight  // CUSTOM
+
+    Loader {  // CUSTOM
+        id: leftActionViewLoader
+        anchors {
+            top: parent.top
+            bottom: parent.bottom
+            right: main.left
+        }
+        asynchronous: true
+        sourceComponent: leftSideAction ? leftActionViewComponent : undefined
+    }
+
+    Component {  // CUSTOM
+        id: leftActionViewComponent
+
+        Rectangle {
+            id: leftActionView
+            width: root.leftActionWidth + actionThreshold
+            color: UbuntuColors.red
+
+            Icon {
+                id: leftActionIcon
+                anchors {
+                    centerIn: parent
+                    horizontalCenterOffset: actionThreshold / 2
+                }
+                objectName: "swipeDeleteAction"  // CUSTOM
+                name: leftSideAction && _showActions ? leftSideAction.iconName : ""
+                color: Theme.palette.selected.field
+                height: units.gu(3)
+                width: units.gu(3)
+            }
+        }
+    }
+
+    //Rectangle {
+    Item {  // CUSTOM
+       id: rightActionsView
+
+       anchors {
+           top: main.top
+           left: main.right
+           bottom: main.bottom
+       }
+       visible: _visibleRightSideActions.length > 0
+       width: rightActionsRepeater.count > 0 ? rightActionsRepeater.count * (root.actionWidth + units.gu(2)) + root.actionThreshold + units.gu(2) : 0
+       // color: "white"  // CUSTOM
+
+       Row {
+           anchors{
+               top: parent.top
+               left: parent.left
+               leftMargin: units.gu(2)
+               right: parent.right
+               rightMargin: units.gu(2)
+               bottom: parent.bottom
+           }
+           spacing: units.gu(2)
+           Repeater {
+               id: rightActionsRepeater
+
+               model: _showActions ? _visibleRightSideActions : []
+               Item {
+                   property alias image: img
+
+                   height: rightActionsView.height
+                   width: root.actionWidth
+
+                   Icon {
+                       id: img
+
+                       anchors.centerIn: parent
+                       objectName: rightSideActions[index].objectName  // CUSTOM
+                       width: units.gu(3)
+                       height: units.gu(3)
+                       name: modelData.iconName
+                       color: root.activeAction === modelData ? UbuntuColors.orange : UbuntuColors.coolGrey  // CUSTOM
+                   }
+               }
+           }
+       }
+    }
+
+    Rectangle {
+        id: main
+        objectName: "mainItem"
+
+        anchors {
+            top: parent.top
+            bottom: parent.bottom
+        }
+
+        width: parent.width
+        color: root.selected ? root.selectedColor : root.color
+
+        Loader {
+            id: selectionIcon
+
+            anchors {
+                left: main.left
+                verticalCenter: main.verticalCenter
+            }
+            asynchronous: true  // CUSTOM
+            width: (status === Loader.Ready) ? item.implicitWidth : 0
+            visible: (status === Loader.Ready) && (item.width === item.implicitWidth)
+
+            Behavior on width {
+                NumberAnimation {
+                    duration: UbuntuAnimation.SnapDuration
+                }
+            }
+        }
+
+        Item {
+            id: mainContents
+
+            anchors {
+                left: selectionIcon.right
+                //leftMargin: units.gu(2)  // CUSTOM
+                top: parent.top
+                //topMargin: units.gu(1)  // CUSTOM
+                right: parent.right
+                //rightMargin: units.gu(2)  // CUSTOM
+                bottom: parent.bottom
+                //bottomMargin: units.gu(1)  // CUSTOM
+            }
+        }
+
+        Behavior on x {
+            UbuntuNumberAnimation {
+                id: mainItemMoving
+
+                easing.type: Easing.OutElastic
+                duration: UbuntuAnimation.SlowDuration
+            }
+        }
+    }
+
+    SequentialAnimation {
+        id: triggerAction
+
+        property var currentItem: root.activeItem ? root.activeItem.image : null
+
+        running: false
+        ParallelAnimation {
+            UbuntuNumberAnimation {
+                target: triggerAction.currentItem
+                property: "opacity"
+                from: 1.0
+                to: 0.0
+                duration: UbuntuAnimation.SlowDuration
+                easing {type: Easing.InOutBack; }
+            }
+            UbuntuNumberAnimation {
+                target: triggerAction.currentItem
+                properties: "width, height"
+                from: units.gu(3)
+                to: root.actionWidth
+                duration: UbuntuAnimation.SlowDuration
+                easing {type: Easing.InOutBack; }
+            }
+        }
+        PropertyAction {
+            target: triggerAction.currentItem
+            properties: "width, height"
+            value: units.gu(3)
+        }
+        PropertyAction {
+            target: triggerAction.currentItem
+            properties: "opacity"
+            value: 1.0
+        }
+        ScriptAction {
+            script: {
+                root.activeAction.triggered(root)
+                mouseArea.state = ""
+            }
+        }
+        PauseAnimation {
+            duration: 500
+        }
+        UbuntuNumberAnimation {
+            target: main
+            property: "x"
+            to: 0
+        }
+    }
+
+    MouseArea {
+        id: mouseArea
+
+        property bool locked: root.locked || ((root.leftSideAction === null) && (root._visibleRightSideActions.count === 0))  // CUSTOM
+        property bool manual: false
+        property string direction: "None"
+        property real lastX: -1
+
+        anchors.fill: parent
+        drag {
+            target: locked ? null : main
+            axis: Drag.XAxis
+            minimumX: rightActionsView.visible ? -(rightActionsView.width) : 0
+            maximumX: leftSideAction ? leftActionViewLoader.item.width : 0
+            threshold: root.actionThreshold
+        }
+
+        states: [
+            State {
+                name: "LeftToRight"
+                PropertyChanges {
+                    target: mouseArea
+                    drag.maximumX: 0
+                }
+            },
+            State {
+                name: "RightToLeft"
+                PropertyChanges {
+                    target: mouseArea
+                    drag.minimumX: 0
+                }
+            }
+        ]
+
+        onMouseXChanged: {
+            var offset = (lastX - mouseX)
+            if (Math.abs(offset) <= root.actionThreshold) {
+                return
+            }
+            lastX = mouseX
+            direction = offset > 0 ? "RTL" : "LTR";
+        }
+
+        onPressed: {
+            lastX = mouse.x
+        }
+
+        onReleased: {
+            if (root.triggerActionOnMouseRelease && root.activeAction) {
+                triggerAction.start()
+            } else {
+                root.returnToBounds()
+                root.activeAction = null
+            }
+            lastX = -1
+            direction = "None"
+        }
+        onClicked: {
+            if (selectionMode) {  // CUSTOM - selecting a listitem should toggle selection if in selectionMode
+                selected = !selected
+                return
+            } else if (main.x === 0) {
+                root.itemClicked(mouse)
+            } else if (main.x > 0) {
+                var action = getActionAt(Qt.point(mouse.x, mouse.y))
+                if (action && action !== -1) {
+                    //action.triggered(root)
+                    removeAnimation.action = action  // CUSTOM - use our animation instead
+                    removeAnimation.start()  // CUSTOM
+                }
+            } else {
+                var actionIndex = getActionAt(Qt.point(mouse.x, mouse.y))
+
+                if (actionIndex !== -1 && actionIndex !== leftSideAction) {  // CUSTOM - can be leftAction
+                    root.activeItem = rightActionsRepeater.itemAt(actionIndex)
+                    root.activeAction = root.rightSideActions[actionIndex]
+                    triggerAction.start()
+                    return
+                }
+            }
+            root.resetSwipe()
+        }
+
+        onPositionChanged: {
+            if (mouseArea.pressed) {
+                updateActiveAction()
+
+                listItemSwiping(index)  // CUSTOM - tells other listitems to dismiss any swipe
+            }
+        }
+        onPressAndHold: {
+            if (main.x === 0) {
+                root.itemPressAndHold(mouse)
+            }
+        }
+
+        z: -1
+    }
+}

=== added file 'app/components/MultiSelectHeadState.qml'
--- app/components/MultiSelectHeadState.qml	1970-01-01 00:00:00 +0000
+++ app/components/MultiSelectHeadState.qml	2015-03-03 18:40:55 +0000
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2015
+ *      Andrew Hayzen <ahayzen@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.3
+import Ubuntu.Components 1.1
+
+PageHeadState {
+    id: selectionState
+    actions: [
+        Action {
+            iconName: "select"
+            text: i18n.tr("Select All")
+            onTriggered: {
+                if (listview.selectedItems.length === listview.model.count) {
+                    listview.clearSelection()
+                } else {
+                    listview.selectAll()
+                }
+            }
+        },
+        Action {
+            enabled: listview.selectedItems.length > 0
+            iconName: "delete"
+            text: i18n.tr("Delete")
+            visible: removable
+
+            onTriggered: {
+                removed(listview.selectedItems)
+
+                listview.closeSelection()
+            }
+        }
+
+    ]
+    backAction: Action {
+        text: i18n.tr("Cancel selection")
+        iconName: "back"
+        onTriggered: {
+            listview.clearSelection()
+            listview.state = "normal"
+        }
+    }
+    head: thisPage.head
+    name: "selection"
+
+    PropertyChanges {
+        target: thisPage.head
+        backAction: selectionState.backAction
+        actions: selectionState.actions
+    }
+
+    property ListView listview
+    property bool removable: false
+    property Page thisPage
+
+    signal removed(var selectedItems)
+}

=== added file 'app/components/MultiSelectListView.qml'
--- app/components/MultiSelectListView.qml	1970-01-01 00:00:00 +0000
+++ app/components/MultiSelectListView.qml	2015-03-03 18:40:55 +0000
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2013, 2014, 2015
+ *      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.3
+import Ubuntu.Components 1.1
+
+
+WeatherListView {
+    property var selectedItems: []
+
+    signal clearSelection()
+    signal closeSelection()
+    signal selectAll()
+
+    onClearSelection: selectedItems = []
+    onCloseSelection: {
+        clearSelection()
+        state = "normal"
+    }
+    onSelectAll: {
+        var tmp = selectedItems
+
+        for (var i=0; i < model.count; i++) {
+            if (tmp.indexOf(i) === -1) {
+                tmp.push(i)
+            }
+        }
+
+        selectedItems = tmp
+    }
+    onVisibleChanged: {
+        if (!visible) {
+            closeSelection()
+        }
+    }
+}

=== added file 'app/components/WeatherListItem.qml'
--- app/components/WeatherListItem.qml	1970-01-01 00:00:00 +0000
+++ app/components/WeatherListItem.qml	2015-03-03 18:40:55 +0000
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2013, 2014, 2015
+ *      Andrew Hayzen <ahayzen@xxxxxxxxx>
+ *      Nekhelesh Ramananthan <krnekhelesh@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.3
+import Ubuntu.Components 1.1
+import Ubuntu.Components.ListItems 0.1 as ListItem
+
+
+ListItemWithActions {
+    id: root
+
+    property int listItemIndex: index
+    property bool multiselectable: false
+    property int previousListItemIndex: -1
+    property bool reorderable: false
+
+    signal reorder(int from, int to)
+
+    onItemPressAndHold: {
+        if (multiselectable) {
+            selectionMode = true
+        }
+    }
+
+    onListItemIndexChanged: {
+        var i = parent.parent.selectedItems.lastIndexOf(previousListItemIndex)
+
+        if (i !== -1) {
+            parent.parent.selectedItems[i] = listItemIndex
+        }
+
+        previousListItemIndex = listItemIndex
+    }
+
+    onSelectedChanged: {
+        if (selectionMode) {
+            var tmp = parent.parent.selectedItems
+
+            if (selected) {
+                if (parent.parent.selectedItems.indexOf(listItemIndex) === -1) {
+                    tmp.push(listItemIndex)
+                    parent.parent.selectedItems = tmp
+                }
+            } else {
+                tmp.splice(parent.parent.selectedItems.indexOf(listItemIndex), 1)
+                parent.parent.selectedItems = tmp
+            }
+        }
+    }
+
+    onSelectionModeChanged: {
+        if (reorderable && selectionMode) {
+            resetSwipe()
+        }
+
+        for (var j=0; j < _main.children.length; j++) {
+            if (_main.children[j] !== actionReorderLoader) {
+                _main.children[j].anchors.rightMargin = reorderable && selectionMode ? actionReorderLoader.width + units.gu(2) : 0
+            }
+        }
+
+        parent.parent.state = selectionMode ? "multiselectable" : "normal"
+
+        if (!selectionMode) {
+            selected = false
+        }
+    }
+
+    /* Highlight the listitem on press */
+    Rectangle {
+        id: listItemBrighten
+        color: root.pressed ? UbuntuColors.coolGrey : "transparent"
+        opacity: 0.1
+        height: root.height
+        x: root.x - parent.x  // -parent.x due to selectionIcon in ListItemWithActions
+        width: root.width
+    }
+
+    /* Reorder Component */
+    Loader {
+        id: actionReorderLoader
+        active: reorderable && selectionMode && root.parent.parent.selectedItems.length === 0
+        anchors {
+            bottom: parent.bottom
+            right: parent.right
+            rightMargin: units.gu(1)
+            top: parent.top
+        }
+        asynchronous: true
+        source: "ListItemReorderComponent.qml"
+    }
+
+    Item {
+        Connections {  // Only allow one ListItem to be swiping at any time
+            target: weatherApp
+            onListItemSwiping: {
+                if (i !== index) {
+                    root.resetSwipe();
+                }
+            }
+        }
+
+        Connections {  // Connections from signals in the ListView
+            target: root.parent.parent
+            onClearSelection: selected = false
+            onFlickingChanged: {
+                if (root.parent.parent.flicking) {
+                    root.resetSwipe()
+                }
+            }
+            onSelectAll: selected = true
+            onStateChanged: selectionMode = root.parent.parent.state === "multiselectable"
+        }
+    }
+
+    Component.onCompleted: {  // reload settings as delegates are destroyed
+        if (parent.parent.selectedItems.indexOf(index) !== -1) {
+            selected = true
+        }
+
+        selectionMode = root.parent.parent.state === "multiselectable"
+    }
+}

=== added file 'app/components/WeatherListView.qml'
--- app/components/WeatherListView.qml	1970-01-01 00:00:00 +0000
+++ app/components/WeatherListView.qml	2015-03-03 18:40:55 +0000
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2013, 2014, 2015
+ *      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.3
+import Ubuntu.Components 1.1
+
+
+ListView {
+    Component.onCompleted: {
+        // FIXME: workaround for qtubuntu not returning values depending on the grid unit definition
+        // for Flickable.maximumFlickVelocity and Flickable.flickDeceleration
+        var scaleFactor = units.gridUnit / 8;
+        maximumFlickVelocity = maximumFlickVelocity * scaleFactor;
+        flickDeceleration = flickDeceleration * scaleFactor;
+    }
+}

=== modified file 'app/data/Storage.qml'
--- app/data/Storage.qml	2015-03-03 00:19:21 +0000
+++ app/data/Storage.qml	2015-03-03 18:40:55 +0000
@@ -134,10 +134,56 @@
         });
     }
 
+    function clearMultiLocation(locations) {
+        openDB();
+
+        db.transaction(function (tx) {
+            // Remove all the deleted indexes
+            for (var i=0; i < locations.length; i++) {
+                tx.executeSql('DELETE FROM Locations WHERE id=?;', [locations[i]])
+            }
+
+            // Rebuild locations in order
+            var rs = tx.executeSql('SELECT id FROM Locations ORDER BY id ASC')
+
+            for (i=0; i < rs.rows.length; i++) {
+                tx.executeSql('UPDATE Locations SET id=? WHERE id=?;',
+                              [i, rs.rows.item(i).id])
+            }
+        })
+    }
+
     function clearDB() { // for dev purposes
         openDB();
         db.transaction(function(tx){
             tx.executeSql('DELETE FROM Locations WHERE 1');
         });
     }
+
+    function reorder(from, to) {
+        openDB();
+
+        db.transaction(function(tx) {
+            // Track to move put as -1 for now
+            tx.executeSql('UPDATE Locations SET id=? WHERE id=?;',
+                          [-1, from])
+
+            // Shuffle locations inbetween from->to
+            if (from > to) {
+                for (var i = from-1; i >= to; i--) {
+                    tx.executeSql('UPDATE Locations SET id=? WHERE id=?;',
+                                  [i+1, i])
+                }
+            } else {
+                for (var j = from+1; j <= to; j++) {
+                    tx.executeSql('UPDATE Locations SET id=? WHERE id=?;',
+                                  [j-1, j])
+                }
+            }
+
+            // Switch moving location to its new position
+            tx.executeSql('UPDATE Locations SET id=? WHERE id=?;',
+                          [to, -1])
+        })
+    }
 }

=== modified file 'app/ubuntu-weather-app.qml'
--- app/ubuntu-weather-app.qml	2015-03-03 00:19:21 +0000
+++ app/ubuntu-weather-app.qml	2015-03-03 18:40:55 +0000
@@ -42,6 +42,8 @@
     useDeprecatedToolbar: false
     anchorToKeyboard: true
 
+    signal listItemSwiping(int i)
+
     /*
       List of locations and their data, accessible through index
     */
@@ -152,7 +154,8 @@
                     storage.insertLocation({location: location});
                 }
 
-                refreshData(false, true)
+                refreshData(true, false)  // load new location into models (without data)
+                refreshData(false, true)  // FIXME: can be really slow as it refreshes all models
             }
 
             return !exists;
@@ -172,6 +175,56 @@
 
             return exists;
         }
+
+        function moveLocation(from, to) {
+            // Update settings to respect new changes
+            if (from === settings.current) {
+                settings.current = to;
+            } else if (from < settings.current && to >= settings.current) {
+                settings.current -= 1;
+            } else if (from > settings.current && to <= settings.current) {
+                settings.current += 1;
+            }
+
+            storage.reorder(locationsList[from].db.id, locationsList[to].db.id);
+
+            refreshData(true, false);
+        }
+
+        // Remove a location from the list
+        function removeLocation(index) {
+            if (settings.current >= index) {  // Update settings to respect new changes
+                settings.current -= settings.current;
+            }
+
+            storage.clearLocations(locationsList[index].db.id);
+
+            refreshData(true, false);
+        }
+
+        function removeMultiLocations(indexes) {
+            var i;
+
+            // Sort the item indexes as loops below assume *numeric* sort
+            indexes.sort(function(a,b) { return a - b })
+
+            // TODO: resort settings
+            for (i=0; i < indexes.length; i++) {
+                if (settings.current >= i) {  // Update settings to respect new changes
+                    settings.current -= settings.current;
+                }
+            }
+
+            var locations = []
+
+            for (i=0; i < indexes.length; i++) {
+                locations.push(locationsList[indexes[i]].db.id)
+            }
+
+            storage.clearMultiLocation(locations);
+
+            refreshData(true, false);
+        }
     }
 
     PageStack {

=== modified file 'app/ui/HomePage.qml'
--- app/ui/HomePage.qml	2015-03-03 10:12:58 +0000
+++ app/ui/HomePage.qml	2015-03-03 18:40:55 +0000
@@ -105,6 +105,7 @@
             highlightRangeMode: ListView.StrictlyEnforceRange
             onCurrentIndexChanged: {
                 if (loaded) {
+                    // FIXME: when a model is reloaded this causes the currentIndex to be lost
                     settings.current = currentIndex
                 }
             }

=== modified file 'app/ui/LocationsPage.qml'
--- app/ui/LocationsPage.qml	2015-03-03 00:19:21 +0000
+++ app/ui/LocationsPage.qml	2015-03-03 18:40:55 +0000
@@ -19,6 +19,8 @@
 import QtQuick 2.3
 import Ubuntu.Components 1.1
 import Ubuntu.Components.ListItems 0.1 as ListItem
+import "../components"
+import "../components/ListItemActions"
 
 
 Page {
@@ -27,26 +29,73 @@
     flickable: null
     title: i18n.tr("Locations")
 
-    head.actions: [
-        Action {
-            iconName: "add"
-            onTriggered: mainPageStack.push(Qt.resolvedUrl("AddPage.qml"))
+    state: locationsListView.state === "multiselectable" ? "selection" : "default"
+    states: [
+        PageHeadState {
+            id: defaultState
+            name: "default"
+            actions: [
+                Action {
+                    iconName: "add"
+                    onTriggered: mainPageStack.push(Qt.resolvedUrl("AddPage.qml"))
+                }
+            ]
+            PropertyChanges {
+                target: locationsPage.head
+                actions: defaultState.actions
+            }
+        },
+        MultiSelectHeadState {
+            listview: locationsListView
+            removable: true
+            thisPage: locationsPage
+
+            onRemoved: storage.removeMultiLocations(selectedItems.slice())
         }
     ]
 
-    ListView {
+    MultiSelectListView {
+        id: locationsListView
         anchors {
             fill: parent
         }
         model: ListModel {
             id: locationsModel
         }
-        delegate: ListItem.Standard {
-            text: model.location.name
-            onClicked: {
+        delegate: WeatherListItem {
+            leftSideAction: Remove {
+                onTriggered: storage.removeLocation(index)
+            }
+            multiselectable: true
+            reorderable: true
+
+            onItemClicked: {
                 settings.current = index;
                 pageStack.pop()
             }
+            onReorder: {
+                console.debug("Move: ", from, to);
+
+                storage.moveLocation(from, to);
+            }
+
+            Label {
+                anchors {
+                    left: parent.left
+                    leftMargin: units.gu(2)
+                    right: parent.right
+                    rightMargin: units.gu(2)
+                    verticalCenter: parent.verticalCenter
+                }
+                elide: Text.ElideRight
+                text: model.location.name
+            }
+
+            ListItem.ThinDivider {
+                anchors {
+                    bottom: parent.bottom
+                }
+            }
         }
     }
 

=== modified file 'po/com.ubuntu.weather.pot'
--- po/com.ubuntu.weather.pot	2015-03-03 00:19:21 +0000
+++ po/com.ubuntu.weather.pot	2015-03-03 18:40:55 +0000
@@ -8,7 +8,7 @@
 msgstr ""
 "Project-Id-Version: ubuntu-weather-app\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2015-03-03 00:18+0000\n"
+"POT-Creation-Date: 2015-03-03 18:35+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@xxxxxx>\n"
@@ -21,6 +21,22 @@
 msgid "Today"
 msgstr ""
 
+#: ../app/components/ListItemActions/Remove.qml:26
+msgid "Remove"
+msgstr ""
+
+#: ../app/components/MultiSelectHeadState.qml:27
+msgid "Select All"
+msgstr ""
+
+#: ../app/components/MultiSelectHeadState.qml:39
+msgid "Delete"
+msgstr ""
+
+#: ../app/components/MultiSelectHeadState.qml:51
+msgid "Cancel selection"
+msgstr ""
+
 #: ../app/ui/AddPage.qml:29
 msgid "Add city"
 msgstr ""
@@ -45,7 +61,7 @@
 msgid "OK"
 msgstr ""
 
-#: ../app/ui/HomePage.qml:30 ../app/ui/LocationsPage.qml:28
+#: ../app/ui/HomePage.qml:30 ../app/ui/LocationsPage.qml:30
 msgid "Locations"
 msgstr ""
 


Follow ups