2 * Copyright (C) 2013-2014 Canonical, Ltd.
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.
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.
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/>.
18 import Ubuntu.Components 1.1
22 import "../Components"
23 import "../Components/ListItems" as ListItems
28 readonly property bool navigationShown: pageHeaderLoader.item ? pageHeaderLoader.item.bottomItem[0].openList : false
29 property bool forceNonInteractive: false
30 property var scope: null
31 property SortFilterProxyModel categories: categoryFilter
32 property bool isCurrent: false
33 property alias moving: categoryView.moving
34 property bool hasBackAction: false
35 property bool enableHeightBehaviorOnNextCreation: false
36 property var categoryView: categoryView
37 property bool showPageHeader: true
38 readonly property alias subPageShown: subPageLoader.subPageShown
39 property int paginationCount: 0
40 property int paginationIndex: 0
41 property alias pageHeaderTotallyVisible: categoryView.pageHeaderTotallyVisible
42 property var holdingList: null
44 property var scopeStyle: ScopeStyle {
45 style: scope ? scope.customizations : {}
48 readonly property bool processing: scope ? scope.searchInProgress || subPageLoader.processing : false
53 floatingSeeLess.companionBase = null;
56 function positionAtBeginning() {
57 categoryView.positionAtBeginning()
60 function showHeader() {
61 categoryView.showHeader()
64 function closePreview() {
65 subPageLoader.closeSubPage()
68 function itemClicked(index, result, item, itemModel, resultsModel, limitedCategoryItemCount) {
69 if (itemModel.uri.indexOf("scope://") === 0 || scope.id === "clickscope") {
70 // TODO Technically it is possible that calling activate() will make the scope emit
71 // previewRequested so that we show a preview but there's no scope that does that yet
72 // so it's not implemented
73 scope.activate(result)
75 if (scope.preview(result)) {
76 openPreview(index, resultsModel, limitedCategoryItemCount);
81 function itemPressedAndHeld(index, result, itemModel, resultsModel, limitedCategoryItemCount) {
82 if (itemModel.uri.indexOf("scope://") !== 0) {
83 if (scope.preview(result)) {
84 openPreview(index, resultsModel, limitedCategoryItemCount);
89 function openPreview(index, resultsModel, limitedCategoryItemCount) {
90 if (limitedCategoryItemCount > 0) {
91 previewLimitModel.model = resultsModel;
92 previewLimitModel.limit = limitedCategoryItemCount;
93 subPageLoader.model = previewLimitModel;
95 subPageLoader.model = resultsModel;
97 subPageLoader.initialIndex = -1;
98 subPageLoader.initialIndex = index;
99 subPageLoader.openSubPage("preview");
105 value: isCurrent && !subPageLoader.open
108 SortFilterProxyModel {
110 model: scope ? scope.categories : null
111 dynamicSortFilter: true
112 filterRole: Categories.RoleCount
117 onIsCurrentChanged: {
118 if (pageHeaderLoader.item && showPageHeader) {
119 pageHeaderLoader.item.resetSearch();
121 subPageLoader.closeSubPage();
125 target: scopeView.scope
126 property: "searchQuery"
127 value: pageHeaderLoader.item ? pageHeaderLoader.item.searchQuery : ""
128 when: isCurrent && showPageHeader
132 target: pageHeaderLoader.item
133 property: "searchQuery"
134 value: scopeView.scope ? scopeView.scope.searchQuery : ""
135 when: isCurrent && showPageHeader
139 target: scopeView.scope
140 onShowDash: subPageLoader.closeSubPage()
141 onHideDash: subPageLoader.closeSubPage()
146 color: scopeView.scopeStyle ? scopeView.scopeStyle.background : "transparent"
147 visible: color != "transparent"
152 objectName: "categoryListView"
153 interactive: !forceNonInteractive
155 x: subPageLoader.open ? -width : 0
157 Behavior on x { UbuntuNumberAnimation { } }
159 height: floatingSeeLess.visible ? parent.height - floatingSeeLess.height + floatingSeeLess.yOffset
161 clip: height != parent.height
163 model: scopeView.categories
164 forceNoClip: subPageLoader.open
167 property string expandedCategoryId: ""
168 property int runMaximizeAfterSizeChanges: 0
170 readonly property bool pageHeaderTotallyVisible: scopeView.showPageHeader &&
171 ((headerItemShownHeight == 0 && categoryView.contentY <= categoryView.originY) || (headerItemShownHeight == pageHeaderLoader.item.height))
173 onExpandedCategoryIdChanged: {
174 var firstCreated = firstCreatedIndex();
175 var shrinkingAny = false;
176 var shrinkHeightDifference = 0;
177 for (var i = 0; i < createdItemCount(); ++i) {
178 var baseItem = item(firstCreated + i);
179 if (baseItem.expandable) {
180 var shouldExpand = baseItem.category === expandedCategoryId;
181 if (shouldExpand != baseItem.expanded) {
183 if (!subPageLoader.open) {
184 var animateShrinking = !shouldExpand && baseItem.y + baseItem.item.collapsedHeight + baseItem.seeAllButton.height < categoryView.height;
185 var animateGrowing = shouldExpand && baseItem.y + baseItem.height < categoryView.height;
186 animate = shrinkingAny || animateShrinking || animateGrowing;
191 shrinkHeightDifference = baseItem.item.expandedHeight - baseItem.item.collapsedHeight;
194 if (shouldExpand && !subPageLoader.open) {
196 categoryView.maximizeVisibleArea(firstCreated + i, baseItem.item.expandedHeight + baseItem.seeAllButton.height);
198 // If the space that shrinking is smaller than the one we need to grow we'll call maximizeVisibleArea
199 // after the shrink/grow animation ends
200 var growHeightDifference = baseItem.item.expandedHeight - baseItem.item.collapsedHeight;
201 if (growHeightDifference > shrinkHeightDifference) {
202 runMaximizeAfterSizeChanges = 2;
204 runMaximizeAfterSizeChanges = 0;
209 baseItem.expand(shouldExpand, animate);
215 delegate: DashCategoryBase {
217 objectName: "dashCategory" + category
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;
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;
239 objectName: "cardTool"
240 count: results ? results.count : 0
241 template: model.renderer
242 components: model.components
243 viewWidth: parent.width
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
250 var shouldExpand = baseItem.category === categoryView.expandedCategoryId;
251 baseItem.expand(shouldExpand, false /*animate*/);
255 onHeightChanged: rendererLoader.updateRanges();
256 onYChanged: rendererLoader.updateRanges();
264 topMargin: name != "" ? 0 : units.gu(2)
270 animation: UbuntuNumberAnimation {
271 duration: UbuntuAnimation.FastDuration
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);
293 readonly property bool expanded: baseItem.expanded || !baseItem.expandable
294 height: expanded ? item.expandedHeight : item.collapsedHeight
297 switch (cardTool.categoryLayout) {
298 case "carousel": return "CardCarousel.qml";
299 case "vertical-journal": return "CardVerticalJournal.qml";
300 case "horizontal-list": return "CardHorizontalList.qml";
302 default: return "CardGrid.qml";
307 if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
308 item.enableHeightBehavior = scopeView.enableHeightBehaviorOnNextCreation;
309 scopeView.enableHeightBehaviorOnNextCreation = false;
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*/);
319 if (scope && scope.id === "clickscope" && (categoryId === "predefined" || categoryId === "local")) {
321 cardTool.artShapeSize = Qt.size(units.gu(8), units.gu(7.5));
323 item.cardTool = cardTool;
326 Component.onDestruction: {
327 if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
328 scopeView.enableHeightBehaviorOnNextCreation = item.enableHeightBehaviorOnNextCreation;
333 target: rendererLoader.item
335 scopeView.itemClicked(index, result, item, itemModel, target.model, categoryItemCount());
339 scopeView.itemPressedAndHeld(index, result, itemModel, target.model, categoryItemCount());
342 function categoryItemCount() {
343 var categoryItemCount = -1;
344 if (!rendererLoader.expanded && !seeAllLabel.visible && target.collapsedItemCount > 0) {
345 categoryItemCount = target.collapsedItemCount;
347 return categoryItemCount;
352 onOriginYChanged: rendererLoader.updateRanges();
353 onContentYChanged: rendererLoader.updateRanges();
354 onHeightChanged: rendererLoader.updateRanges();
355 onContentHeightChanged: rendererLoader.updateRanges();
359 onIsCurrentChanged: rendererLoader.updateRanges();
363 onMovingChanged: if (!moving) rendererLoader.updateRanges();
366 function updateRanges() {
367 if (holdingList && holdingList.moving) {
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) {
376 } else if (categoryView.contentHeight - categoryView.originY > categoryView.height &&
377 categoryView.contentY + categoryView.height > categoryView.contentHeight) {
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)
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 // 1073741823 is s^30 -1. A quite big number so that you have "infinite" display margin, but not so
392 // big so that if you add if with itself you're outside the 2^31 int range
393 item.displayMarginBeginning = 1073741823;
394 item.displayMarginEnd = 1073741823;
395 } else if (baseItem.y + baseItem.height <= 0) {
396 // Not visible (item at top of the list viewport)
397 item.displayMarginBeginning = -baseItem.height;
398 item.displayMarginEnd = 0;
399 } else if (baseItem.y >= categoryView.height) {
400 // Not visible (item at bottom of the list viewport)
401 item.displayMarginBeginning = 0;
402 item.displayMarginEnd = -baseItem.height;
404 item.displayMarginBeginning = Math.round(-Math.max(-baseItem.y, 0));
405 item.displayMarginEnd = -Math.round(Math.max(baseItem.height - seeAll.height -
406 categoryView.height + baseItem.y, 0));
416 top: rendererLoader.bottom
420 height: baseItem.expandable && !baseItem.headerLink ? seeAllLabel.font.pixelSize + units.gu(4) : 0
424 if (categoryView.expandedCategoryId !== baseItem.category) {
425 categoryView.expandedCategoryId = baseItem.category;
426 floatingSeeLess.companionBase = baseItem;
428 categoryView.expandedCategoryId = "";
434 text: baseItem.expanded ? i18n.tr("See less") : i18n.tr("See all")
437 verticalCenterOffset: units.gu(-0.5)
440 font.weight: Font.Bold
441 color: scopeStyle ? scopeStyle.foreground : Theme.palette.normal.baseText
452 fillMode: Image.Stretch
453 source: "graphics/dash_divider_top_lightgrad.png"
458 // FIXME Should not rely on model.count but view.count, but ListViewWithPageHeader doesn't expose it yet.
459 visible: index != categoryView.model.count - 1
461 bottom: seeAll.bottom
465 fillMode: Image.Stretch
466 source: "graphics/dash_divider_top_darkgrad.png"
471 sectionProperty: "name"
472 sectionDelegate: ListItems.Header {
473 objectName: "dashSectionHeader" + (delegate ? delegate.category : "")
474 readonly property var delegate: categoryView.item(delegateIndex)
475 width: categoryView.width
476 height: section != "" ? units.gu(5) : 0
478 color: scopeStyle ? scopeStyle.foreground : Theme.palette.normal.baseText
479 iconName: delegate && delegate.headerLink ? "go-next" : ""
481 if (delegate.headerLink) scopeView.scope.performQuery(delegate.headerLink);
485 pageHeader: scopeView.showPageHeader ? pageHeaderLoader : null
489 sourceComponent: scopeView.showPageHeader ? pageHeaderComponent : undefined
491 id: pageHeaderComponent
493 objectName: "scopePageHeader"
495 title: scopeView.scope ? scopeView.scope.name : ""
496 searchHint: scopeView.scope && scopeView.scope.searchHint || i18n.tr("Search")
497 showBackButton: scopeView.hasBackAction
498 searchEntryEnabled: true
499 settingsEnabled: scopeView.scope && scopeView.scope.settings && scopeView.scope.settings.count > 0 || false
500 favoriteEnabled: scopeView.scope && scopeView.scope.id !== "clickscope"
501 favorite: scopeView.scope && scopeView.scope.favorite
502 scopeStyle: scopeView.scopeStyle
503 paginationCount: scopeView.paginationCount
504 paginationIndex: scopeView.paginationIndex
506 bottomItem: DashNavigation {
507 scope: scopeView.scope
508 anchors { left: parent.left; right: parent.right }
509 windowHeight: scopeView.height
510 windowWidth: scopeView.width
511 scopeStyle: scopeView.scopeStyle
514 onBackClicked: scopeView.backClicked()
515 onSettingsClicked: subPageLoader.openSubPage("settings")
516 onFavoriteClicked: scopeView.scope.favorite = !scopeView.scope.favorite
523 id: pullToRefreshClippingItem
524 anchors.left: parent.left
525 anchors.right: parent.right
526 anchors.bottom: parent.bottom
527 height: parent.height - pullToRefresh.contentY + (pageHeaderLoader.item ? pageHeaderLoader.item.bottomItem[0].height - pageHeaderLoader.item.height : 0)
532 objectName: "pullToRefresh"
535 readonly property real contentY: categoryView.contentY - categoryView.originY
536 y: -contentY - units.gu(5)
540 scopeView.scope.refresh()
542 anchors.left: parent.left
543 anchors.right: parent.right
547 onProcessingChanged: if (!scopeView.processing) pullToRefresh.refreshing = false
550 style: PullToRefreshScopeStyle {
552 activationThreshold: units.gu(14)
559 objectName: "floatingSeeLess"
561 property Item companionTo: companionBase ? companionBase.seeAllButton : null
562 property Item companionBase: null
563 property bool showBecausePosition: false
564 property real yOffset: 0
567 left: categoryView.left
568 right: categoryView.right
570 y: parent.height - height + yOffset
571 height: seeLessLabel.font.pixelSize + units.gu(4)
572 visible: companionTo && showBecausePosition
574 onClicked: categoryView.expandedCategoryId = "";
576 function updateVisibility() {
577 var companionPos = companionTo.mapToItem(floatingSeeLess, 0, 0);
578 showBecausePosition = companionPos.y > 0;
580 var posToBase = floatingSeeLess.mapToItem(companionBase, 0, -yOffset).y;
581 yOffset = Math.max(0, companionBase.item.collapsedHeight - posToBase);
582 yOffset = Math.min(yOffset, height);
584 if (!showBecausePosition && categoryView.expandedCategoryId === "") {
585 companionBase = null;
591 text: i18n.tr("See less")
594 verticalCenterOffset: units.gu(-0.5)
597 font.weight: Font.Bold
598 color: scopeStyle ? scopeStyle.foreground : Theme.palette.normal.baseText
602 target: floatingSeeLess.companionTo ? categoryView : null
603 onContentYChanged: floatingSeeLess.updateVisibility();
607 target: floatingSeeLess.companionTo
608 onYChanged: floatingSeeLess.updateVisibility();
613 id: previewLimitModel
618 objectName: "subPageLoader"
621 height: parent.height
622 anchors.left: categoryView.right
624 property bool open: false
625 property var scope: scopeView.scope
626 property var scopeStyle: scopeView.scopeStyle
627 property int initialIndex: -1
628 property var model: null
630 readonly property bool processing: item && item.processing || false
631 readonly property int count: item && item.count || 0
632 readonly property int currentIndex: item && item.currentIndex || 0
633 readonly property var currentItem: item && item.currentItem || null
635 property string subPage: ""
636 readonly property bool subPageShown: visible && status === Loader.Ready
638 function openSubPage(page) {
642 function closeSubPage() {
646 source: switch(subPage) {
647 case "preview": return "PreviewListView.qml";
648 case "settings": return "ScopeSettingsPage.qml";
653 item.scope = Qt.binding(function() { return subPageLoader.scope; } )
654 item.scopeStyle = Qt.binding(function() { return subPageLoader.scopeStyle; } )
655 if (subPage == "preview") {
656 item.open = Qt.binding(function() { return subPageLoader.open; } )
657 item.initialIndex = Qt.binding(function() { return subPageLoader.initialIndex; } )
658 item.model = Qt.binding(function() { return subPageLoader.model; } )
663 onOpenChanged: pageHeaderLoader.item.unfocus()
665 onVisibleChanged: if (!visible) subPage = ""
668 target: subPageLoader.item
669 onBackClicked: subPageLoader.closeSubPage()