on e2f3577 help sign in
unity8/qml/Dash/GenericScopeView.qml
435 lines 17 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 Unity.Application 0.1
22import Dash 0.1
23import "../Components"
24import "../Components/ListItems" as ListItems
26FocusScope {
27 id: scopeView
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
37 property var scopeStyle: ScopeStyle {
38 style: scope ? scope.customizations : {}
39 }
41 signal backClicked()
43 onScopeChanged: {
44 if (scope) {
45 scope.activateApplication.connect(activateApp);
46 }
47 }
49 function activateApp(appId) {
50 shell.activateApplication(appId);
51 }
53 function positionAtBeginning() {
54 categoryView.positionAtBeginning()
55 }
57 function showHeader() {
58 categoryView.showHeader()
59 }
61 function closePreview() {
62 previewListView.open = false;
63 }
65 Binding {
66 target: scope
67 property: "isActive"
68 value: isCurrent && !previewListView.open
69 }
71 SortFilterProxyModel {
72 id: categoryFilter
73 model: scope ? scope.categories : null
74 dynamicSortFilter: true
75 filterRole: Categories.RoleCount
76 filterRegExp: /^0$/
77 invertMatch: true
78 }
80 onIsCurrentChanged: {
81 pageHeader.resetSearch();
82 previewListView.open = false;
83 }
85 Binding {
86 target: scopeView.scope
87 property: "searchQuery"
88 value: pageHeader.searchQuery
89 when: isCurrent
90 }
92 Binding {
93 target: pageHeader
94 property: "searchQuery"
95 value: scopeView.scope ? scopeView.scope.searchQuery : ""
96 when: isCurrent
97 }
99 Connections {
100 target: scopeView.scope
101 onShowDash: previewListView.open = false;
102 onHideDash: previewListView.open = false;
103 }
105 Rectangle {
106 anchors.fill: parent
107 color: scopeView.scopeStyle ? scopeView.scopeStyle.background : "transparent"
108 visible: color != "transparent"
109 }
111 ScopeListView {
112 id: categoryView
113 objectName: "categoryListView"
115 x: previewListView.open ? -width : 0
116 Behavior on x { UbuntuNumberAnimation { } }
117 width: parent.width
118 height: parent.height
120 model: scopeView.categories
121 forceNoClip: previewListView.open
122 pixelAligned: true
124 property string expandedCategoryId: ""
126 delegate: ListItems.Base {
127 id: baseItem
128 objectName: "dashCategory" + category
129 highlightWhenPressed: false
130 showDivider: false
132 readonly property bool expandable: {
133 if (categoryView.model.count === 1) return false;
134 if (cardTool.template && cardTool.template["collapsed-rows"] === 0) return false;
135 if (item && item.expandedHeight > item.collapsedHeight) return true;
136 return false;
137 }
138 property bool expanded: false
139 readonly property string category: categoryId
140 readonly property string headerLink: model.headerLink
141 readonly property var item: rendererLoader.item
143 function expand(expand, animate) {
144 heightBehaviour.enabled = animate;
145 expanded = expand;
146 }
148 CardTool {
149 id: cardTool
150 objectName: "cardTool"
151 count: results.count
152 template: model.renderer
153 components: model.components
154 viewWidth: parent.width
155 }
157 onExpandableChanged: {
158 // This can happen with the VJ that doesn't know how height it will be on creation
159 // so doesn't set expandable until a bit too late for onLoaded
160 if (expandable) {
161 var shouldExpand = baseItem.category === categoryView.expandedCategoryId;
162 baseItem.expand(shouldExpand, false /*animate*/);
163 }
164 }
166 onHeightChanged: rendererLoader.updateDelegateCreationRange();
167 onYChanged: rendererLoader.updateDelegateCreationRange();
169 Loader {
170 id: rendererLoader
171 anchors {
172 top: parent.top
173 left: parent.left
174 right: parent.right
175 topMargin: name != "" ? 0 : units.gu(2)
176 }
178 Behavior on height {
179 id: heightBehaviour
180 enabled: false
181 animation: UbuntuNumberAnimation {
182 onRunningChanged: {
183 if (!running) {
184 heightBehaviour.enabled = false
185 }
186 }
187 }
188 }
190 readonly property bool expanded: baseItem.expanded || !baseItem.expandable
191 height: expanded ? item.expandedHeight : item.collapsedHeight
193 source: {
194 switch (cardTool.categoryLayout) {
195 case "carousel": return "CardCarousel.qml";
196 case "vertical-journal": return "CardVerticalJournal.qml";
197 case "running-apps": return "Apps/RunningApplicationsGrid.qml";
198 case "grid":
199 default: return "CardGrid.qml";
200 }
201 }
203 onLoaded: {
204 if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
205 item.enableHeightBehavior = scopeView.enableHeightBehaviorOnNextCreation;
206 scopeView.enableHeightBehaviorOnNextCreation = false;
207 }
208 if (source.toString().indexOf("Apps/RunningApplicationsGrid.qml") != -1) {
209 // TODO: this is still a kludge :D Ideally add some kind of hook so that we
210 // can do this from DashApps.qml or think a better way that needs no special casing
211 item.model = Qt.binding(function() { return runningApps; })
212 item.canEnableTerminationMode = Qt.binding(function() { return scopeView.isCurrent })
213 } else {
214 item.model = Qt.binding(function() { return results })
215 }
216 item.objectName = Qt.binding(function() { return categoryId })
217 item.scopeStyle = scopeView.scopeStyle;
218 if (baseItem.expandable) {
219 var shouldExpand = categoryId === categoryView.expandedCategoryId;
220 baseItem.expand(shouldExpand, false /*animate*/);
221 }
222 updateDelegateCreationRange();
223 item.cardTool = cardTool;
224 }
226 Component.onDestruction: {
227 if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
228 scopeView.enableHeightBehaviorOnNextCreation = item.enableHeightBehaviorOnNextCreation;
229 }
230 }
232 Connections {
233 target: rendererLoader.item
234 onClicked: {
235 if (scopeView.scope.id === "scopes" || scopeView.scope.id == "clickscope") {
236 // TODO Technically it is possible that calling activate() will make the scope emit
237 // previewRequested so that we show a preview but there's no scope that does that yet
238 // so it's not implemented
239 scopeView.scope.activate(result)
240 } else {
241 openPreview(index);
242 }
243 }
244 onPressAndHold: openPreview(index)
246 function openPreview(index) {
247 if (!rendererLoader.expanded && !seeAllLabel.visible && target.collapsedItemCount > 0) {
248 previewLimitModel.model = target.model;
249 previewLimitModel.limit = target.collapsedItemCount;
250 previewListView.model = previewLimitModel;
251 } else {
252 previewListView.model = target.model;
253 }
254 previewListView.currentIndex = -1;
255 previewListView.currentIndex = index;
256 previewListView.open = true;
257 }
258 }
259 Connections {
260 target: categoryView
261 onExpandedCategoryIdChanged: {
262 collapseAllButExpandedCategory();
263 }
264 function collapseAllButExpandedCategory() {
265 var item = rendererLoader.item;
266 if (baseItem.expandable) {
267 var shouldExpand = categoryId === categoryView.expandedCategoryId;
268 if (shouldExpand != baseItem.expanded) {
269 // If the filter animation will be seen start it, otherwise, just flip the switch
270 var shrinkingVisible = !shouldExpand && y + item.collapsedHeight + seeAll.height < categoryView.height;
271 var growingVisible = shouldExpand && y + height < categoryView.height;
272 if (!previewListView.open || shouldExpand) {
273 var animate = shrinkingVisible || growingVisible;
274 baseItem.expand(shouldExpand, animate)
275 if (shouldExpand && !previewListView.open) {
276 categoryView.maximizeVisibleArea(index, item.expandedHeight + seeAll.height);
277 }
278 }
279 }
280 }
281 }
282 onOriginYChanged: rendererLoader.updateDelegateCreationRange();
283 onContentYChanged: rendererLoader.updateDelegateCreationRange();
284 onHeightChanged: rendererLoader.updateDelegateCreationRange();
285 onContentHeightChanged: rendererLoader.updateDelegateCreationRange();
286 }
288 function updateDelegateCreationRange() {
289 if (categoryView.moving) {
290 // Do not update the range if we are overshooting up or down, since we'll come back
291 // to the stable position and delete/create items without any reason
292 if (categoryView.contentY < categoryView.originY) {
293 return;
294 } else if (categoryView.contentHeight - categoryView.originY > categoryView.height &&
295 categoryView.contentY + categoryView.height > categoryView.contentHeight) {
296 return;
297 }
298 }
300 if (item && item.hasOwnProperty("displayMarginBeginning")) {
301 // TODO do we need item.originY here, test 1300302 once we have a silo
302 // and we can run it on the phone
303 if (baseItem.y + baseItem.height <= 0) {
304 // Not visible (item at top of the list viewport)
305 item.displayMarginBeginning = -baseItem.height;
306 item.displayMarginEnd = 0;
307 } else if (baseItem.y >= categoryView.height) {
308 // Not visible (item at bottom of the list viewport)
309 item.displayMarginBeginning = 0;
310 item.displayMarginEnd = -baseItem.height;
311 } else {
312 item.displayMarginBeginning = -Math.max(-baseItem.y, 0);
313 item.displayMarginEnd = -Math.max(baseItem.height - seeAll.height
314 - categoryView.height + baseItem.y, 0)
315 }
316 }
317 }
318 }
320 AbstractButton {
321 id: seeAll
322 objectName: "seeAll"
323 anchors {
324 top: rendererLoader.bottom
325 left: parent.left
326 right: parent.right
327 }
328 height: seeAllLabel.visible ? seeAllLabel.font.pixelSize + units.gu(6) : 0
330 onClicked: {
331 if (categoryView.expandedCategoryId != baseItem.category) {
332 categoryView.expandedCategoryId = baseItem.category;
333 } else {
334 categoryView.expandedCategoryId = "";
335 }
336 }
338 Label {
339 id: seeAllLabel
340 text: baseItem.expanded ? i18n.tr("See less") : i18n.tr("See all")
341 anchors {
342 centerIn: parent
343 verticalCenterOffset: units.gu(-0.5)
344 }
345 fontSize: "small"
346 font.weight: Font.Bold
347 color: scopeStyle ? scopeStyle.foreground : "grey"
348 visible: baseItem.expandable && !baseItem.headerLink
349 }
350 }
352 Image {
353 visible: index != 0
354 anchors {
355 top: parent.top
356 left: parent.left
357 right: parent.right
358 }
359 fillMode: Image.Stretch
360 source: "graphics/dash_divider_top_lightgrad.png"
361 z: -1
362 }
364 Image {
365 // FIXME Should not rely on model.count but view.count, but ListViewWithPageHeader doesn't expose it yet.
366 visible: index != categoryView.model.count - 1
367 anchors {
368 bottom: seeAll.bottom
369 left: parent.left
370 right: parent.right
371 }
372 fillMode: Image.Stretch
373 source: "graphics/dash_divider_top_darkgrad.png"
374 z: -1
375 }
376 }
378 sectionProperty: "name"
379 sectionDelegate: ListItems.Header {
380 objectName: "dashSectionHeader" + (delegate ? delegate.category : "")
381 readonly property var delegate: categoryView.item(delegateIndex)
382 width: categoryView.width
383 height: section != "" ? units.gu(5) : 0
384 text: section
385 color: scopeStyle ? scopeStyle.foreground : "grey"
386 iconName: delegate && delegate.headerLink ? "go-next" : ""
387 onClicked: {
388 if (delegate.headerLink) scopeView.scope.performQuery(delegate.headerLink);
389 }
390 }
392 pageHeader: PageHeader {
393 id: pageHeader
394 objectName: "scopePageHeader"
395 width: parent.width
396 title: scopeView.scope ? scopeView.scope.name : ""
397 searchHint: scopeView.scope && scopeView.scope.searchHint || i18n.tr("Search")
398 showBackButton: scopeView.hasBackAction
399 searchEntryEnabled: true
400 searchInProgress: scopeView.scope ? scopeView.scope.searchInProgress : false
401 scopeStyle: scopeView.scopeStyle
403 bottomItem: DashDepartments {
404 scope: scopeView.scope
405 width: parent.width <= units.gu(60) ? parent.width : units.gu(40)
406 anchors.right: parent.right
407 windowHeight: scopeView.height
408 windowWidth: scopeView.width
409 scopeStyle: scopeView.scopeStyle
410 }
412 onBackClicked: scopeView.backClicked()
413 }
414 }
416 LimitProxyModel {
417 id: previewLimitModel
418 }
420 PreviewListView {
421 id: previewListView
422 objectName: "previewListView"
423 visible: x != width
424 scope: scopeView.scope
425 scopeStyle: scopeView.scopeStyle
426 width: parent.width
427 height: parent.height
428 anchors.left: categoryView.right
430 onOpenChanged: {
431 pageHeader.unfocus();
432 }
433 }