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.1
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 || 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 function itemClicked(index, result, item, itemModel, resultsModel, limitedCategoryItemCount, categoryId) {
76 if (itemModel.uri.indexOf("scope://") === 0 || scope.id === "clickscope" || (scope.id === "videoaggregator" && categoryId === "myvideos-getstarted")) {
77 // TODO Technically it is possible that calling activate() will make the scope emit
78 // previewRequested so that we show a preview but there's no scope that does that yet
79 // so it's not implemented
80 scope.activate(result)
82 if (scope.preview(result)) {
83 openPreview(index, resultsModel, limitedCategoryItemCount);
88 function itemPressedAndHeld(index, result, itemModel, resultsModel, limitedCategoryItemCount, categoryId) {
89 if (itemModel.uri.indexOf("scope://") !== 0 && !(scope.id === "videoaggregator" && categoryId === "myvideos-getstarted")) {
90 if (scope.preview(result)) {
91 openPreview(index, resultsModel, limitedCategoryItemCount);
96 function openPreview(index, resultsModel, limitedCategoryItemCount) {
97 if (limitedCategoryItemCount > 0) {
98 previewLimitModel.model = resultsModel;
99 previewLimitModel.limit = limitedCategoryItemCount;
100 subPageLoader.model = previewLimitModel;
102 subPageLoader.model = resultsModel;
104 subPageLoader.initialIndex = -1;
105 subPageLoader.initialIndex = index;
106 subPageLoader.openSubPage("preview");
112 value: isCurrent && !subPageLoader.open && (Qt.application.state == Qt.ApplicationActive)
115 UnitySortFilterProxyModel {
117 model: scope ? scope.categories : null
118 dynamicSortFilter: true
119 filterRole: Categories.RoleCount
124 onIsCurrentChanged: {
125 if (!holdingList || !holdingList.moving) {
126 wasCurrentOnMoveStart = scopeView.isCurrent;
128 if (pageHeaderLoader.item && showPageHeader) {
129 pageHeaderLoader.item.resetSearch();
131 subPageLoader.closeSubPage();
135 target: scopeView.scope
136 property: "searchQuery"
137 value: pageHeaderLoader.item ? pageHeaderLoader.item.searchQuery : ""
138 when: isCurrent && showPageHeader
142 target: pageHeaderLoader.item
143 property: "searchQuery"
144 value: scopeView.scope ? scopeView.scope.searchQuery : ""
145 when: isCurrent && showPageHeader
149 target: scopeView.scope
150 onShowDash: subPageLoader.closeSubPage()
151 onHideDash: subPageLoader.closeSubPage()
158 wasCurrentOnMoveStart = scopeView.isCurrent;
165 color: scopeView.scopeStyle ? scopeView.scopeStyle.background : "transparent"
166 visible: color != "transparent"
171 objectName: "categoryListView"
172 interactive: !forceNonInteractive
174 x: subPageLoader.open ? -width : 0
176 Behavior on x { UbuntuNumberAnimation { } }
178 height: floatingSeeLess.visible ? parent.height - floatingSeeLess.height + floatingSeeLess.yOffset
180 clip: height != parent.height
182 model: scopeView.categories
183 forceNoClip: subPageLoader.open
186 property string expandedCategoryId: ""
187 property int runMaximizeAfterSizeChanges: 0
189 readonly property bool pageHeaderTotallyVisible: scopeView.showPageHeader &&
190 ((headerItemShownHeight == 0 && categoryView.contentY <= categoryView.originY) || (headerItemShownHeight == pageHeaderLoader.item.height))
192 onExpandedCategoryIdChanged: {
193 var firstCreated = firstCreatedIndex();
194 var shrinkingAny = false;
195 var shrinkHeightDifference = 0;
196 for (var i = 0; i < createdItemCount(); ++i) {
197 var baseItem = item(firstCreated + i);
198 if (baseItem.expandable) {
199 var shouldExpand = baseItem.category === expandedCategoryId;
200 if (shouldExpand != baseItem.expanded) {
202 if (!subPageLoader.open) {
203 var animateShrinking = !shouldExpand && baseItem.y + baseItem.item.collapsedHeight + baseItem.seeAllButton.height < categoryView.height;
204 var animateGrowing = shouldExpand && baseItem.y + baseItem.height < categoryView.height;
205 animate = shrinkingAny || animateShrinking || animateGrowing;
210 shrinkHeightDifference = baseItem.item.expandedHeight - baseItem.item.collapsedHeight;
213 if (shouldExpand && !subPageLoader.open) {
215 categoryView.maximizeVisibleArea(firstCreated + i, baseItem.item.expandedHeight + baseItem.seeAllButton.height);
217 // If the space that shrinking is smaller than the one we need to grow we'll call maximizeVisibleArea
218 // after the shrink/grow animation ends
219 var growHeightDifference = baseItem.item.expandedHeight - baseItem.item.collapsedHeight;
220 if (growHeightDifference > shrinkHeightDifference) {
221 runMaximizeAfterSizeChanges = 2;
223 runMaximizeAfterSizeChanges = 0;
228 baseItem.expand(shouldExpand, animate);
234 delegate: DashCategoryBase {
236 objectName: "dashCategory" + category
238 property Item seeAllButton: seeAll
240 readonly property bool expandable: {
241 if (categoryView.model.count === 1) return false;
242 if (cardTool.template && cardTool.template["collapsed-rows"] === 0) return false;
243 if (item && item.expandedHeight > item.collapsedHeight) return true;
246 property bool expanded: false
247 readonly property string category: categoryId
248 readonly property string headerLink: model.headerLink
249 readonly property var item: rendererLoader.item
251 function expand(expand, animate) {
252 heightBehaviour.enabled = animate;
258 objectName: "cardTool"
259 count: results ? results.count : 0
260 template: model.renderer
261 components: model.components
262 viewWidth: parent.width
265 onExpandableChanged: {
266 // This can happen with the VJ that doesn't know how height it will be on creation
267 // so doesn't set expandable until a bit too late for onLoaded
269 var shouldExpand = baseItem.category === categoryView.expandedCategoryId;
270 baseItem.expand(shouldExpand, false /*animate*/);
274 onHeightChanged: rendererLoader.updateRanges();
275 onYChanged: rendererLoader.updateRanges();
283 topMargin: name != "" ? 0 : units.gu(2)
289 animation: UbuntuNumberAnimation {
290 duration: UbuntuAnimation.FastDuration
293 heightBehaviour.enabled = false
294 if (categoryView.runMaximizeAfterSizeChanges > 0) {
295 categoryView.runMaximizeAfterSizeChanges--;
296 if (categoryView.runMaximizeAfterSizeChanges == 0) {
297 var firstCreated = categoryView.firstCreatedIndex();
298 for (var i = 0; i < categoryView.createdItemCount(); ++i) {
299 var baseItem = categoryView.item(firstCreated + i);
300 if (baseItem.category === categoryView.expandedCategoryId) {
301 categoryView.maximizeVisibleArea(firstCreated + i, baseItem.item.expandedHeight + baseItem.seeAllButton.height);
312 readonly property bool expanded: baseItem.expanded || !baseItem.expandable
313 height: expanded ? item.expandedHeight : item.collapsedHeight
316 switch (cardTool.categoryLayout) {
317 case "carousel": return "CardCarousel.qml";
318 case "vertical-journal": return "CardVerticalJournal.qml";
319 case "horizontal-list": return "CardHorizontalList.qml";
321 default: return "CardGrid.qml";
326 if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
327 item.enableHeightBehavior = scopeView.enableHeightBehaviorOnNextCreation;
328 scopeView.enableHeightBehaviorOnNextCreation = false;
330 item.model = Qt.binding(function() { return results })
331 item.objectName = Qt.binding(function() { return categoryId })
332 item.scopeStyle = scopeView.scopeStyle;
333 if (baseItem.expandable) {
334 var shouldExpand = baseItem.category === categoryView.expandedCategoryId;
335 baseItem.expand(shouldExpand, false /*animate*/);
338 if (scope && scope.id === "clickscope" && (categoryId === "predefined" || categoryId === "local")) {
340 if (scopeView.width > units.gu(45)) {
341 if (scopeView.width >= units.gu(70)) {
342 cardTool.cardWidth = units.gu(9);
344 cardTool.cardWidth = units.gu(10);
347 cardTool.artShapeSize = Qt.size(units.gu(8), units.gu(7.5));
349 item.cardTool = cardTool;
352 Component.onDestruction: {
353 if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
354 scopeView.enableHeightBehaviorOnNextCreation = item.enableHeightBehaviorOnNextCreation;
359 target: rendererLoader.item
361 scopeView.itemClicked(index, result, item, itemModel, target.model, categoryItemCount(), baseItem.category);
365 scopeView.itemPressedAndHeld(index, result, itemModel, target.model, categoryItemCount(), baseItem.category);
368 function categoryItemCount() {
369 var categoryItemCount = -1;
370 if (!rendererLoader.expanded && !seeAllLabel.visible && target.collapsedItemCount > 0) {
371 categoryItemCount = target.collapsedItemCount;
373 return categoryItemCount;
378 onOriginYChanged: rendererLoader.updateRanges();
379 onContentYChanged: rendererLoader.updateRanges();
380 onHeightChanged: rendererLoader.updateRanges();
381 onContentHeightChanged: rendererLoader.updateRanges();
385 onIsCurrentChanged: rendererLoader.updateRanges();
386 onVisibleToParentChanged: rendererLoader.updateRanges();
390 onMovingChanged: if (!moving) rendererLoader.updateRanges();
393 function updateRanges() {
394 // Don't want to create stress by requesting more items during scope
395 // changes so unless you're not part of the visible scopes just return.
396 // For the visible scopes we need to do some work, the previously non visible
397 // scope needs to adjust its ranges so that we define the new visible range,
398 // that still means no creation/destruction of delegates, it's just about changing
399 // the culling of the items so they are actually visible
400 if (holdingList && holdingList.moving && !scopeView.visibleToParent) {
404 if (categoryView.moving) {
405 // Do not update the range if we are overshooting up or down, since we'll come back
406 // to the stable position and delete/create items without any reason
407 if (categoryView.contentY < categoryView.originY) {
409 } else if (categoryView.contentHeight - categoryView.originY > categoryView.height &&
410 categoryView.contentY + categoryView.height > categoryView.contentHeight) {
415 if (item && item.hasOwnProperty("displayMarginBeginning")) {
416 var buffer = wasCurrentOnMoveStart ? categoryView.height * 1.5 : 0;
417 var onViewport = baseItem.y + baseItem.height > 0 &&
418 baseItem.y < categoryView.height;
419 var onBufferViewport = baseItem.y + baseItem.height > -buffer &&
420 baseItem.y < categoryView.height + buffer;
422 if (item.growsVertically) {
423 // A item view creates its delegates synchronously from
424 // -displayMarginBeginning
426 // height + displayMarginEnd
427 // Around that area it adds the cacheBuffer area where delegates are created async
429 // We adjust displayMarginBeginning and displayMarginEnd so
430 // * In non visible scopes nothing is considered visible and we set cacheBuffer
431 // so that creates the items that would be in the viewport asynchronously
432 // * For the current scope set the visible range to the viewport and then
433 // use cacheBuffer to create extra items for categoryView.height * 1.5
434 // to make scrolling nicer by mantaining a higher number of
436 // * For non current but visible scopes (i.e. when the user changes from one scope
437 // to the next, we set the visible range to the viewport so
438 // items are not culled (invisible) but still use no cacheBuffer
439 // (it will be set once the scope is the current one)
440 var displayMarginBeginning = baseItem.y + rendererLoader.anchors.topMargin;
441 displayMarginBeginning = -Math.max(-displayMarginBeginning, 0);
442 displayMarginBeginning = -Math.min(-displayMarginBeginning, baseItem.height);
443 displayMarginBeginning = Math.round(displayMarginBeginning);
444 var displayMarginEnd = -baseItem.height + seeAll.height + categoryView.height - baseItem.y;
445 displayMarginEnd = -Math.max(-displayMarginEnd, 0);
446 displayMarginEnd = -Math.min(-displayMarginEnd, baseItem.height);
447 displayMarginEnd = Math.round(displayMarginEnd);
449 if (onBufferViewport && (scopeView.isCurrent || scopeView.visibleToParent)) {
450 item.displayMarginBeginning = displayMarginBeginning;
451 item.displayMarginEnd = displayMarginEnd;
452 if (holdingList && holdingList.moving) {
453 // If we are moving we need to reset the cache buffer of the
454 // view that was not visible (i.e. !wasCurrentOnMoveStart) to 0 since
455 // otherwise the cache buffer we had set to preload the items of the
456 // visible range will trigger some item creations and we want move to
457 // be as smooth as possible meaning no need creations
458 if (!wasCurrentOnMoveStart) {
459 item.cacheBuffer = 0;
462 // Protect us against cases where the item hasn't yet been positioned
463 if (!(categoryView.contentY === 0 && baseItem.y === 0 && index !== 0)) {
464 item.cacheBuffer = categoryView.height * 1.5;
468 var visibleRange = baseItem.height + displayMarginEnd + displayMarginBeginning;
469 if (visibleRange < 0) {
470 item.displayMarginBeginning = displayMarginBeginning;
471 item.displayMarginEnd = displayMarginEnd;
472 item.cacheBuffer = 0;
474 // This should be visibleRange/2 in each of the properties
475 // but some item views still (like GridView) like creating sync delegates even if
476 // the visible range is 0 so let's make sure the visible range is negative
477 item.displayMarginBeginning = displayMarginBeginning - visibleRange;
478 item.displayMarginEnd = displayMarginEnd - visibleRange;
479 item.cacheBuffer = visibleRange;
483 if (!onBufferViewport) {
484 // If not on the buffered viewport, don't load anything
485 item.displayMarginBeginning = 0;
486 item.displayMarginEnd = -item.innerWidth;
487 item.cacheBuffer = 0;
489 if (onViewport && (scopeView.isCurrent || scopeView.visibleToParent)) {
490 // If on the buffered viewport and the viewport and the on a visible scope
491 // Set displayMargin so that cards are rendered
492 // And if not moving the parent list also give it some extra asynchronously
494 item.displayMarginBeginning = 0;
495 item.displayMarginEnd = 0;
496 if (holdingList && holdingList.moving) {
497 // If we are moving we need to reset the cache buffer of the
498 // view that was not visible (i.e. !wasCurrentOnMoveStart) to 0 since
499 // otherwise the cache buffer we had set to preload the items of the
500 // visible range will trigger some item creations and we want move to
501 // be as smooth as possible meaning no need creations
502 if (!wasCurrentOnMoveStart) {
503 item.cacheBuffer = 0;
506 item.cacheBuffer = baseItem.width * 1.5;
509 // If on the buffered viewport but either not in the real viewport
510 // or in the viewport of the non current scope, use displayMargin + cacheBuffer
511 // to render asynchronously the width of cards
512 item.displayMarginBeginning = 0;
513 item.displayMarginEnd = -item.innerWidth;
514 item.cacheBuffer = item.innerWidth;
526 top: rendererLoader.bottom
530 height: baseItem.expandable && !baseItem.headerLink ? seeAllLabel.font.pixelSize + units.gu(4) : 0
534 if (categoryView.expandedCategoryId !== baseItem.category) {
535 categoryView.expandedCategoryId = baseItem.category;
536 floatingSeeLess.companionBase = baseItem;
538 categoryView.expandedCategoryId = "";
544 text: baseItem.expanded ? i18n.tr("See less") : i18n.tr("See all")
547 verticalCenterOffset: units.gu(-0.5)
550 font.weight: Font.Bold
551 color: scopeStyle ? scopeStyle.foreground : Theme.palette.normal.baseText
562 fillMode: Image.Stretch
563 source: "graphics/dash_divider_top_lightgrad.png"
568 // FIXME Should not rely on model.count but view.count, but ListViewWithPageHeader doesn't expose it yet.
569 visible: index != categoryView.model.count - 1
571 bottom: seeAll.bottom
575 fillMode: Image.Stretch
576 source: "graphics/dash_divider_top_darkgrad.png"
581 sectionProperty: "name"
582 sectionDelegate: ListItems.Header {
583 objectName: "dashSectionHeader" + (delegate ? delegate.category : "")
584 readonly property var delegate: categoryView.item(delegateIndex)
585 width: categoryView.width
586 height: section != "" ? units.gu(5) : 0
588 color: scopeStyle ? scopeStyle.foreground : Theme.palette.normal.baseText
589 iconName: delegate && delegate.headerLink ? "go-next" : ""
591 if (delegate.headerLink) scopeView.scope.performQuery(delegate.headerLink);
595 pageHeader: scopeView.showPageHeader ? pageHeaderLoader : null
599 sourceComponent: scopeView.showPageHeader ? pageHeaderComponent : undefined
601 id: pageHeaderComponent
603 objectName: "scopePageHeader"
605 title: scopeView.scope ? scopeView.scope.name : ""
606 searchHint: scopeView.scope && scopeView.scope.searchHint || i18n.ctr("Label: Hint for dash search line edit", "Search")
607 showBackButton: scopeView.hasBackAction
608 searchEntryEnabled: true
609 settingsEnabled: scopeView.scope && scopeView.scope.settings && scopeView.scope.settings.count > 0 || false
610 favoriteEnabled: scopeView.scope && scopeView.scope.id !== "clickscope"
611 favorite: scopeView.scope && scopeView.scope.favorite
612 scopeStyle: scopeView.scopeStyle
613 paginationCount: scopeView.paginationCount
614 paginationIndex: scopeView.paginationIndex
616 bottomItem: DashNavigation {
617 scope: scopeView.scope
618 anchors { left: parent.left; right: parent.right }
619 windowHeight: scopeView.height
620 windowWidth: scopeView.width
621 scopeStyle: scopeView.scopeStyle
624 onBackClicked: scopeView.backClicked()
625 onSettingsClicked: subPageLoader.openSubPage("settings")
626 onFavoriteClicked: scopeView.scope.favorite = !scopeView.scope.favorite
627 onSearchTextFieldFocused: scopeView.showHeader()
634 id: pullToRefreshClippingItem
635 anchors.left: parent.left
636 anchors.right: parent.right
637 anchors.bottom: parent.bottom
638 height: parent.height - pullToRefresh.contentY + (pageHeaderLoader.item ? pageHeaderLoader.item.bottomItem[0].height - pageHeaderLoader.item.height : 0)
643 objectName: "pullToRefresh"
646 readonly property real contentY: categoryView.contentY - categoryView.originY
647 y: -contentY - units.gu(5)
651 scopeView.scope.refresh()
653 anchors.left: parent.left
654 anchors.right: parent.right
658 onProcessingChanged: if (!scopeView.processing) pullToRefresh.refreshing = false
661 style: PullToRefreshScopeStyle {
663 activationThreshold: units.gu(14)
670 objectName: "floatingSeeLess"
672 property Item companionTo: companionBase ? companionBase.seeAllButton : null
673 property Item companionBase: null
674 property bool showBecausePosition: false
675 property real yOffset: 0
678 left: categoryView.left
679 right: categoryView.right
681 y: parent.height - height + yOffset
682 height: seeLessLabel.font.pixelSize + units.gu(4)
683 visible: companionTo && showBecausePosition
685 onClicked: categoryView.expandedCategoryId = "";
687 function updateVisibility() {
688 var companionPos = companionTo.mapToItem(floatingSeeLess, 0, 0);
689 showBecausePosition = companionPos.y > 0;
691 var posToBase = floatingSeeLess.mapToItem(companionBase, 0, -yOffset).y;
692 yOffset = Math.max(0, companionBase.item.collapsedHeight - posToBase);
693 yOffset = Math.min(yOffset, height);
695 if (!showBecausePosition && categoryView.expandedCategoryId === "") {
696 companionBase = null;
702 text: i18n.tr("See less")
705 verticalCenterOffset: units.gu(-0.5)
708 font.weight: Font.Bold
709 color: scopeStyle ? scopeStyle.foreground : Theme.palette.normal.baseText
713 target: floatingSeeLess.companionTo ? categoryView : null
714 onContentYChanged: floatingSeeLess.updateVisibility();
718 target: floatingSeeLess.companionTo
719 onYChanged: floatingSeeLess.updateVisibility();
724 id: previewLimitModel
729 objectName: "subPageLoader"
732 height: parent.height
733 anchors.left: categoryView.right
735 property bool open: false
736 property var scope: scopeView.scope
737 property var scopeStyle: scopeView.scopeStyle
738 property int initialIndex: -1
739 property var model: null
741 readonly property bool processing: item && item.processing || false
742 readonly property int count: item && item.count || 0
743 readonly property int currentIndex: item && item.currentIndex || 0
744 readonly property var currentItem: item && item.currentItem || null
746 property string subPage: ""
747 readonly property bool subPageShown: visible && status === Loader.Ready
749 function openSubPage(page) {
753 function closeSubPage() {
757 source: switch(subPage) {
758 case "preview": return "PreviewListView.qml";
759 case "settings": return "ScopeSettingsPage.qml";
764 item.scope = Qt.binding(function() { return subPageLoader.scope; } )
765 item.scopeStyle = Qt.binding(function() { return subPageLoader.scopeStyle; } )
766 if (subPage == "preview") {
767 item.open = Qt.binding(function() { return subPageLoader.open; } )
768 item.initialIndex = Qt.binding(function() { return subPageLoader.initialIndex; } )
769 item.model = Qt.binding(function() { return subPageLoader.model; } )
774 onOpenChanged: pageHeaderLoader.item.unfocus()
776 onVisibleChanged: if (!visible) subPage = ""
779 target: subPageLoader.item
780 onBackClicked: subPageLoader.closeSubPage()