2 * Copyright (C) 2013-2015 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.3
22 import "../Components"
23 import "../Components/ListItems" as ListItems
28 readonly property bool navigationDisableParentInteractive: pageHeaderLoader.item ? pageHeaderLoader.item.bottomItem[0].disableParentInteractive : false
29 property bool forceNonInteractive: false
30 property var scope: null
31 property UnitySortFilterProxyModel 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 bool visibleToParent: false
42 property alias pageHeaderTotallyVisible: categoryView.pageHeaderTotallyVisible
43 property var holdingList: null
44 property bool wasCurrentOnMoveStart: false
46 property var scopeStyle: ScopeStyle {
47 style: scope ? scope.customizations : {}
50 readonly property bool processing: scope ? (scope.searchInProgress || scope.activationInProgress || subPageLoader.processing) : false
55 floatingSeeLess.companionBase = null;
58 function positionAtBeginning() {
59 categoryView.positionAtBeginning()
62 function showHeader() {
63 categoryView.showHeader()
66 function closePreview() {
67 subPageLoader.closeSubPage()
70 function resetSearch() {
71 if(pageHeaderLoader.item && showPageHeader)
72 pageHeaderLoader.item.resetSearch()
75 property var maybePreviewResult;
76 property string maybePreviewCategoryId;
78 function clearMaybePreviewData() {
79 scopeView.maybePreviewResult = undefined;
80 scopeView.maybePreviewCategoryId = "";
83 function itemClicked(result, categoryId) {
84 scopeView.maybePreviewResult = result;
85 scopeView.maybePreviewCategoryId = categoryId;
87 scope.activate(result, categoryId);
90 function itemPressedAndHeld(result, categoryId) {
91 clearMaybePreviewData();
93 openPreview(result, categoryId);
96 function openPreview(result, categoryId) {
97 var previewModel = scope.preview(result, categoryId);
99 subPageLoader.previewModel = previewModel;
100 subPageLoader.openSubPage("preview");
107 value: isCurrent && !subPageLoader.open && (Qt.application.state == Qt.ApplicationActive)
110 UnitySortFilterProxyModel {
112 model: scope ? scope.categories : null
113 dynamicSortFilter: true
114 filterRole: Categories.RoleCount
119 onIsCurrentChanged: {
120 if (!holdingList || !holdingList.moving) {
121 wasCurrentOnMoveStart = scopeView.isCurrent;
123 if (pageHeaderLoader.item && showPageHeader) {
124 pageHeaderLoader.item.resetSearch();
126 subPageLoader.closeSubPage();
130 target: scopeView.scope
131 property: "searchQuery"
132 value: pageHeaderLoader.item ? pageHeaderLoader.item.searchQuery : ""
133 when: isCurrent && showPageHeader
137 target: pageHeaderLoader.item
138 property: "searchQuery"
139 value: scopeView.scope ? scopeView.scope.searchQuery : ""
140 when: isCurrent && showPageHeader
144 target: scopeView.scope
145 onShowDash: subPageLoader.closeSubPage()
146 onHideDash: subPageLoader.closeSubPage()
147 onPreviewRequested: { // (QVariant const& result)
148 if (result === scopeView.maybePreviewResult) {
150 scopeView.maybePreviewCategoryId);
152 clearMaybePreviewData();
161 wasCurrentOnMoveStart = scopeView.isCurrent;
168 color: scopeView.scopeStyle ? scopeView.scopeStyle.background : "transparent"
169 visible: color != "transparent"
174 objectName: "categoryListView"
175 interactive: !forceNonInteractive
177 x: subPageLoader.open ? -width : 0
179 Behavior on x { UbuntuNumberAnimation { } }
181 height: floatingSeeLess.visible ? parent.height - floatingSeeLess.height + floatingSeeLess.yOffset
183 clip: height != parent.height
185 model: scopeView.categories
186 forceNoClip: subPageLoader.open
189 property string expandedCategoryId: ""
190 property int runMaximizeAfterSizeChanges: 0
192 readonly property bool pageHeaderTotallyVisible: scopeView.showPageHeader &&
193 ((headerItemShownHeight == 0 && categoryView.contentY <= categoryView.originY) || (headerItemShownHeight == pageHeaderLoader.item.height))
195 onExpandedCategoryIdChanged: {
196 var firstCreated = firstCreatedIndex();
197 var shrinkingAny = false;
198 var shrinkHeightDifference = 0;
199 for (var i = 0; i < createdItemCount(); ++i) {
200 var baseItem = item(firstCreated + i);
201 if (baseItem.expandable) {
202 var shouldExpand = baseItem.category === expandedCategoryId;
203 if (shouldExpand != baseItem.expanded) {
205 if (!subPageLoader.open) {
206 var animateShrinking = !shouldExpand && baseItem.y + baseItem.item.collapsedHeight + baseItem.seeAllButton.height < categoryView.height;
207 var animateGrowing = shouldExpand && baseItem.y + baseItem.height < categoryView.height;
208 animate = shrinkingAny || animateShrinking || animateGrowing;
213 shrinkHeightDifference = baseItem.item.expandedHeight - baseItem.item.collapsedHeight;
216 if (shouldExpand && !subPageLoader.open) {
218 categoryView.maximizeVisibleArea(firstCreated + i, baseItem.item.expandedHeight + baseItem.seeAllButton.height);
220 // If the space that shrinking is smaller than the one we need to grow we'll call maximizeVisibleArea
221 // after the shrink/grow animation ends
222 var growHeightDifference = baseItem.item.expandedHeight - baseItem.item.collapsedHeight;
223 if (growHeightDifference > shrinkHeightDifference) {
224 runMaximizeAfterSizeChanges = 2;
226 runMaximizeAfterSizeChanges = 0;
231 baseItem.expand(shouldExpand, animate);
237 delegate: DashCategoryBase {
239 objectName: "dashCategory" + category
241 property Item seeAllButton: seeAll
243 readonly property bool expandable: {
244 if (categoryView.model.count === 1) return false;
245 if (cardTool.template && cardTool.template["collapsed-rows"] === 0) return false;
246 if (item && item.expandedHeight > item.collapsedHeight) return true;
249 property bool expanded: false
250 readonly property string category: categoryId
251 readonly property string headerLink: model.headerLink
252 readonly property var item: rendererLoader.item
254 function expand(expand, animate) {
255 heightBehaviour.enabled = animate;
261 objectName: "cardTool"
262 count: results ? results.count : 0
263 template: model.renderer
264 components: model.components
265 viewWidth: parent.width
268 onExpandableChanged: {
269 // This can happen with the VJ that doesn't know how height it will be on creation
270 // so doesn't set expandable until a bit too late for onLoaded
272 var shouldExpand = baseItem.category === categoryView.expandedCategoryId;
273 baseItem.expand(shouldExpand, false /*animate*/);
277 onHeightChanged: rendererLoader.updateRanges();
278 onYChanged: rendererLoader.updateRanges();
286 topMargin: name != "" ? 0 : units.gu(2)
292 animation: UbuntuNumberAnimation {
293 duration: UbuntuAnimation.FastDuration
296 heightBehaviour.enabled = false
297 if (categoryView.runMaximizeAfterSizeChanges > 0) {
298 categoryView.runMaximizeAfterSizeChanges--;
299 if (categoryView.runMaximizeAfterSizeChanges == 0) {
300 var firstCreated = categoryView.firstCreatedIndex();
301 for (var i = 0; i < categoryView.createdItemCount(); ++i) {
302 var baseItem = categoryView.item(firstCreated + i);
303 if (baseItem.category === categoryView.expandedCategoryId) {
304 categoryView.maximizeVisibleArea(firstCreated + i, baseItem.item.expandedHeight + baseItem.seeAllButton.height);
315 readonly property bool expanded: baseItem.expanded || !baseItem.expandable
316 height: expanded ? item.expandedHeight : item.collapsedHeight
319 switch (cardTool.categoryLayout) {
320 case "carousel": return "CardCarousel.qml";
321 case "vertical-journal": return "CardVerticalJournal.qml";
322 case "horizontal-list": return "CardHorizontalList.qml";
324 default: return "CardGrid.qml";
329 if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
330 item.enableHeightBehavior = scopeView.enableHeightBehaviorOnNextCreation;
331 scopeView.enableHeightBehaviorOnNextCreation = false;
333 item.model = Qt.binding(function() { return results })
334 item.objectName = Qt.binding(function() { return categoryId })
335 item.scopeStyle = scopeView.scopeStyle;
336 if (baseItem.expandable) {
337 var shouldExpand = baseItem.category === categoryView.expandedCategoryId;
338 baseItem.expand(shouldExpand, false /*animate*/);
341 clickScopeSizingHacks();
342 if (scope && scope.id === "clickscope") {
343 if (categoryId === "predefined" || categoryId === "local") {
344 cardTool.artShapeSize = Qt.binding(function() { return Qt.size(units.gu(8), units.gu(7.5)) });
345 item.artShapeStyle = "icon";
347 // Should be ubuntu store icon
348 item.artShapeStyle = "flat";
349 item.backgroundShapeStyle = "shadow";
352 item.cardTool = cardTool;
355 Component.onDestruction: {
356 if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
357 scopeView.enableHeightBehaviorOnNextCreation = item.enableHeightBehaviorOnNextCreation;
361 function clickScopeSizingHacks() {
362 if (scope && scope.id === "clickscope" &&
363 (categoryId === "predefined" || categoryId === "local")) {
365 if (scopeView.width > units.gu(45)) {
366 if (scopeView.width >= units.gu(70)) {
367 cardTool.cardWidth = units.gu(11);
368 item.minimumHorizontalSpacing = units.gu(5);
371 cardTool.cardWidth = units.gu(10);
374 cardTool.cardWidth = units.gu(12);
376 item.minimumHorizontalSpacing = item.defaultMinimumHorizontalSpacing;
381 target: rendererLoader.item
382 onClicked: { // (int index, var result, var item, var itemModel)
383 scopeView.itemClicked(result, baseItem.category);
386 onPressAndHold: { // (int index, var result, var itemModel)
387 scopeView.itemPressedAndHeld(result, baseItem.category);
390 function categoryItemCount() {
391 var categoryItemCount = -1;
392 if (!rendererLoader.expanded && !seeAllLabel.visible && target.collapsedItemCount > 0) {
393 categoryItemCount = target.collapsedItemCount;
395 return categoryItemCount;
400 onOriginYChanged: rendererLoader.updateRanges();
401 onContentYChanged: rendererLoader.updateRanges();
402 onHeightChanged: rendererLoader.updateRanges();
403 onContentHeightChanged: rendererLoader.updateRanges();
407 onIsCurrentChanged: rendererLoader.updateRanges();
408 onVisibleToParentChanged: rendererLoader.updateRanges();
409 onWidthChanged: rendererLoader.clickScopeSizingHacks();
413 onMovingChanged: if (!moving) rendererLoader.updateRanges();
416 function updateRanges() {
417 // Don't want to create stress by requesting more items during scope
418 // changes so unless you're not part of the visible scopes just return.
419 // For the visible scopes we need to do some work, the previously non visible
420 // scope needs to adjust its ranges so that we define the new visible range,
421 // that still means no creation/destruction of delegates, it's just about changing
422 // the culling of the items so they are actually visible
423 if (holdingList && holdingList.moving && !scopeView.visibleToParent) {
427 if (categoryView.moving) {
428 // Do not update the range if we are overshooting up or down, since we'll come back
429 // to the stable position and delete/create items without any reason
430 if (categoryView.contentY < categoryView.originY) {
432 } else if (categoryView.contentHeight - categoryView.originY > categoryView.height &&
433 categoryView.contentY + categoryView.height > categoryView.contentHeight) {
438 if (item && item.hasOwnProperty("displayMarginBeginning")) {
439 var buffer = wasCurrentOnMoveStart ? categoryView.height * 1.5 : 0;
440 var onViewport = baseItem.y + baseItem.height > 0 &&
441 baseItem.y < categoryView.height;
442 var onBufferViewport = baseItem.y + baseItem.height > -buffer &&
443 baseItem.y < categoryView.height + buffer;
445 if (item.growsVertically) {
446 // A item view creates its delegates synchronously from
447 // -displayMarginBeginning
449 // height + displayMarginEnd
450 // Around that area it adds the cacheBuffer area where delegates are created async
452 // We adjust displayMarginBeginning and displayMarginEnd so
453 // * In non visible scopes nothing is considered visible and we set cacheBuffer
454 // so that creates the items that would be in the viewport asynchronously
455 // * For the current scope set the visible range to the viewport and then
456 // use cacheBuffer to create extra items for categoryView.height * 1.5
457 // to make scrolling nicer by mantaining a higher number of
459 // * For non current but visible scopes (i.e. when the user changes from one scope
460 // to the next, we set the visible range to the viewport so
461 // items are not culled (invisible) but still use no cacheBuffer
462 // (it will be set once the scope is the current one)
463 var displayMarginBeginning = baseItem.y + rendererLoader.anchors.topMargin;
464 displayMarginBeginning = -Math.max(-displayMarginBeginning, 0);
465 displayMarginBeginning = -Math.min(-displayMarginBeginning, baseItem.height);
466 displayMarginBeginning = Math.round(displayMarginBeginning);
467 var displayMarginEnd = -baseItem.height + seeAll.height + categoryView.height - baseItem.y;
468 displayMarginEnd = -Math.max(-displayMarginEnd, 0);
469 displayMarginEnd = -Math.min(-displayMarginEnd, baseItem.height);
470 displayMarginEnd = Math.round(displayMarginEnd);
472 if (onBufferViewport && (scopeView.isCurrent || scopeView.visibleToParent)) {
473 item.displayMarginBeginning = displayMarginBeginning;
474 item.displayMarginEnd = displayMarginEnd;
475 if (holdingList && holdingList.moving) {
476 // If we are moving we need to reset the cache buffer of the
477 // view that was not visible (i.e. !wasCurrentOnMoveStart) to 0 since
478 // otherwise the cache buffer we had set to preload the items of the
479 // visible range will trigger some item creations and we want move to
480 // be as smooth as possible meaning no need creations
481 if (!wasCurrentOnMoveStart) {
482 item.cacheBuffer = 0;
485 // Protect us against cases where the item hasn't yet been positioned
486 if (!(categoryView.contentY === 0 && baseItem.y === 0 && index !== 0)) {
487 item.cacheBuffer = categoryView.height * 1.5;
491 var visibleRange = baseItem.height + displayMarginEnd + displayMarginBeginning;
492 if (visibleRange < 0) {
493 item.displayMarginBeginning = displayMarginBeginning;
494 item.displayMarginEnd = displayMarginEnd;
495 item.cacheBuffer = 0;
497 // This should be visibleRange/2 in each of the properties
498 // but some item views still (like GridView) like creating sync delegates even if
499 // the visible range is 0 so let's make sure the visible range is negative
500 item.displayMarginBeginning = displayMarginBeginning - visibleRange;
501 item.displayMarginEnd = displayMarginEnd - visibleRange;
502 item.cacheBuffer = visibleRange;
506 if (!onBufferViewport) {
507 // If not on the buffered viewport, don't load anything
508 item.displayMarginBeginning = 0;
509 item.displayMarginEnd = -item.innerWidth;
510 item.cacheBuffer = 0;
512 if (onViewport && (scopeView.isCurrent || scopeView.visibleToParent)) {
513 // If on the buffered viewport and the viewport and the on a visible scope
514 // Set displayMargin so that cards are rendered
515 // And if not moving the parent list also give it some extra asynchronously
517 item.displayMarginBeginning = 0;
518 item.displayMarginEnd = 0;
519 if (holdingList && holdingList.moving) {
520 // If we are moving we need to reset the cache buffer of the
521 // view that was not visible (i.e. !wasCurrentOnMoveStart) to 0 since
522 // otherwise the cache buffer we had set to preload the items of the
523 // visible range will trigger some item creations and we want move to
524 // be as smooth as possible meaning no need creations
525 if (!wasCurrentOnMoveStart) {
526 item.cacheBuffer = 0;
529 item.cacheBuffer = baseItem.width * 1.5;
532 // If on the buffered viewport but either not in the real viewport
533 // or in the viewport of the non current scope, use displayMargin + cacheBuffer
534 // to render asynchronously the width of cards
535 item.displayMarginBeginning = 0;
536 item.displayMarginEnd = -item.innerWidth;
537 item.cacheBuffer = item.innerWidth;
549 top: rendererLoader.bottom
553 height: baseItem.expandable && !baseItem.headerLink ? seeAllLabel.font.pixelSize + units.gu(4) : 0
557 if (categoryView.expandedCategoryId !== baseItem.category) {
558 categoryView.expandedCategoryId = baseItem.category;
559 floatingSeeLess.companionBase = baseItem;
561 categoryView.expandedCategoryId = "";
567 text: baseItem.expanded ? i18n.tr("See less") : i18n.tr("See all")
570 verticalCenterOffset: units.gu(-0.5)
573 font.weight: Font.Bold
574 color: scopeStyle ? scopeStyle.foreground : theme.palette.normal.baseText
585 fillMode: Image.Stretch
586 source: "graphics/dash_divider_top_lightgrad.png"
591 // FIXME Should not rely on model.count but view.count, but ListViewWithPageHeader doesn't expose it yet.
592 visible: index != categoryView.model.count - 1
594 bottom: seeAll.bottom
598 fillMode: Image.Stretch
599 source: "graphics/dash_divider_top_darkgrad.png"
604 sectionProperty: "name"
605 sectionDelegate: ListItems.Header {
606 objectName: "dashSectionHeader" + (delegate ? delegate.category : "")
607 readonly property var delegate: categoryView.item(delegateIndex)
608 width: categoryView.width
609 height: section != "" ? units.gu(5) : 0
611 color: scopeStyle ? scopeStyle.foreground : theme.palette.normal.baseText
612 iconName: delegate && delegate.headerLink ? "go-next" : ""
614 if (delegate.headerLink) scopeView.scope.performQuery(delegate.headerLink);
618 pageHeader: scopeView.showPageHeader ? pageHeaderLoader : null
622 sourceComponent: scopeView.showPageHeader ? pageHeaderComponent : undefined
624 id: pageHeaderComponent
626 objectName: "scopePageHeader"
628 title: scopeView.scope ? scopeView.scope.name : ""
629 searchHint: scopeView.scope && scopeView.scope.searchHint || i18n.ctr("Label: Hint for dash search line edit", "Search")
630 showBackButton: scopeView.hasBackAction
631 searchEntryEnabled: true
632 settingsEnabled: scopeView.scope && scopeView.scope.settings && scopeView.scope.settings.count > 0 || false
633 favoriteEnabled: scopeView.scope && scopeView.scope.id !== "clickscope"
634 favorite: scopeView.scope && scopeView.scope.favorite
635 scopeStyle: scopeView.scopeStyle
636 paginationCount: scopeView.paginationCount
637 paginationIndex: scopeView.paginationIndex
639 bottomItem: DashNavigation {
640 scope: scopeView.scope
641 anchors { left: parent.left; right: parent.right }
642 windowHeight: scopeView.height
643 windowWidth: scopeView.width
644 scopeStyle: scopeView.scopeStyle
647 onBackClicked: scopeView.backClicked()
648 onSettingsClicked: subPageLoader.openSubPage("settings")
649 onFavoriteClicked: scopeView.scope.favorite = !scopeView.scope.favorite
650 onSearchTextFieldFocused: scopeView.showHeader()
657 id: pullToRefreshClippingItem
658 anchors.left: parent.left
659 anchors.right: parent.right
660 anchors.bottom: parent.bottom
661 height: parent.height - pullToRefresh.contentY + (pageHeaderLoader.item ? pageHeaderLoader.item.bottomItem[0].height - pageHeaderLoader.item.height : 0)
666 objectName: "pullToRefresh"
669 readonly property real contentY: categoryView.contentY - categoryView.originY
670 y: -contentY - units.gu(5)
674 scopeView.scope.refresh()
676 anchors.left: parent.left
677 anchors.right: parent.right
681 onProcessingChanged: if (!scopeView.processing) pullToRefresh.refreshing = false
684 style: PullToRefreshScopeStyle {
686 activationThreshold: units.gu(14)
693 objectName: "floatingSeeLess"
695 property Item companionTo: companionBase ? companionBase.seeAllButton : null
696 property Item companionBase: null
697 property bool showBecausePosition: false
698 property real yOffset: 0
701 left: categoryView.left
702 right: categoryView.right
704 y: parent.height - height + yOffset
705 height: seeLessLabel.font.pixelSize + units.gu(4)
706 visible: companionTo && showBecausePosition
708 onClicked: categoryView.expandedCategoryId = "";
710 function updateVisibility() {
711 var companionPos = companionTo.mapToItem(floatingSeeLess, 0, 0);
712 showBecausePosition = companionPos.y > 0;
714 var posToBase = floatingSeeLess.mapToItem(companionBase, 0, -yOffset).y;
715 yOffset = Math.max(0, companionBase.item.collapsedHeight - posToBase);
716 yOffset = Math.min(yOffset, height);
718 if (!showBecausePosition && categoryView.expandedCategoryId === "") {
719 companionBase = null;
725 text: i18n.tr("See less")
728 verticalCenterOffset: units.gu(-0.5)
731 font.weight: Font.Bold
732 color: scopeStyle ? scopeStyle.foreground : theme.palette.normal.baseText
736 target: floatingSeeLess.companionTo ? categoryView : null
737 onContentYChanged: floatingSeeLess.updateVisibility();
741 target: floatingSeeLess.companionTo
742 onYChanged: floatingSeeLess.updateVisibility();
747 id: previewLimitModel
752 objectName: "subPageLoader"
755 height: parent.height
756 anchors.left: categoryView.right
758 property bool open: false
759 property var scope: scopeView.scope
760 property var scopeStyle: scopeView.scopeStyle
761 property int initialIndex: -1
762 property var previewModel;
764 readonly property bool processing: item && item.processing || false
765 readonly property int count: item && item.count || 0
766 readonly property var currentItem: item && item.currentItem || null
768 property string subPage: ""
769 readonly property bool subPageShown: visible && status === Loader.Ready
771 function openSubPage(page) {
775 function closeSubPage() {
779 source: switch(subPage) {
780 case "preview": return "PreviewView.qml";
781 case "settings": return "ScopeSettingsPage.qml";
786 item.scope = Qt.binding(function() { return subPageLoader.scope; } )
787 item.scopeStyle = Qt.binding(function() { return subPageLoader.scopeStyle; } )
788 if (subPage == "preview") {
789 item.open = Qt.binding(function() { return subPageLoader.open; } )
790 item.previewModel = subPageLoader.previewModel;
791 subPageLoader.previewModel = null;
796 onOpenChanged: pageHeaderLoader.item.unfocus()
798 onVisibleChanged: if (!visible) subPage = ""
801 target: subPageLoader.item
802 onBackClicked: subPageLoader.closeSubPage()