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 0.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 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
42 property var scopeStyle: ScopeStyle {
43 style: scope ? scope.customizations : {}
46 readonly property bool processing: scope ? scope.searchInProgress || subPageLoader.processing : false
50 function positionAtBeginning() {
51 categoryView.positionAtBeginning()
54 function showHeader() {
55 categoryView.showHeader()
58 function closePreview() {
59 subPageLoader.closeSubPage()
62 function itemClicked(index, result, item, itemModel, resultsModel, limitedCategoryItemCount) {
63 if (itemModel.uri.indexOf("scope://") === 0 || scope.id === "clickscope") {
64 // TODO Technically it is possible that calling activate() will make the scope emit
65 // previewRequested so that we show a preview but there's no scope that does that yet
66 // so it's not implemented
67 scope.activate(result)
69 openPreview(index, resultsModel, limitedCategoryItemCount);
73 function itemPressedAndHeld(index, itemModel, resultsModel, limitedCategoryItemCount) {
74 if (itemModel.uri.indexOf("scope://") !== 0) {
75 openPreview(index, resultsModel, limitedCategoryItemCount);
79 function openPreview(index, resultsModel, limitedCategoryItemCount) {
80 if (limitedCategoryItemCount > 0) {
81 previewLimitModel.model = resultsModel;
82 previewLimitModel.limit = limitedCategoryItemCount;
83 subPageLoader.model = previewLimitModel;
85 subPageLoader.model = resultsModel;
87 subPageLoader.initialIndex = -1;
88 subPageLoader.initialIndex = index;
89 subPageLoader.openSubPage("preview");
95 value: isCurrent && !subPageLoader.open
98 SortFilterProxyModel {
100 model: scope ? scope.categories : null
101 dynamicSortFilter: true
102 filterRole: Categories.RoleCount
107 onIsCurrentChanged: {
108 if (pageHeaderLoader.item && showPageHeader) {
109 pageHeaderLoader.item.resetSearch();
111 subPageLoader.closeSubPage();
115 target: scopeView.scope
116 property: "searchQuery"
117 value: pageHeaderLoader.item ? pageHeaderLoader.item.searchQuery : ""
118 when: isCurrent && showPageHeader
122 target: pageHeaderLoader.item
123 property: "searchQuery"
124 value: scopeView.scope ? scopeView.scope.searchQuery : ""
125 when: isCurrent && showPageHeader
129 target: scopeView.scope
130 onShowDash: subPageLoader.closeSubPage()
131 onHideDash: subPageLoader.closeSubPage()
136 color: scopeView.scopeStyle ? scopeView.scopeStyle.background : "transparent"
137 visible: color != "transparent"
142 objectName: "categoryListView"
144 x: subPageLoader.open ? -width : 0
145 Behavior on x { UbuntuNumberAnimation { } }
147 height: floatingSeeLess.visible ? parent.height - floatingSeeLess.height + floatingSeeLess.yOffset
149 clip: height != parent.height
151 model: scopeView.categories
152 forceNoClip: subPageLoader.open
155 property Item expandedCategoryItem: null
157 readonly property bool pageHeaderTotallyVisible: scopeView.showPageHeader &&
158 ((headerItemShownHeight == 0 && categoryView.contentY <= categoryView.originY) || (headerItemShownHeight == pageHeaderLoader.item.height))
160 delegate: ListItems.Base {
162 objectName: "dashCategory" + category
163 highlightWhenPressed: false
166 property Item seeAllButton: seeAll
168 readonly property bool expandable: {
169 if (categoryView.model.count === 1) return false;
170 if (cardTool.template && cardTool.template["collapsed-rows"] === 0) return false;
171 if (item && item.expandedHeight > item.collapsedHeight) return true;
174 property bool expanded: false
175 readonly property string category: categoryId
176 readonly property string headerLink: model.headerLink
177 readonly property var item: rendererLoader.item
179 function expand(expand, animate) {
180 heightBehaviour.enabled = animate;
186 objectName: "cardTool"
188 template: model.renderer
189 components: model.components
190 viewWidth: parent.width
193 onExpandableChanged: {
194 // This can happen with the VJ that doesn't know how height it will be on creation
195 // so doesn't set expandable until a bit too late for onLoaded
197 var shouldExpand = baseItem === categoryView.expandedCategoryItem;
198 baseItem.expand(shouldExpand, false /*animate*/);
202 onHeightChanged: rendererLoader.updateDelegateCreationRange();
203 onYChanged: rendererLoader.updateDelegateCreationRange();
211 topMargin: name != "" ? 0 : units.gu(2)
217 animation: UbuntuNumberAnimation {
220 heightBehaviour.enabled = false
226 readonly property bool expanded: baseItem.expanded || !baseItem.expandable
227 height: expanded ? item.expandedHeight : item.collapsedHeight
230 switch (cardTool.categoryLayout) {
231 case "carousel": return "CardCarousel.qml";
232 case "vertical-journal": return "CardVerticalJournal.qml";
233 case "horizontal-list": return "CardHorizontalList.qml";
235 default: return "CardGrid.qml";
240 if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
241 item.enableHeightBehavior = scopeView.enableHeightBehaviorOnNextCreation;
242 scopeView.enableHeightBehaviorOnNextCreation = false;
244 item.model = Qt.binding(function() { return results })
245 item.objectName = Qt.binding(function() { return categoryId })
246 item.scopeStyle = scopeView.scopeStyle;
247 if (baseItem.expandable) {
248 var shouldExpand = baseItem === categoryView.expandedCategoryItem;
249 baseItem.expand(shouldExpand, false /*animate*/);
251 updateDelegateCreationRange();
252 if (scope && scope.id === "clickscope" && (categoryId === "predefined" || categoryId === "local")) {
254 cardTool.artShapeSize = Qt.size(units.gu(8), units.gu(7.5));
256 item.cardTool = cardTool;
259 Component.onDestruction: {
260 if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
261 scopeView.enableHeightBehaviorOnNextCreation = item.enableHeightBehaviorOnNextCreation;
266 target: rendererLoader.item
268 scopeView.itemClicked(index, result, item, itemModel, target.model, categoryItemCount());
272 scopeView.itemPressedAndHeld(index, itemModel, target.model, categoryItemCount());
275 function categoryItemCount() {
276 var categoryItemCount = -1;
277 if (!rendererLoader.expanded && !seeAllLabel.visible && target.collapsedItemCount > 0) {
278 categoryItemCount = target.collapsedItemCount;
280 return categoryItemCount;
285 onExpandedCategoryItemChanged: {
286 collapseAllButExpandedCategory();
288 function collapseAllButExpandedCategory() {
289 var item = rendererLoader.item;
290 if (baseItem.expandable) {
291 var shouldExpand = baseItem === categoryView.expandedCategoryItem;
292 if (shouldExpand != baseItem.expanded) {
293 // If the filter animation will be seen start it, otherwise, just flip the switch
294 var shrinkingVisible = !shouldExpand && y + item.collapsedHeight + seeAll.height < categoryView.height;
295 var growingVisible = shouldExpand && y + height < categoryView.height;
296 if (!subPageLoader.open || shouldExpand) {
297 var animate = shrinkingVisible || growingVisible;
298 baseItem.expand(shouldExpand, animate)
299 if (shouldExpand && !subPageLoader.open) {
300 categoryView.maximizeVisibleArea(index, item.expandedHeight + seeAll.height);
306 onOriginYChanged: rendererLoader.updateDelegateCreationRange();
307 onContentYChanged: rendererLoader.updateDelegateCreationRange();
308 onHeightChanged: rendererLoader.updateDelegateCreationRange();
309 onContentHeightChanged: rendererLoader.updateDelegateCreationRange();
312 function updateDelegateCreationRange() {
313 if (categoryView.moving) {
314 // Do not update the range if we are overshooting up or down, since we'll come back
315 // to the stable position and delete/create items without any reason
316 if (categoryView.contentY < categoryView.originY) {
318 } else if (categoryView.contentHeight - categoryView.originY > categoryView.height &&
319 categoryView.contentY + categoryView.height > categoryView.contentHeight) {
324 if (item && item.hasOwnProperty("displayMarginBeginning")) {
325 // TODO do we need item.originY here, test 1300302 once we have a silo
326 // and we can run it on the phone
327 if (baseItem.y + baseItem.height <= 0) {
328 // Not visible (item at top of the list viewport)
329 item.displayMarginBeginning = -baseItem.height;
330 item.displayMarginEnd = 0;
331 } else if (baseItem.y >= categoryView.height) {
332 // Not visible (item at bottom of the list viewport)
333 item.displayMarginBeginning = 0;
334 item.displayMarginEnd = -baseItem.height;
336 item.displayMarginBeginning = -Math.max(-baseItem.y, 0);
337 item.displayMarginEnd = -Math.max(baseItem.height - seeAll.height
338 - categoryView.height + baseItem.y, 0)
348 top: rendererLoader.bottom
352 height: seeAllLabel.visible ? seeAllLabel.font.pixelSize + units.gu(4) : 0
355 if (categoryView.expandedCategoryItem !== baseItem) {
356 categoryView.expandedCategoryItem = baseItem;
358 categoryView.expandedCategoryItem = null;
364 text: baseItem.expanded ? i18n.tr("See less") : i18n.tr("See all")
367 verticalCenterOffset: units.gu(-0.5)
370 font.weight: Font.Bold
371 color: scopeStyle ? scopeStyle.foreground : Theme.palette.normal.baseText
372 visible: baseItem.expandable && !baseItem.headerLink
383 fillMode: Image.Stretch
384 source: "graphics/dash_divider_top_lightgrad.png"
389 // FIXME Should not rely on model.count but view.count, but ListViewWithPageHeader doesn't expose it yet.
390 visible: index != categoryView.model.count - 1
392 bottom: seeAll.bottom
396 fillMode: Image.Stretch
397 source: "graphics/dash_divider_top_darkgrad.png"
402 sectionProperty: "name"
403 sectionDelegate: ListItems.Header {
404 objectName: "dashSectionHeader" + (delegate ? delegate.category : "")
405 readonly property var delegate: categoryView.item(delegateIndex)
406 width: categoryView.width
407 height: section != "" ? units.gu(5) : 0
409 color: scopeStyle ? scopeStyle.foreground : Theme.palette.normal.baseText
410 iconName: delegate && delegate.headerLink ? "go-next" : ""
412 if (delegate.headerLink) scopeView.scope.performQuery(delegate.headerLink);
416 pageHeader: scopeView.showPageHeader ? pageHeaderLoader : null
420 sourceComponent: scopeView.showPageHeader ? pageHeaderComponent : undefined
422 id: pageHeaderComponent
424 objectName: "scopePageHeader"
426 title: scopeView.scope ? scopeView.scope.name : ""
427 searchHint: scopeView.scope && scopeView.scope.searchHint || i18n.tr("Search")
428 showBackButton: scopeView.hasBackAction
429 searchEntryEnabled: true
430 settingsEnabled: scopeView.scope && scopeView.scope.settings && scopeView.scope.settings.count > 0 || false
431 favoriteEnabled: scopeView.scope && scopeView.scope.id !== "clickscope"
432 favorite: scopeView.scope && scopeView.scope.favorite
433 scopeStyle: scopeView.scopeStyle
434 paginationCount: scopeView.paginationCount
435 paginationIndex: scopeView.paginationIndex
437 bottomItem: DashNavigation {
438 scope: scopeView.scope
439 anchors { left: parent.left; right: parent.right }
440 windowHeight: scopeView.height
441 windowWidth: scopeView.width
442 scopeStyle: scopeView.scopeStyle
445 onBackClicked: scopeView.backClicked()
446 onSettingsClicked: subPageLoader.openSubPage("settings")
447 onFavoriteClicked: scopeView.scope.favorite = !scopeView.scope.favorite
455 objectName: "floatingSeeLess"
457 property Item companionTo: companionBase ? companionBase.seeAllButton : null
458 property Item companionBase: categoryView.expandedCategoryItem
459 property bool showBecausePosition: false
460 property real yOffset: 0
463 left: categoryView.left
464 right: categoryView.right
466 y: parent.height - height + yOffset
467 height: seeLessLabel.font.pixelSize + units.gu(4)
468 visible: companionTo && showBecausePosition
470 onClicked: categoryView.expandedCategoryItem = null;
472 function updateVisibility() {
473 var companionPos = companionTo.mapToItem(floatingSeeLess, 0, 0);
474 showBecausePosition = companionPos.y > 0;
476 var posToBase = floatingSeeLess.mapToItem(companionBase, 0, -yOffset).y;
477 yOffset = Math.max(0, companionBase.item.collapsedHeight - posToBase);
478 yOffset = Math.min(yOffset, height);
483 text: i18n.tr("See less")
486 verticalCenterOffset: units.gu(-0.5)
489 font.weight: Font.Bold
490 color: scopeStyle ? scopeStyle.foreground : Theme.palette.normal.baseText
494 target: floatingSeeLess.companionTo ? categoryView : null
495 onContentYChanged: floatingSeeLess.updateVisibility();
499 target: floatingSeeLess.companionTo
500 onYChanged: floatingSeeLess.updateVisibility();
505 id: previewLimitModel
510 objectName: "subPageLoader"
513 height: parent.height
514 anchors.left: categoryView.right
516 property bool open: false
517 property var scope: scopeView.scope
518 property var scopeStyle: scopeView.scopeStyle
519 property int initialIndex: -1
520 property var model: null
522 readonly property bool processing: item && item.processing || false
523 readonly property int count: item && item.count || 0
524 readonly property int currentIndex: item && item.currentIndex || 0
525 readonly property var currentItem: item && item.currentItem || null
527 property string subPage: ""
528 readonly property bool subPageShown: visible && status === Loader.Ready
530 function openSubPage(page) {
534 function closeSubPage() {
538 source: switch(subPage) {
539 case "preview": return "PreviewListView.qml";
540 case "settings": return "ScopeSettingsPage.qml";
545 item.scope = Qt.binding(function() { return subPageLoader.scope; } )
546 item.scopeStyle = Qt.binding(function() { return subPageLoader.scopeStyle; } )
547 if (subPage == "preview") {
548 item.open = Qt.binding(function() { return subPageLoader.open; } )
549 item.initialIndex = Qt.binding(function() { return subPageLoader.initialIndex; } )
550 item.model = Qt.binding(function() { return subPageLoader.model; } )
555 onOpenChanged: pageHeaderLoader.item.unfocus()
557 onVisibleChanged: if (!visible) subPage = ""
560 target: subPageLoader.item
561 onBackClicked: subPageLoader.closeSubPage()