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