ubuntu-touch-coreapps-reviewers team mailing list archive
-
ubuntu-touch-coreapps-reviewers team
-
Mailing list archive
-
Message #00284
[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