Merge lp:~fboucault/unity-2d/proper_grid into lp:unity-2d/3.0

Proposed by Florian Boucault
Status: Merged
Approved by: Florian Boucault
Approved revision: 637
Merged at revision: 650
Proposed branch: lp:~fboucault/unity-2d/proper_grid
Merge into: lp:unity-2d/3.0
Diff against target: 642 lines (+363/-135)
9 files modified
places/Home.qml (+22/-11)
places/ListViewWithHeaders.qml (+277/-0)
places/ListViewWithScrollbar.qml (+5/-29)
places/PlaceEntryView.qml (+35/-20)
places/Renderer.qml (+2/-2)
places/RendererGrid.qml (+14/-71)
places/Scrollbar.qml (+1/-1)
places/UnityEmptySearchRenderer.qml (+1/-1)
places/dash.qml (+6/-0)
To merge this branch: bzr merge lp:~fboucault/unity-2d/proper_grid
Reviewer Review Type Date Requested Status
Gerry Boland Pending
Review via email: mp+69460@code.launchpad.net

Description of the change

[dash] Cleaned up grid widget by introducing ListViewWithHeaders.

To post a comment you must log in.
lp:~fboucault/unity-2d/proper_grid updated
635. By Florian Boucault

Added documentation.

636. By Florian Boucault

PlaceEntryView.qml: use bindings so that RendererGrid's properties are synced with its parents

637. By Florian Boucault

