on 3ef6e98 help sign in
unity8/Dash/GenericScopeView.qml
535 lines 21 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 "../Components"
20import "../Components/ListItems" as ListItems
22ScopeView {
23 id: scopeView
24 readonly property alias previewShown: previewListView.onScreen
25 property bool enableHeightBehaviorOnNextCreation: false
27 moving: categoryView.moving
29 onIsCurrentChanged: {
30 pageHeader.resetSearch();
31 previewListView.open = false;
32 }
34 onMovementStarted: categoryView.showHeader()
36 onPositionedAtBeginning: categoryView.positionAtBeginning()
38 Binding {
39 target: scopeView.scope
40 property: "searchQuery"
41 value: pageHeader.searchQuery
42 }
44 Binding {
45 target: pageHeader
46 property: "searchQuery"
47 value: scopeView.scope.searchQuery
48 }
50 Connections {
51 target: panel
52 onSearchClicked: if (isCurrent) {
53 pageHeader.triggerSearch()
54 categoryView.showHeader()
55 }
56 }
58 Connections {
59 target: scopeView.scope
60 onShowDash: previewListView.open = false;
61 onHideDash: previewListView.open = false;
62 }
64 ScopeListView {
65 id: categoryView
66 objectName: "categoryListView"
67 anchors.fill: parent
68 model: scopeView.categories
69 forceNoClip: previewListView.onScreen
71 onAtYEndChanged: if (atYEnd) endReached()
72 onMovingChanged: if (moving && atYEnd) endReached()
74 property string expandedCategoryId: ""
75 signal correctExpandedCategory();
77 Behavior on contentY {
78 enabled: previewListView.open
79 UbuntuNumberAnimation {}
80 }
82 delegate: ListItems.Base {
83 id: baseItem
84 highlightWhenPressed: false
86 readonly property bool expandable: rendererLoader.item ? rendererLoader.item.expandable : false
87 readonly property bool filtered: rendererLoader.item ? rendererLoader.item.filter : true
88 readonly property string category: categoryId
90 Loader {
91 id: rendererLoader
92 anchors {
93 top: parent.top
94 left: parent.left
95 right: parent.right
96 }
98 source: getRenderer(model.renderer, model.contentType, model.rendererHint)
100 onLoaded: {
101 if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
102 item.enableHeightBehavior = scopeView.enableHeightBehaviorOnNextCreation;
103 scopeView.enableHeightBehaviorOnNextCreation = false;
104 }
105 if (source.toString().indexOf("Apps/RunningApplicationsGrid.qml") != -1) {
106 // TODO: the running apps grid doesn't support standard scope results model yet
107 item.firstModel = Qt.binding(function() { return results.firstModel })
108 item.secondModel = Qt.binding(function() { return results.secondModel })
109 item.canEnableTerminationMode = Qt.binding(function() { return scopeView.isCurrent })
110 } else {
111 item.model = Qt.binding(function() { return results })
112 }
113 item.objectName = Qt.binding(function() { return categoryId })
114 if (item.expandable) {
115 var shouldFilter = categoryId != categoryView.expandedCategoryId;
116 if (shouldFilter != item.filter) {
117 item.filter = shouldFilter;
118 }
119 }
120 updateDelegateCreationRange();
121 }
123 Component.onDestruction: {
124 if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
125 scopeView.enableHeightBehaviorOnNextCreation = item.enableHeightBehaviorOnNextCreation;
126 }
127 }
129 Connections {
130 target: rendererLoader.item
131 onClicked: {
132 // Prepare the preview in case activate() triggers a preview only
133 effect.positionPx = mapToItem(categoryView, 0, itemY).y
134 previewListView.categoryId = categoryId
135 previewListView.categoryDelegate = rendererLoader.item
136 previewListView.model = model;
137 previewListView.init = true;
138 previewListView.currentIndex = index;
140 var item = model.get(index);
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
149 scopeView.scope.preview( item.uri, item.icon, item.category, 0, item.mimetype, item.title,
150 item.comment, item.dndUri, item.metadata)
151 }
152 }
153 onPressAndHold: {
154 effect.positionPx = mapToItem(categoryView, 0, itemY).y
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
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)
165 }
166 }
167 Connections {
168 target: categoryView
169 onExpandedCategoryIdChanged: {
170 collapseAllButExpandedCategory();
171 }
172 onCorrectExpandedCategory: {
173 collapseAllButExpandedCategory();
174 }
175 function collapseAllButExpandedCategory() {
176 var item = rendererLoader.item;
177 if (item.expandable) {
178 var shouldFilter = categoryId != categoryView.expandedCategoryId;
179 if (shouldFilter != item.filter) {
180 // If the filter animation will be seen start it, otherwise, just flip the switch
181 var shrinkingVisible = shouldFilter && y + item.collapsedHeight < categoryView.height;
182 var growingVisible = !shouldFilter && y + height < categoryView.height;
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 }
192 }
193 }
194 }
195 }
196 onContentYChanged: rendererLoader.updateDelegateCreationRange();
197 onHeightChanged: rendererLoader.updateDelegateCreationRange();
198 }
200 function updateDelegateCreationRange() {
201 // Do not update the range if we are overshooting up or down, since we'll come back
202 // to the stable position and delete/create items without any reason
203 if (categoryView.contentY < categoryView.originY) {
204 return;
205 } else if (categoryView.contentY + categoryView.height > categoryView.contentHeight) {
206 return;
207 }
209 if (item.hasOwnProperty("delegateCreationBegin")) {
210 if (baseItem.y + baseItem.height <= 0) {
211 // Not visible (item at top of the list)
212 item.delegateCreationBegin = baseItem.height
213 item.delegateCreationEnd = baseItem.height
214 } else if (baseItem.y >= categoryView.height) {
215 // Not visible (item at bottom of the list)
216 item.delegateCreationBegin = 0
217 item.delegateCreationEnd = 0
218 } else {
219 item.delegateCreationBegin = Math.max(-baseItem.y, 0)
220 item.delegateCreationEnd = Math.min(categoryView.height + item.delegateCreationBegin, baseItem.height)
221 }
222 }
223 }
224 }
225 }
227 sectionProperty: "name"
228 sectionDelegate: ListItems.Header {
229 property var delegate: categoryView.item(delegateIndex)
230 width: categoryView.width
231 text: section
232 image: {
233 if (delegate && delegate.expandable)
234 return delegate.filtered ? "graphics/header_handlearrow.png" : "graphics/header_handlearrow2.png"
235 return "";
236 }
237 onClicked: {
238 if (categoryView.expandedCategoryId != delegate.category)
239 categoryView.expandedCategoryId = delegate.category;
240 else
241 categoryView.expandedCategoryId = "";
242 }
243 }
244 pageHeader: PageHeader {
245 id: pageHeader
246 objectName: "pageHeader"
247 width: categoryView.width
248 text: scopeView.scope.name
249 searchEntryEnabled: true
250 scope: scopeView.scope
251 }
252 }
254 function getDefaultRendererId(contentType) {
255 switch (contentType) {
256 default: return "grid";
257 }
258 }
260 function getRenderer(rendererId, contentType, rendererHint) {
261 if (rendererId == "default") {
262 rendererId = getDefaultRendererId(contentType);
263 }
264 switch (rendererId) {
265 case "grid": {
266 switch (contentType) {
267 case "video": return "Video/VideoFilterGrid.qml";
268 case "music": return "Music/MusicFilterGrid.qml";
269 case "apps": {
270 if (rendererHint == "toggled")
271 return "Apps/DashPluginFilterGrid.qml";
272 else
273 return "Generic/GenericFilterGrid.qml";
274 }
275 case "weather": return "Generic/WeatherFilterGrid.qml";
276 default: return "Generic/GenericFilterGrid.qml";
277 }
278 }
279 case "carousel": {
280 switch (contentType) {
281 case "music": return "Music/MusicCarousel.qml";
282 default: return "Generic/GenericCarousel.qml";
283 }
284 }
285 case "special": {
286 switch (contentType) {
287 case "apps": return "Apps/RunningApplicationsGrid.qml";
288 default: return "Generic/GenericFilterGrid.qml";
289 }
290 }
291 default: return "Generic/GenericFilterGrid.qml";
292 }
293 }
295 OpenEffect {
296 id: effect
297 objectName: "openEffect"
298 anchors {
299 fill: parent
300 bottomMargin: -bottomOverflow
301 }
302 sourceItem: categoryView
304 enabled: gap > 0.0
306 topGapPx: (1 - gap) * positionPx
307 topOpacity: (1 - gap * 1.2)
308 bottomGapPx: positionPx + gap * (targetBottomGapPx - positionPx)
309 bottomOverflow: units.gu(20)
310 live: !expansionAnimation.running
312 property int targetBottomGapPx: height - units.gu(8) - bottomOverflow
313 property real gap: previewListView.open ? 1.0 : 0.0
315 Behavior on gap {
316 NumberAnimation {
317 id: expansionAnimation
318 duration: 200
319 easing.type: Easing.InOutQuad
320 onRunningChanged: {
321 if (!previewListView.open && !running) {
322 previewListView.onScreen = false
323 }
324 }
325 }
326 }
327 Behavior on positionPx {
328 enabled: previewListView.open
329 UbuntuNumberAnimation {}
330 }
331 }
333 Connections {
334 target: scopeView.scope
335 onPreviewReady: {
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 }
342 var index = previewListView.currentIndex
343 previewListView.currentIndex = -1
344 previewListView.currentIndex = index
345 previewListView.init = false
346 }
347 previewListView.currentItem.previewData = preview
348 }
349 }
351 PreviewDelegateMapper {
352 id: previewDelegateMapper
353 }
355 ListView {
356 id: previewListView
357 objectName: "previewListView"
358 height: effect.bottomGapPx - effect.topGapPx
359 anchors {
360 top: parent.top
361 topMargin: effect.topGapPx
362 left: parent.left
363 right: parent.right
364 }
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
374 // To be set before opening the preview
375 property string categoryId: ""
376 property var categoryDelegate
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
383 onCurrentIndexChanged: {
384 var row = Math.floor(currentIndex / categoryDelegate.columns);
385 if (categoryDelegate.collapsedRowCount <= row) {
386 categoryView.expandedCategoryId = categoryId
387 }
389 if (open) {
390 categoryDelegate.highlightIndex = currentIndex
391 }
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 }
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 }
410 effect.positionPx = effectAdjust;
411 categoryView.contentY = newContentY - categoryView.originY;
412 }
414 property bool open: false
415 property bool onScreen: false
417 onOpenChanged: {
418 if (open) {
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;
431 }
432 }
434 Rectangle {
435 anchors.fill: parent
436 color: Qt.rgba(0, 0, 0, .3)
437 z: -1
438 }
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") : ""
449 onPreviewDataChanged: {
450 if (previewData !== undefined && source.toString().indexOf("DashPreviewPlaceholder.qml") != -1) {
451 previewLoader.opacity = 0;
452 }
453 }
455 onSourceChanged: {
456 if (previewData !== undefined) {
457 fadeIn.start()
458 }
459 }
461 PropertyAnimation {
462 id: fadeIn
463 target: previewLoader
464 property: "opacity"
465 from: 0.0
466 to: 1.0
467 duration: UbuntuAnimation.BriskDuration
468 }
470 property var previewData
471 property bool valid: item !== null
473 onLoaded: {
474 if (previewListView.onScreen && previewData !== undefined) {
475 item.previewData = Qt.binding(function() { return previewData })
476 }
477 }
479 Connections {
480 ignoreUnknownSignals: true
481 target: item
482 onClose: {
483 previewListView.open = false
484 }
485 }
487 Binding {
488 target: item
489 property: "isCurrent"
490 value: previewListView.currentIndex == index
491 }
493 function closePreviewSpinner() {
494 if(item) {
495 item.showProcessingAction = false;
496 }
497 }
498 }
499 }
501 Image {
502 objectName: "pointerArrow"
503 anchors {
504 top: previewListView.bottom
505 left: parent.left
506 leftMargin: previewListView.categoryDelegate !== undefined && previewListView.categoryDelegate.currentItem ?
507 previewListView.categoryDelegate.currentItem.center + (-width + margins) / 2 : 0
509 Behavior on leftMargin {
510 SmoothedAnimation {
511 duration: UbuntuAnimation.FastDuration
512 }
513 }
514 }
515 height: units.gu(1)
516 width: units.gu(2)
517 property int margins: previewListView.categoryDelegate ? previewListView.categoryDelegate.margins : 0
518 opacity: previewListView.open ? .5 : 0
520 source: "graphics/tooltip_arrow.png"
521 }
523 // TODO: Move as InverseMouseArea to DashPreview
524 MouseArea {
525 objectName: "closePreviewMouseArea"
526 enabled: previewListView.onScreen
527 anchors {
528 fill: parent
529 topMargin: effect.bottomGapPx
530 }
531 onClicked: {
532 previewListView.open = false;
533 }
534 }