on fbe3fd3 help sign in
unity8/qml/Dash/GenericScopeView.qml
349 lines 14 kB history raw
1/*
2 * Copyright (C) 2013 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 "../Components"
22import "../Components/ListItems" as ListItems
24FocusScope {
25 id: scopeView
27 property Scope scope
28 property SortFilterProxyModel categories: categoryFilter
29 property bool isCurrent
30 property alias moving: categoryView.moving
31 property int tabBarHeight: 0
32 property PageHeader pageHeader: null
33 property OpenEffect openEffect: null
34 property Item previewListView: null
36 signal endReached
37 signal movementStarted
38 signal positionedAtBeginning
40 property bool enableHeightBehaviorOnNextCreation: false
41 property var categoryView: categoryView
43 // FIXME delay the search so that daemons have time to settle, note that
44 // removing this will break ScopeView::test_changeScope
45 onScopeChanged: {
46 if (scope) {
47 timer.restart();
48 scope.activateApplication.connect(activateApp);
49 }
50 }
52 function activateApp(appId) {
53 shell.activateApplication(appId);
54 }
56 Binding {
57 target: scope
58 property: "isActive"
59 value: isCurrent && !previewListView.onScreen
60 }
62 Timer {
63 id: timer
64 interval: 2000
65 onTriggered: scope.searchQuery = ""
66 }
68 SortFilterProxyModel {
69 id: categoryFilter
70 model: scope ? scope.categories : null
71 dynamicSortFilter: true
72 filterRole: Categories.RoleCount
73 filterRegExp: /^0$/
74 invertMatch: true
75 }
77 onIsCurrentChanged: {
78 pageHeader.resetSearch();
79 previewListView.open = false;
80 }
82 onMovementStarted: categoryView.showHeader()
84 onPositionedAtBeginning: categoryView.positionAtBeginning()
86 Binding {
87 target: scopeView.scope
88 property: "searchQuery"
89 value: pageHeader.searchQuery
90 when: isCurrent
91 }
93 Binding {
94 target: pageHeader
95 property: "searchQuery"
96 value: scopeView.scope.searchQuery
97 when: isCurrent
98 }
100 Connections {
101 target: panel
102 onSearchClicked: if (isCurrent) {
103 pageHeader.triggerSearch()
104 categoryView.showHeader()
105 }
106 }
108 Connections {
109 target: scopeView.scope
110 onShowDash: previewListView.open = false;
111 onHideDash: previewListView.open = false;
112 }
114 ScopeListView {
115 id: categoryView
116 objectName: "categoryListView"
117 anchors.fill: parent
118 model: scopeView.categories
119 forceNoClip: previewListView.onScreen
121 onAtYEndChanged: if (atYEnd) endReached()
122 onMovingChanged: if (moving && atYEnd) endReached()
124 property string expandedCategoryId: ""
125 signal correctExpandedCategory();
127 onContentYChanged: pageHeader.positionRealHeader();
128 onOriginYChanged: pageHeader.positionRealHeader();
129 onContentHeightChanged: pageHeader.positionRealHeader();
131 Behavior on contentY {
132 enabled: previewListView.open
133 UbuntuNumberAnimation {}
134 }
136 delegate: ListItems.Base {
137 id: baseItem
138 objectName: "dashCategory" + category
139 highlightWhenPressed: false
140 showDivider: false
142 readonly property bool expandable: rendererLoader.item ? rendererLoader.item.expandable : false
143 readonly property bool filtered: rendererLoader.item ? rendererLoader.item.filter : true
144 readonly property string category: categoryId
145 readonly property var item: rendererLoader.item
147 Loader {
148 id: rendererLoader
149 anchors {
150 top: parent.top
151 left: parent.left
152 right: parent.right
153 }
155 source: getRenderer(model.renderer, results)
157 onLoaded: {
158 if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
159 item.enableHeightBehavior = scopeView.enableHeightBehaviorOnNextCreation;
160 scopeView.enableHeightBehaviorOnNextCreation = false;
161 }
162 if (source.toString().indexOf("Apps/RunningApplicationsGrid.qml") != -1) {
163 // TODO: this is still a kludge :D Ideally add some kind of hook so that we
164 // can do this from DashApps.qml or think a better way that needs no special casing
165 item.firstModel = Qt.binding(function() { return mainStageApplicationsModel })
166 item.secondModel = Qt.binding(function() { return sideStageApplicationModel })
167 item.canEnableTerminationMode = Qt.binding(function() { return scopeView.isCurrent })
168 } else {
169 item.model = Qt.binding(function() { return results })
170 }
171 item.objectName = Qt.binding(function() { return categoryId })
172 if (item.expandable) {
173 var shouldFilter = categoryId != categoryView.expandedCategoryId;
174 if (shouldFilter != item.filter) {
175 item.filter = shouldFilter;
176 }
177 }
178 updateDelegateCreationRange();
179 // FIXME: should be "template", not "renderer"
180 item.template = Qt.binding(function() { return model.renderer });
181 item.components = Qt.binding(function() { return model.components });
182 }
184 Component.onDestruction: {
185 if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
186 scopeView.enableHeightBehaviorOnNextCreation = item.enableHeightBehaviorOnNextCreation;
187 }
188 }
190 Connections {
191 target: rendererLoader.item
192 onClicked: {
193 // Prepare the preview in case activate() triggers a preview only
194 openEffect.positionPx = Math.max(mapToItem(categoryView, 0, itemY).y, pageHeader.height + categoryView.stickyHeaderHeight);
195 previewListView.categoryId = categoryId
196 previewListView.categoryDelegate = rendererLoader.item
197 previewListView.model = target.model;
198 previewListView.currentIndex = -1
199 previewListView.currentIndex = index;
201 var item = target.model.get(index);
203 if (scopeView.scope.id === "scopes" || (scopeView.scope.id == "clickscope" && categoryId == "local")) {
204 scopeView.scope.activate(item.result)
205 } else {
206 previewListView.open = true
207 }
208 }
209 onPressAndHold: {
210 openEffect.positionPx = Math.max(mapToItem(categoryView, 0, itemY).y, pageHeader.height + categoryView.stickyHeaderHeight);
211 previewListView.categoryId = categoryId
212 previewListView.categoryDelegate = rendererLoader.item
213 previewListView.model = target.model;
214 previewListView.currentIndex = -1
215 previewListView.currentIndex = index;
216 previewListView.open = true
217 }
218 }
219 Connections {
220 target: categoryView
221 onExpandedCategoryIdChanged: {
222 collapseAllButExpandedCategory();
223 }
224 onCorrectExpandedCategory: {
225 collapseAllButExpandedCategory();
226 }
227 function collapseAllButExpandedCategory() {
228 var item = rendererLoader.item;
229 if (item.expandable) {
230 var shouldFilter = categoryId != categoryView.expandedCategoryId;
231 if (shouldFilter != item.filter) {
232 // If the filter animation will be seen start it, otherwise, just flip the switch
233 var shrinkingVisible = shouldFilter && y + item.collapsedHeight < categoryView.height;
234 var growingVisible = !shouldFilter && y + height < categoryView.height;
235 if (!previewListView.open || !shouldFilter) {
236 if (shrinkingVisible || growingVisible) {
237 item.startFilterAnimation(shouldFilter)
238 } else {
239 item.filter = shouldFilter;
240 }
241 if (!shouldFilter && !previewListView.open) {
242 categoryView.maximizeVisibleArea(index, item.uncollapsedHeight);
243 }
244 }
245 }
246 }
247 }
248 onOriginYChanged: rendererLoader.updateDelegateCreationRange();
249 onContentYChanged: rendererLoader.updateDelegateCreationRange();
250 onHeightChanged: rendererLoader.updateDelegateCreationRange();
251 onContentHeightChanged: rendererLoader.updateDelegateCreationRange();
252 }
254 function updateDelegateCreationRange() {
255 // Do not update the range if we are overshooting up or down, since we'll come back
256 // to the stable position and delete/create items without any reason
257 if (categoryView.contentY < categoryView.originY) {
258 return;
259 } else if (categoryView.contentY + categoryView.height > categoryView.contentHeight) {
260 return;
261 }
263 if (item && item.hasOwnProperty("delegateCreationBegin")) {
264 if (baseItem.y + baseItem.height <= 0) {
265 // Not visible (item at top of the list)
266 item.delegateCreationBegin = baseItem.height
267 item.delegateCreationEnd = baseItem.height
268 } else if (baseItem.y >= categoryView.height) {
269 // Not visible (item at bottom of the list)
270 item.delegateCreationBegin = 0
271 item.delegateCreationEnd = 0
272 } else {
273 item.delegateCreationBegin = Math.max(-baseItem.y, 0)
274 item.delegateCreationEnd = Math.min(categoryView.height + item.delegateCreationBegin, baseItem.height)
275 }
276 }
277 }
278 }
280 onHeightChanged: rendererLoader.updateDelegateCreationRange();
281 onYChanged: rendererLoader.updateDelegateCreationRange();
282 }
284 sectionProperty: "name"
285 sectionDelegate: ListItems.Header {
286 objectName: "dashSectionHeader" + delegate.category
287 property var delegate: categoryView.item(delegateIndex)
288 width: categoryView.width
289 text: section
290 image: {
291 if (delegate && delegate.expandable)
292 return delegate.filtered ? "graphics/header_handlearrow.png" : "graphics/header_handlearrow2.png"
293 return "";
294 }
295 onClicked: {
296 if (categoryView.expandedCategoryId != delegate.category)
297 categoryView.expandedCategoryId = delegate.category;
298 else
299 categoryView.expandedCategoryId = "";
300 }
301 }
302 pageHeader: Item {
303 implicitHeight: scopeView.tabBarHeight
304 onHeightChanged: {
305 if (scopeView.pageHeader && scopeView.isCurrent) {
306 scopeView.pageHeader.height = height;
307 }
308 }
309 onYChanged: positionRealHeader();
311 function positionRealHeader() {
312 if (scopeView.pageHeader && scopeView.isCurrent) {
313 scopeView.pageHeader.y = y + parent.y;
314 }
315 }
316 }
317 }
319 function getRenderer(template, results) {
320 var layout = template["category-layout"];
322 if (layout == "running-apps") {
323 return "Apps/RunningApplicationsGrid.qml";
324 }
326 if (layout === "carousel") {
327 // TODO: Selectively disable carousel, 4 is fixed for now, should change depending on form factor
328 if (results.count <= 4) layout = "grid";
329 }
330 switch (layout) {
331 case "carousel": return "CardCarousel.qml";
332 case "grid":
333 default: return "CardFilterGrid.qml";
334 }
335 }
337 // TODO: Move as InverseMouseArea to DashPreview
338 MouseArea {
339 objectName: "closePreviewMouseArea"
340 enabled: previewListView.onScreen
341 anchors {
342 fill: parent
343 topMargin: openEffect.bottomGapPx
344 }
345 onClicked: {
346 previewListView.open = false;
347 }
348 }