on 5b03213 help sign in
unity8/Dash/GenericScopeView.qml
361 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 "../Components"
20import "../Components/ListItems" as ListItems
22ScopeView {
23 id: scopeView
24 readonly property alias previewShown: previewLoader.onScreen
25 property bool enableHeightBehaviorOnNextCreation: false
27 moving: categoryView.moving
29 onIsCurrentChanged: {
30 pageHeader.resetSearch();
31 previewLoader.open = false;
32 }
34 onMovementStarted: categoryView.showHeader()
36 onPositionedAtBeginning: categoryView.positionAtBeginning()
38 Binding {
39 target: scopeView.scope
40 property: "searchQuery"
41 value: pageHeader.searchQuery
42 }
44 Binding {
45 target: pageHeader
46 property: "searchQuery"
47 value: scopeView.scope.searchQuery
48 }
50 Connections {
51 target: panel
52 onSearchClicked: if (isCurrent) {
53 pageHeader.triggerSearch()
54 categoryView.showHeader()
55 }
56 }
58 Connections {
59 target: scopeView.scope
60 onShowDash: previewLoader.open = false;
61 onHideDash: previewLoader.open = false;
62 }
64 ScopeListView {
65 id: categoryView
66 objectName: "categoryListView"
67 anchors.fill: parent
68 model: scopeView.categories
69 forceNoClip: previewLoader.onScreen
71 onAtYEndChanged: if (atYEnd) endReached()
72 onMovingChanged: if (moving && atYEnd) endReached()
74 property string expandedCategoryId: ""
76 delegate: ListItems.Base {
77 id: baseItem
78 highlightWhenPressed: false
80 readonly property bool expandable: rendererLoader.item ? rendererLoader.item.expandable : false
81 readonly property bool filtered: rendererLoader.item ? rendererLoader.item.filter : true
82 readonly property string category: categoryId
84 Loader {
85 id: rendererLoader
86 anchors {
87 top: parent.top
88 left: parent.left
89 right: parent.right
90 }
92 source: getRenderer(model.renderer, model.contentType, model.rendererHint)
94 onLoaded: {
95 if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
96 item.enableHeightBehavior = scopeView.enableHeightBehaviorOnNextCreation;
97 scopeView.enableHeightBehaviorOnNextCreation = false;
98 }
99 if (source.toString().indexOf("Apps/RunningApplicationsGrid.qml") != -1) {
100 // TODO: the running apps grid doesn't support standard scope results model yet
101 item.firstModel = Qt.binding(function() { return results.firstModel })
102 item.secondModel = Qt.binding(function() { return results.secondModel })
103 item.canEnableTerminationMode = Qt.binding(function() { return scopeView.isCurrent })
104 } else {
105 item.model = Qt.binding(function() { return results })
106 }
107 item.objectName = Qt.binding(function() { return categoryId })
108 if (item.expandable) {
109 var shouldFilter = categoryId != categoryView.expandedCategoryId;
110 if (shouldFilter != item.filter) {
111 item.filter = shouldFilter;
112 }
113 }
114 updateDelegateCreationRange();
115 }
117 Component.onDestruction: {
118 if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
119 scopeView.enableHeightBehaviorOnNextCreation = item.enableHeightBehaviorOnNextCreation;
120 }
121 }
123 Connections {
124 target: rendererLoader.item
125 onClicked: {
126 effect.positionPx = mapToItem(categoryView, 0, itemY).y
127 scopeView.scope.activate(delegateItem.model.uri,
128 delegateItem.model.icon,
129 delegateItem.model.category,
130 0,
131 delegateItem.model.mimetype,
132 delegateItem.model.title,
133 delegateItem.model.comment,
134 delegateItem.model.dndUri,
135 delegateItem.model.metadata)
136 }
137 onPressAndHold: {
138 effect.positionPx = mapToItem(categoryView, 0, itemY).y
139 scopeView.scope.preview( delegateItem.model.uri,
140 delegateItem.model.icon,
141 delegateItem.model.category,
142 0,
143 delegateItem.model.mimetype,
144 delegateItem.model.title,
145 delegateItem.model.comment,
146 delegateItem.model.dndUri,
147 delegateItem.model.metadata)
148 }
149 }
150 Connections {
151 target: categoryView
152 onExpandedCategoryIdChanged: {
153 var item = rendererLoader.item;
154 if (item.expandable) {
155 var shouldFilter = categoryId != categoryView.expandedCategoryId;
156 if (shouldFilter != item.filter) {
157 // If the filter animation will be seen start it, otherwise, just flip the switch
158 var shrinkingVisible = shouldFilter && y + item.collapsedHeight < categoryView.height;
159 var growingVisible = !shouldFilter && y + height < categoryView.height;
160 if (shrinkingVisible || growingVisible) {
161 item.startFilterAnimation(shouldFilter)
162 } else {
163 item.filter = shouldFilter;
164 }
165 if (!shouldFilter) {
166 categoryView.maximizeVisibleArea(index, item.uncollapsedHeight);
167 }
168 }
169 }
170 }
171 onContentYChanged: rendererLoader.updateDelegateCreationRange();
172 onHeightChanged: rendererLoader.updateDelegateCreationRange();
173 }
175 function updateDelegateCreationRange() {
176 // Do not update the range if we are overshooting up or down, since we'll come back
177 // to the stable position and delete/create items without any reason
178 if (categoryView.contentY < categoryView.originY) {
179 return;
180 } else if (categoryView.contentY + categoryView.height > categoryView.contentHeight) {
181 return;
182 }
184 if (item.hasOwnProperty("delegateCreationBegin")) {
185 if (baseItem.y + baseItem.height <= 0) {
186 // Not visible (item at top of the list)
187 item.delegateCreationBegin = baseItem.height
188 item.delegateCreationEnd = baseItem.height
189 } else if (baseItem.y >= categoryView.height) {
190 // Not visible (item at bottom of the list)
191 item.delegateCreationBegin = 0
192 item.delegateCreationEnd = 0
193 } else {
194 item.delegateCreationBegin = Math.max(-baseItem.y, 0)
195 item.delegateCreationEnd = Math.min(categoryView.height + item.delegateCreationBegin, baseItem.height)
196 }
197 }
198 }
199 }
200 }
202 sectionProperty: "name"
203 sectionDelegate: ListItems.Header {
204 property var delegate: categoryView.item(delegateIndex)
205 width: categoryView.width
206 text: section
207 image: {
208 if (delegate && delegate.expandable)
209 return delegate.filtered ? "graphics/header_handlearrow.png" : "graphics/header_handlearrow2.png"
210 return "";
211 }
212 onClicked: {
213 if (categoryView.expandedCategoryId != delegate.category)
214 categoryView.expandedCategoryId = delegate.category;
215 else
216 categoryView.expandedCategoryId = "";
217 }
218 }
219 pageHeader: PageHeader {
220 id: pageHeader
221 objectName: "pageHeader"
222 width: categoryView.width
223 text: scopeView.scope.name
224 searchEntryEnabled: true
225 scope: scopeView.scope
226 }
227 }
229 function getDefaultRendererId(contentType) {
230 switch (contentType) {
231 default: return "grid";
232 }
233 }
235 function getRenderer(rendererId, contentType, rendererHint) {
236 if (rendererId == "default") {
237 rendererId = getDefaultRendererId(contentType);
238 }
239 switch (rendererId) {
240 case "grid": {
241 switch (contentType) {
242 case "video": return "Video/VideoFilterGrid.qml";
243 case "music": return "Music/MusicFilterGrid.qml";
244 case "apps": {
245 if (rendererHint == "toggled")
246 return "Apps/DashPluginFilterGrid.qml";
247 else
248 return "Generic/GenericFilterGrid.qml";
249 }
250 case "weather": return "Generic/WeatherFilterGrid.qml";
251 default: return "Generic/GenericFilterGrid.qml";
252 }
253 }
254 case "carousel": {
255 switch (contentType) {
256 case "music": return "Music/MusicCarousel.qml";
257 default: return "Generic/GenericCarousel.qml";
258 }
259 }
260 case "special": {
261 switch (contentType) {
262 case "apps": return "Apps/RunningApplicationsGrid.qml";
263 default: return "Generic/GenericFilterGrid.qml";
264 }
265 }
266 default: return "Generic/GenericFilterGrid.qml";
267 }
268 }
270 OpenEffect {
271 id: effect
272 anchors {
273 fill: parent
274 bottomMargin: -bottomOverflow
275 }
276 sourceItem: categoryView
278 enabled: gap > 0.0
280 topGapPx: (1 - gap) * positionPx
281 topOpacity: (1 - gap * 1.2)
282 bottomGapPx: positionPx + gap * (targetBottomGapPx - positionPx)
283 bottomOverflow: units.gu(20)
284 bottomOpacity: 1 - (gap * 0.8)
286 property int targetBottomGapPx: height - units.gu(8) - bottomOverflow
287 property real gap: previewLoader.open ? 1.0 : 0.0
289 Behavior on gap {
290 NumberAnimation {
291 duration: 200
292 easing.type: Easing.InOutQuad
293 onRunningChanged: {
294 if (!previewLoader.open && !running) {
295 previewLoader.onScreen = false
296 }
297 }
298 }
299 }
300 }
302 Connections {
303 target: scopeView.scope
304 onPreviewReady: {
305 previewLoader.previewData = preview
306 previewLoader.open = true
307 }
308 }
310 PreviewDelegateMapper {
311 id: previewDelegateMapper
312 }
314 Connections {
315 ignoreUnknownSignals: true
316 target: previewLoader.valid ? previewLoader.item : null
317 onClose: {
318 previewLoader.open = false
319 }
320 }
322 Loader {
323 objectName: "previewLoader"
324 id: previewLoader
325 property var previewData
326 height: effect.bottomGapPx - effect.topGapPx
327 anchors {
328 top: parent.top
329 topMargin: effect.topGapPx
330 left: parent.left
331 right: parent.right
332 }
333 source: onScreen ? previewDelegateMapper.map(previewLoader.previewData.rendererName) : ""
335 property bool open: false
336 property bool onScreen: false
337 property bool valid: item !== null
339 onOpenChanged: {
340 if (open) {
341 onScreen = true
342 }
343 }
345 onLoaded: {
346 item.previewData = Qt.binding(function() { return previewLoader.previewData })
347 }
348 }
350 // TODO: Move as InverseMouseArea to DashPreview
351 MouseArea {
352 enabled: previewLoader.onScreen
353 anchors {
354 fill: parent
355 topMargin: effect.bottomGapPx
356 }
357 onClicked: {
358 previewLoader.open = false;
359 }
360 }