on 950aa38 help sign in
unity8/qml/Dash/GenericScopeView.qml
341 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: null
28 property SortFilterProxyModel categories: categoryFilter
29 property bool isCurrent: false
30 property alias moving: categoryView.moving
31 property int tabBarHeight: 0
32 property PageHeader pageHeader: null
33 property Item previewListView: null
35 signal movementStarted
36 signal positionedAtBeginning
38 property bool enableHeightBehaviorOnNextCreation: false
39 property var categoryView: categoryView
41 // FIXME delay the search so that daemons have time to settle, note that
42 // removing this will break ScopeView::test_changeScope
43 onScopeChanged: {
44 if (scope) {
45 timer.restart();
46 scope.activateApplication.connect(activateApp);
47 }
48 }
50 function activateApp(appId) {
51 shell.activateApplication(appId);
52 }
54 Binding {
55 target: scope
56 property: "isActive"
57 value: isCurrent && !previewListView.open
58 }
60 Timer {
61 id: timer
62 interval: 2000
63 onTriggered: scope.searchQuery = ""
64 }
66 SortFilterProxyModel {
67 id: categoryFilter
68 model: scope ? scope.categories : null
69 dynamicSortFilter: true
70 filterRole: Categories.RoleCount
71 filterRegExp: /^0$/
72 invertMatch: true
73 }
75 onIsCurrentChanged: {
76 pageHeader.resetSearch();
77 previewListView.open = false;
78 }
80 onMovementStarted: categoryView.showHeader()
82 onPositionedAtBeginning: categoryView.positionAtBeginning()
84 Binding {
85 target: scopeView.scope
86 property: "searchQuery"
87 value: pageHeader.searchQuery
88 when: isCurrent
89 }
91 Binding {
92 target: pageHeader
93 property: "searchQuery"
94 value: scopeView.scope ? scopeView.scope.searchQuery : ""
95 when: isCurrent
96 }
98 Connections {
99 target: panel
100 onSearchClicked: if (isCurrent) {
101 pageHeader.triggerSearch()
102 categoryView.showHeader()
103 }
104 }
106 Connections {
107 target: scopeView.scope
108 onShowDash: previewListView.open = false;
109 onHideDash: previewListView.open = false;
110 }
112 ScopeListView {
113 id: categoryView
114 objectName: "categoryListView"
115 anchors.fill: parent
116 model: scopeView.categories
117 forceNoClip: previewListView.open
119 property string expandedCategoryId: ""
121 onContentYChanged: pageHeader.positionRealHeader();
122 onOriginYChanged: pageHeader.positionRealHeader();
123 onContentHeightChanged: pageHeader.positionRealHeader();
125 delegate: ListItems.Base {
126 id: baseItem
127 objectName: "dashCategory" + category
128 highlightWhenPressed: false
129 showDivider: false
131 readonly property bool expandable: rendererLoader.item ? rendererLoader.item.expandable : false
132 readonly property bool filtered: rendererLoader.item ? rendererLoader.item.filter : true
133 readonly property string category: categoryId
134 readonly property var item: rendererLoader.item
136 Loader {
137 id: rendererLoader
138 anchors {
139 top: parent.top
140 left: parent.left
141 right: parent.right
142 }
144 source: getRenderer(model.renderer, results)
146 onLoaded: {
147 if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
148 item.enableHeightBehavior = scopeView.enableHeightBehaviorOnNextCreation;
149 scopeView.enableHeightBehaviorOnNextCreation = false;
150 }
151 if (source.toString().indexOf("Apps/RunningApplicationsGrid.qml") != -1) {
152 // TODO: this is still a kludge :D Ideally add some kind of hook so that we
153 // can do this from DashApps.qml or think a better way that needs no special casing
154 item.firstModel = Qt.binding(function() { return mainStageApplicationsModel })
155 item.secondModel = Qt.binding(function() { return sideStageApplicationModel })
156 item.canEnableTerminationMode = Qt.binding(function() { return scopeView.isCurrent })
157 } else {
158 item.model = Qt.binding(function() { return results })
159 }
160 item.objectName = Qt.binding(function() { return categoryId })
161 if (item.expandable) {
162 var shouldFilter = categoryId != categoryView.expandedCategoryId;
163 if (shouldFilter != item.filter) {
164 item.filter = shouldFilter;
165 }
166 }
167 updateDelegateCreationRange();
168 // FIXME: should be "template", not "renderer"
169 item.template = Qt.binding(function() { return model.renderer });
170 item.components = Qt.binding(function() { return model.components });
171 }
173 Component.onDestruction: {
174 if (item.enableHeightBehavior !== undefined && item.enableHeightBehaviorOnNextCreation !== undefined) {
175 scopeView.enableHeightBehaviorOnNextCreation = item.enableHeightBehaviorOnNextCreation;
176 }
177 }
179 Connections {
180 target: rendererLoader.item
181 onClicked: {
182 if (scopeView.scope.id === "scopes" || (scopeView.scope.id == "clickscope" && categoryId == "local")) {
183 // TODO Technically it is possible that calling activate() will make the scope emit
184 // previewRequested so that we show a preview but there's no scope that does that yet
185 // so it's not implemented
186 var item = target.model.get(index);
187 scopeView.scope.activate(item.result)
188 } else {
189 previewListView.model = target.model;
190 previewListView.currentIndex = -1
191 previewListView.currentIndex = index;
192 previewListView.open = true
193 }
194 }
195 onPressAndHold: {
196 previewListView.model = target.model;
197 previewListView.currentIndex = -1
198 previewListView.currentIndex = index;
199 previewListView.open = true
200 }
201 }
202 Connections {
203 target: categoryView
204 onExpandedCategoryIdChanged: {
205 collapseAllButExpandedCategory();
206 }
207 function collapseAllButExpandedCategory() {
208 var item = rendererLoader.item;
209 if (item.expandable) {
210 var shouldFilter = categoryId != categoryView.expandedCategoryId;
211 if (shouldFilter != item.filter) {
212 // If the filter animation will be seen start it, otherwise, just flip the switch
213 var shrinkingVisible = shouldFilter && y + item.collapsedHeight < categoryView.height;
214 var growingVisible = !shouldFilter && y + height < categoryView.height;
215 if (!previewListView.open || !shouldFilter) {
216 if (shrinkingVisible || growingVisible) {
217 item.startFilterAnimation(shouldFilter)
218 } else {
219 item.filter = shouldFilter;
220 }
221 if (!shouldFilter && !previewListView.open) {
222 categoryView.maximizeVisibleArea(index, item.uncollapsedHeight);
223 }
224 }
225 }
226 }
227 }
228 onOriginYChanged: rendererLoader.updateDelegateCreationRange();
229 onContentYChanged: rendererLoader.updateDelegateCreationRange();
230 onHeightChanged: rendererLoader.updateDelegateCreationRange();
231 onContentHeightChanged: rendererLoader.updateDelegateCreationRange();
232 }
234 function updateDelegateCreationRange() {
235 // Do not update the range if we are overshooting up or down, since we'll come back
236 // to the stable position and delete/create items without any reason
237 if (categoryView.contentY < categoryView.originY) {
238 return;
239 } else if (categoryView.contentY + categoryView.height > categoryView.contentHeight) {
240 return;
241 }
243 if (item && item.hasOwnProperty("delegateCreationBegin")) {
244 if (baseItem.y + baseItem.height <= 0) {
245 // Not visible (item at top of the list)
246 item.delegateCreationBegin = baseItem.height
247 item.delegateCreationEnd = baseItem.height
248 } else if (baseItem.y >= categoryView.height) {
249 // Not visible (item at bottom of the list)
250 item.delegateCreationBegin = 0
251 item.delegateCreationEnd = 0
252 } else {
253 item.delegateCreationBegin = Math.max(-baseItem.y, 0)
254 item.delegateCreationEnd = Math.min(categoryView.height + item.delegateCreationBegin, baseItem.height)
255 }
256 }
257 }
259 Image {
260 visible: index != 0
261 anchors {
262 top: parent.top
263 left: parent.left
264 right: parent.right
265 }
266 fillMode: Image.Stretch
267 source: "graphics/dash_divider_top_lightgrad.png"
268 z: -1
269 }
271 Image {
272 // FIXME Should not rely on count field of model, SortfilterProxyModel in this case.
273 visible: index != categoryView.model.count - 1
274 anchors {
275 bottom: parent.bottom
276 left: parent.left
277 right: parent.right
278 }
279 fillMode: Image.Stretch
280 source: "graphics/dash_divider_top_darkgrad.png"
281 z: -1
282 }
283 }
285 onHeightChanged: rendererLoader.updateDelegateCreationRange();
286 onYChanged: rendererLoader.updateDelegateCreationRange();
287 }
289 sectionProperty: "name"
290 sectionDelegate: ListItems.Header {
291 objectName: "dashSectionHeader" + (delegate ? delegate.category : "")
292 property var delegate: categoryView.item(delegateIndex)
293 width: categoryView.width
294 text: section
295 image: {
296 if (delegate && delegate.expandable)
297 return delegate.filtered ? "graphics/header_handlearrow.png" : "graphics/header_handlearrow2.png"
298 return "";
299 }
300 onClicked: {
301 if (categoryView.expandedCategoryId != delegate.category)
302 categoryView.expandedCategoryId = delegate.category;
303 else
304 categoryView.expandedCategoryId = "";
305 }
306 }
307 pageHeader: Item {
308 implicitHeight: scopeView.tabBarHeight
309 onHeightChanged: {
310 if (scopeView.pageHeader && scopeView.isCurrent) {
311 scopeView.pageHeader.height = height;
312 }
313 }
314 onYChanged: positionRealHeader();
316 function positionRealHeader() {
317 if (scopeView.pageHeader && scopeView.isCurrent) {
318 scopeView.pageHeader.y = y + parent.y;
319 }
320 }
321 }
322 }
324 function getRenderer(template, results) {
325 var layout = template["category-layout"];
327 if (layout == "running-apps") {
328 return "Apps/RunningApplicationsGrid.qml";
329 }
331 if (layout === "carousel") {
332 // TODO: Selectively disable carousel, 4 is fixed for now, should change depending on form factor
333 if (results.count <= 4) layout = "grid";
334 }
335 switch (layout) {
336 case "carousel": return "CardCarousel.qml";
337 case "grid":
338 default: return "CardFilterGrid.qml";
339 }
340 }