on xenial help sign in
Implement switching between previews by swiping. Fixes: https://bugs.launchpad.net/bugs/1088572, https://bugs.launchpad.net/bugs/1220651.

Approved by PS Jenkins bot, Albert Astals Cid, Michal Hruby.

commit0ccfea6b7b231221804ff6c5c7a3dd813f2e841f authorMichael Zanetti <michael.zanetti@canonical.com> dateMon 4 Nov 2013, 17:26 (13 years ago) parent00ef0e60705c47
25 files changed+509−236
Components/Carousel.qml+27−9 Components/FilterGrid.qml+3 Components/LazyImage.qml+2−2 Components/PageHeader.qml+4 Components/ResponsiveGridView.qml+10 Components/Tile.qml+9 Dash/Apps/AppPreview.qml+9 Dash/Apps/ApplicationsFilterGrid.qml+14−5 Dash/Apps/DashPluginFilterGrid.qml+2−4 Dash/DashPreview.qml+1−3 Dash/DashPreviewPlaceholder.qml+25 Dash/Generic/GenericFilterGrid.qml+4−6 Dash/Generic/WeatherFilterGrid.qml+2−4 Dash/GenericScopeView.qml+220−59 Dash/Music/AlbumTile.qml−116 Dash/Music/MusicFilterGrid.qml+2−4 Dash/SearchableResultModel.qml+4 Dash/Video/VideoFilterGrid.qml+2−4 Dash/graphics/tooltip_arrow@27.png tests/mocks/Unity/fake_preview.cpp+9−1 tests/mocks/Unity/fake_preview.h+2 tests/mocks/Unity/fake_scope.cpp+25−4 tests/mocks/Unity/fake_scope.h+7−2 tests/mocks/Unity/fake_unity_plugin.cpp+3 tests/qmltests/Dash/tst_GenericScopeView.qml+123−13
Components/Carousel.qml+27−9
@@ -45,18 +45,24 @@ Item {
4545 property int drawBuffer: width / pathItemCount // an "ok" value - but values used from the listView cause loops
4646 /// The selected item can be shown in a different size controlled by selectedItemScaleFactor
4747 property real selectedItemScaleFactor: 1.1
48+ /// The index of the item that should be highlighted
49+ property alias highlightIndex: listView.highlightIndex
50+ /// exposes the delegate of the currentItem
51+ readonly property alias currentItem: listView.currentItem
52+ /// exposes the distance to the next row (only one row in carousel, so it's the topMargins)
53+ readonly property alias verticalSpacing: listView.verticalMargin
4854
4955 /// Emitted when the user clicked on an item
5056 /// @param index is the index of the clicked item
51− /// @param delegateItem is the clicked component/delegate itself
57+ /// @param model is the model of all the items in the carousel
5258 /// @param itemY is y of the clicked delegate
53− signal clicked(int index, var delegateItem, real itemY)
59+ signal clicked(int index, var model, real itemY)
5460
5561 /// Emitted when the user pressed and held on an item
5662 /// @param index is the index of the held item
57− /// @param delegateItem is the held component/delegate itself
63+ /// @param model is the model of all the items in the carousel
5864 /// @param itemY is y of the held delegate
59− signal pressAndHold(int index, var delegateItem, real itemY)
65+ signal pressAndHold(int index, var model, real itemY)
6066
6167 implicitHeight: listView.tileHeight * selectedItemScaleFactor
6268
@@ -75,6 +81,7 @@ Item {
7581 property real newContentX: disabledNewContentX
7682 property real pathItemCount: referenceWidth / referenceTileWidth
7783 property real tileAspectRatio: 1
84+ property int highlightIndex: -1
7885
7986 /* The positioning and scaling of the items in the carousel is based on the variable
8087 'continuousIndex', a continuous real variable between [0, 'carousel.model.count'],
@@ -143,6 +150,7 @@ Item {
143150 orientation: ListView.Horizontal
144151
145152 function itemClicked(index, delegateItem) {
153+ listView.currentIndex = index
146154 var x = CarouselJS.getXFromContinuousIndex(index,
147155 realWidth,
148156 realContentWidth,
@@ -151,11 +159,11 @@ Item {
151159 gapToEndPhase,
152160 carousel.drawBuffer)
153161
154− if (Math.abs(x - contentX) < 1) {
162+ if (Math.abs(x - contentX) < 1 && delegateItem !== undefined) {
155163 /* We're clicking the selected item and
156164 we're in the neighbourhood of radius 1 pixel from it.
157165 Let's emit the clicked signal. */
158− carousel.clicked(index, delegateItem, delegateItem.y)
166+ carousel.clicked(index, listView.model, delegateItem.y)
159167 return
160168 }
161169
@@ -175,11 +183,11 @@ Item {
175183 gapToEndPhase,
176184 carousel.drawBuffer);
177185
178− if (Math.abs(x - contentX) < 1) {
186+ if (Math.abs(x - contentX) < 1 && delegateItem !== undefined) {
179187 /* We're pressAndHold the selected item and
180188 we're in the neighbourhood of radius 1 pixel from it.
181189 Let's emit the pressAndHold signal. */
182− carousel.pressAndHold(index, delegateItem, delegateItem.y);
190+ carousel.pressAndHold(index, listView.model, delegateItem.y);
183191 return;
184192 }
185193
@@ -190,6 +198,12 @@ Item {
190198 newContentXAnimation.start();
191199 }
192200
201+ onHighlightIndexChanged: {
202+ if (highlightIndex != -1) {
203+ itemClicked(highlightIndex)
204+ }
205+ }
206+
193207 onMovementStarted: {
194208 stepAnimation.stop()
195209 newContentXAnimation.stop()
@@ -273,14 +287,18 @@ Item {
273287 itemTranslationScale,
274288 listView.maximumItemTranslation)
275289
290+ readonly property real xTransform: listView.viewTranslation + translationX * listView.scaleFactor
291+ readonly property real center: x - listView.contentX + xTransform - drawBuffer + (width/2)
292+
276293 width: listView.tileWidth
277294 height: listView.tileHeight
278295 scale: itemScale * explicitScaleFactor
279296 sourceComponent: itemComponent
280297 z: cachedTiles - Math.abs(index - listView.selectedIndex)
298+ opacity: highlightIndex == -1 ? 1 : (highlightIndex == index ? 0.6 : 0.2)
281299
282300 transform: Translate {
283− x: listView.viewTranslation + translationX * listView.scaleFactor
301+ x: xTransform
284302 }
285303
286304 Behavior on explicitScaleFactor {
Components/FilterGrid.qml+3
@@ -54,6 +54,7 @@ Item {
5454 property alias delegateWidth: iconTileGrid.delegateWidth
5555 property alias delegateHeight: iconTileGrid.delegateHeight
5656 property alias verticalSpacing: iconTileGrid.verticalSpacing
57+ readonly property alias margins: iconTileGrid.margins
5758 property alias delegate: iconTileGrid.delegate
5859 property alias cellWidth: iconTileGrid.cellWidth
5960 property alias cellHeight: iconTileGrid.cellHeight
@@ -62,6 +63,8 @@ Item {
6263 readonly property alias flicking: iconTileGrid.flicking
6364 readonly property alias moving: iconTileGrid.moving
6465 readonly property alias pressDelay: iconTileGrid.pressDelay
66+ property alias highlightIndex: iconTileGrid.highlightIndex
67+ readonly property alias currentItem: iconTileGrid.currentItem
6568
6669 height: !filterAnimation.running ? childrenRect.height : height
6770 clip: filterAnimation.running
Components/LazyImage.qml+2−2
@@ -100,10 +100,10 @@ Item {
100100 verticalAlignment: Image.AlignVCenter
101101 sourceSize.width: root.scaleTo == "width" ? root.width
102102 : root.scaleTo == "fit" && root.width < root.height ? root.width
103− : undefined
103+ : 0
104104 sourceSize.height: root.scaleTo == "height" ? root.height
105105 : root.scale == "fit" && root.height < root.width ? root.height
106− : undefined
106+ : 0
107107 }
108108 }
109109
Components/PageHeader.qml+4
@@ -48,6 +48,10 @@ Item {
4848 searchField.focus = false;
4949 }
5050
51+ function unfocus() {
52+ searchField.focus = false;
53+ }
54+
5155 Connections {
5256 target: greeter
5357 onShownChanged: if (shown) resetSearch()
Components/ResponsiveGridView.qml+10
@@ -26,6 +26,7 @@ Item {
2626 property int maximumNumberOfColumns: 6
2727 readonly property int columns: gridView.columns
2828 property alias verticalSpacing: gridView.verticalSpacing
29+ readonly property alias margins: gridView.margin
2930 property int delegateWidth
3031 property int delegateHeight
3132 property alias model: gridView.model
@@ -41,6 +42,8 @@ Item {
4142 readonly property alias pressDelay: gridView.pressDelay
4243 property alias delegateCreationBegin: gridView.delegateCreationBegin
4344 property alias delegateCreationEnd: gridView.delegateCreationEnd
45+ property alias highlightIndex: gridView.highlightIndex
46+ readonly property alias currentItem: gridView.currentItem
4447
4548 function contentHeightForRows(rows) {
4649 return rows * cellHeight + verticalSpacing
@@ -77,8 +80,15 @@ Item {
7780 property real horizontalSpacing: spacingForColumns(columns)
7881 property real verticalSpacing: horizontalSpacing
7982 property int margin: allocatableHorizontalSpace - columns * horizontalSpacing
83+ property int highlightIndex: -1
8084
8185 cellWidth: delegateWidth + horizontalSpacing
8286 cellHeight: delegateHeight + verticalSpacing
87+
88+ onHighlightIndexChanged: {
89+ if (highlightIndex != -1) {
90+ currentIndex = highlightIndex
91+ }
92+ }
8393 }
8494}
Components/Tile.qml+9
@@ -26,5 +26,14 @@ AbstractButton {
2626 property int imageHeight
2727 property int maximumLineCount: 1
2828
29+ readonly property int center: (index % GridView.view.columns * width) + (width / 2)
30+
31+ opacity: GridView.view.highlightIndex === -1 ? 1 :
32+ GridView.view.highlightIndex === index ? 0.6 : 0.2
33+
34+ Behavior on opacity {
35+ UbuntuNumberAnimation {}
36+ }
37+
2938 style: TileStyle {}
3039}
Dash/Apps/AppPreview.qml+9
@@ -44,6 +44,15 @@ GenericPreview {
4444 model: previewData.infoMap["more-screenshots"] != null ? previewData.infoMap["more-screenshots"].value : [previewData.image]
4545 cacheBuffer: width * 3
4646
47+ // FIXME: Because of ListViews inside ListViews inside Flickables inside ListViews (and some more)
48+ // we finally reached the point where this ListView doesn't correctly get swipe input any more but
49+ // instead the parent ListView is the one that is swiped. This MouseArea sort of creates a blocking
50+ // layer to make sure this ListView can be swiped, regardless of what's behind it.
51+ MouseArea {
52+ anchors.fill: parent
53+ enabled: parent.contentWidth > parent.width
54+ }
55+
4756 LazyImage {
4857 objectName: "placeholderScreenshot"
4958 anchors {
Dash/Apps/ApplicationsFilterGrid.qml+14−5
@@ -19,7 +19,7 @@ import "../../Components"
1919import "../../Applications"
2020
2121FilterGrid {
22− id: filterGrid
22+ id: filtergrid
2323
2424 filter: false
2525 minimumHorizontalSpacing: units.gu(0.5)
@@ -28,9 +28,11 @@ FilterGrid {
2828 delegateHeight: units.gu(9.5)
2929 verticalSpacing: units.gu(2)
3030
31− signal clicked(int index, variant data)
31+ signal clicked(int index, var model, real itemY)
32+ signal pressAndHold(int index, var model, real itemY)
3233
3334 delegate: Tile {
35+ id: tile
3436 objectName: "delegate" + index
3537 Application {
3638 id: application
@@ -44,12 +46,19 @@ FilterGrid {
4446
4547 property string icon: model.icon ? model.icon : "../../graphics/applicationIcons/" + application.icon + ".png" // FIXME: this is temporary
4648
47− width: filterGrid.cellWidth
48− height: filterGrid.cellHeight
49+ width: filtergrid.cellWidth
50+ height: filtergrid.cellHeight
4951 text: model.title ? model.title : application.name // FIXME: this is temporary
5052 imageWidth: units.gu(8)
5153 imageHeight: units.gu(7.5)
5254 source: icon
53− onClicked: filterGrid.clicked(index, application.desktopFile);
55+
56+ onClicked: {
57+ filtergrid.clicked(index, filtergrid.model, tile.y)
58+ }
59+
60+ onPressAndHold: {
61+ filtergrid.pressAndHold(index, filtergrid.model, tile.y)
62+ }
5463 }
5564}
Dash/Apps/DashPluginFilterGrid.qml+2−4
@@ -37,13 +37,11 @@ GenericFilterGrid {
3737 }
3838
3939 onClicked: {
40− var data = { model: model }
41− filtergrid.clicked(index, data, tile.y)
40+ filtergrid.clicked(index, filtergrid.model, tile.y)
4241 }
4342
4443 onPressAndHold: {
45− var data = { model: model }
46− filtergrid.pressAndHold(index, data, tile.y)
44+ filtergrid.pressAndHold(index, filtergrid.model, tile.y)
4745 }
4846 }
4947}
Dash/DashPreview.qml+1−3
@@ -17,7 +17,7 @@
1717import QtQuick 2.0
1818import Ubuntu.Components 0.1
1919
20−Rectangle {
20+Item {
2121 id: root
2222
2323 property int keyboardSize: Qt.inputMethod.visible ? Qt.inputMethod.keyboardRectangle.height : 0
@@ -36,7 +36,6 @@ Rectangle {
3636 readonly property int columnWidth: narrowMode ? contentRow.width : (contentRow.width / 3) - contentRow.spacing
3737 readonly property int contentSpacing: units.gu(3)
3838
39− color: Qt.rgba(0, 0, 0, .3)
4039 clip: true
4140
4241 function ensureVisible(item) {
@@ -61,7 +60,6 @@ Rectangle {
6160 anchors.fill: parent
6261 }
6362
64−
6563 Row {
6664 id: contentRow
6765 anchors {
Dash/DashPreviewPlaceholder.qmlnew file+25
@@ -0,0 +1,25 @@
1+/*
2+ * Copyright (C) 2013 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.0
18+import Ubuntu.Components 0.1
19+
20+DashPreview {
21+ ActivityIndicator {
22+ anchors.centerIn: parent
23+ running: true
24+ }
25+}
Dash/Generic/GenericFilterGrid.qml+4−6
@@ -28,8 +28,8 @@ FilterGrid {
2828 property int iconWidth: units.gu(8)
2929 property int iconHeight: units.gu(7.5)
3030
31− signal clicked(int index, var delegateItem, real itemY)
32− signal pressAndHold(int index, var delegateItem, real itemY)
31+ signal clicked(int index, var model, real itemY)
32+ signal pressAndHold(int index, var model, real itemY)
3333
3434 delegate: Tile {
3535 id: tile
@@ -42,13 +42,11 @@ FilterGrid {
4242 source: model.icon
4343
4444 onClicked: {
45− var data = { model: model }
46− filtergrid.clicked(index, data, tile.y)
45+ filtergrid.clicked(index, filtergrid.model, tile.y)
4746 }
4847
4948 onPressAndHold: {
50− var data = { model: model }
51− filtergrid.pressAndHold(index, data, tile.y)
49+ filtergrid.pressAndHold(index, filtergrid.model, tile.y)
5250 }
5351 }
5452}
Dash/Generic/WeatherFilterGrid.qml+2−4
@@ -37,13 +37,11 @@ GenericFilterGrid {
3737 style: FlatTileStyle {}
3838
3939 onClicked: {
40− var data = { model: model }
41− filtergrid.clicked(index, data, tile.y)
40+ filtergrid.clicked(index, filtergrid.model, tile.y)
4241 }
4342
4443 onPressAndHold: {
45− var data = { model: model }
46− filtergrid.pressAndHold(index, data, tile.y)
44+ filtergrid.pressAndHold(index, filtergrid.model, tile.y)
4745 }
4846 }
4947}
Dash/GenericScopeView.qml+220−59
@@ -21,14 +21,14 @@ import "../Components/ListItems" as ListItems
2121
2222ScopeView {
2323 id: scopeView
24− readonly property alias previewShown: previewLoader.onScreen
24+ readonly property alias previewShown: previewListView.onScreen
2525 property bool enableHeightBehaviorOnNextCreation: false
2626
2727 moving: categoryView.moving
2828
2929 onIsCurrentChanged: {
3030 pageHeader.resetSearch();
31− previewLoader.open = false;
31+ previewListView.open = false;
3232 }
3333
3434 onMovementStarted: categoryView.showHeader()
@@ -57,9 +57,8 @@ ScopeView {
5757
5858 Connections {
5959 target: scopeView.scope
60− onShowDash: previewLoader.open = false;
61− onHideDash: previewLoader.open = false;
62− onActivated: previewLoader.closePreviewSpinner();
60+ onShowDash: previewListView.open = false;
61+ onHideDash: previewListView.open = false;
6362 }
6463
6564 ScopeListView {
@@ -67,12 +66,18 @@ ScopeView {
6766 objectName: "categoryListView"
6867 anchors.fill: parent
6968 model: scopeView.categories
70− forceNoClip: previewLoader.onScreen
69+ forceNoClip: previewListView.onScreen
7170
7271 onAtYEndChanged: if (atYEnd) endReached()
7372 onMovingChanged: if (moving && atYEnd) endReached()
7473
7574 property string expandedCategoryId: ""
75+ signal correctExpandedCategory();
76+
77+ Behavior on contentY {
78+ enabled: previewListView.open
79+ UbuntuNumberAnimation {}
80+ }
7681
7782 delegate: ListItems.Base {
7883 id: baseItem
@@ -124,33 +129,50 @@ ScopeView {
124129 Connections {
125130 target: rendererLoader.item
126131 onClicked: {
132+ // Prepare the preview in case activate() triggers a preview only
127133 effect.positionPx = mapToItem(categoryView, 0, itemY).y
128− scopeView.scope.activate(delegateItem.model.uri,
129− delegateItem.model.icon,
130− delegateItem.model.category,
131− 0,
132− delegateItem.model.mimetype,
133− delegateItem.model.title,
134− delegateItem.model.comment,
135− delegateItem.model.dndUri,
136− delegateItem.model.metadata)
134+ previewListView.categoryId = categoryId
135+ previewListView.categoryDelegate = rendererLoader.item
136+ previewListView.model = model;
137+ previewListView.init = true;
138+ previewListView.currentIndex = index;
139+
140+ var item = model.get(index);
141+
142+ if ((scopeView.scope.id == "applications.scope" && categoryId == "installed")
143+ || (scopeView.scope.id == "home.scope" && categoryId == "applications.scope")) {
144+ scopeView.scope.activate(item.uri, item.icon, item.category, 0, item.mimetype, item.title,
145+ item.comment, item.dndUri, item.metadata)
146+ } else {
147+ previewListView.open = true
148+
149+ scopeView.scope.preview( item.uri, item.icon, item.category, 0, item.mimetype, item.title,
150+ item.comment, item.dndUri, item.metadata)
151+ }
137152 }
138153 onPressAndHold: {
139154 effect.positionPx = mapToItem(categoryView, 0, itemY).y
140− scopeView.scope.preview( delegateItem.model.uri,
141− delegateItem.model.icon,
142− delegateItem.model.category,
143− 0,
144− delegateItem.model.mimetype,
145− delegateItem.model.title,
146− delegateItem.model.comment,
147− delegateItem.model.dndUri,
148− delegateItem.model.metadata)
155+ previewListView.categoryId = categoryId
156+ previewListView.categoryDelegate = rendererLoader.item
157+ previewListView.model = model;
158+ previewListView.init = true;
159+ previewListView.currentIndex = index;
160+ previewListView.open = true
161+
162+ var item = model.get(index)
163+ scopeView.scope.preview( item.uri, item.icon, item.category, 0, item.mimetype, item.title,
164+ item.comment, item.dndUri, item.metadata)
149165 }
150166 }
151167 Connections {
152168 target: categoryView
153169 onExpandedCategoryIdChanged: {
170+ collapseAllButExpandedCategory();
171+ }
172+ onCorrectExpandedCategory: {
173+ collapseAllButExpandedCategory();
174+ }
175+ function collapseAllButExpandedCategory() {
154176 var item = rendererLoader.item;
155177 if (item.expandable) {
156178 var shouldFilter = categoryId != categoryView.expandedCategoryId;
@@ -158,13 +180,15 @@ ScopeView {
158180 // If the filter animation will be seen start it, otherwise, just flip the switch
159181 var shrinkingVisible = shouldFilter && y + item.collapsedHeight < categoryView.height;
160182 var growingVisible = !shouldFilter && y + height < categoryView.height;
161− if (shrinkingVisible || growingVisible) {
162− item.startFilterAnimation(shouldFilter)
163− } else {
164− item.filter = shouldFilter;
165− }
166− if (!shouldFilter) {
167− categoryView.maximizeVisibleArea(index, item.uncollapsedHeight);
183+ if (!previewListView.open || !shouldFilter) {
184+ if (shrinkingVisible || growingVisible) {
185+ item.startFilterAnimation(shouldFilter)
186+ } else {
187+ item.filter = shouldFilter;
188+ }
189+ if (!shouldFilter && !previewListView.open) {
190+ categoryView.maximizeVisibleArea(index, item.uncollapsedHeight);
191+ }
168192 }
169193 }
170194 }
@@ -270,6 +294,7 @@ ScopeView {
270294
271295 OpenEffect {
272296 id: effect
297+ objectName: "openEffect"
273298 anchors {
274299 fill: parent
275300 bottomMargin: -bottomOverflow
@@ -282,29 +307,44 @@ ScopeView {
282307 topOpacity: (1 - gap * 1.2)
283308 bottomGapPx: positionPx + gap * (targetBottomGapPx - positionPx)
284309 bottomOverflow: units.gu(20)
285− bottomOpacity: 1 - (gap * 0.8)
310+ live: !expansionAnimation.running
286311
287312 property int targetBottomGapPx: height - units.gu(8) - bottomOverflow
288− property real gap: previewLoader.open ? 1.0 : 0.0
313+ property real gap: previewListView.open ? 1.0 : 0.0
289314
290315 Behavior on gap {
291316 NumberAnimation {
317+ id: expansionAnimation
292318 duration: 200
293319 easing.type: Easing.InOutQuad
294320 onRunningChanged: {
295− if (!previewLoader.open && !running) {
296− previewLoader.onScreen = false
321+ if (!previewListView.open && !running) {
322+ previewListView.onScreen = false
297323 }
298324 }
299325 }
300326 }
327+ Behavior on positionPx {
328+ enabled: previewListView.open
329+ UbuntuNumberAnimation {}
330+ }
301331 }
302332
303333 Connections {
304334 target: scopeView.scope
305335 onPreviewReady: {
306− previewLoader.previewData = preview
307− previewLoader.open = true
336+ if (previewListView.init) {
337+ // Preview was triggered because of a click on the item. Need to expand now.
338+ if (!previewListView.open) {
339+ previewListView.open = true
340+ }
341+
342+ var index = previewListView.currentIndex
343+ previewListView.currentIndex = -1
344+ previewListView.currentIndex = index
345+ previewListView.init = false
346+ }
347+ previewListView.currentItem.previewData = preview
308348 }
309349 }
310350
@@ -312,18 +352,9 @@ ScopeView {
312352 id: previewDelegateMapper
313353 }
314354
315− Connections {
316− ignoreUnknownSignals: true
317− target: previewLoader.valid ? previewLoader.item : null
318− onClose: {
319− previewLoader.open = false
320− }
321− }
322−
323− Loader {
324− objectName: "previewLoader"
325− id: previewLoader
326− property var previewData
355+ ListView {
356+ id: previewListView
357+ objectName: "previewListView"
327358 height: effect.bottomGapPx - effect.topGapPx
328359 anchors {
329360 top: parent.top
@@ -331,38 +362,168 @@ ScopeView {
331362 left: parent.left
332363 right: parent.right
333364 }
334− source: onScreen ? previewDelegateMapper.map(previewLoader.previewData.rendererName) : ""
365+ orientation: ListView.Horizontal
366+ highlightRangeMode: ListView.StrictlyEnforceRange
367+ snapMode: ListView.SnapOneItem
368+ boundsBehavior: Flickable.DragAndOvershootBounds
369+ highlightMoveDuration: 250
370+ flickDeceleration: units.gu(625)
371+ maximumFlickVelocity: width * 5
372+ cacheBuffer: 0
373+
374+ // To be set before opening the preview
375+ property string categoryId: ""
376+ property var categoryDelegate
377+
378+ // because the ListView is built asynchronous, setting the
379+ // currentIndex directly won't work. We need to refresh it
380+ // when the first preview is ready to be displayed.
381+ property bool init: true
382+
383+ onCurrentIndexChanged: {
384+ var row = Math.floor(currentIndex / categoryDelegate.columns);
385+ if (categoryDelegate.collapsedRowCount <= row) {
386+ categoryView.expandedCategoryId = categoryId
387+ }
388+
389+ if (open) {
390+ categoryDelegate.highlightIndex = currentIndex
391+ }
392+
393+ if (!init && model !== undefined) {
394+ var item = model.get(currentIndex)
395+ scopeView.scope.preview( item.uri, item.icon, item.category, 0, item.mimetype, item.title, item.comment, item.dndUri, item.metadata)
396+ }
397+
398+ var itemY = categoryView.contentItem.mapFromItem(categoryDelegate.currentItem).y;
399+ var newContentY = itemY - effect.positionPx - categoryDelegate.verticalSpacing;
400+ var effectAdjust = effect.positionPx;
401+ if (newContentY < 0) {
402+ effectAdjust += newContentY;
403+ newContentY = 0;
404+ }
405+ if (newContentY > Math.max(0, categoryView.contentHeight - categoryView.height)) {
406+ effectAdjust += -(categoryView.contentHeight - categoryView.height) + newContentY
407+ newContentY = categoryView.contentHeight - categoryView.height;
408+ }
409+
410+ effect.positionPx = effectAdjust;
411+ categoryView.contentY = newContentY - categoryView.originY;
412+ }
335413
336414 property bool open: false
337415 property bool onScreen: false
338− property bool valid: item !== null
339416
340417 onOpenChanged: {
341418 if (open) {
342− onScreen = true
419+ onScreen = true;
420+ categoryDelegate.highlightIndex = currentIndex;
421+ pageHeader.unfocus();
422+ } else {
423+ // Cancel any pending preview requests or actions
424+ if (previewListView.currentItem.previewData !== undefined) {
425+ previewListView.currentItem.previewData.cancelAction();
426+ }
427+ scopeView.scope.cancelActivation();
428+ model = undefined;
429+ categoryView.correctExpandedCategory();
430+ categoryDelegate.highlightIndex = -1;
343431 }
344432 }
345433
346− onLoaded: {
347− item.previewData = Qt.binding(function() { return previewLoader.previewData })
434+ Rectangle {
435+ anchors.fill: parent
436+ color: Qt.rgba(0, 0, 0, .3)
437+ z: -1
438+ }
439+
440+ delegate: Loader {
441+ id: previewLoader
442+ objectName: "previewLoader" + index
443+ height: previewListView.height
444+ width: previewListView.width
445+ asynchronous: true
446+ source: previewListView.onScreen ?
447+ (previewData !== undefined ? previewDelegateMapper.map(previewData.rendererName) : "DashPreviewPlaceholder.qml") : ""
448+
449+ onPreviewDataChanged: {
450+ if (previewData !== undefined && source.toString().indexOf("DashPreviewPlaceholder.qml") != -1) {
451+ previewLoader.opacity = 0;
452+ }
453+ }
454+
455+ onSourceChanged: {
456+ if (previewData !== undefined) {
457+ fadeIn.start()
458+ }
459+ }
460+
461+ PropertyAnimation {
462+ id: fadeIn
463+ target: previewLoader
464+ property: "opacity"
465+ from: 0.0
466+ to: 1.0
467+ duration: UbuntuAnimation.BriskDuration
468+ }
469+
470+ property var previewData
471+ property bool valid: item !== null
472+
473+ onLoaded: {
474+ if (previewListView.onScreen && previewData !== undefined) {
475+ item.previewData = Qt.binding(function() { return previewData })
476+ }
477+ }
478+
479+ Connections {
480+ ignoreUnknownSignals: true
481+ target: item
482+ onClose: {
483+ previewListView.open = false
484+ }
485+ }
486+
487+ function closePreviewSpinner() {
488+ if(item) {
489+ item.showProcessingAction = false;
490+ }
491+ }
348492 }
493+ }
494+
495+ Image {
496+ objectName: "pointerArrow"
497+ anchors {
498+ top: previewListView.bottom
499+ left: parent.left
500+ leftMargin: previewListView.categoryDelegate !== undefined && previewListView.categoryDelegate.currentItem ?
501+ previewListView.categoryDelegate.currentItem.center + (-width + margins) / 2 : 0
349502
350− function closePreviewSpinner() {
351− if(item) {
352− item.showProcessingAction = false;
503+ Behavior on leftMargin {
504+ SmoothedAnimation {
505+ duration: UbuntuAnimation.FastDuration
506+ }
353507 }
354508 }
509+ height: units.gu(1)
510+ width: units.gu(2)
511+ property int margins: previewListView.categoryDelegate ? previewListView.categoryDelegate.margins : 0
512+ opacity: previewListView.open ? .5 : 0
513+
514+ source: "graphics/tooltip_arrow.png"
355515 }
356516
357517 // TODO: Move as InverseMouseArea to DashPreview
358518 MouseArea {
359− enabled: previewLoader.onScreen
519+ objectName: "closePreviewMouseArea"
520+ enabled: previewListView.onScreen
360521 anchors {
361522 fill: parent
362523 topMargin: effect.bottomGapPx
363524 }
364525 onClicked: {
365− previewLoader.open = false;
526+ previewListView.open = false;
366527 }
367528 }
368529}
Dash/Music/AlbumTile.qmldeleted−116
@@ -1,116 +0,0 @@
1−/*
2− * Copyright (C) 2013 Canonical, Ltd.
3− *
4− * This program is free software; you can redistribute it and/or modify
5− * it under the terms of the GNU General Public License as published by
6− * the Free Software Foundation; version 3.
7− *
8− * This program is distributed in the hope that it will be useful,
9− * but WITHOUT ANY WARRANTY; without even the implied warranty of
10− * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11− * GNU General Public License for more details.
12− *
13− * You should have received a copy of the GNU General Public License
14− * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− */
16−
17−import QtQuick 2.0
18−import Ubuntu.Components 0.1
19−
20−AbstractButton {
21− id: root
22− property url source
23− property alias album: albumLabel.text
24− property alias artist: artistLabel.text
25− property alias iconWidth: icon.width
26− property alias iconHeight: icon.height
27−
28− UbuntuShape {
29− id: icon
30− anchors {
31− top: parent.top
32− horizontalCenter: parent.horizontalCenter
33− }
34− radius: "medium"
35− image: Image {
36− source: root.source
37− sourceSize { width: icon.width; height: icon.height }
38− asynchronous: true
39− cache: false
40− }
41− }
42−
43− UbuntuShape {
44− id: borderPressed
45−
46− anchors.fill: icon
47− radius: "medium"
48− borderSource: "radius_pressed.sci"
49− opacity: root.pressed ? 1.0 : 0.0
50− Behavior on opacity { NumberAnimation { duration: 200; easing.type: Easing.OutQuint } }
51− }
52−
53− Item {
54− id: artistContainer
55− anchors {
56− left: parent.left
57− right: parent.right
58− top: icon.bottom
59− }
60− height: units.gu(2)
61−
62− Label {
63− id: artistLabel
64− anchors {
65− baseline: parent.bottom
66− left: parent.left
67− right: parent.right
68− leftMargin: units.gu(1)
69− rightMargin: units.gu(1)
70− }
71−
72− color: Theme.palette.selected.backgroundText
73− opacity: 0.9
74− style: Text.Raised
75− styleColor: "black"
76− fontSize: "small"
77− elide: Text.ElideMiddle
78− horizontalAlignment: Text.AlignHCenter
79− }
80− }
81−
82− Item {
83− anchors {
84− left: parent.left
85− right: parent.right
86− top: artistContainer.bottom
87− }
88− height: units.gu(1.5)
89−
90− Label {
91− id: albumLabel
92− anchors {
93− baseline: parent.bottom
94− left: parent.left
95− right: parent.right
96− leftMargin: units.gu(1)
97− rightMargin: units.gu(1)
98− }
99−
100− color: Theme.palette.selected.backgroundText
101− opacity: 0.9
102− style: Text.Raised
103− styleColor: "black"
104− fontSize: "small"
105− font.weight: Font.Light
106− elide: Text.ElideMiddle
107− horizontalAlignment: Text.AlignHCenter
108− }
109− }
110−
111− GridView.onRemove: SequentialAnimation {
112− PropertyAction { target: root; property: "GridView.delayRemove"; value: true }
113− NumberAnimation { target: root; property: "opacity"; from: 1; to: 0; duration: 200; easing.type: Easing.InOutQuad }
114− PropertyAction { target: root; property: "GridView.delayRemove"; value: false }
115− }
116−}
Dash/Music/MusicFilterGrid.qml+2−4
@@ -44,13 +44,11 @@ GenericFilterGrid {
4444 }
4545
4646 onClicked: {
47− var data = { model: model }
48− filtergrid.clicked(index, data, tile.y)
47+ filtergrid.clicked(index, filtergrid.model, tile.y)
4948 }
5049
5150 onPressAndHold: {
52− var data = { model: model }
53− filtergrid.pressAndHold(index, data, tile.y)
51+ filtergrid.pressAndHold(index, filtergrid.model, tile.y)
5452 }
5553 }
5654}
Dash/SearchableResultModel.qml+4
@@ -25,6 +25,10 @@ SortFilterProxyModel {
2525
2626 filterRole: CategoryResults.RoleTitle
2727
28+ function get(index) {
29+ return model.get(mapToSource(index))
30+ }
31+
2832 onSearchQueryChanged: {
2933 if (searchQuery.length == 0) {
3034 filterRegExp = RegExp("");
Dash/Video/VideoFilterGrid.qml+2−4
@@ -42,13 +42,11 @@ GenericFilterGrid {
4242 style: VideoTileStyle {}
4343
4444 onClicked: {
45− var data = { model: model }
46− filtergrid.clicked(index, data, tile.y)
45+ filtergrid.clicked(index, filtergrid.model, tile.y)
4746 }
4847
4948 onPressAndHold: {
50− var data = { model: model }
51− filtergrid.pressAndHold(index, data, tile.y)
49+ filtergrid.pressAndHold(index, filtergrid.model, tile.y)
5250 }
5351 }
5452}
Dash/graphics/tooltip_arrow@27.pngnew file

Binary file not shown.

tests/mocks/Unity/fake_preview.cpp+9−1
@@ -16,9 +16,13 @@
1616
1717#include "fake_preview.h"
1818
19+Preview::Preview(QObject *parent): QObject(parent)
20+{
21+}
22+
1923QString Preview::rendererName() const
2024{
21− return "foo";
25+ return "generic-preview";
2226}
2327
2428QString Preview::title() const
@@ -61,3 +65,7 @@ void Preview::execute(const QString& actionId, const QHash<QString, QVariant>& h
6165 Q_UNUSED(actionId);
6266 Q_UNUSED(hints);
6367}
68+
69+void Preview::cancelAction()
70+{
71+}
tests/mocks/Unity/fake_preview.h+2
@@ -51,6 +51,8 @@ public:
5151
5252 Q_INVOKABLE void execute(const QString& actionId, const QHash<QString, QVariant>& hints);
5353
54+ Q_INVOKABLE void cancelAction();
55+
5456Q_SIGNALS:
5557 void previewChanged();
5658};
tests/mocks/Unity/fake_scope.cpp+25−4
@@ -14,13 +14,11 @@
1414 * along with this program. If not, see <http://www.gnu.org/licenses/>.
1515 */
1616
17−// Self
1817#include "fake_scope.h"
19−
20−#include <dee.h>
18+#include "fake_preview.h"
2119#include "paths.h"
2220
23−#include "fake_preview.h"
21+#include <dee.h>
2422
2523static DeeModel* create_categories_model(unsigned category_count);
2624static DeeModel* create_results_model(unsigned category_count, unsigned result_count);
@@ -39,6 +37,13 @@ Scope::Scope(QObject* parent)
3937 m_categories->setResultModel(results_model);
4038 m_categories->setModel(create_categories_model(4));
4139 m_results->setModel(results_model);
40+
41+ m_timer.setInterval(1000);
42+ m_timer.setSingleShot(true);
43+ connect(&m_timer, &QTimer::timeout, [this]() {
44+ Preview *p = new Preview(this);
45+ Q_EMIT previewReady(p);
46+ });
4247}
4348
4449Scope::Scope(QString const& id, QString const& name, bool visible, QObject* parent)
@@ -55,6 +60,13 @@ Scope::Scope(QString const& id, QString const& name, bool visible, QObject* pare
5560 m_categories->setResultModel(results_model);
5661 m_categories->setModel(create_categories_model(4));
5762 m_results->setModel(results_model);
63+
64+ m_timer.setInterval(1000);
65+ m_timer.setSingleShot(true);
66+ connect(&m_timer, &QTimer::timeout, [this]() {
67+ Preview *p = new Preview(this);
68+ Q_EMIT previewReady(p);
69+ });
5870}
5971
6072QString Scope::id() const {
@@ -168,6 +180,8 @@ void Scope::activate(const QVariant &uri, const QVariant &icon_hint, const QVari
168180 Q_UNUSED(comment);
169181 Q_UNUSED(dnd_uri);
170182 Q_UNUSED(metadata);
183+
184+ m_timer.start();
171185}
172186
173187void Scope::preview(const QVariant &uri, const QVariant &icon_hint, const QVariant &category,
@@ -183,6 +197,8 @@ void Scope::preview(const QVariant &uri, const QVariant &icon_hint, const QVaria
183197 Q_UNUSED(comment);
184198 Q_UNUSED(dnd_uri);
185199 Q_UNUSED(metadata);
200+
201+ m_timer.start();
186202}
187203
188204static const gchar * categories_model_schema[] = {
@@ -248,3 +264,8 @@ DeeModel* create_results_model(unsigned category_count, unsigned result_count) {
248264 }
249265 return results_model;
250266}
267+
268+void Scope::cancelActivation()
269+{
270+ m_timer.stop();
271+}
tests/mocks/Unity/fake_scope.h+7−2
@@ -17,10 +17,11 @@
1717#ifndef FAKE_SCOPE_H
1818#define FAKE_SCOPE_H
1919
20−// Qt
21−#include <QObject>
2220#include "fake_categories.h"
2321
22+#include <QObject>
23+#include <QDebug>
24+
2425class Preview;
2526
2627class Scope : public QObject
@@ -78,6 +79,8 @@ public:
7879 const QVariant &result_type, const QVariant &mimetype, const QVariant &title,
7980 const QVariant &comment, const QVariant &dnd_uri, const QVariant &metadata);
8081
82+ Q_INVOKABLE void cancelActivation();
83+
8184Q_SIGNALS:
8285 void idChanged(const QString&);
8386 void nameChanged(const QString&);
@@ -115,6 +118,8 @@ protected:
115118
116119 Categories* m_categories;
117120 DeeListModel* m_results;
121+
122+ QTimer m_timer;
118123};
119124
120125#endif // FAKE_SCOPE_H
tests/mocks/Unity/fake_unity_plugin.cpp+3
@@ -22,6 +22,7 @@
2222// local
2323#include "fake_scopes.h"
2424#include "fake_categories.h"
25+#include "fake_preview.h"
2526#include "categoryresults.h"
2627
2728// External
@@ -37,8 +38,10 @@ void FakeUnityPlugin::registerTypes(const char *uri)
3738
3839 Q_ASSERT(uri == QLatin1String("Unity"));
3940
41+ qmlRegisterUncreatableType<Preview>(uri, 0, 1, "Preview", "Can't create Preview object in QML.");
4042 qmlRegisterType<Scopes>(uri, 0, 1, "Scopes");
4143 qmlRegisterType<Scope>(uri, 0, 1, "Scope");
4244 qmlRegisterType<Categories>(uri, 0, 1, "Categories");
4345 qmlRegisterUncreatableType<CategoryResults>(uri, 0, 1, "CategoryResults", "Can't create");
46+ qmlRegisterType<DeeListModel>(uri, 0, 1, "DeeListModel");
4447}
tests/qmltests/Dash/tst_GenericScopeView.qml+123−13
@@ -23,11 +23,21 @@ import Ubuntu.Components 0.1
2323import Unity.Test 0.1 as UT
2424
2525Item {
26+ id: shell
2627 width: units.gu(120)
2728 height: units.gu(80)
2829
2930 Scopes {
3031 id: scopes
32+
33+ onLoadedChanged: {
34+ genericScopeView.scope = scopes.get(0)
35+ }
36+ }
37+
38+ property Item applicationManager: Item {
39+ signal sideStageFocusedApplicationChanged()
40+ signal mainStageFocusedApplicationChanged()
3141 }
3242
3343 GenericScopeView {
@@ -38,44 +48,144 @@ Item {
3848 name: "GenericScopeView"
3949 when: scopes.loaded
4050
41− function init() {
42− genericScopeView.scope = scopes.get(0)
43− }
44−
4551 function test_isCurrent() {
4652 var pageHeader = findChild(genericScopeView, "pageHeader");
47− var previewLoader = findChild(genericScopeView, "previewLoader");
53+ var previewListView = findChild(genericScopeView, "previewListView");
4854 genericScopeView.isCurrent = true
4955 pageHeader.searchQuery = "test"
50− previewLoader.open = true
56+ previewListView.open = true
5157 genericScopeView.isCurrent = false
5258 tryCompare(pageHeader, "searchQuery", "")
5359 tryCompare(genericScopeView, "previewShown", false);
5460 }
5561
5662 function test_showDash() {
57− var previewLoader = findChild(genericScopeView, "previewLoader");
58− previewLoader.open = true;
63+ var previewListView = findChild(genericScopeView, "previewListView");
64+ previewListView.open = true;
5965 scopes.get(0).showDash();
6066 tryCompare(genericScopeView, "previewShown", false);
6167 }
6268
6369 function test_hideDash() {
64− var previewLoader = findChild(genericScopeView, "previewLoader");
65− previewLoader.open = true;
70+ var previewListView = findChild(genericScopeView, "previewListView");
71+ previewListView.open = true;
6672 scopes.get(0).hideDash();
6773 tryCompare(genericScopeView, "previewShown", false);
6874 }
6975
76+ function openPreview() {
77+ var tile = findChild(genericScopeView, "delegate0");
78+ mouseClick(tile, tile.width / 2, tile.height / 2);
79+ var openEffect = findChild(genericScopeView, "openEffect");
80+ tryCompare(openEffect, "gap", 1);
81+ }
82+
83+ function checkArrowPosition(index) {
84+ var tile = findChild(genericScopeView, "delegate" + index);
85+ var tileCenter = tile.x + tile.width/2;
86+ var pointerArrow = findChild(genericScopeView, "pointerArrow");
87+ var pointerArrowCenter = pointerArrow.x + pointerArrow.width/2;
88+ compare(pointerArrowCenter, tileCenter, "Pointer did not move to tile");
89+ }
90+
91+ function closePreview() {
92+ var closePreviewMouseArea = findChild(genericScopeView, "closePreviewMouseArea");
93+ mouseClick(closePreviewMouseArea, closePreviewMouseArea.width / 2, closePreviewMouseArea.height / 2);
94+
95+ var previewListView = findChild(genericScopeView, "previewListView");
96+ tryCompare(previewListView, "open", false);
97+ var openEffect = findChild(genericScopeView, "openEffect");
98+ tryCompare(openEffect, "gap", 0);
99+
100+ var categoryListView = findChild(genericScopeView, "categoryListView");
101+ categoryListView.flick(0, units.gu(200));
102+ tryCompare(categoryListView, "flicking", false);
103+ }
104+
105+ function test_previewOpenClose() {
106+ var previewListView = findChild(genericScopeView, "previewListView");
107+ tryCompare(previewListView, "open", false);
108+
109+ openPreview();
110+
111+ // check for it opening successfully
112+ var currentPreviewItem = findChild(genericScopeView, "previewLoader0");
113+ tryCompareFunction(function() {
114+ var parts = currentPreviewItem.source.toString().split("/");
115+ var name = parts[parts.length - 1];
116+ return name == "DashPreviewPlaceholder.qml";
117+ },
118+ true);
119+ tryCompareFunction(function() {
120+ var parts = currentPreviewItem.source.toString().split("/");
121+ var name = parts[parts.length - 1];
122+ return name == "GenericPreview.qml";
123+ },
124+ true);
125+ tryCompare(currentPreviewItem, "progress", 1);
126+ tryCompare(previewListView, "open", true);
127+
128+ closePreview();
129+ tryCompare(previewListView, "open", false);
130+ }
131+
132+ function test_previewCycleOne() {
133+ var previewListView = findChild(genericScopeView, "previewListView");
134+ tryCompare(previewListView, "open", false);
135+
136+ openPreview();
137+
138+ // wait for it to be loaded
139+ var currentPreviewItem = findChild(genericScopeView, "previewLoader0");
140+ tryCompareFunction(function() {
141+ var parts = currentPreviewItem.source.toString().split("/");
142+ var name = parts[parts.length - 1];
143+ return name == "GenericPreview.qml";
144+ },
145+ true);
146+ tryCompare(currentPreviewItem, "progress", 1);
147+ waitForRendering(currentPreviewItem);
148+
149+ checkArrowPosition(0);
150+
151+ // flick to the next previews
152+
153+ for (var i = 1; i < previewListView.count; ++i) {
154+
155+ mouseFlick(previewListView, previewListView.width - units.gu(1),
156+ previewListView.height / 2,
157+ units.gu(2),
158+ previewListView.height / 2);
159+
160+ // wait for it to be loaded
161+ var nextPreviewItem = findChild(genericScopeView, "previewLoader" + i);
162+ tryCompareFunction(function() {
163+ var parts = nextPreviewItem.source.toString().split("/");
164+ var name = parts[parts.length - 1];
165+ return name == "GenericPreview.qml";
166+ },
167+ true);
168+ tryCompare(nextPreviewItem, "progress", 1);
169+ waitForRendering(nextPreviewItem);
170+
171+ checkArrowPosition(i);
172+ }
173+
174+ closePreview();
175+ }
176+
70177 function test_show_spinner() {
71− var previewLoader = findChild(genericScopeView, "previewLoader");
72− previewLoader.open = true;
73− previewLoader.source = "../../../Dash/Generic/GenericPreview.qml";
178+ openPreview();
179+ var previewListView = findChild(genericScopeView, "previewListView");
180+ var previewLoader = findChild(genericScopeView, "previewLoader0");
181+
74182 previewLoader.item.showProcessingAction = true;
75183 var waitingForAction = findChild(genericScopeView, "waitingForActionMouseArea");
76184 tryCompare(waitingForAction, "enabled", true);
77185 previewLoader.closePreviewSpinner();
78186 tryCompare(waitingForAction, "enabled", false);
187+
188+ closePreview();
79189 }
80190 }
81191 }