on 7cd287d help sign in
unity8/Dash/GenericScopeView.qml
390 lines 16 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.1
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
39 signal headerPositionChanged(int position)
40 signal headerHeightChanged(int height)
42 property bool enableHeightBehaviorOnNextCreation: false
43 property var categoryView: categoryView
45 // FIXME delay the search so that daemons have time to settle, note that
46 // removing this will break ScopeView::test_changeScope
47 onScopeChanged: {
48 if (scope) {
49 timer.restart();
50 scope.activateApplication.connect(activateApp);
51 }
52 }
54 function activateApp(appId) {
55 shell.activateApplication(appId);
56 }
58 Binding {
59 target: scope
60 property: "isActive"
61 value: isCurrent
62 }
64 Timer {
65 id: timer
66 interval: 2000
67 onTriggered: scope.searchQuery = ""
68 }
70 SortFilterProxyModel {
71 id: categoryFilter
72 model: scope ? scope.categories : null
73 dynamicSortFilter: true
74 filterRole: Categories.RoleCount
75 filterRegExp: /^0$/
76 invertMatch: true
77 }
79 onIsCurrentChanged: {
80 pageHeader.resetSearch();
81 previewListView.open = false;
82 }
84 onMovementStarted: categoryView.showHeader()
86 onPositionedAtBeginning: categoryView.positionAtBeginning()
88 Binding {
89 target: scopeView.scope
90 property: "searchQuery"
91 value: pageHeader.searchQuery
92 when: isCurrent
93 }
95 Binding {
96 target: pageHeader
97 property: "searchQuery"
98 value: scopeView.scope.searchQuery
99 when: isCurrent
100 }
102 Connections {
103 target: panel
104 onSearchClicked: if (isCurrent) {
105 pageHeader.triggerSearch()
106 categoryView.showHeader()
107 }
108 }
110 Connections {
111 target: scopeView.scope
112 onShowDash: previewListView.open = false;
113 onHideDash: previewListView.open = false;
114 }
116 ScopeListView {
117 id: categoryView
118 objectName: "categoryListView"
119 anchors.fill: parent
120 model: scopeView.categories
121 forceNoClip: previewListView.onScreen
123 onAtYEndChanged: if (atYEnd) endReached()
124 onMovingChanged: if (moving && atYEnd) endReached()
126 property string expandedCategoryId: ""
127 signal correctExpandedCategory();
129 onContentYChanged: pageHeader.positionRealHeader();
130 onOriginYChanged: pageHeader.positionRealHeader();
131 onContentHeightChanged: pageHeader.positionRealHeader();
133 Behavior on contentY {
134 enabled: previewListView.open
135 UbuntuNumberAnimation {}
136 }
138 delegate: ListItems.Base {
139 id: baseItem
140 objectName: "dashCategory" + category
141 highlightWhenPressed: false
143 readonly property bool expandable: rendererLoader.item ? rendererLoader.item.expandable : false
144 readonly property bool filtered: rendererLoader.item ? rendererLoader.item.filter : true
145 readonly property string category: categoryId
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, model.contentType, model.rendererHint, 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: the running apps grid doesn't support standard scope results model yet
164 item.firstModel = Qt.binding(function() { return results.firstModel })
165 item.secondModel = Qt.binding(function() { return results.secondModel })
166 item.canEnableTerminationMode = Qt.binding(function() { return scopeView.isCurrent })
167 } else {
168 item.model = Qt.binding(function() { return results })
169 }
170 item.objectName = Qt.binding(function() { return categoryId })
171 if (item.expandable) {
172 var shouldFilter = categoryId != categoryView.expandedCategoryId;
173 if (shouldFilter != item.filter) {
174 item.filter = shouldFilter;
175 }
176 }
177 updateDelegateCreationRange();
178 }
180 Component.onDestruction: {
181 if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
182 scopeView.enableHeightBehaviorOnNextCreation = item.enableHeightBehaviorOnNextCreation;
183 }
184 }
186 Connections {
187 target: rendererLoader.item
188 onClicked: {
189 // Prepare the preview in case activate() triggers a preview only
190 openEffect.positionPx = Math.max(mapToItem(categoryView, 0, itemY).y, pageHeader.height + categoryView.stickyHeaderHeight);
191 previewListView.categoryId = categoryId
192 previewListView.categoryDelegate = rendererLoader.item
193 previewListView.model = target.model;
194 previewListView.init = true;
195 previewListView.currentIndex = index;
197 var item = target.model.get(index);
199 if ((scopeView.scope.id == "applications.scope" && categoryId == "installed")
200 || (scopeView.scope.id == "home.scope" && categoryId == "applications.scope")) {
201 scopeView.scope.activate(item.uri, item.icon, item.category, 0, item.mimetype, item.title,
202 item.comment, item.dndUri, item.metadata)
203 } else {
204 previewListView.open = true
206 scopeView.scope.preview(item.uri, item.icon, item.category, 0, item.mimetype, item.title,
207 item.comment, item.dndUri, item.metadata)
208 }
209 }
210 onPressAndHold: {
211 openEffect.positionPx = Math.max(mapToItem(categoryView, 0, itemY).y, pageHeader.height + categoryView.stickyHeaderHeight);
212 previewListView.categoryId = categoryId
213 previewListView.categoryDelegate = rendererLoader.item
214 previewListView.model = target.model;
215 previewListView.init = true;
216 previewListView.currentIndex = index;
217 previewListView.open = true
219 var item = target.model.get(index)
220 scopeView.scope.preview(item.uri, item.icon, item.category, 0, item.mimetype, item.title,
221 item.comment, item.dndUri, item.metadata)
222 }
223 }
224 Connections {
225 target: categoryView
226 onExpandedCategoryIdChanged: {
227 collapseAllButExpandedCategory();
228 }
229 onCorrectExpandedCategory: {
230 collapseAllButExpandedCategory();
231 }
232 function collapseAllButExpandedCategory() {
233 var item = rendererLoader.item;
234 if (item.expandable) {
235 var shouldFilter = categoryId != categoryView.expandedCategoryId;
236 if (shouldFilter != item.filter) {
237 // If the filter animation will be seen start it, otherwise, just flip the switch
238 var shrinkingVisible = shouldFilter && y + item.collapsedHeight < categoryView.height;
239 var growingVisible = !shouldFilter && y + height < categoryView.height;
240 if (!previewListView.open || !shouldFilter) {
241 if (shrinkingVisible || growingVisible) {
242 item.startFilterAnimation(shouldFilter)
243 } else {
244 item.filter = shouldFilter;
245 }
246 if (!shouldFilter && !previewListView.open) {
247 categoryView.maximizeVisibleArea(index, item.uncollapsedHeight);
248 }
249 }
250 }
251 }
252 }
253 onContentYChanged: rendererLoader.updateDelegateCreationRange();
254 onHeightChanged: rendererLoader.updateDelegateCreationRange();
255 }
257 function updateDelegateCreationRange() {
258 // Do not update the range if we are overshooting up or down, since we'll come back
259 // to the stable position and delete/create items without any reason
260 if (categoryView.contentY < categoryView.originY) {
261 return;
262 } else if (categoryView.contentY + categoryView.height > categoryView.contentHeight) {
263 return;
264 }
266 if (item.hasOwnProperty("delegateCreationBegin")) {
267 if (baseItem.y + baseItem.height <= 0) {
268 // Not visible (item at top of the list)
269 item.delegateCreationBegin = baseItem.height
270 item.delegateCreationEnd = baseItem.height
271 } else if (baseItem.y >= categoryView.height) {
272 // Not visible (item at bottom of the list)
273 item.delegateCreationBegin = 0
274 item.delegateCreationEnd = 0
275 } else {
276 item.delegateCreationBegin = Math.max(-baseItem.y, 0)
277 item.delegateCreationEnd = Math.min(categoryView.height + item.delegateCreationBegin, baseItem.height)
278 }
279 }
280 }
281 }
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: scopeView.headerHeightChanged(height)
305 onYChanged: positionRealHeader();
307 function positionRealHeader() {
308 scopeView.headerPositionChanged(y + parent.y)
309 }
310 }
311 }
313 function getDefaultRendererId(contentType) {
314 switch (contentType) {
315 default: return "grid";
316 }
317 }
319 function getRenderer(rendererId, contentType, rendererHint, results) {
320 if (rendererId == "default") {
321 rendererId = getDefaultRendererId(contentType);
322 }
323 if (rendererId == "carousel") {
324 // Selectively disable carousel, 6 is fixed for now, should change on the form factor
325 if (results.count <= 6)
326 rendererId = "grid"
327 }
328 switch (rendererId) {
329 case "carousel": {
330 switch (contentType) {
331 case "music": return "Music/MusicCarousel.qml";
332 case "video": return "Video/VideoCarousel.qml";
333 default: return "Generic/GenericCarousel.qml";
334 }
335 }
336 case "grid": {
337 switch (contentType) {
338 case "apps": {
339 if (rendererHint == "toggled")
340 return "Apps/DashPluginFilterGrid.qml";
341 else
342 return "Generic/GenericFilterGrid.qml";
343 }
344 case "music": return "Music/MusicFilterGrid.qml";
345 case "video": return "Video/VideoFilterGrid.qml";
346 case "weather": return "Generic/WeatherFilterGrid.qml";
347 default: return "Generic/GenericFilterGrid.qml";
348 }
349 }
350 case "special": {
351 switch (contentType) {
352 case "apps": return "Apps/RunningApplicationsGrid.qml";
353 default: return "Generic/GenericFilterGrid.qml";
354 }
355 }
356 default: return "Generic/GenericFilterGrid.qml";
357 }
358 }
360 Connections {
361 target: scopeView.scope
362 onPreviewReady: {
363 if (previewListView.init) {
364 // Preview was triggered because of a click on the item. Need to expand now.
365 if (!previewListView.open) {
366 previewListView.open = true
367 }
369 var index = previewListView.currentIndex
370 previewListView.currentIndex = -1
371 previewListView.currentIndex = index
372 previewListView.init = false
373 }
374 previewListView.currentItem.previewData = preview
375 }
376 }
378 // TODO: Move as InverseMouseArea to DashPreview
379 MouseArea {
380 objectName: "closePreviewMouseArea"
381 enabled: previewListView.onScreen
382 anchors {
383 fill: parent
384 topMargin: openEffect.bottomGapPx
385 }
386 onClicked: {
387 previewListView.open = false;
388 }
389 }