on f71ff90 help sign in
unity8/qml/Dash/GenericScopeView.qml
635 lines 26 kB history raw
1/*
2 * Copyright (C) 2013-2014 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 Dash 0.1
22import "../Components"
23import "../Components/ListItems" as ListItems
25FocusScope {
26 id: scopeView
28 readonly property bool navigationShown: pageHeaderLoader.item ? pageHeaderLoader.item.bottomItem[0].openList : false
29 property var scope: null
30 property SortFilterProxyModel categories: categoryFilter
31 property bool isCurrent: false
32 property alias moving: categoryView.moving
33 property bool hasBackAction: false
34 property bool enableHeightBehaviorOnNextCreation: false
35 property var categoryView: categoryView
36 property bool showPageHeader: true
37 readonly property alias subPageShown: subPageLoader.subPageShown
38 property int paginationCount: 0
39 property int paginationIndex: 0
40 property alias pageHeaderTotallyVisible: categoryView.pageHeaderTotallyVisible
41 property var holdingList: null
43 property var scopeStyle: ScopeStyle {
44 style: scope ? scope.customizations : {}
45 }
47 readonly property bool processing: scope ? scope.searchInProgress || subPageLoader.processing : false
49 signal backClicked()
51 onScopeChanged: {
52 floatingSeeLess.companionBase = null;
53 }
55 function positionAtBeginning() {
56 categoryView.positionAtBeginning()
57 }
59 function showHeader() {
60 categoryView.showHeader()
61 }
63 function closePreview() {
64 subPageLoader.closeSubPage()
65 }
67 function itemClicked(index, result, item, itemModel, resultsModel, limitedCategoryItemCount) {
68 if (itemModel.uri.indexOf("scope://") === 0 || scope.id === "clickscope") {
69 // TODO Technically it is possible that calling activate() will make the scope emit
70 // previewRequested so that we show a preview but there's no scope that does that yet
71 // so it's not implemented
72 scope.activate(result)
73 } else {
74 if (scope.preview(result)) {
75 openPreview(index, resultsModel, limitedCategoryItemCount);
76 }
77 }
78 }
80 function itemPressedAndHeld(index, result, itemModel, resultsModel, limitedCategoryItemCount) {
81 if (itemModel.uri.indexOf("scope://") !== 0) {
82 if (scope.preview(result)) {
83 openPreview(index, resultsModel, limitedCategoryItemCount);
84 }
85 }
86 }
88 function openPreview(index, resultsModel, limitedCategoryItemCount) {
89 if (limitedCategoryItemCount > 0) {
90 previewLimitModel.model = resultsModel;
91 previewLimitModel.limit = limitedCategoryItemCount;
92 subPageLoader.model = previewLimitModel;
93 } else {
94 subPageLoader.model = resultsModel;
95 }
96 subPageLoader.initialIndex = -1;
97 subPageLoader.initialIndex = index;
98 subPageLoader.openSubPage("preview");
99 }
101 Binding {
102 target: scope
103 property: "isActive"
104 value: isCurrent && !subPageLoader.open
105 }
107 SortFilterProxyModel {
108 id: categoryFilter
109 model: scope ? scope.categories : null
110 dynamicSortFilter: true
111 filterRole: Categories.RoleCount
112 filterRegExp: /^0$/
113 invertMatch: true
114 }
116 onIsCurrentChanged: {
117 if (pageHeaderLoader.item && showPageHeader) {
118 pageHeaderLoader.item.resetSearch();
119 }
120 subPageLoader.closeSubPage();
121 }
123 Binding {
124 target: scopeView.scope
125 property: "searchQuery"
126 value: pageHeaderLoader.item ? pageHeaderLoader.item.searchQuery : ""
127 when: isCurrent && showPageHeader
128 }
130 Binding {
131 target: pageHeaderLoader.item
132 property: "searchQuery"
133 value: scopeView.scope ? scopeView.scope.searchQuery : ""
134 when: isCurrent && showPageHeader
135 }
137 Connections {
138 target: scopeView.scope
139 onShowDash: subPageLoader.closeSubPage()
140 onHideDash: subPageLoader.closeSubPage()
141 }
143 Rectangle {
144 anchors.fill: parent
145 color: scopeView.scopeStyle ? scopeView.scopeStyle.background : "transparent"
146 visible: color != "transparent"
147 }
149 ScopeListView {
150 id: categoryView
151 objectName: "categoryListView"
153 x: subPageLoader.open ? -width : 0
154 visible: x != -width
155 Behavior on x { UbuntuNumberAnimation { } }
156 width: parent.width
157 height: floatingSeeLess.visible ? parent.height - floatingSeeLess.height + floatingSeeLess.yOffset
158 : parent.height
159 clip: height != parent.height
161 model: scopeView.categories
162 forceNoClip: subPageLoader.open
163 pixelAligned: true
165 property string expandedCategoryId: ""
166 property int runMaximizeAfterSizeChanges: 0
168 readonly property bool pageHeaderTotallyVisible: scopeView.showPageHeader &&
169 ((headerItemShownHeight == 0 && categoryView.contentY <= categoryView.originY) || (headerItemShownHeight == pageHeaderLoader.item.height))
171 onExpandedCategoryIdChanged: {
172 var firstCreated = firstCreatedIndex();
173 var shrinkingAny = false;
174 var shrinkHeightDifference = 0;
175 for (var i = 0; i < createdItemCount(); ++i) {
176 var baseItem = item(firstCreated + i);
177 if (baseItem.expandable) {
178 var shouldExpand = baseItem.category === expandedCategoryId;
179 if (shouldExpand != baseItem.expanded) {
180 var animate = false;
181 if (!subPageLoader.open) {
182 var animateShrinking = !shouldExpand && baseItem.y + baseItem.item.collapsedHeight + baseItem.seeAllButton.height < categoryView.height;
183 var animateGrowing = shouldExpand && baseItem.y + baseItem.height < categoryView.height;
184 animate = shrinkingAny || animateShrinking || animateGrowing;
185 }
187 if (!shouldExpand) {
188 shrinkingAny = true;
189 shrinkHeightDifference = baseItem.item.expandedHeight - baseItem.item.collapsedHeight;
190 }
192 if (shouldExpand && !subPageLoader.open) {
193 if (!shrinkingAny) {
194 categoryView.maximizeVisibleArea(firstCreated + i, baseItem.item.expandedHeight + baseItem.seeAllButton.height);
195 } else {
196 // If the space that shrinking is smaller than the one we need to grow we'll call maximizeVisibleArea
197 // after the shrink/grow animation ends
198 var growHeightDifference = baseItem.item.expandedHeight - baseItem.item.collapsedHeight;
199 if (growHeightDifference > shrinkHeightDifference) {
200 runMaximizeAfterSizeChanges = 2;
201 } else {
202 runMaximizeAfterSizeChanges = 0;
203 }
204 }
205 }
207 baseItem.expand(shouldExpand, animate);
208 }
209 }
210 }
211 }
213 delegate: ListItems.Base {
214 id: baseItem
215 objectName: "dashCategory" + category
216 highlightWhenPressed: false
217 showDivider: false
219 property Item seeAllButton: seeAll
221 readonly property bool expandable: {
222 if (categoryView.model.count === 1) return false;
223 if (cardTool.template && cardTool.template["collapsed-rows"] === 0) return false;
224 if (item && item.expandedHeight > item.collapsedHeight) return true;
225 return false;
226 }
227 property bool expanded: false
228 readonly property string category: categoryId
229 readonly property string headerLink: model.headerLink
230 readonly property var item: rendererLoader.item
232 function expand(expand, animate) {
233 heightBehaviour.enabled = animate;
234 expanded = expand;
235 }
237 CardTool {
238 id: cardTool
239 objectName: "cardTool"
240 count: results.count
241 template: model.renderer
242 components: model.components
243 viewWidth: parent.width
244 }
246 onExpandableChanged: {
247 // This can happen with the VJ that doesn't know how height it will be on creation
248 // so doesn't set expandable until a bit too late for onLoaded
249 if (expandable) {
250 var shouldExpand = baseItem.category === categoryView.expandedCategoryId;
251 baseItem.expand(shouldExpand, false /*animate*/);
252 }
253 }
255 onHeightChanged: rendererLoader.updateRanges();
256 onYChanged: rendererLoader.updateRanges();
258 Loader {
259 id: rendererLoader
260 anchors {
261 top: parent.top
262 left: parent.left
263 right: parent.right
264 topMargin: name != "" ? 0 : units.gu(2)
265 }
267 Behavior on height {
268 id: heightBehaviour
269 enabled: false
270 animation: UbuntuNumberAnimation {
271 duration: UbuntuAnimation.FastDuration
272 onRunningChanged: {
273 if (!running) {
274 heightBehaviour.enabled = false
275 if (categoryView.runMaximizeAfterSizeChanges > 0) {
276 categoryView.runMaximizeAfterSizeChanges--;
277 if (categoryView.runMaximizeAfterSizeChanges == 0) {
278 var firstCreated = categoryView.firstCreatedIndex();
279 for (var i = 0; i < categoryView.createdItemCount(); ++i) {
280 var baseItem = categoryView.item(firstCreated + i);
281 if (baseItem.category === categoryView.expandedCategoryId) {
282 categoryView.maximizeVisibleArea(firstCreated + i, baseItem.item.expandedHeight + baseItem.seeAllButton.height);
283 break;
284 }
285 }
286 }
287 }
288 }
289 }
290 }
291 }
293 readonly property bool expanded: baseItem.expanded || !baseItem.expandable
294 height: expanded ? item.expandedHeight : item.collapsedHeight
296 source: {
297 switch (cardTool.categoryLayout) {
298 case "carousel": return "CardCarousel.qml";
299 case "vertical-journal": return "CardVerticalJournal.qml";
300 case "horizontal-list": return "CardHorizontalList.qml";
301 case "grid":
302 default: return "CardGrid.qml";
303 }
304 }
306 onLoaded: {
307 if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
308 item.enableHeightBehavior = scopeView.enableHeightBehaviorOnNextCreation;
309 scopeView.enableHeightBehaviorOnNextCreation = false;
310 }
311 item.model = Qt.binding(function() { return results })
312 item.objectName = Qt.binding(function() { return categoryId })
313 item.scopeStyle = scopeView.scopeStyle;
314 if (baseItem.expandable) {
315 var shouldExpand = baseItem.category === categoryView.expandedCategoryId;
316 baseItem.expand(shouldExpand, false /*animate*/);
317 }
318 updateRanges();
319 if (scope && scope.id === "clickscope" && (categoryId === "predefined" || categoryId === "local")) {
320 // Yeah, hackish :/
321 cardTool.artShapeSize = Qt.size(units.gu(8), units.gu(7.5));
322 }
323 item.cardTool = cardTool;
324 }
326 Component.onDestruction: {
327 if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
328 scopeView.enableHeightBehaviorOnNextCreation = item.enableHeightBehaviorOnNextCreation;
329 }
330 }
332 Connections {
333 target: rendererLoader.item
334 onClicked: {
335 scopeView.itemClicked(index, result, item, itemModel, target.model, categoryItemCount());
336 }
338 onPressAndHold: {
339 scopeView.itemPressedAndHeld(index, result, itemModel, target.model, categoryItemCount());
340 }
342 function categoryItemCount() {
343 var categoryItemCount = -1;
344 if (!rendererLoader.expanded && !seeAllLabel.visible && target.collapsedItemCount > 0) {
345 categoryItemCount = target.collapsedItemCount;
346 }
347 return categoryItemCount;
348 }
349 }
350 Connections {
351 target: categoryView
352 onOriginYChanged: rendererLoader.updateRanges();
353 onContentYChanged: rendererLoader.updateRanges();
354 onHeightChanged: rendererLoader.updateRanges();
355 onContentHeightChanged: rendererLoader.updateRanges();
356 }
357 Connections {
358 target: scopeView
359 onIsCurrentChanged: rendererLoader.updateRanges();
360 }
361 Connections {
362 target: holdingList
363 onMovingChanged: if (!moving) rendererLoader.updateRanges();
364 }
366 function updateRanges() {
367 if (holdingList && holdingList.moving) {
368 return;
369 }
371 if (categoryView.moving) {
372 // Do not update the range if we are overshooting up or down, since we'll come back
373 // to the stable position and delete/create items without any reason
374 if (categoryView.contentY < categoryView.originY) {
375 return;
376 } else if (categoryView.contentHeight - categoryView.originY > categoryView.height &&
377 categoryView.contentY + categoryView.height > categoryView.contentHeight) {
378 return;
379 }
380 }
382 if (item && item.hasOwnProperty("visibleRangeBegin")) {
383 item.visibleRangeBegin = Math.max(-baseItem.y, 0)
384 item.visibleRangeEnd = item.visibleRangeBegin + Math.min(categoryView.height, rendererLoader.height)
385 }
387 if (item && item.hasOwnProperty("displayMarginBeginning")) {
388 // TODO do we need item.originY here, test 1300302 once we have a silo
389 // and we can run it on the phone
390 if (scopeView.isCurrent) {
391 item.displayMarginBeginning = 1073741823;
392 item.displayMarginEnd = 1073741823;
393 } else if (baseItem.y + baseItem.height <= 0) {
394 // Not visible (item at top of the list viewport)
395 item.displayMarginBeginning = -baseItem.height;
396 item.displayMarginEnd = 0;
397 } else if (baseItem.y >= categoryView.height) {
398 // Not visible (item at bottom of the list viewport)
399 item.displayMarginBeginning = 0;
400 item.displayMarginEnd = -baseItem.height;
401 } else {
402 item.displayMarginBeginning = Math.round(-Math.max(-baseItem.y, 0));
403 item.displayMarginEnd = -Math.round(Math.max(baseItem.height - seeAll.height
404 - categoryView.height + baseItem.y, 0));
405 }
406 }
407 }
408 }
410 AbstractButton {
411 id: seeAll
412 objectName: "seeAll"
413 anchors {
414 top: rendererLoader.bottom
415 left: parent.left
416 right: parent.right
417 }
418 height: baseItem.expandable && !baseItem.headerLink ? seeAllLabel.font.pixelSize + units.gu(4) : 0
419 visible: height != 0
421 onClicked: {
422 if (categoryView.expandedCategoryId !== baseItem.category) {
423 categoryView.expandedCategoryId = baseItem.category;
424 floatingSeeLess.companionBase = baseItem;
425 } else {
426 categoryView.expandedCategoryId = "";
427 }
428 }
430 Label {
431 id: seeAllLabel
432 text: baseItem.expanded ? i18n.tr("See less") : i18n.tr("See all")
433 anchors {
434 centerIn: parent
435 verticalCenterOffset: units.gu(-0.5)
436 }
437 fontSize: "small"
438 font.weight: Font.Bold
439 color: scopeStyle ? scopeStyle.foreground : Theme.palette.normal.baseText
440 }
441 }
443 Image {
444 visible: index != 0
445 anchors {
446 top: parent.top
447 left: parent.left
448 right: parent.right
449 }
450 fillMode: Image.Stretch
451 source: "graphics/dash_divider_top_lightgrad.png"
452 z: -1
453 }
455 Image {
456 // FIXME Should not rely on model.count but view.count, but ListViewWithPageHeader doesn't expose it yet.
457 visible: index != categoryView.model.count - 1
458 anchors {
459 bottom: seeAll.bottom
460 left: parent.left
461 right: parent.right
462 }
463 fillMode: Image.Stretch
464 source: "graphics/dash_divider_top_darkgrad.png"
465 z: -1
466 }
467 }
469 sectionProperty: "name"
470 sectionDelegate: ListItems.Header {
471 objectName: "dashSectionHeader" + (delegate ? delegate.category : "")
472 readonly property var delegate: categoryView.item(delegateIndex)
473 width: categoryView.width
474 height: section != "" ? units.gu(5) : 0
475 text: section
476 color: scopeStyle ? scopeStyle.foreground : Theme.palette.normal.baseText
477 iconName: delegate && delegate.headerLink ? "go-next" : ""
478 onClicked: {
479 if (delegate.headerLink) scopeView.scope.performQuery(delegate.headerLink);
480 }
481 }
483 pageHeader: scopeView.showPageHeader ? pageHeaderLoader : null
484 Loader {
485 id: pageHeaderLoader
486 width: parent.width
487 sourceComponent: scopeView.showPageHeader ? pageHeaderComponent : undefined
488 Component {
489 id: pageHeaderComponent
490 PageHeader {
491 objectName: "scopePageHeader"
492 width: parent.width
493 title: scopeView.scope ? scopeView.scope.name : ""
494 searchHint: scopeView.scope && scopeView.scope.searchHint || i18n.tr("Search")
495 showBackButton: scopeView.hasBackAction
496 searchEntryEnabled: true
497 settingsEnabled: scopeView.scope && scopeView.scope.settings && scopeView.scope.settings.count > 0 || false
498 favoriteEnabled: scopeView.scope && scopeView.scope.id !== "clickscope"
499 favorite: scopeView.scope && scopeView.scope.favorite
500 scopeStyle: scopeView.scopeStyle
501 paginationCount: scopeView.paginationCount
502 paginationIndex: scopeView.paginationIndex
504 bottomItem: DashNavigation {
505 scope: scopeView.scope
506 anchors { left: parent.left; right: parent.right }
507 windowHeight: scopeView.height
508 windowWidth: scopeView.width
509 scopeStyle: scopeView.scopeStyle
510 }
512 onBackClicked: scopeView.backClicked()
513 onSettingsClicked: subPageLoader.openSubPage("settings")
514 onFavoriteClicked: scopeView.scope.favorite = !scopeView.scope.favorite
515 }
516 }
517 }
518 }
520 AbstractButton {
521 id: floatingSeeLess
522 objectName: "floatingSeeLess"
524 property Item companionTo: companionBase ? companionBase.seeAllButton : null
525 property Item companionBase: null
526 property bool showBecausePosition: false
527 property real yOffset: 0
529 anchors {
530 left: categoryView.left
531 right: categoryView.right
532 }
533 y: parent.height - height + yOffset
534 height: seeLessLabel.font.pixelSize + units.gu(4)
535 visible: companionTo && showBecausePosition
537 onClicked: categoryView.expandedCategoryId = "";
539 function updateVisibility() {
540 var companionPos = companionTo.mapToItem(floatingSeeLess, 0, 0);
541 showBecausePosition = companionPos.y > 0;
543 var posToBase = floatingSeeLess.mapToItem(companionBase, 0, -yOffset).y;
544 yOffset = Math.max(0, companionBase.item.collapsedHeight - posToBase);
545 yOffset = Math.min(yOffset, height);
547 if (!showBecausePosition && categoryView.expandedCategoryId === "") {
548 companionBase = null;
549 }
550 }
552 Label {
553 id: seeLessLabel
554 text: i18n.tr("See less")
555 anchors {
556 centerIn: parent
557 verticalCenterOffset: units.gu(-0.5)
558 }
559 fontSize: "small"
560 font.weight: Font.Bold
561 color: scopeStyle ? scopeStyle.foreground : Theme.palette.normal.baseText
562 }
564 Connections {
565 target: floatingSeeLess.companionTo ? categoryView : null
566 onContentYChanged: floatingSeeLess.updateVisibility();
567 }
569 Connections {
570 target: floatingSeeLess.companionTo
571 onYChanged: floatingSeeLess.updateVisibility();
572 }
573 }
575 LimitProxyModel {
576 id: previewLimitModel
577 }
579 Loader {
580 id: subPageLoader
581 objectName: "subPageLoader"
582 visible: x != width
583 width: parent.width
584 height: parent.height
585 anchors.left: categoryView.right
587 property bool open: false
588 property var scope: scopeView.scope
589 property var scopeStyle: scopeView.scopeStyle
590 property int initialIndex: -1
591 property var model: null
593 readonly property bool processing: item && item.processing || false
594 readonly property int count: item && item.count || 0
595 readonly property int currentIndex: item && item.currentIndex || 0
596 readonly property var currentItem: item && item.currentItem || null
598 property string subPage: ""
599 readonly property bool subPageShown: visible && status === Loader.Ready
601 function openSubPage(page) {
602 subPage = page;
603 }
605 function closeSubPage() {
606 open = false;
607 }
609 source: switch(subPage) {
610 case "preview": return "PreviewListView.qml";
611 case "settings": return "ScopeSettingsPage.qml";
612 default: return "";
613 }
615 onLoaded: {
616 item.scope = Qt.binding(function() { return subPageLoader.scope; } )
617 item.scopeStyle = Qt.binding(function() { return subPageLoader.scopeStyle; } )
618 if (subPage == "preview") {
619 item.open = Qt.binding(function() { return subPageLoader.open; } )
620 item.initialIndex = Qt.binding(function() { return subPageLoader.initialIndex; } )
621 item.model = Qt.binding(function() { return subPageLoader.model; } )
622 }
623 open = true;
624 }
626 onOpenChanged: pageHeaderLoader.item.unfocus()
628 onVisibleChanged: if (!visible) subPage = ""
630 Connections {
631 target: subPageLoader.item
632 onBackClicked: subPageLoader.closeSubPage()
633 }
634 }