on 9ee0777 help sign in
unity8/Dash/GenericScopeView.qml
583 lines 22 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.1
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(desktopFilePath) {
50 shell.activateApplication(desktopFilePath);
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)
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 }
168 Component.onDestruction: {
169 if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
170 scopeView.enableHeightBehaviorOnNextCreation = item.enableHeightBehaviorOnNextCreation;
171 }
172 }
174 Connections {
175 target: rendererLoader.item
176 onClicked: {
177 // Prepare the preview in case activate() triggers a preview only
178 effect.positionPx = Math.max(mapToItem(categoryView, 0, itemY).y, pageHeader.height + categoryView.stickyHeaderHeight);
179 previewListView.categoryId = categoryId
180 previewListView.categoryDelegate = rendererLoader.item
181 previewListView.model = target.model;
182 previewListView.init = true;
183 previewListView.currentIndex = index;
185 var item = target.model.get(index);
187 if ((scopeView.scope.id == "applications.scope" && categoryId == "installed")
188 || (scopeView.scope.id == "home.scope" && categoryId == "applications.scope")) {
189 scopeView.scope.activate(item.uri, item.icon, item.category, 0, item.mimetype, item.title,
190 item.comment, item.dndUri, item.metadata)
191 } else {
192 previewListView.open = true
194 scopeView.scope.preview( item.uri, item.icon, item.category, 0, item.mimetype, item.title,
195 item.comment, item.dndUri, item.metadata)
196 }
197 }
198 onPressAndHold: {
199 effect.positionPx = Math.max(mapToItem(categoryView, 0, itemY).y, pageHeader.height + categoryView.stickyHeaderHeight);
200 previewListView.categoryId = categoryId
201 previewListView.categoryDelegate = rendererLoader.item
202 previewListView.model = target.model;
203 previewListView.init = true;
204 previewListView.currentIndex = index;
205 previewListView.open = true
207 var item = target.model.get(index)
208 scopeView.scope.preview( item.uri, item.icon, item.category, 0, item.mimetype, item.title,
209 item.comment, item.dndUri, item.metadata)
210 }
211 }
212 Connections {
213 target: categoryView
214 onExpandedCategoryIdChanged: {
215 collapseAllButExpandedCategory();
216 }
217 onCorrectExpandedCategory: {
218 collapseAllButExpandedCategory();
219 }
220 function collapseAllButExpandedCategory() {
221 var item = rendererLoader.item;
222 if (item.expandable) {
223 var shouldFilter = categoryId != categoryView.expandedCategoryId;
224 if (shouldFilter != item.filter) {
225 // If the filter animation will be seen start it, otherwise, just flip the switch
226 var shrinkingVisible = shouldFilter && y + item.collapsedHeight < categoryView.height;
227 var growingVisible = !shouldFilter && y + height < categoryView.height;
228 if (!previewListView.open || !shouldFilter) {
229 if (shrinkingVisible || growingVisible) {
230 item.startFilterAnimation(shouldFilter)
231 } else {
232 item.filter = shouldFilter;
233 }
234 if (!shouldFilter && !previewListView.open) {
235 categoryView.maximizeVisibleArea(index, item.uncollapsedHeight);
236 }
237 }
238 }
239 }
240 }
241 onContentYChanged: rendererLoader.updateDelegateCreationRange();
242 onHeightChanged: rendererLoader.updateDelegateCreationRange();
243 }
245 function updateDelegateCreationRange() {
246 // Do not update the range if we are overshooting up or down, since we'll come back
247 // to the stable position and delete/create items without any reason
248 if (categoryView.contentY < categoryView.originY) {
249 return;
250 } else if (categoryView.contentY + categoryView.height > categoryView.contentHeight) {
251 return;
252 }
254 if (item.hasOwnProperty("delegateCreationBegin")) {
255 if (baseItem.y + baseItem.height <= 0) {
256 // Not visible (item at top of the list)
257 item.delegateCreationBegin = baseItem.height
258 item.delegateCreationEnd = baseItem.height
259 } else if (baseItem.y >= categoryView.height) {
260 // Not visible (item at bottom of the list)
261 item.delegateCreationBegin = 0
262 item.delegateCreationEnd = 0
263 } else {
264 item.delegateCreationBegin = Math.max(-baseItem.y, 0)
265 item.delegateCreationEnd = Math.min(categoryView.height + item.delegateCreationBegin, baseItem.height)
266 }
267 }
268 }
269 }
270 }
272 sectionProperty: "name"
273 sectionDelegate: ListItems.Header {
274 property var delegate: categoryView.item(delegateIndex)
275 width: categoryView.width
276 text: section
277 image: {
278 if (delegate && delegate.expandable)
279 return delegate.filtered ? "graphics/header_handlearrow.png" : "graphics/header_handlearrow2.png"
280 return "";
281 }
282 onClicked: {
283 if (categoryView.expandedCategoryId != delegate.category)
284 categoryView.expandedCategoryId = delegate.category;
285 else
286 categoryView.expandedCategoryId = "";
287 }
288 }
289 pageHeader: PageHeader {
290 id: pageHeader
291 objectName: "pageHeader"
292 width: categoryView.width
293 text: scopeView.scope.name
294 searchEntryEnabled: true
295 scope: scopeView.scope
296 searchHistory: scopeView.searchHistory
297 }
298 }
300 function getDefaultRendererId(contentType) {
301 switch (contentType) {
302 default: return "grid";
303 }
304 }
306 function getRenderer(rendererId, contentType, rendererHint) {
307 if (rendererId == "default") {
308 rendererId = getDefaultRendererId(contentType);
309 }
310 switch (rendererId) {
311 case "carousel": {
312 switch (contentType) {
313 case "music": return "Music/MusicCarouselLoader.qml";
314 case "video": return "Video/VideoCarouselLoader.qml";
315 default: return "Generic/GenericCarouselLoader.qml";
316 }
317 }
318 case "grid": {
319 switch (contentType) {
320 case "apps": {
321 if (rendererHint == "toggled")
322 return "Apps/DashPluginFilterGrid.qml";
323 else
324 return "Generic/GenericFilterGrid.qml";
325 }
326 case "music": return "Music/MusicFilterGrid.qml";
327 case "video": return "Video/VideoFilterGrid.qml";
328 case "weather": return "Generic/WeatherFilterGrid.qml";
329 default: return "Generic/GenericFilterGrid.qml";
330 }
331 }
332 case "special": {
333 switch (contentType) {
334 case "apps": return "Apps/RunningApplicationsGrid.qml";
335 default: return "Generic/GenericFilterGrid.qml";
336 }
337 }
338 default: return "Generic/GenericFilterGrid.qml";
339 }
340 }
342 OpenEffect {
343 id: effect
344 objectName: "openEffect"
345 anchors {
346 fill: parent
347 bottomMargin: -bottomOverflow
348 }
349 sourceItem: categoryView
351 enabled: gap > 0.0
353 topGapPx: (1 - gap) * positionPx
354 topOpacity: (1 - gap * 1.2)
355 bottomGapPx: positionPx + gap * (targetBottomGapPx - positionPx)
356 bottomOverflow: units.gu(20)
357 live: !expansionAnimation.running
359 property int targetBottomGapPx: height - units.gu(8) - bottomOverflow
360 property real gap: previewListView.open ? 1.0 : 0.0
362 Behavior on gap {
363 NumberAnimation {
364 id: expansionAnimation
365 duration: 200
366 easing.type: Easing.InOutQuad
367 onRunningChanged: {
368 if (!previewListView.open && !running) {
369 previewListView.onScreen = false
370 }
371 }
372 }
373 }
374 Behavior on positionPx {
375 enabled: previewListView.open
376 UbuntuNumberAnimation {}
377 }
378 }
380 Connections {
381 target: scopeView.scope
382 onPreviewReady: {
383 if (previewListView.init) {
384 // Preview was triggered because of a click on the item. Need to expand now.
385 if (!previewListView.open) {
386 previewListView.open = true
387 }
389 var index = previewListView.currentIndex
390 previewListView.currentIndex = -1
391 previewListView.currentIndex = index
392 previewListView.init = false
393 }
394 previewListView.currentItem.previewData = preview
395 }
396 }
398 PreviewDelegateMapper {
399 id: previewDelegateMapper
400 }
402 ListView {
403 id: previewListView
404 objectName: "previewListView"
405 height: effect.bottomGapPx - effect.topGapPx
406 anchors {
407 top: parent.top
408 topMargin: effect.topGapPx
409 left: parent.left
410 right: parent.right
411 }
412 orientation: ListView.Horizontal
413 highlightRangeMode: ListView.StrictlyEnforceRange
414 snapMode: ListView.SnapOneItem
415 boundsBehavior: Flickable.DragAndOvershootBounds
416 highlightMoveDuration: 250
417 flickDeceleration: units.gu(625)
418 maximumFlickVelocity: width * 5
419 cacheBuffer: 0
421 // To be set before opening the preview
422 property string categoryId: ""
423 property var categoryDelegate
425 // because the ListView is built asynchronous, setting the
426 // currentIndex directly won't work. We need to refresh it
427 // when the first preview is ready to be displayed.
428 property bool init: true
430 onCurrentIndexChanged: {
431 var row = Math.floor(currentIndex / categoryDelegate.columns);
432 if (categoryDelegate.collapsedRowCount <= row) {
433 categoryView.expandedCategoryId = categoryId
434 }
436 if (open) {
437 categoryDelegate.highlightIndex = currentIndex
438 }
440 if (!init && model !== undefined) {
441 var item = model.get(currentIndex)
442 scopeView.scope.preview(item.uri, item.icon, item.category, 0, item.mimetype, item.title, item.comment, item.dndUri, item.metadata)
443 }
445 var itemY = categoryView.contentItem.mapFromItem(categoryDelegate.currentItem).y;
447 // Find new contentY and effect.postionPx
448 var newContentY = itemY - effect.positionPx - categoryDelegate.verticalSpacing;
450 // Make sure the item is not covered by a header. Move the effect split down if necessary
451 var headerHeight = pageHeader.height + categoryView.stickyHeaderHeight;
452 var effectAdjust = Math.max(effect.positionPx, headerHeight);
454 // Make sure we don't overscroll the listview. If yes, adjust effect position
455 if (newContentY < 0) {
456 effectAdjust += newContentY;
457 newContentY = 0;
458 }
459 if (newContentY > Math.max(0, categoryView.contentHeight - categoryView.height)) {
460 effectAdjust += -(categoryView.contentHeight - categoryView.height) + newContentY
461 newContentY = categoryView.contentHeight - categoryView.height;
462 }
464 effect.positionPx = effectAdjust;
465 categoryView.contentY = newContentY;
466 }
468 property bool open: false
469 property bool onScreen: false
471 onOpenChanged: {
472 if (open) {
473 onScreen = true;
474 categoryDelegate.highlightIndex = currentIndex;
475 pageHeader.unfocus();
476 } else {
477 // Cancel any pending preview requests or actions
478 if (previewListView.currentItem.previewData !== undefined) {
479 previewListView.currentItem.previewData.cancelAction();
480 }
481 scopeView.scope.cancelActivation();
482 model = undefined;
483 categoryView.correctExpandedCategory();
484 categoryDelegate.highlightIndex = -1;
485 }
486 }
488 Rectangle {
489 anchors.fill: parent
490 color: Qt.rgba(0, 0, 0, .3)
491 z: -1
492 }
494 delegate: Loader {
495 id: previewLoader
496 objectName: "previewLoader" + index
497 height: previewListView.height
498 width: previewListView.width
499 asynchronous: true
500 source: previewListView.onScreen ?
501 (previewData !== undefined ? previewDelegateMapper.map(previewData.rendererName) : "DashPreviewPlaceholder.qml") : ""
503 onPreviewDataChanged: {
504 if (previewData !== undefined && source.toString().indexOf("DashPreviewPlaceholder.qml") != -1) {
505 previewLoader.opacity = 0;
506 }
507 }
509 onSourceChanged: {
510 if (previewData !== undefined) {
511 fadeIn.start()
512 }
513 }
515 PropertyAnimation {
516 id: fadeIn
517 target: previewLoader
518 property: "opacity"
519 from: 0.0
520 to: 1.0
521 duration: UbuntuAnimation.BriskDuration
522 }
524 property var previewData
525 property bool valid: item !== null
527 onLoaded: {
528 if (previewListView.onScreen && previewData !== undefined) {
529 item.previewData = Qt.binding(function() { return previewData })
530 }
531 }
533 Connections {
534 ignoreUnknownSignals: true
535 target: item
536 onClose: {
537 previewListView.open = false
538 }
539 }
541 function closePreviewSpinner() {
542 if(item) {
543 item.showProcessingAction = false;
544 }
545 }
546 }
547 }
549 Image {
550 objectName: "pointerArrow"
551 anchors {
552 top: previewListView.bottom
553 left: parent.left
554 leftMargin: previewListView.categoryDelegate !== undefined && previewListView.categoryDelegate.currentItem ?
555 previewListView.categoryDelegate.currentItem.center + (-width + margins) / 2 : 0
557 Behavior on leftMargin {
558 SmoothedAnimation {
559 duration: UbuntuAnimation.FastDuration
560 }
561 }
562 }
563 height: units.gu(1)
564 width: units.gu(2)
565 property int margins: previewListView.categoryDelegate ? previewListView.categoryDelegate.margins : 0
566 opacity: previewListView.open ? .5 : 0
568 source: "graphics/tooltip_arrow.png"
569 }
571 // TODO: Move as InverseMouseArea to DashPreview
572 MouseArea {
573 objectName: "closePreviewMouseArea"
574 enabled: previewListView.onScreen
575 anchors {
576 fill: parent
577 topMargin: effect.bottomGapPx
578 }
579 onClicked: {
580 previewListView.open = false;
581 }
582 }