on eebdfc5 help sign in
unity8/qml/Dash/GenericScopeView.qml
426 lines 17 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
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
141 readonly property bool expandable: rendererLoader.item ? rendererLoader.item.expandable : false
142 readonly property bool filtered: rendererLoader.item ? rendererLoader.item.filter : true
143 readonly property string category: categoryId
144 readonly property var item: rendererLoader.item
146 Image {
147 id: bottomGradient
148 anchors {
149 bottom: parent.bottom
150 left: parent.left
151 right: parent.right
152 }
153 fillMode: Image.Stretch
154 verticalAlignment: Image.AlignBottom
155 source: "graphics/dash_divider_top_darkgrad.png"
156 z: -1
157 }
159 Loader {
160 id: rendererLoader
161 anchors {
162 top: parent.top
163 left: parent.left
164 right: parent.right
165 }
167 source: getRenderer(model.renderer, model.contentType, model.rendererHint, results)
169 onLoaded: {
170 if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
171 item.enableHeightBehavior = scopeView.enableHeightBehaviorOnNextCreation;
172 scopeView.enableHeightBehaviorOnNextCreation = false;
173 }
174 if (source.toString().indexOf("Apps/RunningApplicationsGrid.qml") != -1) {
175 // TODO: the running apps grid doesn't support standard scope results model yet
176 item.firstModel = Qt.binding(function() { return results.firstModel })
177 item.secondModel = Qt.binding(function() { return results.secondModel })
178 item.canEnableTerminationMode = Qt.binding(function() { return scopeView.isCurrent })
179 } else {
180 item.model = Qt.binding(function() { return results })
181 }
182 item.objectName = Qt.binding(function() { return categoryId })
183 if (item.expandable) {
184 var shouldFilter = categoryId != categoryView.expandedCategoryId;
185 if (shouldFilter != item.filter) {
186 item.filter = shouldFilter;
187 }
188 }
189 updateDelegateCreationRange();
190 }
192 Component.onDestruction: {
193 if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
194 scopeView.enableHeightBehaviorOnNextCreation = item.enableHeightBehaviorOnNextCreation;
195 }
196 }
198 Connections {
199 target: rendererLoader.item
200 onClicked: {
201 // Prepare the preview in case activate() triggers a preview only
202 openEffect.positionPx = Math.max(mapToItem(categoryView, 0, itemY).y, pageHeader.height + categoryView.stickyHeaderHeight);
203 previewListView.categoryId = categoryId
204 previewListView.categoryDelegate = rendererLoader.item
205 previewListView.model = target.model;
206 previewListView.init = true;
207 previewListView.currentIndex = index;
209 var item = target.model.get(index);
211 if ((scopeView.scope.id == "applications.scope" && categoryId == "installed")
212 || (scopeView.scope.id == "home.scope" && categoryId == "applications.scope")) {
213 scopeView.scope.activate(item.uri, item.icon, item.category, 0, item.mimetype, item.title,
214 item.comment, item.dndUri, item.metadata)
215 } else {
216 previewListView.open = true
218 scopeView.scope.preview(item.uri, item.icon, item.category, 0, item.mimetype, item.title,
219 item.comment, item.dndUri, item.metadata)
220 }
221 }
222 onPressAndHold: {
223 openEffect.positionPx = Math.max(mapToItem(categoryView, 0, itemY).y, pageHeader.height + categoryView.stickyHeaderHeight);
224 previewListView.categoryId = categoryId
225 previewListView.categoryDelegate = rendererLoader.item
226 previewListView.model = target.model;
227 previewListView.init = true;
228 previewListView.currentIndex = index;
229 previewListView.open = true
231 var item = target.model.get(index)
232 scopeView.scope.preview(item.uri, item.icon, item.category, 0, item.mimetype, item.title,
233 item.comment, item.dndUri, item.metadata)
234 }
235 }
236 Connections {
237 target: categoryView
238 onExpandedCategoryIdChanged: {
239 collapseAllButExpandedCategory();
240 }
241 onCorrectExpandedCategory: {
242 collapseAllButExpandedCategory();
243 }
244 function collapseAllButExpandedCategory() {
245 var item = rendererLoader.item;
246 if (item.expandable) {
247 var shouldFilter = categoryId != categoryView.expandedCategoryId;
248 if (shouldFilter != item.filter) {
249 // If the filter animation will be seen start it, otherwise, just flip the switch
250 var shrinkingVisible = shouldFilter && y + item.collapsedHeight < categoryView.height;
251 var growingVisible = !shouldFilter && y + height < categoryView.height;
252 if (!previewListView.open || !shouldFilter) {
253 if (shrinkingVisible || growingVisible) {
254 item.startFilterAnimation(shouldFilter)
255 } else {
256 item.filter = shouldFilter;
257 }
258 if (!shouldFilter && !previewListView.open) {
259 categoryView.maximizeVisibleArea(index, item.uncollapsedHeight);
260 }
261 }
262 }
263 }
264 }
265 onOriginYChanged: rendererLoader.updateDelegateCreationRange();
266 onContentYChanged: rendererLoader.updateDelegateCreationRange();
267 onHeightChanged: rendererLoader.updateDelegateCreationRange();
268 onContentHeightChanged: rendererLoader.updateDelegateCreationRange();
269 }
271 function updateDelegateCreationRange() {
272 // Do not update the range if we are overshooting up or down, since we'll come back
273 // to the stable position and delete/create items without any reason
274 if (categoryView.contentY < categoryView.originY) {
275 return;
276 } else if (categoryView.contentY + categoryView.height > categoryView.contentHeight) {
277 return;
278 }
280 if (item && item.hasOwnProperty("delegateCreationBegin")) {
281 if (baseItem.y + baseItem.height <= 0) {
282 // Not visible (item at top of the list)
283 item.delegateCreationBegin = baseItem.height
284 item.delegateCreationEnd = baseItem.height
285 } else if (baseItem.y >= categoryView.height) {
286 // Not visible (item at bottom of the list)
287 item.delegateCreationBegin = 0
288 item.delegateCreationEnd = 0
289 } else {
290 item.delegateCreationBegin = Math.max(-baseItem.y, 0)
291 item.delegateCreationEnd = Math.min(categoryView.height + item.delegateCreationBegin, baseItem.height)
292 }
293 }
294 }
295 }
297 onHeightChanged: rendererLoader.updateDelegateCreationRange();
298 onYChanged: rendererLoader.updateDelegateCreationRange();
299 }
301 sectionProperty: "name"
302 sectionDelegate: ListItems.Header {
303 objectName: "dashSectionHeader" + delegate.category
304 property var delegate: categoryView.item(delegateIndex)
305 width: categoryView.width
306 text: section
307 image: {
308 if (delegate && delegate.expandable)
309 return delegate.filtered ? "graphics/header_handlearrow.png" : "graphics/header_handlearrow2.png"
310 return "";
311 }
312 onClicked: {
313 if (categoryView.expandedCategoryId != delegate.category)
314 categoryView.expandedCategoryId = delegate.category;
315 else
316 categoryView.expandedCategoryId = "";
317 }
319 Image {
320 id: topGradient
321 anchors {
322 top: parent.top
323 left: parent.left
324 right: parent.right
325 }
326 fillMode: Image.Stretch
327 verticalAlignment: Image.AlignTop
328 source: "graphics/dash_divider_top_lightgrad.png"
329 z: -1
330 }
331 }
332 pageHeader: Item {
333 implicitHeight: scopeView.tabBarHeight
334 onHeightChanged: {
335 if (scopeView.pageHeader) {
336 scopeView.pageHeader.height = height;
337 }
338 }
339 onYChanged: positionRealHeader();
341 function positionRealHeader() {
342 if (scopeView.pageHeader) {
343 scopeView.pageHeader.y = y + parent.y;
344 }
345 }
346 }
347 }
349 function getDefaultRendererId(contentType) {
350 switch (contentType) {
351 default: return "grid";
352 }
353 }
355 function getRenderer(rendererId, contentType, rendererHint, results) {
356 if (rendererId == "default") {
357 rendererId = getDefaultRendererId(contentType);
358 }
359 if (rendererId == "carousel") {
360 // Selectively disable carousel, 6 is fixed for now, should change on the form factor
361 if (results.count <= 6)
362 rendererId = "grid"
363 }
364 switch (rendererId) {
365 case "carousel": {
366 switch (contentType) {
367 case "music": return "Music/MusicCarousel.qml";
368 case "video": return "Video/VideoCarousel.qml";
369 default: return "Generic/GenericCarousel.qml";
370 }
371 }
372 case "grid": {
373 switch (contentType) {
374 case "apps": {
375 if (rendererHint == "toggled")
376 return "Apps/DashPluginFilterGrid.qml";
377 else
378 return "Generic/GenericFilterGrid.qml";
379 }
380 case "music": return "Music/MusicFilterGrid.qml";
381 case "video": return "Video/VideoFilterGrid.qml";
382 case "weather": return "Generic/WeatherFilterGrid.qml";
383 default: return "Generic/GenericFilterGrid.qml";
384 }
385 }
386 case "special": {
387 switch (contentType) {
388 case "apps": return "Apps/RunningApplicationsGrid.qml";
389 default: return "Generic/GenericFilterGrid.qml";
390 }
391 }
392 default: return "Generic/GenericFilterGrid.qml";
393 }
394 }
396 Connections {
397 target: scopeView.scope
398 onPreviewReady: {
399 if (previewListView.init) {
400 // Preview was triggered because of a click on the item. Need to expand now.
401 if (!previewListView.open) {
402 previewListView.open = true
403 }
405 var index = previewListView.currentIndex
406 previewListView.currentIndex = -1
407 previewListView.currentIndex = index
408 previewListView.init = false
409 }
410 previewListView.currentItem.previewData = preview
411 }
412 }
414 // TODO: Move as InverseMouseArea to DashPreview
415 MouseArea {
416 objectName: "closePreviewMouseArea"
417 enabled: previewListView.onScreen
418 anchors {
419 fill: parent
420 topMargin: openEffect.bottomGapPx
421 }
422 onClicked: {
423 previewListView.open = false;
424 }
425 }