on 20f7e61 help sign in
unity8/qml/Dash/GenericScopeView.qml
506 lines 21 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
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 function positionAtBeginning() {
51 categoryView.positionAtBeginning()
52 }
54 function showHeader() {
55 categoryView.showHeader()
56 }
58 function closePreview() {
59 subPageLoader.closeSubPage()
60 }
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)
68 } else {
69 openPreview(index, resultsModel, limitedCategoryItemCount);
70 }
71 }
73 function itemPressedAndHeld(index, itemModel, resultsModel, limitedCategoryItemCount) {
74 if (itemModel.uri.indexOf("scope://") !== 0) {
75 openPreview(index, resultsModel, limitedCategoryItemCount);
76 }
77 }
79 function openPreview(index, resultsModel, limitedCategoryItemCount) {
80 if (limitedCategoryItemCount > 0) {
81 previewLimitModel.model = resultsModel;
82 previewLimitModel.limit = limitedCategoryItemCount;
83 subPageLoader.model = previewLimitModel;
84 } else {
85 subPageLoader.model = resultsModel;
86 }
87 subPageLoader.initialIndex = -1;
88 subPageLoader.initialIndex = index;
89 subPageLoader.openSubPage("preview");
90 }
92 Binding {
93 target: scope
94 property: "isActive"
95 value: isCurrent && !subPageLoader.open
96 }
98 SortFilterProxyModel {
99 id: categoryFilter
100 model: scope ? scope.categories : null
101 dynamicSortFilter: true
102 filterRole: Categories.RoleCount
103 filterRegExp: /^0$/
104 invertMatch: true
105 }
107 onIsCurrentChanged: {
108 if (pageHeaderLoader.item && showPageHeader) {
109 pageHeaderLoader.item.resetSearch();
110 }
111 subPageLoader.closeSubPage();
112 }
114 Binding {
115 target: scopeView.scope
116 property: "searchQuery"
117 value: pageHeaderLoader.item ? pageHeaderLoader.item.searchQuery : ""
118 when: isCurrent && showPageHeader
119 }
121 Binding {
122 target: pageHeaderLoader.item
123 property: "searchQuery"
124 value: scopeView.scope ? scopeView.scope.searchQuery : ""
125 when: isCurrent && showPageHeader
126 }
128 Connections {
129 target: scopeView.scope
130 onShowDash: subPageLoader.closeSubPage()
131 onHideDash: subPageLoader.closeSubPage()
132 }
134 Rectangle {
135 anchors.fill: parent
136 color: scopeView.scopeStyle ? scopeView.scopeStyle.background : "transparent"
137 visible: color != "transparent"
138 }
140 ScopeListView {
141 id: categoryView
142 objectName: "categoryListView"
144 x: subPageLoader.open ? -width : 0
145 Behavior on x { UbuntuNumberAnimation { } }
146 width: parent.width
147 height: parent.height
149 model: scopeView.categories
150 forceNoClip: subPageLoader.open
151 pixelAligned: true
152 interactive: !navigationShown
154 property string expandedCategoryId: ""
156 readonly property bool pageHeaderTotallyVisible: scopeView.showPageHeader &&
157 ((headerItemShownHeight == 0 && categoryView.contentY <= categoryView.originY) || (headerItemShownHeight == pageHeaderLoader.item.height))
159 delegate: ListItems.Base {
160 id: baseItem
161 objectName: "dashCategory" + category
162 highlightWhenPressed: false
163 showDivider: false
165 readonly property bool expandable: {
166 if (categoryView.model.count === 1) return false;
167 if (cardTool.template && cardTool.template["collapsed-rows"] === 0) return false;
168 if (item && item.expandedHeight > item.collapsedHeight) return true;
169 return false;
170 }
171 property bool expanded: false
172 readonly property string category: categoryId
173 readonly property string headerLink: model.headerLink
174 readonly property var item: rendererLoader.item
176 function expand(expand, animate) {
177 heightBehaviour.enabled = animate;
178 expanded = expand;
179 }
181 CardTool {
182 id: cardTool
183 objectName: "cardTool"
184 count: results.count
185 template: model.renderer
186 components: model.components
187 viewWidth: parent.width
188 }
190 onExpandableChanged: {
191 // This can happen with the VJ that doesn't know how height it will be on creation
192 // so doesn't set expandable until a bit too late for onLoaded
193 if (expandable) {
194 var shouldExpand = baseItem.category === categoryView.expandedCategoryId;
195 baseItem.expand(shouldExpand, false /*animate*/);
196 }
197 }
199 onHeightChanged: rendererLoader.updateDelegateCreationRange();
200 onYChanged: rendererLoader.updateDelegateCreationRange();
202 Loader {
203 id: rendererLoader
204 anchors {
205 top: parent.top
206 left: parent.left
207 right: parent.right
208 topMargin: name != "" ? 0 : units.gu(2)
209 }
211 Behavior on height {
212 id: heightBehaviour
213 enabled: false
214 animation: UbuntuNumberAnimation {
215 onRunningChanged: {
216 if (!running) {
217 heightBehaviour.enabled = false
218 }
219 }
220 }
221 }
223 readonly property bool expanded: baseItem.expanded || !baseItem.expandable
224 height: expanded ? item.expandedHeight : item.collapsedHeight
226 source: {
227 switch (cardTool.categoryLayout) {
228 case "carousel": return "CardCarousel.qml";
229 case "vertical-journal": return "CardVerticalJournal.qml";
230 case "horizontal-list": return "CardHorizontalList.qml";
231 case "grid":
232 default: return "CardGrid.qml";
233 }
234 }
236 onLoaded: {
237 if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
238 item.enableHeightBehavior = scopeView.enableHeightBehaviorOnNextCreation;
239 scopeView.enableHeightBehaviorOnNextCreation = false;
240 }
241 item.model = Qt.binding(function() { return results })
242 item.objectName = Qt.binding(function() { return categoryId })
243 item.scopeStyle = scopeView.scopeStyle;
244 if (baseItem.expandable) {
245 var shouldExpand = categoryId === categoryView.expandedCategoryId;
246 baseItem.expand(shouldExpand, false /*animate*/);
247 }
248 updateDelegateCreationRange();
249 if (scope && scope.id === "clickscope" && (categoryId === "predefined" || categoryId === "local")) {
250 // Yeah, hackish :/
251 cardTool.artShapeSize = Qt.size(units.gu(8), units.gu(7.5));
252 }
253 item.cardTool = cardTool;
254 }
256 Component.onDestruction: {
257 if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
258 scopeView.enableHeightBehaviorOnNextCreation = item.enableHeightBehaviorOnNextCreation;
259 }
260 }
262 Connections {
263 target: rendererLoader.item
264 onClicked: {
265 scopeView.itemClicked(index, result, item, itemModel, target.model, categoryItemCount());
266 }
268 onPressAndHold: {
269 scopeView.itemPressedAndHeld(index, itemModel, target.model, categoryItemCount());
270 }
272 function categoryItemCount() {
273 var categoryItemCount = -1;
274 if (!rendererLoader.expanded && !seeAllLabel.visible && target.collapsedItemCount > 0) {
275 categoryItemCount = target.collapsedItemCount;
276 }
277 return categoryItemCount;
278 }
279 }
280 Connections {
281 target: categoryView
282 onExpandedCategoryIdChanged: {
283 collapseAllButExpandedCategory();
284 }
285 function collapseAllButExpandedCategory() {
286 var item = rendererLoader.item;
287 if (baseItem.expandable) {
288 var shouldExpand = categoryId === categoryView.expandedCategoryId;
289 if (shouldExpand != baseItem.expanded) {
290 // If the filter animation will be seen start it, otherwise, just flip the switch
291 var shrinkingVisible = !shouldExpand && y + item.collapsedHeight + seeAll.height < categoryView.height;
292 var growingVisible = shouldExpand && y + height < categoryView.height;
293 if (!subPageLoader.open || shouldExpand) {
294 var animate = shrinkingVisible || growingVisible;
295 baseItem.expand(shouldExpand, animate)
296 if (shouldExpand && !subPageLoader.open) {
297 categoryView.maximizeVisibleArea(index, item.expandedHeight + seeAll.height);
298 }
299 }
300 }
301 }
302 }
303 onOriginYChanged: rendererLoader.updateDelegateCreationRange();
304 onContentYChanged: rendererLoader.updateDelegateCreationRange();
305 onHeightChanged: rendererLoader.updateDelegateCreationRange();
306 onContentHeightChanged: rendererLoader.updateDelegateCreationRange();
307 }
309 function updateDelegateCreationRange() {
310 if (categoryView.moving) {
311 // Do not update the range if we are overshooting up or down, since we'll come back
312 // to the stable position and delete/create items without any reason
313 if (categoryView.contentY < categoryView.originY) {
314 return;
315 } else if (categoryView.contentHeight - categoryView.originY > categoryView.height &&
316 categoryView.contentY + categoryView.height > categoryView.contentHeight) {
317 return;
318 }
319 }
321 if (item && item.hasOwnProperty("displayMarginBeginning")) {
322 // TODO do we need item.originY here, test 1300302 once we have a silo
323 // and we can run it on the phone
324 if (baseItem.y + baseItem.height <= 0) {
325 // Not visible (item at top of the list viewport)
326 item.displayMarginBeginning = -baseItem.height;
327 item.displayMarginEnd = 0;
328 } else if (baseItem.y >= categoryView.height) {
329 // Not visible (item at bottom of the list viewport)
330 item.displayMarginBeginning = 0;
331 item.displayMarginEnd = -baseItem.height;
332 } else {
333 item.displayMarginBeginning = -Math.max(-baseItem.y, 0);
334 item.displayMarginEnd = -Math.max(baseItem.height - seeAll.height
335 - categoryView.height + baseItem.y, 0)
336 }
337 }
338 }
339 }
341 AbstractButton {
342 id: seeAll
343 objectName: "seeAll"
344 anchors {
345 top: rendererLoader.bottom
346 left: parent.left
347 right: parent.right
348 }
349 height: seeAllLabel.visible ? seeAllLabel.font.pixelSize + units.gu(6) : 0
351 onClicked: {
352 if (categoryView.expandedCategoryId != baseItem.category) {
353 categoryView.expandedCategoryId = baseItem.category;
354 } else {
355 categoryView.expandedCategoryId = "";
356 }
357 }
359 Label {
360 id: seeAllLabel
361 text: baseItem.expanded ? i18n.tr("See less") : i18n.tr("See all")
362 anchors {
363 centerIn: parent
364 verticalCenterOffset: units.gu(-0.5)
365 }
366 fontSize: "small"
367 font.weight: Font.Bold
368 color: scopeStyle ? scopeStyle.foreground : Theme.palette.normal.baseText
369 visible: baseItem.expandable && !baseItem.headerLink
370 }
371 }
373 Image {
374 visible: index != 0
375 anchors {
376 top: parent.top
377 left: parent.left
378 right: parent.right
379 }
380 fillMode: Image.Stretch
381 source: "graphics/dash_divider_top_lightgrad.png"
382 z: -1
383 }
385 Image {
386 // FIXME Should not rely on model.count but view.count, but ListViewWithPageHeader doesn't expose it yet.
387 visible: index != categoryView.model.count - 1
388 anchors {
389 bottom: seeAll.bottom
390 left: parent.left
391 right: parent.right
392 }
393 fillMode: Image.Stretch
394 source: "graphics/dash_divider_top_darkgrad.png"
395 z: -1
396 }
397 }
399 sectionProperty: "name"
400 sectionDelegate: ListItems.Header {
401 objectName: "dashSectionHeader" + (delegate ? delegate.category : "")
402 readonly property var delegate: categoryView.item(delegateIndex)
403 width: categoryView.width
404 height: section != "" ? units.gu(5) : 0
405 text: section
406 color: scopeStyle ? scopeStyle.foreground : Theme.palette.normal.baseText
407 iconName: delegate && delegate.headerLink ? "go-next" : ""
408 onClicked: {
409 if (delegate.headerLink) scopeView.scope.performQuery(delegate.headerLink);
410 }
411 }
413 pageHeader: scopeView.showPageHeader ? pageHeaderLoader : null
414 Loader {
415 id: pageHeaderLoader
416 width: parent.width
417 sourceComponent: scopeView.showPageHeader ? pageHeaderComponent : undefined
418 Component {
419 id: pageHeaderComponent
420 PageHeader {
421 objectName: "scopePageHeader"
422 width: parent.width
423 title: scopeView.scope ? scopeView.scope.name : ""
424 searchHint: scopeView.scope && scopeView.scope.searchHint || i18n.tr("Search")
425 showBackButton: scopeView.hasBackAction
426 searchEntryEnabled: true
427 settingsEnabled: scopeView.scope ? scopeView.scope.settings && scopeView.scope.settings.count > 0 : false
428 scopeStyle: scopeView.scopeStyle
429 paginationCount: scopeView.paginationCount
430 paginationIndex: scopeView.paginationIndex
432 bottomItem: DashNavigation {
433 scope: scopeView.scope
434 width: parent.width <= units.gu(60) ? parent.width : units.gu(40)
435 anchors.right: parent.right
436 windowHeight: scopeView.height
437 windowWidth: scopeView.width
438 scopeStyle: scopeView.scopeStyle
439 }
441 onBackClicked: scopeView.backClicked()
442 onSettingsClicked: subPageLoader.openSubPage("settings")
443 }
444 }
445 }
446 }
448 LimitProxyModel {
449 id: previewLimitModel
450 }
452 Loader {
453 id: subPageLoader
454 objectName: "subPageLoader"
455 visible: x != width
456 width: parent.width
457 height: parent.height
458 anchors.left: categoryView.right
460 property bool open: false
461 property var scope: scopeView.scope
462 property var scopeStyle: scopeView.scopeStyle
463 property int initialIndex: -1
464 property var model: null
466 readonly property bool processing: item && item.processing || false
467 readonly property int count: item && item.count || 0
468 readonly property int currentIndex: item && item.currentIndex || 0
469 readonly property var currentItem: item && item.currentItem || null
471 property string subPage: ""
472 readonly property bool subPageShown: visible && status === Loader.Ready
474 function openSubPage(page) {
475 subPage = page;
476 }
478 function closeSubPage() {
479 open = false;
480 }
482 source: subPage && (subPage == "preview" ? "PreviewListView.qml" : "ScopeSettingsPage.qml") || ""
484 onLoaded: {
485 if (status === Loader.Ready) {
486 item.scope = Qt.binding(function() { return subPageLoader.scope } )
487 item.scopeStyle = Qt.binding(function() { return subPageLoader.scopeStyle } )
488 if (subPage == "preview") {
489 item.open = Qt.binding(function() { return subPageLoader.open } )
490 item.initialIndex = Qt.binding(function() { return subPageLoader.initialIndex } )
491 item.model = Qt.binding(function() { return subPageLoader.model } )
492 }
493 open = true;
494 }
495 }
497 onOpenChanged: pageHeaderLoader.item.unfocus();
499 onVisibleChanged: if (!visible) subPage = "";
501 Connections {
502 target: subPageLoader.item
503 onBackClicked: subPageLoader.closeSubPage()
504 }
505 }