on xenial help sign in
Allow the scopes list to automatically scroll when a scope is being dragged past the bounds of the screen. (LP: #1575319)

Approved by: Andrea Cimitan, Unity8 CI Bot

commit825433321356488944e87b48e4235a5519924e88 authorJosh Arenson <joshua.arenson@canonical.com> dateTue 10 Jan 2017, 14:46 (10 years ago) parentd4c9eab9f0b577
6 files changed+139−47
qml/Dash/ScopesList.qml+120−39 qml/Dash/ScopesListCategory.qml+3 tests/mocks/Unity/fake_scopes.cpp+7 tests/qmltests/Dash/tst_Dash.qml+5−4 tests/qmltests/Dash/tst_DashContent.qml+3−3 tests/qmltests/Dash/tst_DashShell.qml+1−1
qml/Dash/ScopesList.qml+120−39
@@ -22,6 +22,8 @@ import "../Components"
2222Item {
2323 id: root
2424
25+ property alias scopesListFlickable: scopesListFlickable
26+
2527 // Properties set by parent
2628 property var scope: null
2729
@@ -35,11 +37,98 @@ Item {
3537 signal requestFavoriteMoveTo(string scopeId, int index)
3638 signal requestRestore(string scopeId)
3739
38− state: "browse"
40+ Item {
41+ id: autoscroller
42+
43+ property bool dragging: false
44+ property var dragItem: new Object();
45+
46+ readonly property bool fuzzyAtYEnd: {
47+ var contentHeight = root.scopesListFlickable.contentHeight
48+ var contentY = root.scopesListFlickable.contentY
49+ var dragItemHeight = dragItem ? autoscroller.dragItem.height : 0
50+ var flickableHeight = root.scopesListFlickable.height
51+
52+ if (!dragItem) {
53+ return true;
54+ } else {
55+ return contentY >= (contentHeight - flickableHeight) - dragItemHeight
56+ }
57+ }
58+
59+ readonly property real bottomBoundary: {
60+ var contentHeight = root.scopesListFlickable.contentHeight
61+ var contentY = root.scopesListFlickable.contentY
62+ var dragItemHeight = dragItem ? autoscroller.dragItem.height : 0
63+ var heightRatio = root.scopesListFlickable.visibleArea.heightRatio
64+
65+ if (!dragItem) {
66+ return true;
67+ } else {
68+ return (heightRatio * contentHeight) -
69+ (1.5 * dragItemHeight) + contentY
70+ }
71+ }
72+
73+ readonly property int delayMs: 32
74+ readonly property real topBoundary: dragItem ? root.scopesListFlickable.contentY + (.5 * dragItem.height) : 0
75+
76+ visible: false
77+ readonly property real maxStep: units.dp(10)
78+ function stepSize(scrollingUp) {
79+ var delta, step;
80+ if (scrollingUp) {
81+ delta = dragItem.y - topBoundary;
82+ delta /= (1.5 * dragItem.height);
83+ } else {
84+ delta = dragItem.y - bottomBoundary;
85+ delta /= (1.5 * dragItem.height);
86+ }
87+
88+ step = Math.abs(delta) * autoscroller.maxStep
89+ return Math.ceil(step);
90+ }
91+
92+
93+ Timer {
94+ interval: autoscroller.delayMs
95+ running: autoscroller.dragItem ? (autoscroller.dragging &&
96+ autoscroller.dragItem.y < autoscroller.topBoundary &&
97+ !root.scopesListFlickable.atYBeginning) : false
98+ repeat: true
99+ onTriggered: {
100+ root.scopesListFlickable.contentY -= autoscroller.stepSize(true);
101+ autoscroller.dragItem.y -= autoscroller.stepSize(true);
102+ }
103+ }
104+
105+ Timer {
106+ interval: autoscroller.delayMs
107+ running: autoscroller.dragItem ? (autoscroller.dragging &&
108+ autoscroller.dragItem.y >= autoscroller.bottomBoundary &&
109+ !autoscroller.fuzzyAtYEnd) : false
110+ repeat: true
111+ onTriggered: {
112+ root.scopesListFlickable.contentY += autoscroller.stepSize(false);
113+ autoscroller.dragItem.y += autoscroller.stepSize(false);
114+ }
115+ }
116+ }
39117
40− property var scopeStyle: ScopeStyle {
118+ function autoscroll(dragging, dragItem) {
119+ if (dragging) {
120+ autoscroller.dragItem = dragItem
121+ autoscroller.dragging = true;
122+ } else {
123+ autoscroller.dragItem = null;
124+ autoscroller.dragging = false
125+ }
41126 }
42127
128+ state: "browse"
129+
130+ property var scopeStyle: ScopeStyle {}
131+
43132 onStateChanged: {
44133 if (state == "edit") {
45134 // As per design entering edit mode clears the possible existing search
@@ -73,7 +162,8 @@ Item {
73162 z: 1
74163 }
75164
76− Flickable {
165+ ListView {
166+ id: scopesListFlickable
77167 objectName: "scopesListFlickable"
78168 anchors {
79169 top: header.bottom
@@ -82,43 +172,34 @@ Item {
82172 right: parent.right
83173 }
84174 clip: true
85− contentWidth: root.width
86− contentHeight: column.height
87− onContentHeightChanged: returnToBounds();
88− Column {
89− id: column
90− Repeater {
91− model: scope ? scope.categories : null
92−
93− delegate: Loader {
94− asynchronous: true
95− width: root.width
96− active: results.count > 0
97− visible: active
98− sourceComponent: ScopesListCategory {
99− objectName: "scopesListCategory" + categoryId
100−
101− model: results
102−
103− title: {
104− if (isFavoritesFeed) return i18n.tr("Home");
105− else if (isAlsoInstalled) return i18n.tr("Also installed");
106− else return name;
107− }
108−
109− editMode: root.state == "edit"
110−
111− scopeStyle: root.scopeStyle
112− isFavoritesFeed: categoryId == "favorites"
113− isAlsoInstalled: categoryId == "other"
114−
115− onRequestFavorite: root.requestFavorite(scopeId, favorite);
116− onRequestEditMode: root.state = "edit";
117− onRequestScopeMoveTo: root.requestFavoriteMoveTo(scopeId, index);
118− onRequestActivate: root.scope.activate(result, categoryId);
119− onRequestRestore: root.requestRestore(scopeId);
175+ model: scope ? scope.categories : null
176+ delegate: Loader {
177+ asynchronous: true
178+ width: root.width
179+ active: results.count > 0
180+ visible: active
181+ sourceComponent: ScopesListCategory {
182+ objectName: "scopesListCategory" + categoryId
183+
184+ model: results
185+
186+ title: {
187+ if (isFavoritesFeed) return i18n.tr("Home");
188+ else if (isAlsoInstalled) return i18n.tr("Also installed");
189+ else return name;
120190 }
121− }
191+
192+ editMode: root.state == "edit"
193+ scopeStyle: root.scopeStyle
194+ isFavoritesFeed: categoryId == "favorites"
195+ isAlsoInstalled: categoryId == "other"
196+
197+ onItemDragging: autoscroll(dragging, dragItem);
198+ onRequestFavorite: root.requestFavorite(scopeId, favorite);
199+ onRequestEditMode: root.state = "edit";
200+ onRequestScopeMoveTo: root.requestFavoriteMoveTo(scopeId, index);
201+ onRequestActivate: root.scope.activate(result, categoryId);
202+ onRequestRestore: root.requestRestore(scopeId);
122203 }
123204 }
124205 }
qml/Dash/ScopesListCategory.qml+3
@@ -32,6 +32,7 @@ Item {
3232
3333 visible: !editMode || isFavoritesFeed
3434
35+ signal itemDragging(bool dragging, var dragItem)
3536 signal requestFavorite(string scopeId, bool favorite)
3637 signal requestEditMode()
3738 signal requestScopeMoveTo(string scopeId, int index)
@@ -109,6 +110,7 @@ Item {
109110 onRequestFavorite: root.requestFavorite(model.scopeId, favorite);
110111 onHandlePressed: {
111112 if (editMode) {
113+ root.itemDragging(true, dragItem);
112114 handle.drag.target = dragItem;
113115 handle.drag.maximumX = units.gu(1);
114116 handle.drag.minimumX = units.gu(1);
@@ -126,6 +128,7 @@ Item {
126128 }
127129 }
128130 onHandleReleased: {
131+ root.itemDragging(false, dragItem);
129132 if (dragItem.visible) {
130133 handle.drag.target = undefined;
131134 dragItem.visible = false;
tests/mocks/Unity/fake_scopes.cpp+7
@@ -73,6 +73,13 @@ void Scopes::updateScopes()
7373 addScope(longNavigationScope);
7474
7575 addScope(new Scope("NullPreviewScope", "NPS", false, this, 1, true));
76+
77+ // Add enough scopes to test autoscrolling on the scopes managment page
78+ for (int i = 0; i < 20; i++) {
79+ addScope(new Scope("UselessScope" + QString::number(i),
80+ "US" + QString::number(i), true, this));
81+ }
82+
7683 m_scopesOverview = new ScopesOverview(this);
7784
7885 if (!m_loaded) {
tests/qmltests/Dash/tst_Dash.qml+5−4
@@ -58,7 +58,7 @@ Item {
5858 tryCompare(dashContentList, "count", 0);
5959 scopes.load();
6060 tryCompare(dashContentList, "currentIndex", 0);
61− tryCompare(dashContentList, "count", 8);
61+ tryCompare(dashContentList, "count", 28);
6262 tryCompare(scopes, "loaded", true);
6363 tryCompareFunction(function() {
6464 var mockScope1Loader = findChild(dash, "scopeLoader0");
@@ -589,10 +589,10 @@ Item {
589589
590590 // Go to a temp scope
591591 open_manage_dash();
592+ flickToYEnd(findChild(dash, "scopesListFlickable"));
592593
593594 var nonfavScopesListCategory = findChild(dash, "scopesListCategoryother");
594595 var nonfavScopesListCategoryList = findChild(nonfavScopesListCategory, "scopesListCategoryInnerList");
595− flickToYEnd(findChild(dash, "scopesListFlickable"));
596596 tryCompare(nonfavScopesListCategoryList, "currentIndex", 0);
597597 mouseClick(nonfavScopesListCategoryList.currentItem);
598598 var dashTempScopeItem = findChild(dash, "dashTempScopeItem");
@@ -661,13 +661,14 @@ Item {
661661 // Go to a temp scope
662662 open_manage_dash();
663663
664+ var scopesListFlickable = findChild(dash, "scopesListFlickable");
665+ flickToYEnd(scopesListFlickable);
666+
664667 var nonfavScopesListCategory = findChild(dash, "scopesListCategoryother");
665668 var nonfavScopesListCategoryList = findChild(nonfavScopesListCategory, "scopesListCategoryInnerList");
666− var scopesListFlickable = findChild(dash, "scopesListFlickable");
667669 tryCompare(nonfavScopesListCategoryList, "currentIndex", 0);
668670
669671 // Ensure the non-favorites, located at the bottom, are visible
670− flickToYEnd(scopesListFlickable);
671672 mouseClick(nonfavScopesListCategoryList.currentItem);
672673 var dashTempScopeItem = findChild(dash, "dashTempScopeItem");
673674 tryCompare(dashTempScopeItem, "x", 0);
tests/qmltests/Dash/tst_DashContent.qml+3−3
@@ -70,7 +70,7 @@ Item {
7070 function loadScopes() {
7171 scopeLoadedSpy.clear();
7272 scopesModel.load();
73− tryCompare(scopeLoadedSpy, "count", 8, 15000);
73+ tryCompare(scopeLoadedSpy, "count", 28, 15000);
7474 tryCompare(scopesModel, "loaded", true);
7575 tryCompareFunction(function() {
7676 var mockScope1Loader = findChild(shell, "scopeLoader0");
@@ -124,7 +124,7 @@ Item {
124124
125125 loadScopes();
126126
127− compare(dashContentList.count, 8);
127+ compare(dashContentList.count, 28);
128128 verify(dashContentList.currentIndex >= 0 && dashContentList.currentIndex < dashContentList.count);
129129 }
130130
@@ -181,7 +181,7 @@ Item {
181181
182182 // test greater than scope count.
183183 var currentScopeIndex = dashContent.currentIndex;
184− dashContent.setCurrentScopeAtIndex(18, true, false);
184+ dashContent.setCurrentScopeAtIndex(29, true, false);
185185 compare(dashContent.currentIndex, currentScopeIndex, "Scope should not change if changing to greater index than count");
186186 }
187187
tests/qmltests/Dash/tst_DashShell.qml+1−1
@@ -60,7 +60,7 @@ Item {
6060 tryCompare(dashContentList, "count", 0);
6161 scopes.load();
6262 tryCompare(dashContentList, "currentIndex", 0);
63− tryCompare(dashContentList, "count", 8);
63+ tryCompare(dashContentList, "count", 28);
6464 tryCompare(scopes, "loaded", true);
6565 tryCompareFunction(function() {
6666 var mockScope1Loader = findChild(dash, "scopeLoader0");