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