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
23 import "../Components"
24 import "../Components/ListItems" as ListItems
29 readonly property bool navigationDisableParentInteractive: pageHeaderLoader.item ? pageHeaderLoader.item.bottomItem[0].disableParentInteractive : false
30 property bool forceNonInteractive: false
31 property var scope: null
32 property UnitySortFilterProxyModel categories: categoryFilter
33 property bool isCurrent: false
34 property alias moving: categoryView.moving
35 property bool hasBackAction: false
36 property bool enableHeightBehaviorOnNextCreation: false
37 property var categoryView: categoryView
38 property bool showPageHeader: true
39 readonly property alias subPageShown: subPageLoader.subPageShown
40 property int paginationCount: 0
41 property int paginationIndex: 0
42 property bool visibleToParent: false
43 property alias pageHeaderTotallyVisible: categoryView.pageHeaderTotallyVisible
44 property var holdingList: null
45 property bool wasCurrentOnMoveStart: false
47 property var scopeStyle: ScopeStyle {
48 style: scope ? scope.customizations : {}
51 readonly property bool processing: scope ? scope.searchInProgress || subPageLoader.processing : false
56 floatingSeeLess.companionBase = null;
59 function positionAtBeginning() {
60 categoryView.positionAtBeginning()
63 function showHeader() {
64 categoryView.showHeader()
67 function closePreview() {
68 subPageLoader.closeSubPage()
71 function itemClicked(index, result, item, itemModel, resultsModel, limitedCategoryItemCount) {
72 if (itemModel.uri.indexOf("scope://") === 0 || scope.id === "clickscope") {
73 // TODO Technically it is possible that calling activate() will make the scope emit
74 // previewRequested so that we show a preview but there's no scope that does that yet
75 // so it's not implemented
76 scope.activate(result)
78 if (scope.preview(result)) {
79 openPreview(index, resultsModel, limitedCategoryItemCount);
84 function itemPressedAndHeld(index, result, itemModel, resultsModel, limitedCategoryItemCount) {
85 if (itemModel.uri.indexOf("scope://") !== 0) {
86 if (scope.preview(result)) {
87 openPreview(index, resultsModel, limitedCategoryItemCount);
92 function openPreview(index, resultsModel, limitedCategoryItemCount) {
93 if (limitedCategoryItemCount > 0) {
94 previewLimitModel.model = resultsModel;
95 previewLimitModel.limit = limitedCategoryItemCount;
96 subPageLoader.model = previewLimitModel;
98 subPageLoader.model = resultsModel;
100 subPageLoader.initialIndex = -1;
101 subPageLoader.initialIndex = index;
102 subPageLoader.openSubPage("preview");
108 value: isCurrent && !subPageLoader.open && (Powerd.status === Powerd.On)
111 UnitySortFilterProxyModel {
113 model: scope ? scope.categories : null
114 dynamicSortFilter: true
115 filterRole: Categories.RoleCount
120 onIsCurrentChanged: {
121 if (!holdingList || !holdingList.moving) {
122 wasCurrentOnMoveStart = scopeView.isCurrent;
124 if (pageHeaderLoader.item && showPageHeader) {
125 pageHeaderLoader.item.resetSearch();
127 subPageLoader.closeSubPage();
131 target: scopeView.scope
132 property: "searchQuery"
133 value: pageHeaderLoader.item ? pageHeaderLoader.item.searchQuery : ""
134 when: isCurrent && showPageHeader
138 target: pageHeaderLoader.item
139 property: "searchQuery"
140 value: scopeView.scope ? scopeView.scope.searchQuery : ""
141 when: isCurrent && showPageHeader
145 target: scopeView.scope
146 onShowDash: subPageLoader.closeSubPage()
147 onHideDash: subPageLoader.closeSubPage()
154 wasCurrentOnMoveStart = scopeView.isCurrent;
161 color: scopeView.scopeStyle ? scopeView.scopeStyle.background : "transparent"
162 visible: color != "transparent"
167 objectName: "categoryListView"
168 interactive: !forceNonInteractive
170 x: subPageLoader.open ? -width : 0
172 Behavior on x { UbuntuNumberAnimation { } }
174 height: floatingSeeLess.visible ? parent.height - floatingSeeLess.height + floatingSeeLess.yOffset
176 clip: height != parent.height
178 model: scopeView.categories
179 forceNoClip: subPageLoader.open
182 property string expandedCategoryId: ""
183 property int runMaximizeAfterSizeChanges: 0
185 readonly property bool pageHeaderTotallyVisible: scopeView.showPageHeader &&
186 ((headerItemShownHeight == 0 && categoryView.contentY <= categoryView.originY) || (headerItemShownHeight == pageHeaderLoader.item.height))
188 onExpandedCategoryIdChanged: {
189 var firstCreated = firstCreatedIndex();
190 var shrinkingAny = false;
191 var shrinkHeightDifference = 0;
192 for (var i = 0; i < createdItemCount(); ++i) {
193 var baseItem = item(firstCreated + i);
194 if (baseItem.expandable) {
195 var shouldExpand = baseItem.category === expandedCategoryId;
196 if (shouldExpand != baseItem.expanded) {
198 if (!subPageLoader.open) {
199 var animateShrinking = !shouldExpand && baseItem.y + baseItem.item.collapsedHeight + baseItem.seeAllButton.height < categoryView.height;
200 var animateGrowing = shouldExpand && baseItem.y + baseItem.height < categoryView.height;
201 animate = shrinkingAny || animateShrinking || animateGrowing;
206 shrinkHeightDifference = baseItem.item.expandedHeight - baseItem.item.collapsedHeight;
209 if (shouldExpand && !subPageLoader.open) {
211 categoryView.maximizeVisibleArea(firstCreated + i, baseItem.item.expandedHeight + baseItem.seeAllButton.height);
213 // If the space that shrinking is smaller than the one we need to grow we'll call maximizeVisibleArea
214 // after the shrink/grow animation ends
215 var growHeightDifference = baseItem.item.expandedHeight - baseItem.item.collapsedHeight;
216 if (growHeightDifference > shrinkHeightDifference) {
217 runMaximizeAfterSizeChanges = 2;
219 runMaximizeAfterSizeChanges = 0;
224 baseItem.expand(shouldExpand, animate);
230 delegate: DashCategoryBase {
232 objectName: "dashCategory" + category
234 property Item seeAllButton: seeAll
236 readonly property bool expandable: {
237 if (categoryView.model.count === 1) return false;
238 if (cardTool.template && cardTool.template["collapsed-rows"] === 0) return false;
239 if (item && item.expandedHeight > item.collapsedHeight) return true;
242 property bool expanded: false
243 readonly property string category: categoryId
244 readonly property string headerLink: model.headerLink
245 readonly property var item: rendererLoader.item
247 function expand(expand, animate) {
248 heightBehaviour.enabled = animate;
254 objectName: "cardTool"
255 count: results ? results.count : 0
256 template: model.renderer
257 components: model.components
258 viewWidth: parent.width
261 onExpandableChanged: {
262 // This can happen with the VJ that doesn't know how height it will be on creation
263 // so doesn't set expandable until a bit too late for onLoaded
265 var shouldExpand = baseItem.category === categoryView.expandedCategoryId;
266 baseItem.expand(shouldExpand, false /*animate*/);
270 onHeightChanged: rendererLoader.updateRanges();
271 onYChanged: rendererLoader.updateRanges();
279 topMargin: name != "" ? 0 : units.gu(2)
285 animation: UbuntuNumberAnimation {
286 duration: UbuntuAnimation.FastDuration
289 heightBehaviour.enabled = false
290 if (categoryView.runMaximizeAfterSizeChanges > 0) {
291 categoryView.runMaximizeAfterSizeChanges--;
292 if (categoryView.runMaximizeAfterSizeChanges == 0) {
293 var firstCreated = categoryView.firstCreatedIndex();
294 for (var i = 0; i < categoryView.createdItemCount(); ++i) {
295 var baseItem = categoryView.item(firstCreated + i);
296 if (baseItem.category === categoryView.expandedCategoryId) {
297 categoryView.maximizeVisibleArea(firstCreated + i, baseItem.item.expandedHeight + baseItem.seeAllButton.height);
308 readonly property bool expanded: baseItem.expanded || !baseItem.expandable
309 height: expanded ? item.expandedHeight : item.collapsedHeight
312 switch (cardTool.categoryLayout) {
313 case "carousel": return "CardCarousel.qml";
314 case "vertical-journal": return "CardVerticalJournal.qml";
315 case "horizontal-list": return "CardHorizontalList.qml";
317 default: return "CardGrid.qml";
322 if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
323 item.enableHeightBehavior = scopeView.enableHeightBehaviorOnNextCreation;
324 scopeView.enableHeightBehaviorOnNextCreation = false;
326 item.model = Qt.binding(function() { return results })
327 item.objectName = Qt.binding(function() { return categoryId })
328 item.scopeStyle = scopeView.scopeStyle;
329 if (baseItem.expandable) {
330 var shouldExpand = baseItem.category === categoryView.expandedCategoryId;
331 baseItem.expand(shouldExpand, false /*animate*/);
334 if (scope && scope.id === "clickscope" && (categoryId === "predefined" || categoryId === "local")) {
336 cardTool.artShapeSize = Qt.size(units.gu(8), units.gu(7.5));
338 item.cardTool = cardTool;
341 Component.onDestruction: {
342 if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
343 scopeView.enableHeightBehaviorOnNextCreation = item.enableHeightBehaviorOnNextCreation;
348 target: rendererLoader.item
350 scopeView.itemClicked(index, result, item, itemModel, target.model, categoryItemCount());
354 scopeView.itemPressedAndHeld(index, result, itemModel, target.model, categoryItemCount());
357 function categoryItemCount() {
358 var categoryItemCount = -1;
359 if (!rendererLoader.expanded && !seeAllLabel.visible && target.collapsedItemCount > 0) {
360 categoryItemCount = target.collapsedItemCount;
362 return categoryItemCount;
367 onOriginYChanged: rendererLoader.updateRanges();
368 onContentYChanged: rendererLoader.updateRanges();
369 onHeightChanged: rendererLoader.updateRanges();
370 onContentHeightChanged: rendererLoader.updateRanges();
374 onIsCurrentChanged: rendererLoader.updateRanges();
375 onVisibleToParentChanged: rendererLoader.updateRanges();
379 onMovingChanged: if (!moving) rendererLoader.updateRanges();
382 function updateRanges() {
383 // Don't want to create stress by requesting more items during scope
384 // changes so unless you're not part of the visible scopes just return.
385 // For the visible scopes we need to do some work, the previously non visible
386 // scope needs to adjust its ranges so that we define the new visible range,
387 // that still means no creation/destruction of delegates, it's just about changing
388 // the culling of the items so they are actually visible
389 if (holdingList && holdingList.moving && !scopeView.visibleToParent) {
393 if (categoryView.moving) {
394 // Do not update the range if we are overshooting up or down, since we'll come back
395 // to the stable position and delete/create items without any reason
396 if (categoryView.contentY < categoryView.originY) {
398 } else if (categoryView.contentHeight - categoryView.originY > categoryView.height &&
399 categoryView.contentY + categoryView.height > categoryView.contentHeight) {
404 if (item && item.hasOwnProperty("displayMarginBeginning")) {
405 var buffer = wasCurrentOnMoveStart ? categoryView.height * 1.5 : 0;
406 var onViewport = baseItem.y + baseItem.height > 0 &&
407 baseItem.y < categoryView.height;
408 var onBufferViewport = baseItem.y + baseItem.height > -buffer &&
409 baseItem.y < categoryView.height + buffer;
411 if (item.growsVertically) {
412 // A item view creates its delegates synchronously from
413 // -displayMarginBeginning
415 // height + displayMarginEnd
416 // Around that area it adds the cacheBuffer area where delegates are created async
418 // We adjust displayMarginBeginning and displayMarginEnd so
419 // * In non visible scopes nothing is considered visible and we set cacheBuffer
420 // so that creates the items that would be in the viewport asynchronously
421 // * For the current scope set the visible range to the viewport and then
422 // use cacheBuffer to create extra items for categoryView.height * 1.5
423 // to make scrolling nicer by mantaining a higher number of
425 // * For non current but visible scopes (i.e. when the user changes from one scope
426 // to the next, we set the visible range to the viewport so
427 // items are not culled (invisible) but still use no cacheBuffer
428 // (it will be set once the scope is the current one)
429 var displayMarginBeginning = baseItem.y;
430 displayMarginBeginning = -Math.max(-displayMarginBeginning, 0);
431 displayMarginBeginning = -Math.min(-displayMarginBeginning, baseItem.height);
432 displayMarginBeginning = Math.round(displayMarginBeginning);
433 var displayMarginEnd = -baseItem.height + seeAll.height + categoryView.height - baseItem.y;
434 displayMarginEnd = -Math.max(-displayMarginEnd, 0);
435 displayMarginEnd = -Math.min(-displayMarginEnd, baseItem.height);
436 displayMarginEnd = Math.round(displayMarginEnd);
438 if (onBufferViewport && (scopeView.isCurrent || scopeView.visibleToParent)) {
439 item.displayMarginBeginning = displayMarginBeginning;
440 item.displayMarginEnd = displayMarginEnd;
441 if (holdingList && holdingList.moving) {
442 // If we are moving we need to reset the cache buffer of the
443 // view that was not visible (i.e. !wasCurrentOnMoveStart) to 0 since
444 // otherwise the cache buffer we had set to preload the items of the
445 // visible range will trigger some item creations and we want move to
446 // be as smooth as possible meaning no need creations
447 if (!wasCurrentOnMoveStart) {
448 item.cacheBuffer = 0;
451 // Protect us against cases where the item hasn't yet been positioned
452 if (!(categoryView.contentY === 0 && baseItem.y === 0 && index !== 0)) {
453 item.cacheBuffer = categoryView.height * 1.5;
457 var visibleRange = baseItem.height + displayMarginEnd + displayMarginBeginning;
458 if (visibleRange < 0) {
459 item.displayMarginBeginning = displayMarginBeginning;
460 item.displayMarginEnd = displayMarginEnd;
461 item.cacheBuffer = 0;
463 // This should be visibleRange/2 in each of the properties
464 // but some item views still (like GridView) like creating sync delegates even if
465 // the visible range is 0 so let's make sure the visible range is negative
466 item.displayMarginBeginning = displayMarginBeginning - visibleRange;
467 item.displayMarginEnd = displayMarginEnd - visibleRange;
468 item.cacheBuffer = visibleRange;
472 if (!onBufferViewport) {
473 // If not on the buffered viewport, don't load anything
474 item.displayMarginBeginning = 0;
475 item.displayMarginEnd = -item.innerWidth;
476 item.cacheBuffer = 0;
478 if (onViewport && (scopeView.isCurrent || scopeView.visibleToParent)) {
479 // If on the buffered viewport and the viewport and the on a visible scope
480 // Set displayMargin so that cards are rendered
481 // And if not moving the parent list also give it some extra asynchronously
483 item.displayMarginBeginning = 0;
484 item.displayMarginEnd = 0;
485 if (holdingList && holdingList.moving) {
486 // If we are moving we need to reset the cache buffer of the
487 // view that was not visible (i.e. !wasCurrentOnMoveStart) to 0 since
488 // otherwise the cache buffer we had set to preload the items of the
489 // visible range will trigger some item creations and we want move to
490 // be as smooth as possible meaning no need creations
491 if (!wasCurrentOnMoveStart) {
492 item.cacheBuffer = 0;
495 item.cacheBuffer = baseItem.width * 1.5;
498 // If on the buffered viewport but either not in the real viewport
499 // or in the viewport of the non current scope, use displayMargin + cacheBuffer
500 // to render asynchronously the width of cards
501 item.displayMarginBeginning = 0;
502 item.displayMarginEnd = -item.innerWidth;
503 item.cacheBuffer = item.innerWidth;
515 top: rendererLoader.bottom
519 height: baseItem.expandable && !baseItem.headerLink ? seeAllLabel.font.pixelSize + units.gu(4) : 0
523 if (categoryView.expandedCategoryId !== baseItem.category) {
524 categoryView.expandedCategoryId = baseItem.category;
525 floatingSeeLess.companionBase = baseItem;
527 categoryView.expandedCategoryId = "";
533 text: baseItem.expanded ? i18n.tr("See less") : i18n.tr("See all")
536 verticalCenterOffset: units.gu(-0.5)
539 font.weight: Font.Bold
540 color: scopeStyle ? scopeStyle.foreground : Theme.palette.normal.baseText
551 fillMode: Image.Stretch
552 source: "graphics/dash_divider_top_lightgrad.png"
557 // FIXME Should not rely on model.count but view.count, but ListViewWithPageHeader doesn't expose it yet.
558 visible: index != categoryView.model.count - 1
560 bottom: seeAll.bottom
564 fillMode: Image.Stretch
565 source: "graphics/dash_divider_top_darkgrad.png"
570 sectionProperty: "name"
571 sectionDelegate: ListItems.Header {
572 objectName: "dashSectionHeader" + (delegate ? delegate.category : "")
573 readonly property var delegate: categoryView.item(delegateIndex)
574 width: categoryView.width
575 height: section != "" ? units.gu(5) : 0
577 color: scopeStyle ? scopeStyle.foreground : Theme.palette.normal.baseText
578 iconName: delegate && delegate.headerLink ? "go-next" : ""
580 if (delegate.headerLink) scopeView.scope.performQuery(delegate.headerLink);
584 pageHeader: scopeView.showPageHeader ? pageHeaderLoader : null
588 sourceComponent: scopeView.showPageHeader ? pageHeaderComponent : undefined
590 id: pageHeaderComponent
592 objectName: "scopePageHeader"
594 title: scopeView.scope ? scopeView.scope.name : ""
595 searchHint: scopeView.scope && scopeView.scope.searchHint || i18n.tr("Search")
596 showBackButton: scopeView.hasBackAction
597 searchEntryEnabled: true
598 settingsEnabled: scopeView.scope && scopeView.scope.settings && scopeView.scope.settings.count > 0 || false
599 favoriteEnabled: scopeView.scope && scopeView.scope.id !== "clickscope"
600 favorite: scopeView.scope && scopeView.scope.favorite
601 scopeStyle: scopeView.scopeStyle
602 paginationCount: scopeView.paginationCount
603 paginationIndex: scopeView.paginationIndex
605 bottomItem: DashNavigation {
606 scope: scopeView.scope
607 anchors { left: parent.left; right: parent.right }
608 windowHeight: scopeView.height
609 windowWidth: scopeView.width
610 scopeStyle: scopeView.scopeStyle
613 onBackClicked: scopeView.backClicked()
614 onSettingsClicked: subPageLoader.openSubPage("settings")
615 onFavoriteClicked: scopeView.scope.favorite = !scopeView.scope.favorite
616 onSearchTextFieldFocused: scopeView.showHeader()
623 id: pullToRefreshClippingItem
624 anchors.left: parent.left
625 anchors.right: parent.right
626 anchors.bottom: parent.bottom
627 height: parent.height - pullToRefresh.contentY + (pageHeaderLoader.item ? pageHeaderLoader.item.bottomItem[0].height - pageHeaderLoader.item.height : 0)
632 objectName: "pullToRefresh"
635 readonly property real contentY: categoryView.contentY - categoryView.originY
636 y: -contentY - units.gu(5)
640 scopeView.scope.refresh()
642 anchors.left: parent.left
643 anchors.right: parent.right
647 onProcessingChanged: if (!scopeView.processing) pullToRefresh.refreshing = false
650 style: PullToRefreshScopeStyle {
652 activationThreshold: units.gu(14)
659 objectName: "floatingSeeLess"
661 property Item companionTo: companionBase ? companionBase.seeAllButton : null
662 property Item companionBase: null
663 property bool showBecausePosition: false
664 property real yOffset: 0
667 left: categoryView.left
668 right: categoryView.right
670 y: parent.height - height + yOffset
671 height: seeLessLabel.font.pixelSize + units.gu(4)
672 visible: companionTo && showBecausePosition
674 onClicked: categoryView.expandedCategoryId = "";
676 function updateVisibility() {
677 var companionPos = companionTo.mapToItem(floatingSeeLess, 0, 0);
678 showBecausePosition = companionPos.y > 0;
680 var posToBase = floatingSeeLess.mapToItem(companionBase, 0, -yOffset).y;
681 yOffset = Math.max(0, companionBase.item.collapsedHeight - posToBase);
682 yOffset = Math.min(yOffset, height);
684 if (!showBecausePosition && categoryView.expandedCategoryId === "") {
685 companionBase = null;
691 text: i18n.tr("See less")
694 verticalCenterOffset: units.gu(-0.5)
697 font.weight: Font.Bold
698 color: scopeStyle ? scopeStyle.foreground : Theme.palette.normal.baseText
702 target: floatingSeeLess.companionTo ? categoryView : null
703 onContentYChanged: floatingSeeLess.updateVisibility();
707 target: floatingSeeLess.companionTo
708 onYChanged: floatingSeeLess.updateVisibility();
713 id: previewLimitModel
718 objectName: "subPageLoader"
721 height: parent.height
722 anchors.left: categoryView.right
724 property bool open: false
725 property var scope: scopeView.scope
726 property var scopeStyle: scopeView.scopeStyle
727 property int initialIndex: -1
728 property var model: null
730 readonly property bool processing: item && item.processing || false
731 readonly property int count: item && item.count || 0
732 readonly property int currentIndex: item && item.currentIndex || 0
733 readonly property var currentItem: item && item.currentItem || null
735 property string subPage: ""
736 readonly property bool subPageShown: visible && status === Loader.Ready
738 function openSubPage(page) {
742 function closeSubPage() {
746 source: switch(subPage) {
747 case "preview": return "PreviewListView.qml";
748 case "settings": return "ScopeSettingsPage.qml";
753 item.scope = Qt.binding(function() { return subPageLoader.scope; } )
754 item.scopeStyle = Qt.binding(function() { return subPageLoader.scopeStyle; } )
755 if (subPage == "preview") {
756 item.open = Qt.binding(function() { return subPageLoader.open; } )
757 item.initialIndex = Qt.binding(function() { return subPageLoader.initialIndex; } )
758 item.model = Qt.binding(function() { return subPageLoader.model; } )
763 onOpenChanged: pageHeaderLoader.item.unfocus()
765 onVisibleChanged: if (!visible) subPage = ""
768 target: subPageLoader.item
769 onBackClicked: subPageLoader.closeSubPage()