on 2dc0cae help sign in
unity8/qml/Dash/GenericScopeView.qml
695 lines 30 kB history raw
1/*
2 * Copyright (C) 2013-2014 Canonical, Ltd.
3 *
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.
7 *
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.
12 *
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/>.
15 */
17import QtQuick 2.0
18import Ubuntu.Components 1.1
19import Utils 0.1
20import Unity 0.2
21import Dash 0.1
22import "../Components"
23import "../Components/ListItems" as ListItems
25FocusScope {
26 id: scopeView
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 bool visibleToParent: false
42 property alias pageHeaderTotallyVisible: categoryView.pageHeaderTotallyVisible
43 property var holdingList: null
45 property var scopeStyle: ScopeStyle {
46 style: scope ? scope.customizations : {}
47 }
49 readonly property bool processing: scope ? scope.searchInProgress || subPageLoader.processing : false
51 signal backClicked()
53 onScopeChanged: {
54 floatingSeeLess.companionBase = null;
55 }
57 function positionAtBeginning() {
58 categoryView.positionAtBeginning()
59 }
61 function showHeader() {
62 categoryView.showHeader()
63 }
65 function closePreview() {
66 subPageLoader.closeSubPage()
67 }
69 function itemClicked(index, result, item, itemModel, resultsModel, limitedCategoryItemCount) {
70 if (itemModel.uri.indexOf("scope://") === 0 || scope.id === "clickscope") {
71 // TODO Technically it is possible that calling activate() will make the scope emit
72 // previewRequested so that we show a preview but there's no scope that does that yet
73 // so it's not implemented
74 scope.activate(result)
75 } else {
76 if (scope.preview(result)) {
77 openPreview(index, resultsModel, limitedCategoryItemCount);
78 }
79 }
80 }
82 function itemPressedAndHeld(index, result, itemModel, resultsModel, limitedCategoryItemCount) {
83 if (itemModel.uri.indexOf("scope://") !== 0) {
84 if (scope.preview(result)) {
85 openPreview(index, resultsModel, limitedCategoryItemCount);
86 }
87 }
88 }
90 function openPreview(index, resultsModel, limitedCategoryItemCount) {
91 if (limitedCategoryItemCount > 0) {
92 previewLimitModel.model = resultsModel;
93 previewLimitModel.limit = limitedCategoryItemCount;
94 subPageLoader.model = previewLimitModel;
95 } else {
96 subPageLoader.model = resultsModel;
97 }
98 subPageLoader.initialIndex = -1;
99 subPageLoader.initialIndex = index;
100 subPageLoader.openSubPage("preview");
101 }
103 Binding {
104 target: scope
105 property: "isActive"
106 value: isCurrent && !subPageLoader.open
107 }
109 SortFilterProxyModel {
110 id: categoryFilter
111 model: scope ? scope.categories : null
112 dynamicSortFilter: true
113 filterRole: Categories.RoleCount
114 filterRegExp: /^0$/
115 invertMatch: true
116 }
118 onIsCurrentChanged: {
119 if (pageHeaderLoader.item && showPageHeader) {
120 pageHeaderLoader.item.resetSearch();
121 }
122 subPageLoader.closeSubPage();
123 }
125 Binding {
126 target: scopeView.scope
127 property: "searchQuery"
128 value: pageHeaderLoader.item ? pageHeaderLoader.item.searchQuery : ""
129 when: isCurrent && showPageHeader
130 }
132 Binding {
133 target: pageHeaderLoader.item
134 property: "searchQuery"
135 value: scopeView.scope ? scopeView.scope.searchQuery : ""
136 when: isCurrent && showPageHeader
137 }
139 Connections {
140 target: scopeView.scope
141 onShowDash: subPageLoader.closeSubPage()
142 onHideDash: subPageLoader.closeSubPage()
143 }
145 Rectangle {
146 anchors.fill: parent
147 color: scopeView.scopeStyle ? scopeView.scopeStyle.background : "transparent"
148 visible: color != "transparent"
149 }
151 ScopeListView {
152 id: categoryView
153 objectName: "categoryListView"
154 interactive: !forceNonInteractive
156 x: subPageLoader.open ? -width : 0
157 visible: x != -width
158 Behavior on x { UbuntuNumberAnimation { } }
159 width: parent.width
160 height: floatingSeeLess.visible ? parent.height - floatingSeeLess.height + floatingSeeLess.yOffset
161 : parent.height
162 clip: height != parent.height
164 model: scopeView.categories
165 forceNoClip: subPageLoader.open
166 pixelAligned: true
168 property string expandedCategoryId: ""
169 property int runMaximizeAfterSizeChanges: 0
171 readonly property bool pageHeaderTotallyVisible: scopeView.showPageHeader &&
172 ((headerItemShownHeight == 0 && categoryView.contentY <= categoryView.originY) || (headerItemShownHeight == pageHeaderLoader.item.height))
174 onExpandedCategoryIdChanged: {
175 var firstCreated = firstCreatedIndex();
176 var shrinkingAny = false;
177 var shrinkHeightDifference = 0;
178 for (var i = 0; i < createdItemCount(); ++i) {
179 var baseItem = item(firstCreated + i);
180 if (baseItem.expandable) {
181 var shouldExpand = baseItem.category === expandedCategoryId;
182 if (shouldExpand != baseItem.expanded) {
183 var animate = false;
184 if (!subPageLoader.open) {
185 var animateShrinking = !shouldExpand && baseItem.y + baseItem.item.collapsedHeight + baseItem.seeAllButton.height < categoryView.height;
186 var animateGrowing = shouldExpand && baseItem.y + baseItem.height < categoryView.height;
187 animate = shrinkingAny || animateShrinking || animateGrowing;
188 }
190 if (!shouldExpand) {
191 shrinkingAny = true;
192 shrinkHeightDifference = baseItem.item.expandedHeight - baseItem.item.collapsedHeight;
193 }
195 if (shouldExpand && !subPageLoader.open) {
196 if (!shrinkingAny) {
197 categoryView.maximizeVisibleArea(firstCreated + i, baseItem.item.expandedHeight + baseItem.seeAllButton.height);
198 } else {
199 // If the space that shrinking is smaller than the one we need to grow we'll call maximizeVisibleArea
200 // after the shrink/grow animation ends
201 var growHeightDifference = baseItem.item.expandedHeight - baseItem.item.collapsedHeight;
202 if (growHeightDifference > shrinkHeightDifference) {
203 runMaximizeAfterSizeChanges = 2;
204 } else {
205 runMaximizeAfterSizeChanges = 0;
206 }
207 }
208 }
210 baseItem.expand(shouldExpand, animate);
211 }
212 }
213 }
214 }
216 delegate: DashCategoryBase {
217 id: baseItem
218 objectName: "dashCategory" + category
220 property Item seeAllButton: seeAll
222 readonly property bool expandable: {
223 if (categoryView.model.count === 1) return false;
224 if (cardTool.template && cardTool.template["collapsed-rows"] === 0) return false;
225 if (item && item.expandedHeight > item.collapsedHeight) return true;
226 return false;
227 }
228 property bool expanded: false
229 readonly property string category: categoryId
230 readonly property string headerLink: model.headerLink
231 readonly property var item: rendererLoader.item
233 function expand(expand, animate) {
234 heightBehaviour.enabled = animate;
235 expanded = expand;
236 }
238 CardTool {
239 id: cardTool
240 objectName: "cardTool"
241 count: results ? results.count : 0
242 template: model.renderer
243 components: model.components
244 viewWidth: parent.width
245 }
247 onExpandableChanged: {
248 // This can happen with the VJ that doesn't know how height it will be on creation
249 // so doesn't set expandable until a bit too late for onLoaded
250 if (expandable) {
251 var shouldExpand = baseItem.category === categoryView.expandedCategoryId;
252 baseItem.expand(shouldExpand, false /*animate*/);
253 }
254 }
256 onHeightChanged: rendererLoader.updateRanges();
257 onYChanged: rendererLoader.updateRanges();
259 Loader {
260 id: rendererLoader
261 anchors {
262 top: parent.top
263 left: parent.left
264 right: parent.right
265 topMargin: name != "" ? 0 : units.gu(2)
266 }
268 Behavior on height {
269 id: heightBehaviour
270 enabled: false
271 animation: UbuntuNumberAnimation {
272 duration: UbuntuAnimation.FastDuration
273 onRunningChanged: {
274 if (!running) {
275 heightBehaviour.enabled = false
276 if (categoryView.runMaximizeAfterSizeChanges > 0) {
277 categoryView.runMaximizeAfterSizeChanges--;
278 if (categoryView.runMaximizeAfterSizeChanges == 0) {
279 var firstCreated = categoryView.firstCreatedIndex();
280 for (var i = 0; i < categoryView.createdItemCount(); ++i) {
281 var baseItem = categoryView.item(firstCreated + i);
282 if (baseItem.category === categoryView.expandedCategoryId) {
283 categoryView.maximizeVisibleArea(firstCreated + i, baseItem.item.expandedHeight + baseItem.seeAllButton.height);
284 break;
285 }
286 }
287 }
288 }
289 }
290 }
291 }
292 }
294 readonly property bool expanded: baseItem.expanded || !baseItem.expandable
295 height: expanded ? item.expandedHeight : item.collapsedHeight
297 source: {
298 switch (cardTool.categoryLayout) {
299 case "carousel": return "CardCarousel.qml";
300 case "vertical-journal": return "CardVerticalJournal.qml";
301 case "horizontal-list": return "CardHorizontalList.qml";
302 case "grid":
303 default: return "CardGrid.qml";
304 }
305 }
307 onLoaded: {
308 if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
309 item.enableHeightBehavior = scopeView.enableHeightBehaviorOnNextCreation;
310 scopeView.enableHeightBehaviorOnNextCreation = false;
311 }
312 item.model = Qt.binding(function() { return results })
313 item.objectName = Qt.binding(function() { return categoryId })
314 item.scopeStyle = scopeView.scopeStyle;
315 if (baseItem.expandable) {
316 var shouldExpand = baseItem.category === categoryView.expandedCategoryId;
317 baseItem.expand(shouldExpand, false /*animate*/);
318 }
319 updateRanges();
320 if (scope && scope.id === "clickscope" && (categoryId === "predefined" || categoryId === "local")) {
321 // Yeah, hackish :/
322 cardTool.artShapeSize = Qt.size(units.gu(8), units.gu(7.5));
323 }
324 item.cardTool = cardTool;
325 }
327 Component.onDestruction: {
328 if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
329 scopeView.enableHeightBehaviorOnNextCreation = item.enableHeightBehaviorOnNextCreation;
330 }
331 }
333 Connections {
334 target: rendererLoader.item
335 onClicked: {
336 scopeView.itemClicked(index, result, item, itemModel, target.model, categoryItemCount());
337 }
339 onPressAndHold: {
340 scopeView.itemPressedAndHeld(index, result, itemModel, target.model, categoryItemCount());
341 }
343 function categoryItemCount() {
344 var categoryItemCount = -1;
345 if (!rendererLoader.expanded && !seeAllLabel.visible && target.collapsedItemCount > 0) {
346 categoryItemCount = target.collapsedItemCount;
347 }
348 return categoryItemCount;
349 }
350 }
351 Connections {
352 target: categoryView
353 onOriginYChanged: rendererLoader.updateRanges();
354 onContentYChanged: rendererLoader.updateRanges();
355 onHeightChanged: rendererLoader.updateRanges();
356 onContentHeightChanged: rendererLoader.updateRanges();
357 }
358 Connections {
359 target: scopeView
360 onIsCurrentChanged: rendererLoader.updateRanges();
361 onVisibleToParentChanged: rendererLoader.updateRanges();
362 }
363 Connections {
364 target: holdingList
365 onMovingChanged: if (!moving) rendererLoader.updateRanges();
366 }
368 function updateRanges() {
369 // Don't want to create stress by requesting more items during scope
370 // changes so unless you're not part of the visible scopes just return
371 if (holdingList && holdingList.moving && !scopeView.visibleToParent) {
372 return;
373 }
375 if (categoryView.moving) {
376 // Do not update the range if we are overshooting up or down, since we'll come back
377 // to the stable position and delete/create items without any reason
378 if (categoryView.contentY < categoryView.originY) {
379 return;
380 } else if (categoryView.contentHeight - categoryView.originY > categoryView.height &&
381 categoryView.contentY + categoryView.height > categoryView.contentHeight) {
382 return;
383 }
384 }
386 if (item && item.hasOwnProperty("displayMarginBeginning")) {
387 // A item view creates its delegates synchronously from
388 // -displayMarginBeginning
389 // to
390 // height + displayMarginEnd
391 // Around that area it adds the cacheBuffer area where delegates are created async
392 //
393 // We adjust displayMarginBeginning and displayMarginEnd so
394 // * In non visible scopes nothing is considered visible and we set cacheBuffer
395 // so that creates the items that would be in the viewport asynchronously
396 // * For the current scope set the visible range to the viewport and then
397 // use cacheBuffer to create extra items for categoryView.height * 1.5
398 // to make scrolling nicer by mantaining a higher number of
399 // cached items
400 // * For non current but visible scopes (i.e. when the user changes from one scope
401 // to the next, we set the visible range to the viewport so
402 // items are not culled (invisible) but still use no cacheBuffer
403 // (it will be set once the scope is the current one)
404 var displayMarginBeginning = baseItem.y;
405 displayMarginBeginning = -Math.max(-displayMarginBeginning, 0);
406 displayMarginBeginning = -Math.min(-displayMarginBeginning, baseItem.height);
407 displayMarginBeginning = Math.round(displayMarginBeginning);
408 var displayMarginEnd = -baseItem.height + seeAll.height + categoryView.height - baseItem.y;
409 displayMarginEnd = -Math.max(-displayMarginEnd, 0);
410 displayMarginEnd = -Math.min(-displayMarginEnd, baseItem.height);
411 displayMarginEnd = Math.round(displayMarginEnd);
412 if (scopeView.isCurrent || scopeView.visibleToParent) {
413 item.displayMarginBeginning = displayMarginBeginning;
414 item.displayMarginEnd = displayMarginEnd;
415 item.cacheBuffer = scopeView.isCurrent ? categoryView.height * 1.5 : 0;
416 } else {
417 var visibleRange = baseItem.height + displayMarginEnd + displayMarginBeginning;
418 if (visibleRange < 0) {
419 item.displayMarginBeginning = displayMarginBeginning;
420 item.displayMarginEnd = displayMarginEnd;
421 item.cacheBuffer = 0;
422 } else {
423 // This should be visibleRange/2 in each of the properties
424 // but some item views still (like GridView) like creating sync delegates even if
425 // the visible range is 0 so let's make sure the visible range is negative
426 item.displayMarginBeginning = displayMarginBeginning - visibleRange;
427 item.displayMarginEnd = displayMarginEnd - visibleRange;
428 item.cacheBuffer = visibleRange;
429 }
430 }
431 }
432 }
433 }
435 AbstractButton {
436 id: seeAll
437 objectName: "seeAll"
438 anchors {
439 top: rendererLoader.bottom
440 left: parent.left
441 right: parent.right
442 }
443 height: baseItem.expandable && !baseItem.headerLink ? seeAllLabel.font.pixelSize + units.gu(4) : 0
444 visible: height != 0
446 onClicked: {
447 if (categoryView.expandedCategoryId !== baseItem.category) {
448 categoryView.expandedCategoryId = baseItem.category;
449 floatingSeeLess.companionBase = baseItem;
450 } else {
451 categoryView.expandedCategoryId = "";
452 }
453 }
455 Label {
456 id: seeAllLabel
457 text: baseItem.expanded ? i18n.tr("See less") : i18n.tr("See all")
458 anchors {
459 centerIn: parent
460 verticalCenterOffset: units.gu(-0.5)
461 }
462 fontSize: "small"
463 font.weight: Font.Bold
464 color: scopeStyle ? scopeStyle.foreground : Theme.palette.normal.baseText
465 }
466 }
468 Image {
469 visible: index != 0
470 anchors {
471 top: parent.top
472 left: parent.left
473 right: parent.right
474 }
475 fillMode: Image.Stretch
476 source: "graphics/dash_divider_top_lightgrad.png"
477 z: -1
478 }
480 Image {
481 // FIXME Should not rely on model.count but view.count, but ListViewWithPageHeader doesn't expose it yet.
482 visible: index != categoryView.model.count - 1
483 anchors {
484 bottom: seeAll.bottom
485 left: parent.left
486 right: parent.right
487 }
488 fillMode: Image.Stretch
489 source: "graphics/dash_divider_top_darkgrad.png"
490 z: -1
491 }
492 }
494 sectionProperty: "name"
495 sectionDelegate: ListItems.Header {
496 objectName: "dashSectionHeader" + (delegate ? delegate.category : "")
497 readonly property var delegate: categoryView.item(delegateIndex)
498 width: categoryView.width
499 height: section != "" ? units.gu(5) : 0
500 text: section
501 color: scopeStyle ? scopeStyle.foreground : Theme.palette.normal.baseText
502 iconName: delegate && delegate.headerLink ? "go-next" : ""
503 onClicked: {
504 if (delegate.headerLink) scopeView.scope.performQuery(delegate.headerLink);
505 }
506 }
508 pageHeader: scopeView.showPageHeader ? pageHeaderLoader : null
509 Loader {
510 id: pageHeaderLoader
511 width: parent.width
512 sourceComponent: scopeView.showPageHeader ? pageHeaderComponent : undefined
513 Component {
514 id: pageHeaderComponent
515 PageHeader {
516 objectName: "scopePageHeader"
517 width: parent.width
518 title: scopeView.scope ? scopeView.scope.name : ""
519 searchHint: scopeView.scope && scopeView.scope.searchHint || i18n.tr("Search")
520 showBackButton: scopeView.hasBackAction
521 searchEntryEnabled: true
522 settingsEnabled: scopeView.scope && scopeView.scope.settings && scopeView.scope.settings.count > 0 || false
523 favoriteEnabled: scopeView.scope
524 favorite: scopeView.scope && scopeView.scope.favorite
525 scopeStyle: scopeView.scopeStyle
526 paginationCount: scopeView.paginationCount
527 paginationIndex: scopeView.paginationIndex
529 bottomItem: DashNavigation {
530 scope: scopeView.scope
531 anchors { left: parent.left; right: parent.right }
532 windowHeight: scopeView.height
533 windowWidth: scopeView.width
534 scopeStyle: scopeView.scopeStyle
535 }
537 onBackClicked: scopeView.backClicked()
538 onSettingsClicked: subPageLoader.openSubPage("settings")
539 onFavoriteClicked: scopeView.scope.favorite = !scopeView.scope.favorite
540 }
541 }
542 }
543 }
545 Item {
546 id: pullToRefreshClippingItem
547 anchors.left: parent.left
548 anchors.right: parent.right
549 anchors.bottom: parent.bottom
550 height: parent.height - pullToRefresh.contentY + (pageHeaderLoader.item ? pageHeaderLoader.item.bottomItem[0].height - pageHeaderLoader.item.height : 0)
551 clip: true
553 PullToRefresh {
554 id: pullToRefresh
555 objectName: "pullToRefresh"
556 target: categoryView
558 readonly property real contentY: categoryView.contentY - categoryView.originY
559 y: -contentY - units.gu(5)
561 onRefresh: {
562 refreshing = true
563 scopeView.scope.refresh()
564 }
565 anchors.left: parent.left
566 anchors.right: parent.right
568 Connections {
569 target: scopeView
570 onProcessingChanged: if (!scopeView.processing) pullToRefresh.refreshing = false
571 }
573 style: PullToRefreshScopeStyle {
574 anchors.fill: parent
575 activationThreshold: units.gu(14)
576 }
577 }
578 }
580 AbstractButton {
581 id: floatingSeeLess
582 objectName: "floatingSeeLess"
584 property Item companionTo: companionBase ? companionBase.seeAllButton : null
585 property Item companionBase: null
586 property bool showBecausePosition: false
587 property real yOffset: 0
589 anchors {
590 left: categoryView.left
591 right: categoryView.right
592 }
593 y: parent.height - height + yOffset
594 height: seeLessLabel.font.pixelSize + units.gu(4)
595 visible: companionTo && showBecausePosition
597 onClicked: categoryView.expandedCategoryId = "";
599 function updateVisibility() {
600 var companionPos = companionTo.mapToItem(floatingSeeLess, 0, 0);
601 showBecausePosition = companionPos.y > 0;
603 var posToBase = floatingSeeLess.mapToItem(companionBase, 0, -yOffset).y;
604 yOffset = Math.max(0, companionBase.item.collapsedHeight - posToBase);
605 yOffset = Math.min(yOffset, height);
607 if (!showBecausePosition && categoryView.expandedCategoryId === "") {
608 companionBase = null;
609 }
610 }
612 Label {
613 id: seeLessLabel
614 text: i18n.tr("See less")
615 anchors {
616 centerIn: parent
617 verticalCenterOffset: units.gu(-0.5)
618 }
619 fontSize: "small"
620 font.weight: Font.Bold
621 color: scopeStyle ? scopeStyle.foreground : Theme.palette.normal.baseText
622 }
624 Connections {
625 target: floatingSeeLess.companionTo ? categoryView : null
626 onContentYChanged: floatingSeeLess.updateVisibility();
627 }
629 Connections {
630 target: floatingSeeLess.companionTo
631 onYChanged: floatingSeeLess.updateVisibility();
632 }
633 }
635 LimitProxyModel {
636 id: previewLimitModel
637 }
639 Loader {
640 id: subPageLoader
641 objectName: "subPageLoader"
642 visible: x != width
643 width: parent.width
644 height: parent.height
645 anchors.left: categoryView.right
647 property bool open: false
648 property var scope: scopeView.scope
649 property var scopeStyle: scopeView.scopeStyle
650 property int initialIndex: -1
651 property var model: null
653 readonly property bool processing: item && item.processing || false
654 readonly property int count: item && item.count || 0
655 readonly property int currentIndex: item && item.currentIndex || 0
656 readonly property var currentItem: item && item.currentItem || null
658 property string subPage: ""
659 readonly property bool subPageShown: visible && status === Loader.Ready
661 function openSubPage(page) {
662 subPage = page;
663 }
665 function closeSubPage() {
666 open = false;
667 }
669 source: switch(subPage) {
670 case "preview": return "PreviewListView.qml";
671 case "settings": return "ScopeSettingsPage.qml";
672 default: return "";
673 }
675 onLoaded: {
676 item.scope = Qt.binding(function() { return subPageLoader.scope; } )
677 item.scopeStyle = Qt.binding(function() { return subPageLoader.scopeStyle; } )
678 if (subPage == "preview") {
679 item.open = Qt.binding(function() { return subPageLoader.open; } )
680 item.initialIndex = Qt.binding(function() { return subPageLoader.initialIndex; } )
681 item.model = Qt.binding(function() { return subPageLoader.model; } )
682 }
683 open = true;
684 }
686 onOpenChanged: pageHeaderLoader.item.unfocus()
688 onVisibleChanged: if (!visible) subPage = ""
690 Connections {
691 target: subPageLoader.item
692 onBackClicked: subPageLoader.closeSubPage()
693 }
694 }