@@ -16,19 +16,64 @@
1616
1717import QtQuick 2.0
1818import Ubuntu.Components 0.1
19+import Utils 0.1
20+import Unity 0.1
1921import "../Components"
2022import "../Components/ListItems" as ListItems
2123
22−ScopeView {
24+FocusScope {
2325 id: scopeView
24− readonly property alias previewShown: previewLoader.onScreen
26+
27+ property Scope scope
28+ property SortFilterProxyModel categories: categoryFilter
29+ property bool isCurrent
30+ property ListModel searchHistory
31+ property alias moving: categoryView.moving
32+
33+ signal endReached
34+ signal movementStarted
35+ signal positionedAtBeginning
36+
37+ readonly property alias previewShown: previewListView.onScreen
2538 property bool enableHeightBehaviorOnNextCreation: false
2639
27− moving: categoryView.moving
40+ // FIXME delay the search so that daemons have time to settle, note that
41+ // removing this will break ScopeView::test_changeScope
42+ onScopeChanged: {
43+ if (scope) {
44+ timer.restart();
45+ scope.activateApplication.connect(activateApp);
46+ }
47+ }
48+
49+ function activateApp(appId) {
50+ shell.activateApplication(appId);
51+ }
52+
53+ Binding {
54+ target: scope
55+ property: "isActive"
56+ value: isCurrent
57+ }
58+
59+ Timer {
60+ id: timer
61+ interval: 2000
62+ onTriggered: scope.searchQuery = ""
63+ }
64+
65+ SortFilterProxyModel {
66+ id: categoryFilter
67+ model: scope ? scope.categories : null
68+ dynamicSortFilter: true
69+ filterRole: Categories.RoleCount
70+ filterRegExp: /^0$/
71+ invertMatch: true
72+ }
2873
2974 onIsCurrentChanged: {
3075 pageHeader.resetSearch();
31− previewLoader.open = false;
76+ previewListView.open = false;
3277 }
3378
3479 onMovementStarted: categoryView.showHeader()
@@ -57,8 +102,8 @@ ScopeView {
57102
58103 Connections {
59104 target: scopeView.scope
60− onShowDash: previewLoader.open = false;
61− onHideDash: previewLoader.open = false;
105+ onShowDash: previewListView.open = false;
106+ onHideDash: previewListView.open = false;
62107 }
63108
64109 ScopeListView {
@@ -66,15 +111,22 @@ ScopeView {
66111 objectName: "categoryListView"
67112 anchors.fill: parent
68113 model: scopeView.categories
69− forceNoClip: previewLoader.onScreen
114+ forceNoClip: previewListView.onScreen
70115
71116 onAtYEndChanged: if (atYEnd) endReached()
72117 onMovingChanged: if (moving && atYEnd) endReached()
73118
74119 property string expandedCategoryId: ""
120+ signal correctExpandedCategory();
121+
122+ Behavior on contentY {
123+ enabled: previewListView.open
124+ UbuntuNumberAnimation {}
125+ }
75126
76127 delegate: ListItems.Base {
77128 id: baseItem
129+ objectName: "dashCategory" + category
78130 highlightWhenPressed: false
79131
80132 readonly property bool expandable: rendererLoader.item ? rendererLoader.item.expandable : false
@@ -89,7 +141,7 @@ ScopeView {
89141 right: parent.right
90142 }
91143
92− source: getRenderer(model.renderer, model.contentType, model.rendererHint)
144+ source: getRenderer(model.renderer, model.contentType, model.rendererHint, results)
93145
94146 onLoaded: {
95147 if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
@@ -123,33 +175,50 @@ ScopeView {
123175 Connections {
124176 target: rendererLoader.item
125177 onClicked: {
126− effect.positionPx = mapToItem(categoryView, 0, itemY).y
127− scopeView.scope.activate(delegateItem.model.uri,
128− delegateItem.model.icon,
129− delegateItem.model.category,
130− 0,
131− delegateItem.model.mimetype,
132− delegateItem.model.title,
133− delegateItem.model.comment,
134− delegateItem.model.dndUri,
135− delegateItem.model.metadata)
178+ // Prepare the preview in case activate() triggers a preview only
179+ effect.positionPx = Math.max(mapToItem(categoryView, 0, itemY).y, pageHeader.height + categoryView.stickyHeaderHeight);
180+ previewListView.categoryId = categoryId
181+ previewListView.categoryDelegate = rendererLoader.item
182+ previewListView.model = target.model;
183+ previewListView.init = true;
184+ previewListView.currentIndex = index;
185+
186+ var item = target.model.get(index);
187+
188+ if ((scopeView.scope.id == "applications.scope" && categoryId == "installed")
189+ || (scopeView.scope.id == "home.scope" && categoryId == "applications.scope")) {
190+ scopeView.scope.activate(item.uri, item.icon, item.category, 0, item.mimetype, item.title,
191+ item.comment, item.dndUri, item.metadata)
192+ } else {
193+ previewListView.open = true
194+
195+ scopeView.scope.preview(item.uri, item.icon, item.category, 0, item.mimetype, item.title,
196+ item.comment, item.dndUri, item.metadata)
197+ }
136198 }
137199 onPressAndHold: {
138− effect.positionPx = mapToItem(categoryView, 0, itemY).y
139− scopeView.scope.preview( delegateItem.model.uri,
140− delegateItem.model.icon,
141− delegateItem.model.category,
142− 0,
143− delegateItem.model.mimetype,
144− delegateItem.model.title,
145− delegateItem.model.comment,
146− delegateItem.model.dndUri,
147− delegateItem.model.metadata)
200+ effect.positionPx = Math.max(mapToItem(categoryView, 0, itemY).y, pageHeader.height + categoryView.stickyHeaderHeight);
201+ previewListView.categoryId = categoryId
202+ previewListView.categoryDelegate = rendererLoader.item
203+ previewListView.model = target.model;
204+ previewListView.init = true;
205+ previewListView.currentIndex = index;
206+ previewListView.open = true
207+
208+ var item = target.model.get(index)
209+ scopeView.scope.preview(item.uri, item.icon, item.category, 0, item.mimetype, item.title,
210+ item.comment, item.dndUri, item.metadata)
148211 }
149212 }
150213 Connections {
151214 target: categoryView
152215 onExpandedCategoryIdChanged: {
216+ collapseAllButExpandedCategory();
217+ }
218+ onCorrectExpandedCategory: {
219+ collapseAllButExpandedCategory();
220+ }
221+ function collapseAllButExpandedCategory() {
153222 var item = rendererLoader.item;
154223 if (item.expandable) {
155224 var shouldFilter = categoryId != categoryView.expandedCategoryId;
@@ -157,13 +226,15 @@ ScopeView {
157226 // If the filter animation will be seen start it, otherwise, just flip the switch
158227 var shrinkingVisible = shouldFilter && y + item.collapsedHeight < categoryView.height;
159228 var growingVisible = !shouldFilter && y + height < categoryView.height;
160− if (shrinkingVisible || growingVisible) {
161− item.startFilterAnimation(shouldFilter)
162− } else {
163− item.filter = shouldFilter;
164− }
165− if (!shouldFilter) {
166− categoryView.maximizeVisibleArea(index, item.uncollapsedHeight);
229+ if (!previewListView.open || !shouldFilter) {
230+ if (shrinkingVisible || growingVisible) {
231+ item.startFilterAnimation(shouldFilter)
232+ } else {
233+ item.filter = shouldFilter;
234+ }
235+ if (!shouldFilter && !previewListView.open) {
236+ categoryView.maximizeVisibleArea(index, item.uncollapsedHeight);
237+ }
167238 }
168239 }
169240 }
@@ -201,6 +272,7 @@ ScopeView {
201272
202273 sectionProperty: "name"
203274 sectionDelegate: ListItems.Header {
275+ objectName: "dashSectionHeader" + delegate.category
204276 property var delegate: categoryView.item(delegateIndex)
205277 width: categoryView.width
206278 text: section
@@ -223,6 +295,7 @@ ScopeView {
223295 text: scopeView.scope.name
224296 searchEntryEnabled: true
225297 scope: scopeView.scope
298+ searchHistory: scopeView.searchHistory
226299 }
227300 }
228301
@@ -232,31 +305,37 @@ ScopeView {
232305 }
233306 }
234307
235− function getRenderer(rendererId, contentType, rendererHint) {
308+ function getRenderer(rendererId, contentType, rendererHint, results) {
236309 if (rendererId == "default") {
237310 rendererId = getDefaultRendererId(contentType);
238311 }
312+ if (rendererId == "carousel") {
313+ // Selectively disable carousel, 6 is fixed for now, should change on the form factor
314+ if (results.count <= 6)
315+ rendererId = "grid"
316+ }
239317 switch (rendererId) {
318+ case "carousel": {
319+ switch (contentType) {
320+ case "music": return "Music/MusicCarousel.qml";
321+ case "video": return "Video/VideoCarousel.qml";
322+ default: return "Generic/GenericCarousel.qml";
323+ }
324+ }
240325 case "grid": {
241326 switch (contentType) {
242− case "video": return "Video/VideoFilterGrid.qml";
243− case "music": return "Music/MusicFilterGrid.qml";
244327 case "apps": {
245328 if (rendererHint == "toggled")
246329 return "Apps/DashPluginFilterGrid.qml";
247330 else
248331 return "Generic/GenericFilterGrid.qml";
249332 }
333+ case "music": return "Music/MusicFilterGrid.qml";
334+ case "video": return "Video/VideoFilterGrid.qml";
250335 case "weather": return "Generic/WeatherFilterGrid.qml";
251336 default: return "Generic/GenericFilterGrid.qml";
252337 }
253338 }
254− case "carousel": {
255− switch (contentType) {
256− case "music": return "Music/MusicCarousel.qml";
257− default: return "Generic/GenericCarousel.qml";
258− }
259− }
260339 case "special": {
261340 switch (contentType) {
262341 case "apps": return "Apps/RunningApplicationsGrid.qml";
@@ -269,6 +348,7 @@ ScopeView {
269348
270349 OpenEffect {
271350 id: effect
351+ objectName: "openEffect"
272352 anchors {
273353 fill: parent
274354 bottomMargin: -bottomOverflow
@@ -281,29 +361,44 @@ ScopeView {
281361 topOpacity: (1 - gap * 1.2)
282362 bottomGapPx: positionPx + gap * (targetBottomGapPx - positionPx)
283363 bottomOverflow: units.gu(20)
284− bottomOpacity: 1 - (gap * 0.8)
364+ live: !expansionAnimation.running
285365
286366 property int targetBottomGapPx: height - units.gu(8) - bottomOverflow
287− property real gap: previewLoader.open ? 1.0 : 0.0
367+ property real gap: previewListView.open ? 1.0 : 0.0
288368
289369 Behavior on gap {
290370 NumberAnimation {
371+ id: expansionAnimation
291372 duration: 200
292373 easing.type: Easing.InOutQuad
293374 onRunningChanged: {
294− if (!previewLoader.open && !running) {
295− previewLoader.onScreen = false
375+ if (!previewListView.open && !running) {
376+ previewListView.onScreen = false
296377 }
297378 }
298379 }
299380 }
381+ Behavior on positionPx {
382+ enabled: previewListView.open
383+ UbuntuNumberAnimation {}
384+ }
300385 }
301386
302387 Connections {
303388 target: scopeView.scope
304389 onPreviewReady: {
305− previewLoader.previewData = preview
306− previewLoader.open = true
390+ if (previewListView.init) {
391+ // Preview was triggered because of a click on the item. Need to expand now.
392+ if (!previewListView.open) {
393+ previewListView.open = true
394+ }
395+
396+ var index = previewListView.currentIndex
397+ previewListView.currentIndex = -1
398+ previewListView.currentIndex = index
399+ previewListView.init = false
400+ }
401+ previewListView.currentItem.previewData = preview
307402 }
308403 }
309404
@@ -311,18 +406,9 @@ ScopeView {
311406 id: previewDelegateMapper
312407 }
313408
314− Connections {
315− ignoreUnknownSignals: true
316− target: previewLoader.valid ? previewLoader.item : null
317− onClose: {
318− previewLoader.open = false
319− }
320− }
321−
322− Loader {
323− objectName: "previewLoader"
324− id: previewLoader
325− property var previewData
409+ ListView {
410+ id: previewListView
411+ objectName: "previewListView"
326412 height: effect.bottomGapPx - effect.topGapPx
327413 anchors {
328414 top: parent.top
@@ -330,32 +416,179 @@ ScopeView {
330416 left: parent.left
331417 right: parent.right
332418 }
333− source: onScreen ? previewDelegateMapper.map(previewLoader.previewData.rendererName) : ""
419+ orientation: ListView.Horizontal
420+ highlightRangeMode: ListView.StrictlyEnforceRange
421+ snapMode: ListView.SnapOneItem
422+ boundsBehavior: Flickable.DragAndOvershootBounds
423+ highlightMoveDuration: 250
424+ flickDeceleration: units.gu(625)
425+ maximumFlickVelocity: width * 5
426+ cacheBuffer: 0
427+
428+ // To be set before opening the preview
429+ property string categoryId: ""
430+ property var categoryDelegate
431+
432+ // because the ListView is built asynchronous, setting the
433+ // currentIndex directly won't work. We need to refresh it
434+ // when the first preview is ready to be displayed.
435+ property bool init: true
436+
437+ onCurrentIndexChanged: {
438+ var row = Math.floor(currentIndex / categoryDelegate.columns);
439+ if (categoryDelegate.collapsedRowCount <= row) {
440+ categoryView.expandedCategoryId = categoryId
441+ }
442+
443+ if (open) {
444+ categoryDelegate.highlightIndex = currentIndex
445+ }
446+
447+ if (!init && model !== undefined) {
448+ var item = model.get(currentIndex)
449+ scopeView.scope.preview(item.uri, item.icon, item.category, 0, item.mimetype, item.title, item.comment, item.dndUri, item.metadata)
450+ }
451+
452+ // Adjust contentY in case we need to change to it to show the next row
453+ if (categoryDelegate.rows > 1) {
454+ var itemY = categoryView.contentItem.mapFromItem(categoryDelegate.currentItem).y;
455+
456+ // Find new contentY and effect.postionPx
457+ var newContentY = itemY - effect.positionPx - categoryDelegate.verticalSpacing;
458+
459+ // Make sure the item is not covered by a header. Move the effect split down if necessary
460+ var headerHeight = pageHeader.height + categoryView.stickyHeaderHeight;
461+ var effectAdjust = Math.max(effect.positionPx, headerHeight);
462+
463+ // Make sure we don't overscroll the listview. If yes, adjust effect position
464+ if (newContentY < 0) {
465+ effectAdjust += newContentY;
466+ newContentY = 0;
467+ }
468+ if (newContentY > Math.max(0, categoryView.contentHeight - categoryView.height)) {
469+ effectAdjust += -(categoryView.contentHeight - categoryView.height) + newContentY
470+ newContentY = categoryView.contentHeight - categoryView.height;
471+ }
472+
473+ effect.positionPx = effectAdjust;
474+ categoryView.contentY = newContentY;
475+ }
476+ }
334477
335478 property bool open: false
336479 property bool onScreen: false
337− property bool valid: item !== null
338480
339481 onOpenChanged: {
340482 if (open) {
341− onScreen = true
483+ onScreen = true;
484+ categoryDelegate.highlightIndex = currentIndex;
485+ pageHeader.unfocus();
486+ } else {
487+ // Cancel any pending preview requests or actions
488+ if (previewListView.currentItem.previewData !== undefined) {
489+ previewListView.currentItem.previewData.cancelAction();
490+ }
491+ scopeView.scope.cancelActivation();
492+ model = undefined;
493+ categoryView.correctExpandedCategory();
494+ categoryDelegate.highlightIndex = -1;
342495 }
343496 }
344497
345− onLoaded: {
346− item.previewData = Qt.binding(function() { return previewLoader.previewData })
498+ Rectangle {
499+ anchors.fill: parent
500+ color: Qt.rgba(0, 0, 0, .3)
501+ z: -1
347502 }
503+
504+ delegate: Loader {
505+ id: previewLoader
506+ objectName: "previewLoader" + index
507+ height: previewListView.height
508+ width: previewListView.width
509+ asynchronous: true
510+ source: previewListView.onScreen ?
511+ (previewData !== undefined ? previewDelegateMapper.map(previewData.rendererName) : "DashPreviewPlaceholder.qml") : ""
512+
513+ onPreviewDataChanged: {
514+ if (previewData !== undefined && source.toString().indexOf("DashPreviewPlaceholder.qml") != -1) {
515+ previewLoader.opacity = 0;
516+ }
517+ }
518+
519+ onSourceChanged: {
520+ if (previewData !== undefined) {
521+ fadeIn.start()
522+ }
523+ }
524+
525+ PropertyAnimation {
526+ id: fadeIn
527+ target: previewLoader
528+ property: "opacity"
529+ from: 0.0
530+ to: 1.0
531+ duration: UbuntuAnimation.BriskDuration
532+ }
533+
534+ property var previewData
535+ property bool valid: item !== null
536+
537+ onLoaded: {
538+ if (previewListView.onScreen && previewData !== undefined) {
539+ item.previewData = Qt.binding(function() { return previewData })
540+ item.isCurrent = Qt.binding(function() { return ListView.isCurrentItem })
541+ }
542+ }
543+
544+ Connections {
545+ ignoreUnknownSignals: true
546+ target: item
547+ onClose: {
548+ previewListView.open = false
549+ }
550+ }
551+
552+ function closePreviewSpinner() {
553+ if (item) {
554+ item.showProcessingAction = false;
555+ }
556+ }
557+ }
558+ }
559+
560+ Image {
561+ objectName: "pointerArrow"
562+ anchors {
563+ top: previewListView.bottom
564+ left: parent.left
565+ leftMargin: previewListView.categoryDelegate !== undefined && previewListView.categoryDelegate.currentItem ?
566+ previewListView.categoryDelegate.currentItem.center + (-width + margins) / 2 : 0
567+
568+ Behavior on leftMargin {
569+ SmoothedAnimation {
570+ duration: UbuntuAnimation.FastDuration
571+ }
572+ }
573+ }
574+ height: units.gu(1)
575+ width: units.gu(2)
576+ property int margins: previewListView.categoryDelegate ? previewListView.categoryDelegate.margins : 0
577+ opacity: previewListView.open ? .5 : 0
578+
579+ source: "graphics/tooltip_arrow.png"
348580 }
349581
350582 // TODO: Move as InverseMouseArea to DashPreview
351583 MouseArea {
352− enabled: previewLoader.onScreen
584+ objectName: "closePreviewMouseArea"
585+ enabled: previewListView.onScreen
353586 anchors {
354587 fill: parent
355588 topMargin: effect.bottomGapPx
356589 }
357590 onClicked: {
358− previewLoader.open = false;
591+ previewListView.open = false;
359592 }
360593 }
361594}