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)
8254333Josh Arenson
qml/Dash/ScopesList.qml+120−39
@@ -22,6 +22,8 @@ import "../Components"
22Item {
22Item {
23 id: root
23 id: root
24
24
25+ property alias scopesListFlickable: scopesListFlickable
26+
25 // Properties set by parent
27 // Properties set by parent
26 property var scope: null
28 property var scope: null
27
29
@@ -35,11 +37,98 @@ Item {
35 signal requestFavoriteMoveTo(string scopeId, int index)
37 signal requestFavoriteMoveTo(string scopeId, int index)
36 signal requestRestore(string scopeId)
38 signal requestRestore(string scopeId)
37
39
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+ }
39
117
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+ }
41 }
126 }
42
127
128+ state: "browse"
129+
130+ property var scopeStyle: ScopeStyle {}
131+
43 onStateChanged: {
132 onStateChanged: {
44 if (state == "edit") {
133 if (state == "edit") {
45 // As per design entering edit mode clears the possible existing search
134 // As per design entering edit mode clears the possible existing search
@@ -73,7 +162,8 @@ Item {
73 z: 1
162 z: 1
74 }
163 }
75
164
76− Flickable {
165+ ListView {
166+ id: scopesListFlickable
77 objectName: "scopesListFlickable"
167 objectName: "scopesListFlickable"
78 anchors {
168 anchors {
79 top: header.bottom
169 top: header.bottom
@@ -82,43 +172,34 @@ Item {
82 right: parent.right
172 right: parent.right
83 }
173 }
84 clip: true
174 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
175+ model: scope ? scope.categories : null
92−
93− delegate: Loader {
176+ delegate: Loader {
94− asynchronous: true
177+ asynchronous: true
95− width: root.width
178+ width: root.width
96− active: results.count > 0
179+ active: results.count > 0
97− visible: active
180+ visible: active
98− sourceComponent: ScopesListCategory {
181+ sourceComponent: ScopesListCategory {
99− objectName: "scopesListCategory" + categoryId
182+ objectName: "scopesListCategory" + categoryId
100−
183+
101− model: results
184+ model: results
102−
185+
103− title: {
186+ title: {
104− if (isFavoritesFeed) return i18n.tr("Home");
187+ if (isFavoritesFeed) return i18n.tr("Home");
105− else if (isAlsoInstalled) return i18n.tr("Also installed");
188+ else if (isAlsoInstalled) return i18n.tr("Also installed");
106− else return name;
189+ 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);
120 }
190 }
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);
122 }
203 }
123 }
204 }
124 }
205 }
qml/Dash/ScopesListCategory.qml+3
@@ -32,6 +32,7 @@ Item {
32
32
33 visible: !editMode || isFavoritesFeed
33 visible: !editMode || isFavoritesFeed
34
34
35+ signal itemDragging(bool dragging, var dragItem)
35 signal requestFavorite(string scopeId, bool favorite)
36 signal requestFavorite(string scopeId, bool favorite)
36 signal requestEditMode()
37 signal requestEditMode()
37 signal requestScopeMoveTo(string scopeId, int index)
38 signal requestScopeMoveTo(string scopeId, int index)
@@ -109,6 +110,7 @@ Item {
109 onRequestFavorite: root.requestFavorite(model.scopeId, favorite);
110 onRequestFavorite: root.requestFavorite(model.scopeId, favorite);
110 onHandlePressed: {
111 onHandlePressed: {
111 if (editMode) {
112 if (editMode) {
113+ root.itemDragging(true, dragItem);
112 handle.drag.target = dragItem;
114 handle.drag.target = dragItem;
113 handle.drag.maximumX = units.gu(1);
115 handle.drag.maximumX = units.gu(1);
114 handle.drag.minimumX = units.gu(1);
116 handle.drag.minimumX = units.gu(1);
@@ -126,6 +128,7 @@ Item {
126 }
128 }
127 }
129 }
128 onHandleReleased: {
130 onHandleReleased: {
131+ root.itemDragging(false, dragItem);
129 if (dragItem.visible) {
132 if (dragItem.visible) {
130 handle.drag.target = undefined;
133 handle.drag.target = undefined;
131 dragItem.visible = false;
134 dragItem.visible = false;
tests/mocks/Unity/fake_scopes.cpp+7
@@ -73,6 +73,13 @@ void Scopes::updateScopes()
73 addScope(longNavigationScope);
73 addScope(longNavigationScope);
74
74
75 addScope(new Scope("NullPreviewScope", "NPS", false, this, 1, true));
75 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+
76 m_scopesOverview = new ScopesOverview(this);
83 m_scopesOverview = new ScopesOverview(this);
77
84
78 if (!m_loaded) {
85 if (!m_loaded) {
tests/qmltests/Dash/tst_Dash.qml+5−4
@@ -58,7 +58,7 @@ Item {
58 tryCompare(dashContentList, "count", 0);
58 tryCompare(dashContentList, "count", 0);
59 scopes.load();
59 scopes.load();
60 tryCompare(dashContentList, "currentIndex", 0);
60 tryCompare(dashContentList, "currentIndex", 0);
61− tryCompare(dashContentList, "count", 8);
61+ tryCompare(dashContentList, "count", 28);
62 tryCompare(scopes, "loaded", true);
62 tryCompare(scopes, "loaded", true);
63 tryCompareFunction(function() {
63 tryCompareFunction(function() {
64 var mockScope1Loader = findChild(dash, "scopeLoader0");
64 var mockScope1Loader = findChild(dash, "scopeLoader0");
@@ -589,10 +589,10 @@ Item {
589
589
590 // Go to a temp scope
590 // Go to a temp scope
591 open_manage_dash();
591 open_manage_dash();
592+ flickToYEnd(findChild(dash, "scopesListFlickable"));
592
593
593 var nonfavScopesListCategory = findChild(dash, "scopesListCategoryother");
594 var nonfavScopesListCategory = findChild(dash, "scopesListCategoryother");
594 var nonfavScopesListCategoryList = findChild(nonfavScopesListCategory, "scopesListCategoryInnerList");
595 var nonfavScopesListCategoryList = findChild(nonfavScopesListCategory, "scopesListCategoryInnerList");
595− flickToYEnd(findChild(dash, "scopesListFlickable"));
596 tryCompare(nonfavScopesListCategoryList, "currentIndex", 0);
596 tryCompare(nonfavScopesListCategoryList, "currentIndex", 0);
597 mouseClick(nonfavScopesListCategoryList.currentItem);
597 mouseClick(nonfavScopesListCategoryList.currentItem);
598 var dashTempScopeItem = findChild(dash, "dashTempScopeItem");
598 var dashTempScopeItem = findChild(dash, "dashTempScopeItem");
@@ -661,13 +661,14 @@ Item {
661 // Go to a temp scope
661 // Go to a temp scope
662 open_manage_dash();
662 open_manage_dash();
663
663
664+ var scopesListFlickable = findChild(dash, "scopesListFlickable");
665+ flickToYEnd(scopesListFlickable);
666+
664 var nonfavScopesListCategory = findChild(dash, "scopesListCategoryother");
667 var nonfavScopesListCategory = findChild(dash, "scopesListCategoryother");
665 var nonfavScopesListCategoryList = findChild(nonfavScopesListCategory, "scopesListCategoryInnerList");
668 var nonfavScopesListCategoryList = findChild(nonfavScopesListCategory, "scopesListCategoryInnerList");
666− var scopesListFlickable = findChild(dash, "scopesListFlickable");
667 tryCompare(nonfavScopesListCategoryList, "currentIndex", 0);
669 tryCompare(nonfavScopesListCategoryList, "currentIndex", 0);
668
670
669 // Ensure the non-favorites, located at the bottom, are visible
671 // Ensure the non-favorites, located at the bottom, are visible
670− flickToYEnd(scopesListFlickable);
671 mouseClick(nonfavScopesListCategoryList.currentItem);
672 mouseClick(nonfavScopesListCategoryList.currentItem);
672 var dashTempScopeItem = findChild(dash, "dashTempScopeItem");
673 var dashTempScopeItem = findChild(dash, "dashTempScopeItem");
673 tryCompare(dashTempScopeItem, "x", 0);
674 tryCompare(dashTempScopeItem, "x", 0);
tests/qmltests/Dash/tst_DashContent.qml+3−3
@@ -70,7 +70,7 @@ Item {
70 function loadScopes() {
70 function loadScopes() {
71 scopeLoadedSpy.clear();
71 scopeLoadedSpy.clear();
72 scopesModel.load();
72 scopesModel.load();
73− tryCompare(scopeLoadedSpy, "count", 8, 15000);
73+ tryCompare(scopeLoadedSpy, "count", 28, 15000);
74 tryCompare(scopesModel, "loaded", true);
74 tryCompare(scopesModel, "loaded", true);
75 tryCompareFunction(function() {
75 tryCompareFunction(function() {
76 var mockScope1Loader = findChild(shell, "scopeLoader0");
76 var mockScope1Loader = findChild(shell, "scopeLoader0");
@@ -124,7 +124,7 @@ Item {
124
124
125 loadScopes();
125 loadScopes();
126
126
127− compare(dashContentList.count, 8);
127+ compare(dashContentList.count, 28);
128 verify(dashContentList.currentIndex >= 0 && dashContentList.currentIndex < dashContentList.count);
128 verify(dashContentList.currentIndex >= 0 && dashContentList.currentIndex < dashContentList.count);
129 }
129 }
130
130
@@ -181,7 +181,7 @@ Item {
181
181
182 // test greater than scope count.
182 // test greater than scope count.
183 var currentScopeIndex = dashContent.currentIndex;
183 var currentScopeIndex = dashContent.currentIndex;
184− dashContent.setCurrentScopeAtIndex(18, true, false);
184+ dashContent.setCurrentScopeAtIndex(29, true, false);
185 compare(dashContent.currentIndex, currentScopeIndex, "Scope should not change if changing to greater index than count");
185 compare(dashContent.currentIndex, currentScopeIndex, "Scope should not change if changing to greater index than count");
186 }
186 }
187
187
tests/qmltests/Dash/tst_DashShell.qml+1−1
@@ -60,7 +60,7 @@ Item {
60 tryCompare(dashContentList, "count", 0);
60 tryCompare(dashContentList, "count", 0);
61 scopes.load();
61 scopes.load();
62 tryCompare(dashContentList, "currentIndex", 0);
62 tryCompare(dashContentList, "currentIndex", 0);
63− tryCompare(dashContentList, "count", 8);
63+ tryCompare(dashContentList, "count", 28);
64 tryCompare(scopes, "loaded", true);
64 tryCompare(scopes, "loaded", true);
65 tryCompareFunction(function() {
65 tryCompareFunction(function() {
66 var mockScope1Loader = findChild(dash, "scopeLoader0");
66 var mockScope1Loader = findChild(dash, "scopeLoader0");