Unload the current page when closing the dash

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'places/Home.qml'
2--- places/Home.qml 2011-07-26 16:36:51 +0000
3+++ places/Home.qml 2011-07-27 19:03:02 +0000
4@@ -99,17 +99,28 @@
5 opacity: globalSearchActive ? 1 : 0
6 anchors.fill: parent
7
8- list.model: dash.places
9-
10- list.delegate: UnityDefaultRenderer {
11- width: ListView.view.width
12-
13- parentListView: list
14- placeEntryModel: item
15- displayName: item.name
16- iconHint: item.icon
17-
18- model: item.globalResultsModel
19+ model: dash.places
20+
21+ bodyDelegate: UnityDefaultRenderer {
22+ placeEntryModel: model.item
23+ displayName: model.item.name
24+ iconHint: model.item.icon
25+
26+ group_model: model.item.globalResultsModel
27+ property bool focusable: group_model != undefined && group_model.count > 0
28+ }
29+
30+ headerDelegate: GroupHeader {
31+ visible: body.needHeader && body.focusable
32+ height: visible ? 32 : 0
33+
34+ property bool foldable: body.folded != undefined
35+ availableCount: foldable && body.group_model != null ? body.group_model.count - body.cellsPerRow : 0
36+ folded: foldable ? body.folded : false
37+ onClicked: if(foldable) body.folded = !body.folded
38+
39+ icon: body.iconHint
40+ label: body.displayName
41 }
42 }
43
44
45=== added file 'places/ListViewWithHeaders.qml'
46--- places/ListViewWithHeaders.qml 1970-01-01 00:00:00 +0000
47+++ places/ListViewWithHeaders.qml 2011-07-27 19:03:02 +0000
48@@ -0,0 +1,277 @@
49+/*
50+ * This file is part of unity-2d
51+ *
52+ * Copyright 2010-2011 Canonical Ltd.
53+ *
54+ * This program is free software; you can redistribute it and/or modify
55+ * it under the terms of the GNU General Public License as published by
56+ * the Free Software Foundation; version 3.
57+ *
58+ * This program is distributed in the hope that it will be useful,
59+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
60+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
61+ * GNU General Public License for more details.
62+ *
63+ * You should have received a copy of the GNU General Public License
64+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
65+ */
66+
67+import QtQuick 1.0
68+
69+/*
70+ List item that behaves similarly to a ListView but supports adding headers
71+ before every delegate.
72+ It works around the lack of flexibility in section headers positioning of
73+ ListView (cf. http://bugreports.qt.nokia.com/browse/QTBUG-12880). It also
74+ supports delegates that are flickable by flicking their content properly
75+ depending on where you are in the list.
76+
77+ To use it the following properties need to be set:
78+ - bodyDelegate: Component used as a template for each item of the model; it
79+ must have the following properties:
80+ - 'contentY': same definition as a Flickable's
81+ - 'totalHeight': the total height of the content of the body
82+ - 'currentItem': a reference to the item of the body (e.g. a delegate of
83+ a ListView) currently focused by the body
84+ - headerDelegate: Component used as a template for the header preceding each body
85+
86+ Two behaviours are available for the headers positioning:
87+ - normal: the headers are always positioned just before the body
88+ - accordion: the headers are stacked at the top and bottom of the list
89+
90+ Currently, it only works in a vertical layout.
91+*/
92+FocusScope {
93+ id: list
94+
95+ property alias flickable: mouse
96+ property alias model: repeater.model
97+ property bool accordion: false
98+ /* bodyDelegate must be an item that has the following properties:
99+ - 'contentY'
100+ - 'totalHeight'
101+ - 'currentItem'
102+ */
103+ property Component bodyDelegate
104+ property Component headerDelegate
105+ property int currentIndex: -1
106+ /* FIXME: Should be read-only but it is not possible to define a read-only property from QML.
107+ Ref.: https://bugreports.qt.nokia.com//browse/QTBUG-15257
108+ */
109+ property variant currentItem: items.childFromIndex(currentIndex)
110+
111+
112+ clip: true
113+
114+ /* updateMouseContentY() needs to be called when any of the variable
115+ involved in the computation of the 'y' property of the currentSubItem changes.
116+
117+ if accordion is false:
118+ - heightFirstChildren(index)
119+ - headerLoader.height
120+ - list.height
121+
122+ if accordion is true:
123+ - heightFirstChildren(index)
124+ - items.availableHeight
125+ - heightFirstHeaders(index)
126+
127+ items.contentHeight seems to depend on all of those so it's enough to
128+ depend only on it.
129+ */
130+ function updateMouseContentY() {
131+ if (currentSubItem != undefined) {
132+ mouse.contentY = Math.max(currentSubItem.mapToItem(mouse.contentItem, 0, 0).y -list.height/2, 0)
133+ }
134+ }
135+ property variant currentSubItem: currentItem != undefined ? currentItem.bodyLoader.item.currentItem : undefined
136+ onCurrentSubItemChanged: updateMouseContentY()
137+
138+ Connections {
139+ target: items
140+ onContentHeightChanged: updateMouseContentY()
141+ }
142+
143+ FocusScope {
144+ id: items
145+
146+ property int availableHeight: list.height - heightFirstHeaders(repeater.count)
147+ property int contentHeight: items.heightFirstChildren(repeater.count)
148+ property real value: mouse.contentY
149+
150+ anchors.fill: parent
151+
152+ function heightFirstChildren(n) {
153+ var i
154+ var totalHeight = 0
155+ /* items.children contains both the repeated items and the repeater
156+ itself. Skip and ignore the repeater. */
157+ for (i=0; i<n && i<children.length; i++) {
158+ if(children[i] == repeater) {n += 1; continue}
159+ totalHeight += children[i].height
160+ }
161+ return totalHeight
162+ }
163+
164+ function heightFirstHeaders(n) {
165+ var i
166+ var totalHeight = 0
167+ /* items.children contains both the repeated items and the repeater
168+ itself. Skip and ignore the repeater. */
169+ for (i=0; i<n && i<children.length; i++) {
170+ if(children[i] == repeater) {n += 1; continue}
171+ totalHeight += children[i].headerLoader.height
172+ }
173+ return totalHeight
174+ }
175+
176+ function clamp(x, min, max) {
177+ return Math.max(Math.min(x, max), min)
178+ }
179+
180+
181+ /* Keyboard navigation */
182+ function isIndexValid(index) {
183+ /* Assuming that children contains exactly one item that is not a child (repeater) */
184+ return index >= 0 && index < children.length-1
185+ }
186+
187+ focus: true
188+ Keys.onPressed: if (handleKeyPress(event.key)) event.accepted = true
189+ function handleKeyPress(key) {
190+ switch (key) {
191+ case Qt.Key_Down:
192+ return selectNextEnabled()
193+ case Qt.Key_Up:
194+ return selectPreviousEnabled()
195+ }
196+ }
197+
198+ function childFromIndex(index) {
199+ var indexInChildren = 0
200+ for(var i=0; i<children.length; i++) {
201+ if (children[i] != repeater) {
202+ if (indexInChildren == index) return children[i]
203+ indexInChildren++
204+ }
205+ }
206+
207+ return undefined
208+ }
209+
210+ function selectNextEnabled() {
211+ var index = currentIndex
212+ do {
213+ index += 1
214+ if (!isIndexValid(index)) return false
215+ } while(!childFromIndex(index).focusable)
216+ currentIndex = index
217+ return true
218+ }
219+
220+ function selectPreviousEnabled() {
221+ var index = currentIndex
222+ do {
223+ index -= 1
224+ if (!isIndexValid(index)) return false
225+ } while(!childFromIndex(index).focusable)
226+ currentIndex = index
227+ return true
228+ }
229+
230+ property bool needsFocus: false
231+ onChildrenChanged: {
232+ /* FIXME: this workarounds the fact that list.focus is set to false
233+ when the child with focus is destroyed
234+ */
235+ if (needsFocus) {
236+ list.focus = true
237+ needsFocus = false
238+ }
239+ /* Assuming that children contains exactly one item that is not a child (repeater) */
240+ if(children.length <= 1) {
241+ list.currentIndex = -1
242+ } else {
243+ list.currentIndex = -1
244+ selectNextEnabled()
245+ }
246+ }
247+
248+ Repeater {
249+ id: repeater
250+
251+ FocusScope {
252+ property alias bodyLoader: bodyLoader
253+ property alias headerLoader: headerLoader
254+
255+ focus: index == list.currentIndex
256+ Component.onDestruction: items.needsFocus = true
257+
258+ width: list.width
259+ height: headerLoader.height + bodyLoader.item.totalHeight
260+ property bool focusable: bodyLoader.item.focusable
261+
262+ property int pmin: pmax - (ymax - ymin)
263+ property int pmax: items.heightFirstChildren(index) - ymin
264+ property int ymin: list.accordion ? items.heightFirstHeaders(index) : -headerLoader.height
265+ property int ymax: list.accordion ? ymin + items.availableHeight : list.height
266+ y: items.clamp(-items.value + ymax + pmin, ymin, ymax)
267+
268+ Loader {
269+ id: headerLoader
270+
271+ focus: visible
272+ KeyNavigation.down: bodyLoader
273+ sourceComponent: headerDelegate
274+ onLoaded: item.focus = true
275+ width: parent.width
276+
277+ /* Workaround Qt bug http://bugreports.qt.nokia.com/browse/QTBUG-18857
278+ More documentation at http://bugreports.qt.nokia.com/browse/QTBUG-18011
279+ */
280+ property int index
281+ Binding { target: headerLoader; property: "index"; value: index }
282+ property variant model
283+ Binding { target: headerLoader; property: "model"; value: model }
284+ property variant body
285+ Binding { target: headerLoader; property: "body"; value: bodyLoader.item }
286+ }
287+
288+ Loader {
289+ id: bodyLoader
290+
291+ focus: !headerLoader.focus
292+ KeyNavigation.up: headerLoader
293+ sourceComponent: list.bodyDelegate
294+ onLoaded: item.focus = true
295+ width: parent.width
296+ anchors.top: headerLoader.bottom
297+ height: items.clamp(parent.ymax - parent.y, 0, item.totalHeight)
298+
299+ Binding {
300+ target: bodyLoader.item
301+ property: "contentY"
302+ value: Math.max(items.value - pmax, 0)
303+ }
304+
305+ /* Workaround Qt bug http://bugreports.qt.nokia.com/browse/QTBUG-18857
306+ More documentation at http://bugreports.qt.nokia.com/browse/QTBUG-18011
307+ */
308+ property int index
309+ Binding { target: bodyLoader; property: "index"; value: index }
310+ property variant model
311+ Binding { target: bodyLoader; property: "model"; value: model }
312+ }
313+ }
314+ }
315+ }
316+
317+ Flickable {
318+ id: mouse
319+
320+ z: -1
321+ anchors.fill: parent
322+ contentWidth: parent.width
323+ contentHeight: items.contentHeight
324+ }
325+}
326
327=== modified file 'places/ListViewWithScrollbar.qml'
328--- places/ListViewWithScrollbar.qml 2011-06-23 17:08:53 +0000
329+++ places/ListViewWithScrollbar.qml 2011-07-27 19:03:02 +0000
330@@ -19,10 +19,12 @@
331 import QtQuick 1.0
332
333 Item {
334- property alias list: list
335 property alias scrollbar: scrollbar
336+ property alias model: list.model
337+ property alias bodyDelegate: list.bodyDelegate
338+ property alias headerDelegate: list.headerDelegate
339
340- ListView {
341+ ListViewWithHeaders {
342 id: list
343
344 anchors.top: parent.top
345@@ -30,32 +32,6 @@
346 anchors.left: parent.left
347 anchors.right: scrollbar.left
348 anchors.rightMargin: 15
349-
350- clip: true
351- /* FIXME: proper spacing cannot be set because of the hack in Group.qml
352- whereby empty groups are still in the list but invisible and of
353- height 0.
354- */
355- //spacing: 31
356-
357- orientation: ListView.Vertical
358-
359- /* WARNING - HACK - FIXME
360- Issue:
361- User wise annoying jumps in the list are observable if cacheBuffer is
362- set to 0 (which is the default value). States such as 'folded' are
363- lost when scrolling a lot.
364-
365- Explanation:
366- The height of the Group delegate depends on its content. However its
367- content is not known until the delegate is instantiated because it
368- depends on the number of results displayed by its GridView.
369-
370- Resolution:
371- We set the cacheBuffer to the biggest possible int in order to make
372- sure all delegates are always instantiated.
373- */
374- cacheBuffer: 2147483647
375 }
376
377 Scrollbar {
378@@ -67,7 +43,7 @@
379 anchors.bottomMargin: 10
380 anchors.right: parent.right
381
382- targetFlickable: list
383+ targetFlickable: list.flickable
384
385 /* Hide the scrollbar if there is less than a page of results */
386 opacity: targetFlickable.visibleArea.heightRatio < 1.0 ? 1.0 : 0.0
387
388=== modified file 'places/PlaceEntryView.qml'
389--- places/PlaceEntryView.qml 2011-07-19 07:46:03 +0000
390+++ places/PlaceEntryView.qml 2011-07-27 19:03:02 +0000
391@@ -74,10 +74,10 @@
392 If groupRenderer == 'UnityShowcaseRenderer' then it will look for
393 the file 'UnityShowcaseRenderer.qml' and use it to render the group.
394 */
395- list.delegate: Loader {
396- property string groupRenderer: column_0
397- property string displayName: column_1
398- property string iconHint: column_2
399+ bodyDelegate: Loader {
400+ property string groupRenderer: model.column_0
401+ property string displayName: model.column_1
402+ property string iconHint: model.column_2
403 property int groupId: index
404
405 source: groupRenderer ? groupRenderer+".qml" : ""
406@@ -86,12 +86,8 @@
407 console.log("Failed to load renderer", groupRenderer)
408 }
409
410- width: ListView.view.width
411-
412 /* Model that will be used by the group's delegate */
413- SortFilterProxyModel {
414- id: group_model
415-
416+ property variant group_model: SortFilterProxyModel {
417 model: placeEntryView.model.entryResultsModel
418
419 /* resultsModel contains data for all the groups of a given Place.
420@@ -102,16 +98,35 @@
421 filterRegExp: RegExp("^%1$".arg(groupId)) /* exact match */
422 }
423
424- onLoaded: {
425- item.parentListView = results.list
426- item.displayName = displayName
427- item.iconHint = iconHint
428- item.groupId = groupId
429- item.model = group_model
430- item.placeEntryModel = placeEntryView.model
431- }
432- }
433-
434- list.model: placeEntryView.model != undefined ? placeEntryView.model.entryGroupsModel : undefined
435+ /* Required by ListViewWithHeaders when the loaded Renderer is a Flickable.
436+ In that case the list view scrolls the Flickable appropriately.
437+ */
438+ property int totalHeight: item.totalHeight != undefined ? item.totalHeight : 0
439+ property int contentY
440+ Binding { target: item; property: "contentY"; value: contentY }
441+ property bool focusable: group_model.count > 0
442+ property variant currentItem: item.currentItem
443+
444+ Binding { target: item; property: "displayName"; value: displayName }
445+ Binding { target: item; property: "iconHint"; value: iconHint }
446+ Binding { target: item; property: "groupId"; value: groupId }
447+ Binding { target: item; property: "group_model"; value: group_model }
448+ Binding { target: item; property: "placeEntryModel"; value: placeEntryView.model }
449+ }
450+
451+ headerDelegate: GroupHeader {
452+ visible: body.item.needHeader && body.focusable
453+ height: visible ? 32 : 0
454+
455+ property bool foldable: body.item.folded != undefined
456+ availableCount: foldable ? body.group_model.count - body.item.cellsPerRow : 0
457+ folded: foldable ? body.item.folded : false
458+ onClicked: if(foldable) body.item.folded = !body.item.folded
459+
460+ icon: body.iconHint
461+ label: body.displayName
462+ }
463+
464+ model: placeEntryView.model != undefined ? placeEntryView.model.entryGroupsModel : undefined
465 }
466 }
467
468=== modified file 'places/Renderer.qml'
469--- places/Renderer.qml 2011-06-23 17:08:53 +0000
470+++ places/Renderer.qml 2011-07-27 19:03:02 +0000
471@@ -30,7 +30,7 @@
472 property string displayName /* Name of the group typically displayed in the header */
473 property string iconHint /* Icon id of the group */
474 property int groupId /* Index of the group */
475- property variant model /* List model containing the items to be displayed by the renderer */
476+ property variant group_model /* List model containing the items to be displayed by the renderer */
477 property variant placeEntryModel /* Reference to the place entry the group belongs to */
478- property variant parentListView /* Reference to the ListView the renderer is nested into */
479+ property bool needHeader: false /* Whether or not the renderer requires a header to be displayed */
480 }
481
482=== modified file 'places/RendererGrid.qml'
483--- places/RendererGrid.qml 2011-06-26 15:41:06 +0000
484+++ places/RendererGrid.qml 2011-07-27 19:03:02 +0000
485@@ -28,6 +28,11 @@
486 Renderer {
487 id: renderer
488
489+ needHeader: true
490+ property alias cellsPerRow: results.cellsPerRow
491+ property alias contentY: results.contentY
492+ property alias currentItem: results.currentItem
493+
494 property variant cellRenderer
495 property bool folded
496 folded: {
497@@ -44,85 +49,23 @@
498 property int horizontalSpacing: 26
499 property int verticalSpacing: 26
500
501- /* Using results.contentHeight produces binding loop warnings and potential
502- rendering issues. We compute the height manually.
503- */
504- /* FIXME: tricking the system by making the delegate of height 0 and invisible
505- is no good: the item in the model still exists and some things
506- such as keyboard selection break.
507- */
508- visible: results.model.totalCount > 0
509- height: visible ? header.height + results_layout.anchors.topMargin + results.totalHeight : 0
510- //Behavior on height {NumberAnimation {duration: 200}}
511-
512- GroupHeader {
513- id: header
514-
515- availableCount: results.model.totalCount - results.cellsPerRow
516- folded: parent.folded
517- anchors.top: parent.top
518- anchors.left: parent.left
519- anchors.right: parent.right
520- height: 32
521- icon: parent.iconHint
522- label: parent.displayName
523-
524- onClicked: parent.folded = !parent.folded
525- }
526+ /* FIXME: using results_layout.anchors.topMargin in the following expression
527+ causes QML to think they might be an anchor loop. */
528+ property int totalHeight: results.count > 0 ? results_layout.anchors.topMargin + results.totalHeight : 0
529
530 Item {
531 id: results_layout
532
533- anchors.top: header.bottom
534- anchors.topMargin: 22
535- anchors.left: parent.left
536+ anchors.fill: parent
537+ anchors.topMargin: 12
538 anchors.leftMargin: 2
539- anchors.right: parent.right
540- anchors.bottom: parent.bottom
541
542 CenteredGridView {
543 id: results
544
545- /* FIXME: this is a gross hack compensating for the lack of sections
546- in GridView (see ListView.section).
547-
548- We nest GridViews inside a ListView and add headers manually
549- (GroupHeader). The total height of each Group is computed
550- manually and given back to the ListView. However that size cannot
551- be used by the individual GridViews because it would make them
552- load all of their delegates at once using far too much memory and
553- processing power. Instead we constrain the height of the GridViews
554- and compute their position manually to compensate for the position
555- changes when flicking the ListView.
556-
557- We assume that renderer.parentListView is the ListView we nest our
558- GridView into.
559- */
560- property variant flickable: renderer.parentListView.contentItem
561-
562- /* flickable.contentY*0 is equal to 0 but is necessary in order to
563- have the entire expression being evaluated at the right moment.
564- */
565- property int inFlickableY: flickable.contentY*0+parent.mapToItem(flickable, 0, 0).y
566- /* note: testing for flickable.height < 0 is probably useless since it is
567- unlikely flickable.height will ever be negative.
568- */
569- property int compensateY: inFlickableY > 0 || flickable.height < 0 || totalHeight < flickable.height ? 0 : -inFlickableY
570-
571- /* Synchronise the position and content's position of the GridView
572- with the current position of flickable's visibleArea */
573- function synchronisePosition() {
574- y = compensateY
575- contentY = compensateY
576- }
577-
578- onCompensateYChanged: synchronisePosition()
579- /* Any change in content needs to trigger a synchronisation */
580- onCountChanged: synchronisePosition()
581- onModelChanged: synchronisePosition()
582-
583- width: flickable.width
584- height: Math.min(totalHeight, flickable.height)
585+ focus: true
586+
587+ anchors.fill: parent
588
589 property int totalHeight: results.cellHeight*Math.ceil(count/cellsPerRow)
590
591@@ -168,7 +111,7 @@
592
593 /* Only display one line of items when folded */
594 model: SortFilterProxyModel {
595- model: renderer.model != undefined ? renderer.model : null
596+ model: renderer.group_model != undefined ? renderer.group_model : null
597 limit: folded ? results.cellsPerRow : -1
598 }
599 }
600
601=== modified file 'places/Scrollbar.qml'
602--- places/Scrollbar.qml 2011-06-26 15:16:43 +0000
603+++ places/Scrollbar.qml 2011-07-27 19:03:02 +0000
604@@ -83,7 +83,7 @@
605 when: !dragMouseArea.drag.active
606 }
607
608- height: Math.max(minimalHeight, targetFlickable.visibleArea.heightRatio * scrollbar.height)
609+ height: Math.min(scrollbar.height, Math.max(minimalHeight, targetFlickable.visibleArea.heightRatio * scrollbar.height))
610
611 Behavior on height {NumberAnimation {duration: 200; easing.type: Easing.InOutQuad}}
612
613
614=== modified file 'places/UnityEmptySearchRenderer.qml'
615--- places/UnityEmptySearchRenderer.qml 2011-06-23 17:08:53 +0000
616+++ places/UnityEmptySearchRenderer.qml 2011-07-27 19:03:02 +0000
617@@ -40,7 +40,7 @@
618 boundsBehavior: ListView.StopAtBounds
619 orientation: ListView.Vertical
620
621- model: renderer.model
622+ model: renderer.group_model
623 delegate: Button {
624 property string uri: column_0
625 property string iconHint: column_1
626
627=== modified file 'places/dash.qml'
628--- places/dash.qml 2011-07-05 19:01:18 +0000
629+++ places/dash.qml 2011-07-27 19:03:02 +0000
630@@ -30,6 +30,12 @@
631 value: (currentPage && currentPage.expanded != undefined) ? currentPage.expanded : true
632 }
633
634+ /* Unload the current page when closing the dash */
635+ Connections {
636+ target: dashView
637+ onActiveChanged: if (!dashView.active) pageLoader.source = ""
638+ }
639+
640 function activatePage(page) {
641 if (page == currentPage) {
642 return

Subscribers

People subscribed via source and target branches