on a3e784d help sign in
unity8/Dash/GenericScopeView.qml
595 lines 23 kB history raw
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 */
17import QtQuick 2.0
18import Ubuntu.Components 0.1
19import Utils 0.1
20import Unity 0.2
21import "../Components"
22import "../Components/ListItems" as ListItems
24FocusScope {
25 id: scopeView
27 property Scope scope
28 property SortFilterProxyModel categories: categoryFilter
29 property bool isCurrent
30 property ListModel searchHistory
31 property alias moving: categoryView.moving
33 signal endReached
34 signal movementStarted
35 signal positionedAtBeginning
37 readonly property alias previewShown: previewListView.onScreen
38 property bool enableHeightBehaviorOnNextCreation: false
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 }
49 function activateApp(appId) {
50 shell.activateApplication(appId);
51 }
53 Binding {
54 target: scope
55 property: "isActive"
56 value: isCurrent
57 }
59 Timer {
60 id: timer
61 interval: 2000
62 onTriggered: scope.searchQuery = ""
63 }
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 }
74 onIsCurrentChanged: {
75 pageHeader.resetSearch();
76 previewListView.open = false;
77 }
79 onMovementStarted: categoryView.showHeader()
81 onPositionedAtBeginning: categoryView.positionAtBeginning()
83 Binding {
84 target: scopeView.scope
85 property: "searchQuery"
86 value: pageHeader.searchQuery
87 }
89 Binding {
90 target: pageHeader
91 property: "searchQuery"
92 value: scopeView.scope.searchQuery
93 }
95 Connections {
96 target: panel
97 onSearchClicked: if (isCurrent) {
98 pageHeader.triggerSearch()
99 categoryView.showHeader()
100 }
101 }
103 Connections {
104 target: scopeView.scope
105 onShowDash: previewListView.open = false;
106 onHideDash: previewListView.open = false;
107 }
109 ScopeListView {
110 id: categoryView
111 objectName: "categoryListView"
112 anchors.fill: parent
113 model: scopeView.categories
114 forceNoClip: previewListView.onScreen
116 onAtYEndChanged: if (atYEnd) endReached()
117 onMovingChanged: if (moving && atYEnd) endReached()
119 property string expandedCategoryId: ""
120 signal correctExpandedCategory();
122 Behavior on contentY {
123 enabled: previewListView.open
124 UbuntuNumberAnimation {}
125 }
127 delegate: ListItems.Base {
128 id: baseItem
129 highlightWhenPressed: false
131 readonly property bool expandable: rendererLoader.item ? rendererLoader.item.expandable : false
132 readonly property bool filtered: rendererLoader.item ? rendererLoader.item.filter : true
133 readonly property string category: categoryId
135 Loader {
136 id: rendererLoader
137 anchors {
138 top: parent.top
139 left: parent.left
140 right: parent.right
141 }
143 source: getRenderer(model.renderer, model.contentType, model.rendererHint, results)
145 onLoaded: {
146 if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
147 item.enableHeightBehavior = scopeView.enableHeightBehaviorOnNextCreation;
148 scopeView.enableHeightBehaviorOnNextCreation = false;
149 }
150 if (source.toString().indexOf("Apps/RunningApplicationsGrid.qml") != -1) {
151 // TODO: the running apps grid doesn't support standard scope results model yet
152 item.firstModel = Qt.binding(function() { return results.firstModel })
153 item.secondModel = Qt.binding(function() { return results.secondModel })
154 item.canEnableTerminationMode = Qt.binding(function() { return scopeView.isCurrent })
155 } else {
156 item.model = Qt.binding(function() { return results })
157 }
158 item.objectName = Qt.binding(function() { return categoryId })
159 if (item.expandable) {
160 var shouldFilter = categoryId != categoryView.expandedCategoryId;
161 if (shouldFilter != item.filter) {
162 item.filter = shouldFilter;
163 }
164 }
165 updateDelegateCreationRange();
166 // FIXME: should be "template", not "renderer"
167 item.template = Qt.binding(function() { return model.renderer });
168 item.components = Qt.binding(function() { return model.components });
169 }
171 Component.onDestruction: {
172 if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
173 scopeView.enableHeightBehaviorOnNextCreation = item.enableHeightBehaviorOnNextCreation;
174 }
175 }
177 Connections {
178 target: rendererLoader.item
179 onClicked: {
180 // Prepare the preview in case activate() triggers a preview only
181 effect.positionPx = Math.max(mapToItem(categoryView, 0, itemY).y, pageHeader.height + categoryView.stickyHeaderHeight);
182 previewListView.categoryId = categoryId
183 previewListView.categoryDelegate = rendererLoader.item
184 previewListView.model = target.model;
185 previewListView.init = true;
186 previewListView.currentIndex = index;
188 var item = target.model.get(index);
190 if ((scopeView.scope.id == "applications.scope" && categoryId == "installed")
191 || (scopeView.scope.id == "home.scope" && categoryId == "applications.scope")) {
192 scopeView.scope.activate(item.uri, item.icon, item.category, 0, item.mimetype, item.title,
193 item.comment, item.dndUri, item.metadata)
194 } else {
195 previewListView.open = true
197 scopeView.scope.preview(item.uri, item.icon, item.category, 0, item.mimetype, item.title,
198 item.comment, item.dndUri, item.metadata)
199 }
200 }
201 onPressAndHold: {
202 effect.positionPx = Math.max(mapToItem(categoryView, 0, itemY).y, pageHeader.height + categoryView.stickyHeaderHeight);
203 previewListView.categoryId = categoryId
204 previewListView.categoryDelegate = rendererLoader.item
205 previewListView.model = target.model;
206 previewListView.init = true;
207 previewListView.currentIndex = index;
208 previewListView.open = true
210 var item = target.model.get(index)
211 scopeView.scope.preview(item.uri, item.icon, item.category, 0, item.mimetype, item.title,
212 item.comment, item.dndUri, item.metadata)
213 }
214 }
215 Connections {
216 target: categoryView
217 onExpandedCategoryIdChanged: {
218 collapseAllButExpandedCategory();
219 }
220 onCorrectExpandedCategory: {
221 collapseAllButExpandedCategory();
222 }
223 function collapseAllButExpandedCategory() {
224 var item = rendererLoader.item;
225 if (item.expandable) {
226 var shouldFilter = categoryId != categoryView.expandedCategoryId;
227 if (shouldFilter != item.filter) {
228 // If the filter animation will be seen start it, otherwise, just flip the switch
229 var shrinkingVisible = shouldFilter && y + item.collapsedHeight < categoryView.height;
230 var growingVisible = !shouldFilter && y + height < categoryView.height;
231 if (!previewListView.open || !shouldFilter) {
232 if (shrinkingVisible || growingVisible) {
233 item.startFilterAnimation(shouldFilter)
234 } else {
235 item.filter = shouldFilter;
236 }
237 if (!shouldFilter && !previewListView.open) {
238 categoryView.maximizeVisibleArea(index, item.uncollapsedHeight);
239 }
240 }
241 }
242 }
243 }
244 onContentYChanged: rendererLoader.updateDelegateCreationRange();
245 onHeightChanged: rendererLoader.updateDelegateCreationRange();
246 }
248 function updateDelegateCreationRange() {
249 // Do not update the range if we are overshooting up or down, since we'll come back
250 // to the stable position and delete/create items without any reason
251 if (categoryView.contentY < categoryView.originY) {
252 return;
253 } else if (categoryView.contentY + categoryView.height > categoryView.contentHeight) {
254 return;
255 }
257 if (item.hasOwnProperty("delegateCreationBegin")) {
258 if (baseItem.y + baseItem.height <= 0) {
259 // Not visible (item at top of the list)
260 item.delegateCreationBegin = baseItem.height
261 item.delegateCreationEnd = baseItem.height
262 } else if (baseItem.y >= categoryView.height) {
263 // Not visible (item at bottom of the list)
264 item.delegateCreationBegin = 0
265 item.delegateCreationEnd = 0
266 } else {
267 item.delegateCreationBegin = Math.max(-baseItem.y, 0)
268 item.delegateCreationEnd = Math.min(categoryView.height + item.delegateCreationBegin, baseItem.height)
269 }
270 }
271 }
272 }
273 }
275 sectionProperty: "name"
276 sectionDelegate: ListItems.Header {
277 property var delegate: categoryView.item(delegateIndex)
278 width: categoryView.width
279 text: section
280 image: {
281 if (delegate && delegate.expandable)
282 return delegate.filtered ? "graphics/header_handlearrow.png" : "graphics/header_handlearrow2.png"
283 return "";
284 }
285 onClicked: {
286 if (categoryView.expandedCategoryId != delegate.category)
287 categoryView.expandedCategoryId = delegate.category;
288 else
289 categoryView.expandedCategoryId = "";
290 }
291 }
292 pageHeader: PageHeader {
293 id: pageHeader
294 objectName: "pageHeader"
295 width: categoryView.width
296 text: scopeView.scope.name
297 searchEntryEnabled: true
298 scope: scopeView.scope
299 searchHistory: scopeView.searchHistory
300 }
301 }
303 function getDefaultRendererId(contentType) {
304 switch (contentType) {
305 default: return "grid";
306 }
307 }
309 function getRenderer(rendererId, contentType, rendererHint, results) {
310 if (rendererId == "default") {
311 rendererId = getDefaultRendererId(contentType);
312 }
313 if (rendererId == "carousel") {
314 // Selectively disable carousel, 6 is fixed for now, should change on the form factor
315 if (results.count <= 6)
316 rendererId = "grid"
317 }
318 switch (rendererId) {
319 case "carousel": {
320 switch (contentType) {
321 case "music": return "Music/MusicCarousel.qml";
322 case "video": return "Video/VideoCarousel.qml";
323 default: return "Generic/GenericCarousel.qml";
324 }
325 }
326 case "grid": {
327 switch (contentType) {
328 case "apps": {
329 if (rendererHint == "toggled")
330 return "Apps/DashPluginFilterGrid.qml";
331 else
332 return "Generic/GenericFilterGrid.qml";
333 }
334 case "music": return "Music/MusicFilterGrid.qml";
335 case "video": return "Video/VideoFilterGrid.qml";
336 case "weather": return "Generic/WeatherFilterGrid.qml";
337 default: return "Generic/GenericFilterGrid.qml";
338 }
339 }
340 case "special": {
341 switch (contentType) {
342 case "apps": return "Apps/RunningApplicationsGrid.qml";
343 default: return "Generic/GenericFilterGrid.qml";
344 }
345 }
346 default: return "CardFilterGrid.qml";
347 }
348 }
350 OpenEffect {
351 id: effect
352 objectName: "openEffect"
353 anchors {
354 fill: parent
355 bottomMargin: -bottomOverflow
356 }
357 sourceItem: categoryView
359 enabled: gap > 0.0
361 topGapPx: (1 - gap) * positionPx
362 topOpacity: (1 - gap * 1.2)
363 bottomGapPx: positionPx + gap * (targetBottomGapPx - positionPx)
364 bottomOverflow: units.gu(20)
365 live: !expansionAnimation.running
367 property int targetBottomGapPx: height - units.gu(8) - bottomOverflow
368 property real gap: previewListView.open ? 1.0 : 0.0
370 Behavior on gap {
371 NumberAnimation {
372 id: expansionAnimation
373 duration: 200
374 easing.type: Easing.InOutQuad
375 onRunningChanged: {
376 if (!previewListView.open && !running) {
377 previewListView.onScreen = false
378 }
379 }
380 }
381 }
382 Behavior on positionPx {
383 enabled: previewListView.open
384 UbuntuNumberAnimation {}
385 }
386 }
388 Connections {
389 target: scopeView.scope
390 onPreviewReady: {
391 if (previewListView.init) {
392 // Preview was triggered because of a click on the item. Need to expand now.
393 if (!previewListView.open) {
394 previewListView.open = true
395 }
397 var index = previewListView.currentIndex
398 previewListView.currentIndex = -1
399 previewListView.currentIndex = index
400 previewListView.init = false
401 }
402 previewListView.currentItem.previewData = preview
403 }
404 }
406 PreviewDelegateMapper {
407 id: previewDelegateMapper
408 }
410 ListView {
411 id: previewListView
412 objectName: "previewListView"
413 height: effect.bottomGapPx - effect.topGapPx
414 anchors {
415 top: parent.top
416 topMargin: effect.topGapPx
417 left: parent.left
418 right: parent.right
419 }
420 orientation: ListView.Horizontal
421 highlightRangeMode: ListView.StrictlyEnforceRange
422 snapMode: ListView.SnapOneItem
423 boundsBehavior: Flickable.DragAndOvershootBounds
424 highlightMoveDuration: 250
425 flickDeceleration: units.gu(625)
426 maximumFlickVelocity: width * 5
427 cacheBuffer: 0
429 // To be set before opening the preview
430 property string categoryId: ""
431 property var categoryDelegate
433 // because the ListView is built asynchronous, setting the
434 // currentIndex directly won't work. We need to refresh it
435 // when the first preview is ready to be displayed.
436 property bool init: true
438 onCurrentIndexChanged: {
439 var row = Math.floor(currentIndex / categoryDelegate.columns);
440 if (categoryDelegate.collapsedRowCount <= row) {
441 categoryView.expandedCategoryId = categoryId
442 }
444 if (open) {
445 categoryDelegate.highlightIndex = currentIndex
446 }
448 if (!init && model !== undefined) {
449 var item = model.get(currentIndex)
450 scopeView.scope.preview(item.uri, item.icon, item.category, 0, item.mimetype, item.title, item.comment, item.dndUri, item.metadata)
451 }
453 // Adjust contentY in case we need to change to it to show the next row
454 if (categoryDelegate.rows > 1) {
455 var itemY = categoryView.contentItem.mapFromItem(categoryDelegate.currentItem).y;
457 // Find new contentY and effect.postionPx
458 var newContentY = itemY - effect.positionPx - categoryDelegate.verticalSpacing;
460 // Make sure the item is not covered by a header. Move the effect split down if necessary
461 var headerHeight = pageHeader.height + categoryView.stickyHeaderHeight;
462 var effectAdjust = Math.max(effect.positionPx, headerHeight);
464 // Make sure we don't overscroll the listview. If yes, adjust effect position
465 if (newContentY < 0) {
466 effectAdjust += newContentY;
467 newContentY = 0;
468 }
469 if (newContentY > Math.max(0, categoryView.contentHeight - categoryView.height)) {
470 effectAdjust += -(categoryView.contentHeight - categoryView.height) + newContentY
471 newContentY = categoryView.contentHeight - categoryView.height;
472 }
474 effect.positionPx = effectAdjust;
475 categoryView.contentY = newContentY;
476 }
477 }
479 property bool open: false
480 property bool onScreen: false
482 onOpenChanged: {
483 if (open) {
484 onScreen = true;
485 categoryDelegate.highlightIndex = currentIndex;
486 pageHeader.unfocus();
487 } else {
488 // Cancel any pending preview requests or actions
489 if (previewListView.currentItem.previewData !== undefined) {
490 previewListView.currentItem.previewData.cancelAction();
491 }
492 scopeView.scope.cancelActivation();
493 model = undefined;
494 categoryView.correctExpandedCategory();
495 categoryDelegate.highlightIndex = -1;
496 }
497 }
499 Rectangle {
500 anchors.fill: parent
501 color: Qt.rgba(0, 0, 0, .3)
502 z: -1
503 }
505 delegate: Loader {
506 id: previewLoader
507 objectName: "previewLoader" + index
508 height: previewListView.height
509 width: previewListView.width
510 asynchronous: true
511 source: previewListView.onScreen ?
512 (previewData !== undefined ? previewDelegateMapper.map(previewData.rendererName) : "DashPreviewPlaceholder.qml") : ""
514 onPreviewDataChanged: {
515 if (previewData !== undefined && source.toString().indexOf("DashPreviewPlaceholder.qml") != -1) {
516 previewLoader.opacity = 0;
517 }
518 }
520 onSourceChanged: {
521 if (previewData !== undefined) {
522 fadeIn.start()
523 }
524 }
526 PropertyAnimation {
527 id: fadeIn
528 target: previewLoader
529 property: "opacity"
530 from: 0.0
531 to: 1.0
532 duration: UbuntuAnimation.BriskDuration
533 }
535 property var previewData
536 property bool valid: item !== null
538 onLoaded: {
539 if (previewListView.onScreen && previewData !== undefined) {
540 item.previewData = Qt.binding(function() { return previewData })
541 item.isCurrent = Qt.binding(function() { return ListView.isCurrentItem })
542 }
543 }
545 Connections {
546 ignoreUnknownSignals: true
547 target: item
548 onClose: {
549 previewListView.open = false
550 }
551 }
553 function closePreviewSpinner() {
554 if (item) {
555 item.showProcessingAction = false;
556 }
557 }
558 }
559 }
561 Image {
562 objectName: "pointerArrow"
563 anchors {
564 top: previewListView.bottom
565 left: parent.left
566 leftMargin: previewListView.categoryDelegate !== undefined && previewListView.categoryDelegate.currentItem ?
567 previewListView.categoryDelegate.currentItem.center + (-width + margins) / 2 : 0
569 Behavior on leftMargin {
570 SmoothedAnimation {
571 duration: UbuntuAnimation.FastDuration
572 }
573 }
574 }
575 height: units.gu(1)
576 width: units.gu(2)
577 property int margins: previewListView.categoryDelegate ? previewListView.categoryDelegate.margins : 0
578 opacity: previewListView.open ? .5 : 0
580 source: "graphics/tooltip_arrow.png"
581 }
583 // TODO: Move as InverseMouseArea to DashPreview
584 MouseArea {
585 objectName: "closePreviewMouseArea"
586 enabled: previewListView.onScreen
587 anchors {
588 fill: parent
589 topMargin: effect.bottomGapPx
590 }
591 onClicked: {
592 previewListView.open = false;
593 }
594 }