on 5ef5f08 help sign in
unity8/qml/Dash/GenericScopeView.qml
654 lines 28 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 0.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].showList : 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
41 property var holdingList: null
43 property var scopeStyle: ScopeStyle {
44 style: scope ? scope.customizations : {}
45 }
47 readonly property bool processing: scope ? scope.searchInProgress || subPageLoader.processing : false
49 signal backClicked()
51 onScopeChanged: {
52 floatingSeeLess.companionBase = null;
53 }
55 function positionAtBeginning() {
56 categoryView.positionAtBeginning()
57 }
59 function showHeader() {
60 categoryView.showHeader()
61 }
63 function closePreview() {
64 subPageLoader.closeSubPage()
65 }
67 function itemClicked(index, result, item, itemModel, resultsModel, limitedCategoryItemCount) {
68 if (itemModel.uri.indexOf("scope://") === 0 || scope.id === "clickscope") {
69 // TODO Technically it is possible that calling activate() will make the scope emit
70 // previewRequested so that we show a preview but there's no scope that does that yet
71 // so it's not implemented
72 scope.activate(result)
73 } else {
74 openPreview(index, resultsModel, limitedCategoryItemCount);
75 }
76 }
78 function itemPressedAndHeld(index, itemModel, resultsModel, limitedCategoryItemCount) {
79 if (itemModel.uri.indexOf("scope://") !== 0) {
80 openPreview(index, resultsModel, limitedCategoryItemCount);
81 }
82 }
84 function openPreview(index, resultsModel, limitedCategoryItemCount) {
85 if (limitedCategoryItemCount > 0) {
86 previewLimitModel.model = resultsModel;
87 previewLimitModel.limit = limitedCategoryItemCount;
88 subPageLoader.model = previewLimitModel;
89 } else {
90 subPageLoader.model = resultsModel;
91 }
92 subPageLoader.initialIndex = -1;
93 subPageLoader.initialIndex = index;
94 subPageLoader.openSubPage("preview");
95 }
97 Binding {
98 target: scope
99 property: "isActive"
100 value: isCurrent && !subPageLoader.open
101 }
103 SortFilterProxyModel {
104 id: categoryFilter
105 model: scope ? scope.categories : null
106 dynamicSortFilter: true
107 filterRole: Categories.RoleCount
108 filterRegExp: /^0$/
109 invertMatch: true
110 }
112 onIsCurrentChanged: {
113 if (pageHeaderLoader.item && showPageHeader) {
114 pageHeaderLoader.item.resetSearch();
115 }
116 subPageLoader.closeSubPage();
117 }
119 Binding {
120 target: scopeView.scope
121 property: "searchQuery"
122 value: pageHeaderLoader.item ? pageHeaderLoader.item.searchQuery : ""
123 when: isCurrent && showPageHeader
124 }
126 Binding {
127 target: pageHeaderLoader.item
128 property: "searchQuery"
129 value: scopeView.scope ? scopeView.scope.searchQuery : ""
130 when: isCurrent && showPageHeader
131 }
133 Connections {
134 target: scopeView.scope
135 onShowDash: subPageLoader.closeSubPage()
136 onHideDash: subPageLoader.closeSubPage()
137 }
139 Rectangle {
140 anchors.fill: parent
141 color: scopeView.scopeStyle ? scopeView.scopeStyle.background : "transparent"
142 visible: color != "transparent"
143 }
145 ScopeListView {
146 id: categoryView
147 objectName: "categoryListView"
149 x: subPageLoader.open ? -width : 0
150 Behavior on x { UbuntuNumberAnimation { } }
151 width: parent.width
152 height: floatingSeeLess.visible ? parent.height - floatingSeeLess.height + floatingSeeLess.yOffset
153 : parent.height
154 clip: height != parent.height
156 model: scopeView.categories
157 forceNoClip: subPageLoader.open
158 pixelAligned: true
159 interactive: !navigationShown
161 property string expandedCategoryId: ""
162 property int runMaximizeAfterSizeChanges: 0
164 readonly property bool pageHeaderTotallyVisible: scopeView.showPageHeader &&
165 ((headerItemShownHeight == 0 && categoryView.contentY <= categoryView.originY) || (headerItemShownHeight == pageHeaderLoader.item.height))
167 onExpandedCategoryIdChanged: {
168 var firstCreated = firstCreatedIndex();
169 var shrinkingAny = false;
170 var shrinkHeightDifference = 0;
171 for (var i = 0; i < createdItemCount(); ++i) {
172 var baseItem = item(firstCreated + i);
173 if (baseItem.expandable) {
174 var shouldExpand = baseItem.category === expandedCategoryId;
175 if (shouldExpand != baseItem.expanded) {
177 var animate = false;
178 if (!subPageLoader.open) {
179 var animateShrinking = !shouldExpand && baseItem.y + baseItem.item.collapsedHeight + baseItem.seeAll.height < categoryView.height;
180 var animateGrowing = shouldExpand && baseItem.y + baseItem.height < categoryView.height;
181 animate = shrinkingAny || animateShrinking || animateGrowing;
182 }
184 if (!shouldExpand) {
185 shrinkingAny = true;
186 shrinkHeightDifference = baseItem.item.expandedHeight - baseItem.item.collapsedHeight;
187 }
189 if (shouldExpand && !subPageLoader.open) {
190 if (!shrinkingAny) {
191 categoryView.maximizeVisibleArea(firstCreated + i, baseItem.item.expandedHeight + baseItem.seeAll.height);
192 } else {
193 // If the space that shrkinking is smaller than the one we need to grow we'll call maximizeVisibleArea
194 // after the shrink/grow animation ends
195 var growHeightDifference = baseItem.item.expandedHeight - baseItem.item.collapsedHeight;
196 if (growHeightDifference > shrinkHeightDifference) {
197 runMaximizeAfterSizeChanges = 2;
198 } else {
199 runMaximizeAfterSizeChanges = 0;
200 }
201 }
202 }
204 baseItem.expand(shouldExpand, animate);
205 }
206 }
207 }
208 }
210 delegate: ListItems.Base {
211 id: baseItem
212 objectName: "dashCategory" + category
213 highlightWhenPressed: false
214 showDivider: false
216 property Item seeAllButton: seeAll
218 readonly property bool expandable: {
219 if (categoryView.model.count === 1) return false;
220 if (cardTool.template && cardTool.template["collapsed-rows"] === 0) return false;
221 if (item && item.expandedHeight > item.collapsedHeight) return true;
222 return false;
223 }
224 property bool expanded: false
225 readonly property string category: categoryId
226 readonly property string headerLink: model.headerLink
227 readonly property var item: rendererLoader.item
228 readonly property var seeAll: seeAll
230 function expand(expand, animate) {
231 heightBehaviour.enabled = animate;
232 expanded = expand;
233 }
235 CardTool {
236 id: cardTool
237 objectName: "cardTool"
238 count: results.count
239 template: model.renderer
240 components: model.components
241 viewWidth: parent.width
242 }
244 onExpandableChanged: {
245 // This can happen with the VJ that doesn't know how height it will be on creation
246 // so doesn't set expandable until a bit too late for onLoaded
247 if (expandable) {
248 var shouldExpand = baseItem.category === categoryView.expandedCategoryId;
249 baseItem.expand(shouldExpand, false /*animate*/);
250 }
251 }
253 onHeightChanged: rendererLoader.updateRanges();
254 onYChanged: rendererLoader.updateRanges();
256 Loader {
257 id: rendererLoader
258 anchors {
259 top: parent.top
260 left: parent.left
261 right: parent.right
262 topMargin: name != "" ? 0 : units.gu(2)
263 }
265 Behavior on height {
266 id: heightBehaviour
267 enabled: false
268 animation: UbuntuNumberAnimation {
269 duration: UbuntuAnimation.FastDuration
270 onRunningChanged: {
271 if (!running) {
272 heightBehaviour.enabled = false
273 if (categoryView.runMaximizeAfterSizeChanges > 0) {
274 categoryView.runMaximizeAfterSizeChanges--;
275 if (categoryView.runMaximizeAfterSizeChanges == 0) {
276 var firstCreated = categoryView.firstCreatedIndex();
277 for (var i = 0; i < categoryView.createdItemCount(); ++i) {
278 var baseItem = categoryView.item(firstCreated + i);
279 if (baseItem.category === categoryView.expandedCategoryId) {
280 categoryView.maximizeVisibleArea(firstCreated + i, baseItem.item.expandedHeight + baseItem.seeAll.height);
281 break;
282 }
283 }
284 }
285 }
286 }
287 }
288 }
289 }
291 readonly property bool expanded: baseItem.expanded || !baseItem.expandable
292 height: expanded ? item.expandedHeight : item.collapsedHeight
294 source: {
295 switch (cardTool.categoryLayout) {
296 case "carousel": return "CardCarousel.qml";
297 case "vertical-journal": return "CardVerticalJournal.qml";
298 case "horizontal-list": return "CardHorizontalList.qml";
299 case "grid":
300 default: return "CardGrid.qml";
301 }
302 }
304 onLoaded: {
305 if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
306 item.enableHeightBehavior = scopeView.enableHeightBehaviorOnNextCreation;
307 scopeView.enableHeightBehaviorOnNextCreation = false;
308 }
309 item.model = Qt.binding(function() { return results })
310 item.objectName = Qt.binding(function() { return categoryId })
311 item.scopeStyle = scopeView.scopeStyle;
312 if (baseItem.expandable) {
313 var shouldExpand = baseItem.category === categoryView.expandedCategoryId;
314 baseItem.expand(shouldExpand, false /*animate*/);
315 }
316 updateRanges();
317 if (scope && scope.id === "clickscope" && (categoryId === "predefined" || categoryId === "local")) {
318 // Yeah, hackish :/
319 cardTool.artShapeSize = Qt.size(units.gu(8), units.gu(7.5));
320 }
321 item.cardTool = cardTool;
322 }
324 Component.onDestruction: {
325 if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
326 scopeView.enableHeightBehaviorOnNextCreation = item.enableHeightBehaviorOnNextCreation;
327 }
328 }
330 Connections {
331 target: rendererLoader.item
332 onClicked: {
333 scopeView.itemClicked(index, result, item, itemModel, target.model, categoryItemCount());
334 }
336 onPressAndHold: {
337 scopeView.itemPressedAndHeld(index, itemModel, target.model, categoryItemCount());
338 }
340 function categoryItemCount() {
341 var categoryItemCount = -1;
342 if (!rendererLoader.expanded && !seeAllLabel.visible && target.collapsedItemCount > 0) {
343 categoryItemCount = target.collapsedItemCount;
344 }
345 return categoryItemCount;
346 }
347 }
348 Connections {
349 target: categoryView
350 onExpandedCategoryItemChanged: {
351 collapseAllButExpandedCategory();
352 }
353 function collapseAllButExpandedCategory() {
354 var item = rendererLoader.item;
355 if (baseItem.expandable) {
356 var shouldExpand = baseItem === categoryView.expandedCategoryItem;
357 if (shouldExpand != baseItem.expanded) {
358 // If the filter animation will be seen start it, otherwise, just flip the switch
359 var shrinkingVisible = !shouldExpand && y + item.collapsedHeight + seeAll.height < categoryView.height;
360 var growingVisible = shouldExpand && y + height < categoryView.height;
361 if (!subPageLoader.open || shouldExpand) {
362 var animate = shrinkingVisible || growingVisible;
363 baseItem.expand(shouldExpand, animate)
364 if (shouldExpand && !subPageLoader.open) {
365 categoryView.maximizeVisibleArea(index, item.expandedHeight + seeAll.height);
366 }
367 }
368 }
369 }
370 }
371 onOriginYChanged: rendererLoader.updateRanges();
372 onContentYChanged: rendererLoader.updateRanges();
373 onHeightChanged: rendererLoader.updateRanges();
374 onContentHeightChanged: rendererLoader.updateRanges();
375 }
376 Connections {
377 target: scopeView
378 onIsCurrentChanged: rendererLoader.updateRanges();
379 }
380 Connections {
381 target: holdingList
382 onMovingChanged: if (!moving) rendererLoader.updateRanges();
383 }
385 function updateRanges() {
386 if (holdingList && holdingList.moving) {
387 return;
388 }
390 if (categoryView.moving) {
391 // Do not update the range if we are overshooting up or down, since we'll come back
392 // to the stable position and delete/create items without any reason
393 if (categoryView.contentY < categoryView.originY) {
394 return;
395 } else if (categoryView.contentHeight - categoryView.originY > categoryView.height &&
396 categoryView.contentY + categoryView.height > categoryView.contentHeight) {
397 return;
398 }
399 }
401 if (item && item.hasOwnProperty("visibleRangeBegin")) {
402 item.visibleRangeBegin = Math.max(-baseItem.y, 0)
403 item.visibleRangeEnd = item.visibleRangeBegin + Math.min(categoryView.height, rendererLoader.height)
404 }
406 if (item && item.hasOwnProperty("displayMarginBeginning")) {
407 // TODO do we need item.originY here, test 1300302 once we have a silo
408 // and we can run it on the phone
409 if (scopeView.isCurrent) {
410 item.displayMarginBeginning = 1073741823;
411 item.displayMarginEnd = 1073741823;
412 } else if (baseItem.y + baseItem.height <= 0) {
413 // Not visible (item at top of the list viewport)
414 item.displayMarginBeginning = -baseItem.height;
415 item.displayMarginEnd = 0;
416 } else if (baseItem.y >= categoryView.height) {
417 // Not visible (item at bottom of the list viewport)
418 item.displayMarginBeginning = 0;
419 item.displayMarginEnd = -baseItem.height;
420 } else {
421 item.displayMarginBeginning = Math.round(-Math.max(-baseItem.y, 0));
422 item.displayMarginEnd = -Math.round(Math.max(baseItem.height - seeAll.height
423 - categoryView.height + baseItem.y, 0));
424 }
425 }
426 }
427 }
429 AbstractButton {
430 id: seeAll
431 objectName: "seeAll"
432 anchors {
433 top: rendererLoader.bottom
434 left: parent.left
435 right: parent.right
436 }
437 height: seeAllLabel.visible ? seeAllLabel.font.pixelSize + units.gu(4) : 0
439 onClicked: {
440 if (categoryView.expandedCategoryId !== baseItem.category) {
441 categoryView.expandedCategoryId = baseItem.category;
442 floatingSeeLess.companionBase = baseItem;
443 } else {
444 categoryView.expandedCategoryId = "";
445 }
446 }
448 Label {
449 id: seeAllLabel
450 text: baseItem.expanded ? i18n.tr("See less") : i18n.tr("See all")
451 anchors {
452 centerIn: parent
453 verticalCenterOffset: units.gu(-0.5)
454 }
455 fontSize: "small"
456 font.weight: Font.Bold
457 color: scopeStyle ? scopeStyle.foreground : Theme.palette.normal.baseText
458 visible: baseItem.expandable && !baseItem.headerLink
459 }
460 }
462 Image {
463 visible: index != 0
464 anchors {
465 top: parent.top
466 left: parent.left
467 right: parent.right
468 }
469 fillMode: Image.Stretch
470 source: "graphics/dash_divider_top_lightgrad.png"
471 z: -1
472 }
474 Image {
475 // FIXME Should not rely on model.count but view.count, but ListViewWithPageHeader doesn't expose it yet.
476 visible: index != categoryView.model.count - 1
477 anchors {
478 bottom: seeAll.bottom
479 left: parent.left
480 right: parent.right
481 }
482 fillMode: Image.Stretch
483 source: "graphics/dash_divider_top_darkgrad.png"
484 z: -1
485 }
486 }
488 sectionProperty: "name"
489 sectionDelegate: ListItems.Header {
490 objectName: "dashSectionHeader" + (delegate ? delegate.category : "")
491 readonly property var delegate: categoryView.item(delegateIndex)
492 width: categoryView.width
493 height: section != "" ? units.gu(5) : 0
494 text: section
495 color: scopeStyle ? scopeStyle.foreground : Theme.palette.normal.baseText
496 iconName: delegate && delegate.headerLink ? "go-next" : ""
497 onClicked: {
498 if (delegate.headerLink) scopeView.scope.performQuery(delegate.headerLink);
499 }
500 }
502 pageHeader: scopeView.showPageHeader ? pageHeaderLoader : null
503 Loader {
504 id: pageHeaderLoader
505 width: parent.width
506 sourceComponent: scopeView.showPageHeader ? pageHeaderComponent : undefined
507 Component {
508 id: pageHeaderComponent
509 PageHeader {
510 objectName: "scopePageHeader"
511 width: parent.width
512 title: scopeView.scope ? scopeView.scope.name : ""
513 searchHint: scopeView.scope && scopeView.scope.searchHint || i18n.tr("Search")
514 showBackButton: scopeView.hasBackAction
515 searchEntryEnabled: true
516 settingsEnabled: scopeView.scope && scopeView.scope.settings && scopeView.scope.settings.count > 0 || false
517 favoriteEnabled: scopeView.scope && scopeView.scope.id !== "clickscope"
518 favorite: scopeView.scope && scopeView.scope.favorite
519 scopeStyle: scopeView.scopeStyle
520 paginationCount: scopeView.paginationCount
521 paginationIndex: scopeView.paginationIndex
523 bottomItem: DashNavigation {
524 scope: scopeView.scope
525 anchors { left: parent.left; right: parent.right }
526 windowHeight: scopeView.height
527 windowWidth: scopeView.width
528 scopeStyle: scopeView.scopeStyle
529 }
531 onBackClicked: scopeView.backClicked()
532 onSettingsClicked: subPageLoader.openSubPage("settings")
533 onFavoriteClicked: scopeView.scope.favorite = !scopeView.scope.favorite
534 }
535 }
536 }
537 }
539 AbstractButton {
540 id: floatingSeeLess
541 objectName: "floatingSeeLess"
543 property Item companionTo: companionBase ? companionBase.seeAllButton : null
544 property Item companionBase: null
545 property bool showBecausePosition: false
546 property real yOffset: 0
548 anchors {
549 left: categoryView.left
550 right: categoryView.right
551 }
552 y: parent.height - height + yOffset
553 height: seeLessLabel.font.pixelSize + units.gu(4)
554 visible: companionTo && showBecausePosition
556 onClicked: categoryView.expandedCategoryId = "";
558 function updateVisibility() {
559 var companionPos = companionTo.mapToItem(floatingSeeLess, 0, 0);
560 showBecausePosition = companionPos.y > 0;
562 var posToBase = floatingSeeLess.mapToItem(companionBase, 0, -yOffset).y;
563 yOffset = Math.max(0, companionBase.item.collapsedHeight - posToBase);
564 yOffset = Math.min(yOffset, height);
566 if (!showBecausePosition && categoryView.expandedCategoryId === "") {
567 companionBase = null;
568 }
569 }
571 Label {
572 id: seeLessLabel
573 text: i18n.tr("See less")
574 anchors {
575 centerIn: parent
576 verticalCenterOffset: units.gu(-0.5)
577 }
578 fontSize: "small"
579 font.weight: Font.Bold
580 color: scopeStyle ? scopeStyle.foreground : Theme.palette.normal.baseText
581 }
583 Connections {
584 target: floatingSeeLess.companionTo ? categoryView : null
585 onContentYChanged: floatingSeeLess.updateVisibility();
586 }
588 Connections {
589 target: floatingSeeLess.companionTo
590 onYChanged: floatingSeeLess.updateVisibility();
591 }
592 }
594 LimitProxyModel {
595 id: previewLimitModel
596 }
598 Loader {
599 id: subPageLoader
600 objectName: "subPageLoader"
601 visible: x != width
602 width: parent.width
603 height: parent.height
604 anchors.left: categoryView.right
606 property bool open: false
607 property var scope: scopeView.scope
608 property var scopeStyle: scopeView.scopeStyle
609 property int initialIndex: -1
610 property var model: null
612 readonly property bool processing: item && item.processing || false
613 readonly property int count: item && item.count || 0
614 readonly property int currentIndex: item && item.currentIndex || 0
615 readonly property var currentItem: item && item.currentItem || null
617 property string subPage: ""
618 readonly property bool subPageShown: visible && status === Loader.Ready
620 function openSubPage(page) {
621 subPage = page;
622 }
624 function closeSubPage() {
625 open = false;
626 }
628 source: switch(subPage) {
629 case "preview": return "PreviewListView.qml";
630 case "settings": return "ScopeSettingsPage.qml";
631 default: return "";
632 }
634 onLoaded: {
635 item.scope = Qt.binding(function() { return subPageLoader.scope; } )
636 item.scopeStyle = Qt.binding(function() { return subPageLoader.scopeStyle; } )
637 if (subPage == "preview") {
638 item.open = Qt.binding(function() { return subPageLoader.open; } )
639 item.initialIndex = Qt.binding(function() { return subPageLoader.initialIndex; } )
640 item.model = Qt.binding(function() { return subPageLoader.model; } )
641 }
642 open = true;
643 }
645 onOpenChanged: pageHeaderLoader.item.unfocus()
647 onVisibleChanged: if (!visible) subPage = ""
649 Connections {
650 target: subPageLoader.item
651 onBackClicked: subPageLoader.closeSubPage()
652 }
653 }