Merge lp:~fboucault/unity-2d/keyboard_navigation_experimental into lp:unity-2d/3.0
- keyboard_navigation_experimental
- Merge into natty
Status: | Merged |
---|---|
Approved by: | Florian Boucault |
Approved revision: | 642 |
Merged at revision: | 651 |
Proposed branch: | lp:~fboucault/unity-2d/keyboard_navigation_experimental |
Merge into: | lp:unity-2d/3.0 |
Diff against target: |
1004 lines (+476/-157) 14 files modified
places/CenteredGridView.qml (+26/-0) places/Home.qml (+36/-18) places/HomeShortcuts.qml (+26/-1) places/ListViewWithHeaders.qml (+277/-0) places/ListViewWithScrollbar.qml (+7/-30) places/PlaceEntryView.qml (+40/-22) places/Renderer.qml (+3/-3) places/RendererGrid.qml (+17/-72) places/Scrollbar.qml (+1/-1) places/SearchEntry.qml (+3/-1) places/SearchRefine.qml (+9/-0) places/SearchRefineOptionType.qml (+5/-1) places/UnityEmptySearchRenderer.qml (+1/-1) places/dash.qml (+25/-7) |
To merge this branch: | bzr merge lp:~fboucault/unity-2d/keyboard_navigation_experimental |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gerry Boland (community) | search for bugs | Needs Fixing | |
Review via email: mp+69480@code.launchpad.net |
Commit message
Description of the change
[dash] Implemented keyboard navigation.
Gerry Boland (gerboland) wrote : | # |
Florian Boucault (fboucault) wrote : | # |
> I've found bugs:
>
> - if number of results less than 6, the "See more/fewer results" option is of
> course hidden but
> is still found by the keyboard navigation. Repro: do a search which returns <6
> Applications. Hit down: where did focus go? Hit down again: there it is!
[REPORT BUG] It is done that way for accessibility reasons. The name of the group needs to be spoken to blind people. Maybe there is a nicer way to do it.
>
> - Do search which returns large list of applications, scroll down. Now return
> to search box &
> do new search (again returning large list). New search results are still
> scrolled down.
[NEEDS FIXING] Scrolling down is done with the keyboard.
>
> - At Home, if you leave mouse highlighting one gridItem, using keyboard to
> navigate gives
> you a second "active" looking gridItem.
>
[REPORT BUG] Not sure what the solution should be. Should the keyboard focus follow the mouse?
> - At Home, press down, down, right, right, up, up, then Esc to close it. Now
> open Home again, press down, down and you appear on the third column now.
>
The dash is going stateful so that behaviour is _probably_ ok.
> - At Search List of more than 6 rows or so, be about half-way down the
> applications list.
> Press up, down, left, right, and see how the list moves on the left/right
> motion - it
> shouldn't. This one is hard to reproduce - I get it searching for "e".
>
[REPORT BUG]
> - At "Search Applications", at the search box, press right. Where is focus?
>
[NEEDS FIXING] I think the focus was sent to the search filters previously.
> - Quickly type Meta, "w", down. See where focus appears - at "See Fewer
> Results" in the Files&Folders section
>
[REPORT BUG] Not a big issue but to solve it it would require acting on keyboard presses only when the results are all in. Not sure if it's right especially if we consider network sources.
>
> Suggestions/
> - highlighted grid item's text has underline. Correct?
>
[REPORT BUG] Should be like in Unity 3D instead.
> - At Home, continually pressing right on gridItems should wrap around??
> Instead of stopping?
> Similarly for down & up? Fewer barriers?
It's fine like it is I think.
- 639. By Florian Boucault
-
Remade RendererGrid.
interactive false thus preventing mouse interaction with the individual groups. - 640. By Florian Boucault
-
Prevent the focus to be on the refine search pane when it is folded.
- 641. By Florian Boucault
-
Added FIXME/
- 642. By Florian Boucault
-
Merged colo:proper_grid
Florian Boucault (fboucault) wrote : | # |
> > I've found bugs:
> >
> > - if number of results less than 6, the "See more/fewer results" option is
> of
> > course hidden but
> > is still found by the keyboard navigation. Repro: do a search which returns
> <6
> > Applications. Hit down: where did focus go? Hit down again: there it is!
>
> [REPORT BUG] It is done that way for accessibility reasons. The name of the
> group needs to be spoken to blind people. Maybe there is a nicer way to do it.
>
> >
> > - Do search which returns large list of applications, scroll down. Now
> return
> > to search box &
> > do new search (again returning large list). New search results are still
> > scrolled down.
>
> [NEEDS FIXING] Scrolling down is done with the keyboard.
>
Fixed.
> >
> > - At Home, if you leave mouse highlighting one gridItem, using keyboard to
> > navigate gives
> > you a second "active" looking gridItem.
> >
>
> [REPORT BUG] Not sure what the solution should be. Should the keyboard focus
> follow the mouse?
>
> > - At Home, press down, down, right, right, up, up, then Esc to close it. Now
> > open Home again, press down, down and you appear on the third column now.
> >
>
> The dash is going stateful so that behaviour is _probably_ ok.
>
> > - At Search List of more than 6 rows or so, be about half-way down the
> > applications list.
> > Press up, down, left, right, and see how the list moves on the left/right
> > motion - it
> > shouldn't. This one is hard to reproduce - I get it searching for "e".
> >
>
> [REPORT BUG]
>
> > - At "Search Applications", at the search box, press right. Where is focus?
> >
>
> [NEEDS FIXING] I think the focus was sent to the search filters previously.
>
Fixed.
> > - Quickly type Meta, "w", down. See where focus appears - at "See Fewer
> > Results" in the Files&Folders section
> >
>
> [REPORT BUG] Not a big issue but to solve it it would require acting on
> keyboard presses only when the results are all in. Not sure if it's right
> especially if we consider network sources.
>
> >
> > Suggestions/
> > - highlighted grid item's text has underline. Correct?
> >
>
> [REPORT BUG] Should be like in Unity 3D instead.
>
> > - At Home, continually pressing right on gridItems should wrap around??
> > Instead of stopping?
> > Similarly for down & up? Fewer barriers?
>
> It's fine like it is I think.
Preview Diff
1 | === modified file 'places/CenteredGridView.qml' | |||
2 | --- places/CenteredGridView.qml 2011-06-23 17:08:53 +0000 | |||
3 | +++ places/CenteredGridView.qml 2011-07-27 19:03:19 +0000 | |||
4 | @@ -24,6 +24,32 @@ | |||
5 | 24 | property int delegateWidth: 100 | 24 | property int delegateWidth: 100 |
6 | 25 | property int delegateHeight: 100 | 25 | property int delegateHeight: 100 |
7 | 26 | 26 | ||
8 | 27 | /* Manually handle key presses so that when the 'interactive' property is | ||
9 | 28 | set to false the keyboard navigation still works. | ||
10 | 29 | |||
11 | 30 | Ref.: https://bugreports.qt.nokia.com/browse/QTBUG-17051 | ||
12 | 31 | */ | ||
13 | 32 | function selectChild(index) { | ||
14 | 33 | if (index < 0 || index >= count) return false | ||
15 | 34 | currentIndex = index | ||
16 | 35 | return true | ||
17 | 36 | } | ||
18 | 37 | |||
19 | 38 | Keys.onPressed: if (handleKeyPress(event.key)) event.accepted = true | ||
20 | 39 | function handleKeyPress(key) { | ||
21 | 40 | switch (key) { | ||
22 | 41 | case Qt.Key_Right: | ||
23 | 42 | return selectChild(currentIndex+1) | ||
24 | 43 | case Qt.Key_Left: | ||
25 | 44 | return selectChild(currentIndex-1) | ||
26 | 45 | case Qt.Key_Up: | ||
27 | 46 | return selectChild(currentIndex-cellsPerRow) | ||
28 | 47 | case Qt.Key_Down: | ||
29 | 48 | return selectChild(currentIndex+cellsPerRow) | ||
30 | 49 | } | ||
31 | 50 | return false | ||
32 | 51 | } | ||
33 | 52 | |||
34 | 27 | /* Compute the number of cells per row and column so that: | 53 | /* Compute the number of cells per row and column so that: |
35 | 28 | - the items are always centered horizontally within the grid (= same | 54 | - the items are always centered horizontally within the grid (= same |
36 | 29 | margins on the right and left sides of the grid) when flow is set | 55 | margins on the right and left sides of the grid) when flow is set |
37 | 30 | 56 | ||
38 | === modified file 'places/Home.qml' | |||
39 | --- places/Home.qml 2011-07-26 16:36:51 +0000 | |||
40 | +++ places/Home.qml 2011-07-27 19:03:19 +0000 | |||
41 | @@ -19,7 +19,7 @@ | |||
42 | 19 | import QtQuick 1.0 | 19 | import QtQuick 1.0 |
43 | 20 | import Unity2d 1.0 /* Necessary for SortFilterProxyModel and for the ImageProvider serving image://icons/theme_name/icon_name */ | 20 | import Unity2d 1.0 /* Necessary for SortFilterProxyModel and for the ImageProvider serving image://icons/theme_name/icon_name */ |
44 | 21 | 21 | ||
46 | 22 | Item { | 22 | FocusScope { |
47 | 23 | property variant model: PageModel { | 23 | property variant model: PageModel { |
48 | 24 | /* model.entrySearchQuery is copied over to all place entries's globalSearchQuery property */ | 24 | /* model.entrySearchQuery is copied over to all place entries's globalSearchQuery property */ |
49 | 25 | onEntrySearchQueryChanged: { | 25 | onEntrySearchQueryChanged: { |
50 | @@ -96,26 +96,39 @@ | |||
51 | 96 | ListViewWithScrollbar { | 96 | ListViewWithScrollbar { |
52 | 97 | id: globalSearch | 97 | id: globalSearch |
53 | 98 | 98 | ||
54 | 99 | focus: globalSearchActive | ||
55 | 99 | opacity: globalSearchActive ? 1 : 0 | 100 | opacity: globalSearchActive ? 1 : 0 |
56 | 100 | anchors.fill: parent | 101 | anchors.fill: parent |
57 | 101 | 102 | ||
69 | 102 | list.model: dash.places | 103 | model: dash.places |
70 | 103 | 104 | ||
71 | 104 | list.delegate: UnityDefaultRenderer { | 105 | bodyDelegate: UnityDefaultRenderer { |
72 | 105 | width: ListView.view.width | 106 | placeEntryModel: model.item |
73 | 106 | 107 | displayName: model.item.name | |
74 | 107 | parentListView: list | 108 | iconHint: model.item.icon |
75 | 108 | placeEntryModel: item | 109 | |
76 | 109 | displayName: item.name | 110 | group_model: model.item.globalResultsModel |
77 | 110 | iconHint: item.icon | 111 | property bool focusable: group_model != undefined && group_model.count > 0 |
78 | 111 | 112 | } | |
79 | 112 | model: item.globalResultsModel | 113 | |
80 | 114 | headerDelegate: GroupHeader { | ||
81 | 115 | visible: body.needHeader && body.focusable | ||
82 | 116 | height: visible ? 32 : 0 | ||
83 | 117 | |||
84 | 118 | property bool foldable: body.folded != undefined | ||
85 | 119 | availableCount: foldable && body.group_model != null ? body.group_model.count - body.cellsPerRow : 0 | ||
86 | 120 | folded: foldable ? body.folded : false | ||
87 | 121 | onClicked: if(foldable) body.folded = !body.folded | ||
88 | 122 | |||
89 | 123 | icon: body.iconHint | ||
90 | 124 | label: body.displayName | ||
91 | 113 | } | 125 | } |
92 | 114 | } | 126 | } |
93 | 115 | 127 | ||
95 | 116 | Rectangle { | 128 | FocusScope { |
96 | 117 | id: shortcuts | 129 | id: shortcuts |
97 | 118 | 130 | ||
98 | 131 | focus: !globalSearchActive | ||
99 | 119 | opacity: (!globalSearchActive && (shortcutsActive || dashView.dashMode == DashDeclarativeView.FullScreenMode)) ? 1 : 0 | 132 | opacity: (!globalSearchActive && (shortcutsActive || dashView.dashMode == DashDeclarativeView.FullScreenMode)) ? 1 : 0 |
100 | 120 | anchors.horizontalCenter: parent.horizontalCenter | 133 | anchors.horizontalCenter: parent.horizontalCenter |
101 | 121 | anchors.verticalCenter: parent.verticalCenter | 134 | anchors.verticalCenter: parent.verticalCenter |
102 | @@ -123,11 +136,14 @@ | |||
103 | 123 | width: 888 | 136 | width: 888 |
104 | 124 | height: 466 | 137 | height: 466 |
105 | 125 | 138 | ||
111 | 126 | radius: 5 | 139 | Rectangle { |
112 | 127 | border.width: 1 | 140 | anchors.fill: parent |
113 | 128 | /* FIXME: wrong colors */ | 141 | radius: 5 |
114 | 129 | border.color: Qt.rgba(1, 1, 1, 0.2) | 142 | border.width: 1 |
115 | 130 | color: Qt.rgba(0, 0, 0, 0.3) | 143 | /* FIXME: wrong colors */ |
116 | 144 | border.color: Qt.rgba(1, 1, 1, 0.2) | ||
117 | 145 | color: Qt.rgba(0, 0, 0, 0.3) | ||
118 | 146 | } | ||
119 | 131 | 147 | ||
120 | 132 | Button { | 148 | Button { |
121 | 133 | id: closeShortcutsButton | 149 | id: closeShortcutsButton |
122 | @@ -158,11 +174,13 @@ | |||
123 | 158 | on the default version if a custom one doesn’t exist. */ | 174 | on the default version if a custom one doesn’t exist. */ |
124 | 159 | Loader { | 175 | Loader { |
125 | 160 | id: customShortcutsLoader | 176 | id: customShortcutsLoader |
126 | 177 | focus: status == Loader.Ready | ||
127 | 161 | anchors.fill: parent | 178 | anchors.fill: parent |
128 | 162 | source: "HomeShortcutsCustomized.qml" | 179 | source: "HomeShortcutsCustomized.qml" |
129 | 163 | } | 180 | } |
130 | 164 | Loader { | 181 | Loader { |
131 | 165 | id: defaultShortcutsLoader | 182 | id: defaultShortcutsLoader |
132 | 183 | focus: !customShortcutsLoader.focus | ||
133 | 166 | anchors.fill: parent | 184 | anchors.fill: parent |
134 | 167 | source: (customShortcutsLoader.status == Loader.Error) ? "HomeShortcuts.qml" : "" | 185 | source: (customShortcutsLoader.status == Loader.Error) ? "HomeShortcuts.qml" : "" |
135 | 168 | } | 186 | } |
136 | 169 | 187 | ||
137 | === modified file 'places/HomeShortcuts.qml' | |||
138 | --- places/HomeShortcuts.qml 2011-07-04 23:07:20 +0000 | |||
139 | +++ places/HomeShortcuts.qml 2011-07-27 19:03:19 +0000 | |||
140 | @@ -19,16 +19,41 @@ | |||
141 | 19 | import QtQuick 1.0 | 19 | import QtQuick 1.0 |
142 | 20 | import Unity2d 1.0 /* Necessary for the ImageProvider serving image://icons/theme_name/icon_name */ | 20 | import Unity2d 1.0 /* Necessary for the ImageProvider serving image://icons/theme_name/icon_name */ |
143 | 21 | 21 | ||
145 | 22 | Flow { | 22 | Grid { |
146 | 23 | anchors.fill: parent | 23 | anchors.fill: parent |
147 | 24 | anchors.topMargin: 26 | 24 | anchors.topMargin: 26 |
148 | 25 | anchors.bottomMargin: 35 | 25 | anchors.bottomMargin: 35 |
149 | 26 | anchors.leftMargin: 32 | 26 | anchors.leftMargin: 32 |
150 | 27 | anchors.rightMargin: 32 | 27 | anchors.rightMargin: 32 |
151 | 28 | spacing: 61 | 28 | spacing: 61 |
152 | 29 | columns: 4 | ||
153 | 30 | rows: 2 | ||
154 | 31 | |||
155 | 32 | function selectChild(index) { | ||
156 | 33 | if (index < 0 || index >= children.length) return false | ||
157 | 34 | currentIndex = index | ||
158 | 35 | children[index].focus = true | ||
159 | 36 | return true | ||
160 | 37 | } | ||
161 | 38 | |||
162 | 39 | property int currentIndex: 0 | ||
163 | 40 | Keys.onPressed: if (handleKeyPress(event.key)) event.accepted = true | ||
164 | 41 | function handleKeyPress(key) { | ||
165 | 42 | switch (key) { | ||
166 | 43 | case Qt.Key_Right: | ||
167 | 44 | return selectChild(currentIndex+1) | ||
168 | 45 | case Qt.Key_Left: | ||
169 | 46 | return selectChild(currentIndex-1) | ||
170 | 47 | case Qt.Key_Up: | ||
171 | 48 | return selectChild(currentIndex-columns) | ||
172 | 49 | case Qt.Key_Down: | ||
173 | 50 | return selectChild(currentIndex+columns) | ||
174 | 51 | } | ||
175 | 52 | } | ||
176 | 29 | 53 | ||
177 | 30 | /* FIXME: dummy icons need to be replaced by design's */ | 54 | /* FIXME: dummy icons need to be replaced by design's */ |
178 | 31 | HomeButton { | 55 | HomeButton { |
179 | 56 | focus: true | ||
180 | 32 | label: u2d.tr("Media Apps") | 57 | label: u2d.tr("Media Apps") |
181 | 33 | icon: "artwork/find_media_apps.png" | 58 | icon: "artwork/find_media_apps.png" |
182 | 34 | onClicked: activatePlaceEntry("/usr/share/unity/places/applications.place", "Files", 9) | 59 | onClicked: activatePlaceEntry("/usr/share/unity/places/applications.place", "Files", 9) |
183 | 35 | 60 | ||
184 | === added file 'places/ListViewWithHeaders.qml' | |||
185 | --- places/ListViewWithHeaders.qml 1970-01-01 00:00:00 +0000 | |||
186 | +++ places/ListViewWithHeaders.qml 2011-07-27 19:03:19 +0000 | |||
187 | @@ -0,0 +1,277 @@ | |||
188 | 1 | /* | ||
189 | 2 | * This file is part of unity-2d | ||
190 | 3 | * | ||
191 | 4 | * Copyright 2010-2011 Canonical Ltd. | ||
192 | 5 | * | ||
193 | 6 | * This program is free software; you can redistribute it and/or modify | ||
194 | 7 | * it under the terms of the GNU General Public License as published by | ||
195 | 8 | * the Free Software Foundation; version 3. | ||
196 | 9 | * | ||
197 | 10 | * This program is distributed in the hope that it will be useful, | ||
198 | 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
199 | 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
200 | 13 | * GNU General Public License for more details. | ||
201 | 14 | * | ||
202 | 15 | * You should have received a copy of the GNU General Public License | ||
203 | 16 | * along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
204 | 17 | */ | ||
205 | 18 | |||
206 | 19 | import QtQuick 1.0 | ||
207 | 20 | |||
208 | 21 | /* | ||
209 | 22 | List item that behaves similarly to a ListView but supports adding headers | ||
210 | 23 | before every delegate. | ||
211 | 24 | It works around the lack of flexibility in section headers positioning of | ||
212 | 25 | ListView (cf. http://bugreports.qt.nokia.com/browse/QTBUG-12880). It also | ||
213 | 26 | supports delegates that are flickable by flicking their content properly | ||
214 | 27 | depending on where you are in the list. | ||
215 | 28 | |||
216 | 29 | To use it the following properties need to be set: | ||
217 | 30 | - bodyDelegate: Component used as a template for each item of the model; it | ||
218 | 31 | must have the following properties: | ||
219 | 32 | - 'contentY': same definition as a Flickable's | ||
220 | 33 | - 'totalHeight': the total height of the content of the body | ||
221 | 34 | - 'currentItem': a reference to the item of the body (e.g. a delegate of | ||
222 | 35 | a ListView) currently focused by the body | ||
223 | 36 | - headerDelegate: Component used as a template for the header preceding each body | ||
224 | 37 | |||
225 | 38 | Two behaviours are available for the headers positioning: | ||
226 | 39 | - normal: the headers are always positioned just before the body | ||
227 | 40 | - accordion: the headers are stacked at the top and bottom of the list | ||
228 | 41 | |||
229 | 42 | Currently, it only works in a vertical layout. | ||
230 | 43 | */ | ||
231 | 44 | FocusScope { | ||
232 | 45 | id: list | ||
233 | 46 | |||
234 | 47 | property alias flickable: mouse | ||
235 | 48 | property alias model: repeater.model | ||
236 | 49 | property bool accordion: false | ||
237 | 50 | /* bodyDelegate must be an item that has the following properties: | ||
238 | 51 | - 'contentY' | ||
239 | 52 | - 'totalHeight' | ||
240 | 53 | - 'currentItem' | ||
241 | 54 | */ | ||
242 | 55 | property Component bodyDelegate | ||
243 | 56 | property Component headerDelegate | ||
244 | 57 | property int currentIndex: -1 | ||
245 | 58 | /* FIXME: Should be read-only but it is not possible to define a read-only property from QML. | ||
246 | 59 | Ref.: https://bugreports.qt.nokia.com//browse/QTBUG-15257 | ||
247 | 60 | */ | ||
248 | 61 | property variant currentItem: items.childFromIndex(currentIndex) | ||
249 | 62 | |||
250 | 63 | |||
251 | 64 | clip: true | ||
252 | 65 | |||
253 | 66 | /* updateMouseContentY() needs to be called when any of the variable | ||
254 | 67 | involved in the computation of the 'y' property of the currentSubItem changes. | ||
255 | 68 | |||
256 | 69 | if accordion is false: | ||
257 | 70 | - heightFirstChildren(index) | ||
258 | 71 | - headerLoader.height | ||
259 | 72 | - list.height | ||
260 | 73 | |||
261 | 74 | if accordion is true: | ||
262 | 75 | - heightFirstChildren(index) | ||
263 | 76 | - items.availableHeight | ||
264 | 77 | - heightFirstHeaders(index) | ||
265 | 78 | |||
266 | 79 | items.contentHeight seems to depend on all of those so it's enough to | ||
267 | 80 | depend only on it. | ||
268 | 81 | */ | ||
269 | 82 | function updateMouseContentY() { | ||
270 | 83 | if (currentSubItem != undefined) { | ||
271 | 84 | mouse.contentY = Math.max(currentSubItem.mapToItem(mouse.contentItem, 0, 0).y -list.height/2, 0) | ||
272 | 85 | } | ||
273 | 86 | } | ||
274 | 87 | property variant currentSubItem: currentItem != undefined ? currentItem.bodyLoader.item.currentItem : undefined | ||
275 | 88 | onCurrentSubItemChanged: updateMouseContentY() | ||
276 | 89 | |||
277 | 90 | Connections { | ||
278 | 91 | target: items | ||
279 | 92 | onContentHeightChanged: updateMouseContentY() | ||
280 | 93 | } | ||
281 | 94 | |||
282 | 95 | FocusScope { | ||
283 | 96 | id: items | ||
284 | 97 | |||
285 | 98 | property int availableHeight: list.height - heightFirstHeaders(repeater.count) | ||
286 | 99 | property int contentHeight: items.heightFirstChildren(repeater.count) | ||
287 | 100 | property real value: mouse.contentY | ||
288 | 101 | |||
289 | 102 | anchors.fill: parent | ||
290 | 103 | |||
291 | 104 | function heightFirstChildren(n) { | ||
292 | 105 | var i | ||
293 | 106 | var totalHeight = 0 | ||
294 | 107 | /* items.children contains both the repeated items and the repeater | ||
295 | 108 | itself. Skip and ignore the repeater. */ | ||
296 | 109 | for (i=0; i<n && i<children.length; i++) { | ||
297 | 110 | if(children[i] == repeater) {n += 1; continue} | ||
298 | 111 | totalHeight += children[i].height | ||
299 | 112 | } | ||
300 | 113 | return totalHeight | ||
301 | 114 | } | ||
302 | 115 | |||
303 | 116 | function heightFirstHeaders(n) { | ||
304 | 117 | var i | ||
305 | 118 | var totalHeight = 0 | ||
306 | 119 | /* items.children contains both the repeated items and the repeater | ||
307 | 120 | itself. Skip and ignore the repeater. */ | ||
308 | 121 | for (i=0; i<n && i<children.length; i++) { | ||
309 | 122 | if(children[i] == repeater) {n += 1; continue} | ||
310 | 123 | totalHeight += children[i].headerLoader.height | ||
311 | 124 | } | ||
312 | 125 | return totalHeight | ||
313 | 126 | } | ||
314 | 127 | |||
315 | 128 | function clamp(x, min, max) { | ||
316 | 129 | return Math.max(Math.min(x, max), min) | ||
317 | 130 | } | ||
318 | 131 | |||
319 | 132 | |||
320 | 133 | /* Keyboard navigation */ | ||
321 | 134 | function isIndexValid(index) { | ||
322 | 135 | /* Assuming that children contains exactly one item that is not a child (repeater) */ | ||
323 | 136 | return index >= 0 && index < children.length-1 | ||
324 | 137 | } | ||
325 | 138 | |||
326 | 139 | focus: true | ||
327 | 140 | Keys.onPressed: if (handleKeyPress(event.key)) event.accepted = true | ||
328 | 141 | function handleKeyPress(key) { | ||
329 | 142 | switch (key) { | ||
330 | 143 | case Qt.Key_Down: | ||
331 | 144 | return selectNextEnabled() | ||
332 | 145 | case Qt.Key_Up: | ||
333 | 146 | return selectPreviousEnabled() | ||
334 | 147 | } | ||
335 | 148 | } | ||
336 | 149 | |||
337 | 150 | function childFromIndex(index) { | ||
338 | 151 | var indexInChildren = 0 | ||
339 | 152 | for(var i=0; i<children.length; i++) { | ||
340 | 153 | if (children[i] != repeater) { | ||
341 | 154 | if (indexInChildren == index) return children[i] | ||
342 | 155 | indexInChildren++ | ||
343 | 156 | } | ||
344 | 157 | } | ||
345 | 158 | |||
346 | 159 | return undefined | ||
347 | 160 | } | ||
348 | 161 | |||
349 | 162 | function selectNextEnabled() { | ||
350 | 163 | var index = currentIndex | ||
351 | 164 | do { | ||
352 | 165 | index += 1 | ||
353 | 166 | if (!isIndexValid(index)) return false | ||
354 | 167 | } while(!childFromIndex(index).focusable) | ||
355 | 168 | currentIndex = index | ||
356 | 169 | return true | ||
357 | 170 | } | ||
358 | 171 | |||
359 | 172 | function selectPreviousEnabled() { | ||
360 | 173 | var index = currentIndex | ||
361 | 174 | do { | ||
362 | 175 | index -= 1 | ||
363 | 176 | if (!isIndexValid(index)) return false | ||
364 | 177 | } while(!childFromIndex(index).focusable) | ||
365 | 178 | currentIndex = index | ||
366 | 179 | return true | ||
367 | 180 | } | ||
368 | 181 | |||
369 | 182 | property bool needsFocus: false | ||
370 | 183 | onChildrenChanged: { | ||
371 | 184 | /* FIXME: this workarounds the fact that list.focus is set to false | ||
372 | 185 | when the child with focus is destroyed | ||
373 | 186 | */ | ||
374 | 187 | if (needsFocus) { | ||
375 | 188 | list.focus = true | ||
376 | 189 | needsFocus = false | ||
377 | 190 | } | ||
378 | 191 | /* Assuming that children contains exactly one item that is not a child (repeater) */ | ||
379 | 192 | if(children.length <= 1) { | ||
380 | 193 | list.currentIndex = -1 | ||
381 | 194 | } else { | ||
382 | 195 | list.currentIndex = -1 | ||
383 | 196 | selectNextEnabled() | ||
384 | 197 | } | ||
385 | 198 | } | ||
386 | 199 | |||
387 | 200 | Repeater { | ||
388 | 201 | id: repeater | ||
389 | 202 | |||
390 | 203 | FocusScope { | ||
391 | 204 | property alias bodyLoader: bodyLoader | ||
392 | 205 | property alias headerLoader: headerLoader | ||
393 | 206 | |||
394 | 207 | focus: index == list.currentIndex | ||
395 | 208 | Component.onDestruction: items.needsFocus = true | ||
396 | 209 | |||
397 | 210 | width: list.width | ||
398 | 211 | height: headerLoader.height + bodyLoader.item.totalHeight | ||
399 | 212 | property bool focusable: bodyLoader.item.focusable | ||
400 | 213 | |||
401 | 214 | property int pmin: pmax - (ymax - ymin) | ||
402 | 215 | property int pmax: items.heightFirstChildren(index) - ymin | ||
403 | 216 | property int ymin: list.accordion ? items.heightFirstHeaders(index) : -headerLoader.height | ||
404 | 217 | property int ymax: list.accordion ? ymin + items.availableHeight : list.height | ||
405 | 218 | y: items.clamp(-items.value + ymax + pmin, ymin, ymax) | ||
406 | 219 | |||
407 | 220 | Loader { | ||
408 | 221 | id: headerLoader | ||
409 | 222 | |||
410 | 223 | focus: visible | ||
411 | 224 | KeyNavigation.down: bodyLoader | ||
412 | 225 | sourceComponent: headerDelegate | ||
413 | 226 | onLoaded: item.focus = true | ||
414 | 227 | width: parent.width | ||
415 | 228 | |||
416 | 229 | /* Workaround Qt bug http://bugreports.qt.nokia.com/browse/QTBUG-18857 | ||
417 | 230 | More documentation at http://bugreports.qt.nokia.com/browse/QTBUG-18011 | ||
418 | 231 | */ | ||
419 | 232 | property int index | ||
420 | 233 | Binding { target: headerLoader; property: "index"; value: index } | ||
421 | 234 | property variant model | ||
422 | 235 | Binding { target: headerLoader; property: "model"; value: model } | ||
423 | 236 | property variant body | ||
424 | 237 | Binding { target: headerLoader; property: "body"; value: bodyLoader.item } | ||
425 | 238 | } | ||
426 | 239 | |||
427 | 240 | Loader { | ||
428 | 241 | id: bodyLoader | ||
429 | 242 | |||
430 | 243 | focus: !headerLoader.focus | ||
431 | 244 | KeyNavigation.up: headerLoader | ||
432 | 245 | sourceComponent: list.bodyDelegate | ||
433 | 246 | onLoaded: item.focus = true | ||
434 | 247 | width: parent.width | ||
435 | 248 | anchors.top: headerLoader.bottom | ||
436 | 249 | height: items.clamp(parent.ymax - parent.y, 0, item.totalHeight) | ||
437 | 250 | |||
438 | 251 | Binding { | ||
439 | 252 | target: bodyLoader.item | ||
440 | 253 | property: "contentY" | ||
441 | 254 | value: Math.max(items.value - pmax, 0) | ||
442 | 255 | } | ||
443 | 256 | |||
444 | 257 | /* Workaround Qt bug http://bugreports.qt.nokia.com/browse/QTBUG-18857 | ||
445 | 258 | More documentation at http://bugreports.qt.nokia.com/browse/QTBUG-18011 | ||
446 | 259 | */ | ||
447 | 260 | property int index | ||
448 | 261 | Binding { target: bodyLoader; property: "index"; value: index } | ||
449 | 262 | property variant model | ||
450 | 263 | Binding { target: bodyLoader; property: "model"; value: model } | ||
451 | 264 | } | ||
452 | 265 | } | ||
453 | 266 | } | ||
454 | 267 | } | ||
455 | 268 | |||
456 | 269 | Flickable { | ||
457 | 270 | id: mouse | ||
458 | 271 | |||
459 | 272 | z: -1 | ||
460 | 273 | anchors.fill: parent | ||
461 | 274 | contentWidth: parent.width | ||
462 | 275 | contentHeight: items.contentHeight | ||
463 | 276 | } | ||
464 | 277 | } | ||
465 | 0 | 278 | ||
466 | === modified file 'places/ListViewWithScrollbar.qml' | |||
467 | --- places/ListViewWithScrollbar.qml 2011-06-23 17:08:53 +0000 | |||
468 | +++ places/ListViewWithScrollbar.qml 2011-07-27 19:03:19 +0000 | |||
469 | @@ -18,44 +18,21 @@ | |||
470 | 18 | 18 | ||
471 | 19 | import QtQuick 1.0 | 19 | import QtQuick 1.0 |
472 | 20 | 20 | ||
475 | 21 | Item { | 21 | FocusScope { |
474 | 22 | property alias list: list | ||
476 | 23 | property alias scrollbar: scrollbar | 22 | property alias scrollbar: scrollbar |
477 | 23 | property alias model: list.model | ||
478 | 24 | property alias bodyDelegate: list.bodyDelegate | ||
479 | 25 | property alias headerDelegate: list.headerDelegate | ||
480 | 24 | 26 | ||
482 | 25 | ListView { | 27 | ListViewWithHeaders { |
483 | 26 | id: list | 28 | id: list |
484 | 27 | 29 | ||
485 | 30 | focus: true | ||
486 | 28 | anchors.top: parent.top | 31 | anchors.top: parent.top |
487 | 29 | anchors.bottom: parent.bottom | 32 | anchors.bottom: parent.bottom |
488 | 30 | anchors.left: parent.left | 33 | anchors.left: parent.left |
489 | 31 | anchors.right: scrollbar.left | 34 | anchors.right: scrollbar.left |
490 | 32 | anchors.rightMargin: 15 | 35 | anchors.rightMargin: 15 |
491 | 33 | |||
492 | 34 | clip: true | ||
493 | 35 | /* FIXME: proper spacing cannot be set because of the hack in Group.qml | ||
494 | 36 | whereby empty groups are still in the list but invisible and of | ||
495 | 37 | height 0. | ||
496 | 38 | */ | ||
497 | 39 | //spacing: 31 | ||
498 | 40 | |||
499 | 41 | orientation: ListView.Vertical | ||
500 | 42 | |||
501 | 43 | /* WARNING - HACK - FIXME | ||
502 | 44 | Issue: | ||
503 | 45 | User wise annoying jumps in the list are observable if cacheBuffer is | ||
504 | 46 | set to 0 (which is the default value). States such as 'folded' are | ||
505 | 47 | lost when scrolling a lot. | ||
506 | 48 | |||
507 | 49 | Explanation: | ||
508 | 50 | The height of the Group delegate depends on its content. However its | ||
509 | 51 | content is not known until the delegate is instantiated because it | ||
510 | 52 | depends on the number of results displayed by its GridView. | ||
511 | 53 | |||
512 | 54 | Resolution: | ||
513 | 55 | We set the cacheBuffer to the biggest possible int in order to make | ||
514 | 56 | sure all delegates are always instantiated. | ||
515 | 57 | */ | ||
516 | 58 | cacheBuffer: 2147483647 | ||
517 | 59 | } | 36 | } |
518 | 60 | 37 | ||
519 | 61 | Scrollbar { | 38 | Scrollbar { |
520 | @@ -67,7 +44,7 @@ | |||
521 | 67 | anchors.bottomMargin: 10 | 44 | anchors.bottomMargin: 10 |
522 | 68 | anchors.right: parent.right | 45 | anchors.right: parent.right |
523 | 69 | 46 | ||
525 | 70 | targetFlickable: list | 47 | targetFlickable: list.flickable |
526 | 71 | 48 | ||
527 | 72 | /* Hide the scrollbar if there is less than a page of results */ | 49 | /* Hide the scrollbar if there is less than a page of results */ |
528 | 73 | opacity: targetFlickable.visibleArea.heightRatio < 1.0 ? 1.0 : 0.0 | 50 | opacity: targetFlickable.visibleArea.heightRatio < 1.0 ? 1.0 : 0.0 |
529 | 74 | 51 | ||
530 | === modified file 'places/PlaceEntryView.qml' | |||
531 | --- places/PlaceEntryView.qml 2011-07-19 07:46:03 +0000 | |||
532 | +++ places/PlaceEntryView.qml 2011-07-27 19:03:19 +0000 | |||
533 | @@ -19,7 +19,7 @@ | |||
534 | 19 | import QtQuick 1.0 | 19 | import QtQuick 1.0 |
535 | 20 | import Unity2d 1.0 /* Necessary for SortFilterProxyModel */ | 20 | import Unity2d 1.0 /* Necessary for SortFilterProxyModel */ |
536 | 21 | 21 | ||
538 | 22 | Item { | 22 | FocusScope { |
539 | 23 | id: placeEntryView | 23 | id: placeEntryView |
540 | 24 | 24 | ||
541 | 25 | /* An instance of PlaceEntryModel */ | 25 | /* An instance of PlaceEntryModel */ |
542 | @@ -33,7 +33,7 @@ | |||
543 | 33 | var placeEntry, i | 33 | var placeEntry, i |
544 | 34 | for (i=0; i<placeEntryView.model.entryGroupsModel.count; i=i+1) { | 34 | for (i=0; i<placeEntryView.model.entryGroupsModel.count; i=i+1) { |
545 | 35 | firstGroupModel.groupId = i | 35 | firstGroupModel.groupId = i |
547 | 36 | if (firstGroupModel.count() != 0) { | 36 | if (firstGroupModel.count != 0) { |
548 | 37 | var firstResult = firstGroupModel.get(0) | 37 | var firstResult = firstGroupModel.get(0) |
549 | 38 | /* Places give back the uri of the item in 'column_0' per specification */ | 38 | /* Places give back the uri of the item in 'column_0' per specification */ |
550 | 39 | var uri = firstResult.column_0 | 39 | var uri = firstResult.column_0 |
551 | @@ -62,6 +62,7 @@ | |||
552 | 62 | ListViewWithScrollbar { | 62 | ListViewWithScrollbar { |
553 | 63 | id: results | 63 | id: results |
554 | 64 | 64 | ||
555 | 65 | focus: true | ||
556 | 65 | anchors.fill: parent | 66 | anchors.fill: parent |
557 | 66 | 67 | ||
558 | 67 | /* The group's delegate is chosen dynamically depending on what | 68 | /* The group's delegate is chosen dynamically depending on what |
559 | @@ -74,10 +75,10 @@ | |||
560 | 74 | If groupRenderer == 'UnityShowcaseRenderer' then it will look for | 75 | If groupRenderer == 'UnityShowcaseRenderer' then it will look for |
561 | 75 | the file 'UnityShowcaseRenderer.qml' and use it to render the group. | 76 | the file 'UnityShowcaseRenderer.qml' and use it to render the group. |
562 | 76 | */ | 77 | */ |
567 | 77 | list.delegate: Loader { | 78 | bodyDelegate: Loader { |
568 | 78 | property string groupRenderer: column_0 | 79 | property string groupRenderer: model.column_0 |
569 | 79 | property string displayName: column_1 | 80 | property string displayName: model.column_1 |
570 | 80 | property string iconHint: column_2 | 81 | property string iconHint: model.column_2 |
571 | 81 | property int groupId: index | 82 | property int groupId: index |
572 | 82 | 83 | ||
573 | 83 | source: groupRenderer ? groupRenderer+".qml" : "" | 84 | source: groupRenderer ? groupRenderer+".qml" : "" |
574 | @@ -86,12 +87,8 @@ | |||
575 | 86 | console.log("Failed to load renderer", groupRenderer) | 87 | console.log("Failed to load renderer", groupRenderer) |
576 | 87 | } | 88 | } |
577 | 88 | 89 | ||
578 | 89 | width: ListView.view.width | ||
579 | 90 | |||
580 | 91 | /* Model that will be used by the group's delegate */ | 90 | /* Model that will be used by the group's delegate */ |
584 | 92 | SortFilterProxyModel { | 91 | property variant group_model: SortFilterProxyModel { |
582 | 93 | id: group_model | ||
583 | 94 | |||
585 | 95 | model: placeEntryView.model.entryResultsModel | 92 | model: placeEntryView.model.entryResultsModel |
586 | 96 | 93 | ||
587 | 97 | /* resultsModel contains data for all the groups of a given Place. | 94 | /* resultsModel contains data for all the groups of a given Place. |
588 | @@ -102,16 +99,37 @@ | |||
589 | 102 | filterRegExp: RegExp("^%1$".arg(groupId)) /* exact match */ | 99 | filterRegExp: RegExp("^%1$".arg(groupId)) /* exact match */ |
590 | 103 | } | 100 | } |
591 | 104 | 101 | ||
603 | 105 | onLoaded: { | 102 | /* Required by ListViewWithHeaders when the loaded Renderer is a Flickable. |
604 | 106 | item.parentListView = results.list | 103 | In that case the list view scrolls the Flickable appropriately. |
605 | 107 | item.displayName = displayName | 104 | */ |
606 | 108 | item.iconHint = iconHint | 105 | property int totalHeight: item.totalHeight != undefined ? item.totalHeight : 0 |
607 | 109 | item.groupId = groupId | 106 | property int contentY |
608 | 110 | item.model = group_model | 107 | Binding { target: item; property: "contentY"; value: contentY } |
609 | 111 | item.placeEntryModel = placeEntryView.model | 108 | property bool focusable: group_model.count > 0 |
610 | 112 | } | 109 | property variant currentItem: item.currentItem |
611 | 113 | } | 110 | |
612 | 114 | 111 | Binding { target: item; property: "displayName"; value: displayName } | |
613 | 115 | list.model: placeEntryView.model != undefined ? placeEntryView.model.entryGroupsModel : undefined | 112 | Binding { target: item; property: "iconHint"; value: iconHint } |
614 | 113 | Binding { target: item; property: "groupId"; value: groupId } | ||
615 | 114 | Binding { target: item; property: "group_model"; value: group_model } | ||
616 | 115 | Binding { target: item; property: "placeEntryModel"; value: placeEntryView.model } | ||
617 | 116 | |||
618 | 117 | onLoaded: item.focus = true | ||
619 | 118 | } | ||
620 | 119 | |||
621 | 120 | headerDelegate: GroupHeader { | ||
622 | 121 | visible: body.item.needHeader && body.focusable | ||
623 | 122 | height: visible ? 32 : 0 | ||
624 | 123 | |||
625 | 124 | property bool foldable: body.item.folded != undefined | ||
626 | 125 | availableCount: foldable ? body.group_model.count - body.item.cellsPerRow : 0 | ||
627 | 126 | folded: foldable ? body.item.folded : false | ||
628 | 127 | onClicked: if(foldable) body.item.folded = !body.item.folded | ||
629 | 128 | |||
630 | 129 | icon: body.iconHint | ||
631 | 130 | label: body.displayName | ||
632 | 131 | } | ||
633 | 132 | |||
634 | 133 | model: placeEntryView.model != undefined ? placeEntryView.model.entryGroupsModel : undefined | ||
635 | 116 | } | 134 | } |
636 | 117 | } | 135 | } |
637 | 118 | 136 | ||
638 | === modified file 'places/Renderer.qml' | |||
639 | --- places/Renderer.qml 2011-06-23 17:08:53 +0000 | |||
640 | +++ places/Renderer.qml 2011-07-27 19:03:19 +0000 | |||
641 | @@ -26,11 +26,11 @@ | |||
642 | 26 | itself. A typical renderer is the UnityDefaultRender that lays out the items | 26 | itself. A typical renderer is the UnityDefaultRender that lays out the items |
643 | 27 | in a grid of icons with the item's title underneath it. | 27 | in a grid of icons with the item's title underneath it. |
644 | 28 | */ | 28 | */ |
646 | 29 | Item { | 29 | FocusScope { |
647 | 30 | property string displayName /* Name of the group typically displayed in the header */ | 30 | property string displayName /* Name of the group typically displayed in the header */ |
648 | 31 | property string iconHint /* Icon id of the group */ | 31 | property string iconHint /* Icon id of the group */ |
649 | 32 | property int groupId /* Index of the group */ | 32 | property int groupId /* Index of the group */ |
651 | 33 | property variant model /* List model containing the items to be displayed by the renderer */ | 33 | property variant group_model /* List model containing the items to be displayed by the renderer */ |
652 | 34 | property variant placeEntryModel /* Reference to the place entry the group belongs to */ | 34 | property variant placeEntryModel /* Reference to the place entry the group belongs to */ |
654 | 35 | property variant parentListView /* Reference to the ListView the renderer is nested into */ | 35 | property bool needHeader: false /* Whether or not the renderer requires a header to be displayed */ |
655 | 36 | } | 36 | } |
656 | 37 | 37 | ||
657 | === modified file 'places/RendererGrid.qml' | |||
658 | --- places/RendererGrid.qml 2011-06-26 15:41:06 +0000 | |||
659 | +++ places/RendererGrid.qml 2011-07-27 19:03:19 +0000 | |||
660 | @@ -28,6 +28,11 @@ | |||
661 | 28 | Renderer { | 28 | Renderer { |
662 | 29 | id: renderer | 29 | id: renderer |
663 | 30 | 30 | ||
664 | 31 | needHeader: true | ||
665 | 32 | property alias cellsPerRow: results.cellsPerRow | ||
666 | 33 | property alias contentY: results.contentY | ||
667 | 34 | property alias currentItem: results.currentItem | ||
668 | 35 | |||
669 | 31 | property variant cellRenderer | 36 | property variant cellRenderer |
670 | 32 | property bool folded | 37 | property bool folded |
671 | 33 | folded: { | 38 | folded: { |
672 | @@ -44,85 +49,23 @@ | |||
673 | 44 | property int horizontalSpacing: 26 | 49 | property int horizontalSpacing: 26 |
674 | 45 | property int verticalSpacing: 26 | 50 | property int verticalSpacing: 26 |
675 | 46 | 51 | ||
701 | 47 | /* Using results.contentHeight produces binding loop warnings and potential | 52 | /* FIXME: using results_layout.anchors.topMargin in the following expression |
702 | 48 | rendering issues. We compute the height manually. | 53 | causes QML to think they might be an anchor loop. */ |
703 | 49 | */ | 54 | property int totalHeight: results.count > 0 ? results_layout.anchors.topMargin + results.totalHeight : 0 |
679 | 50 | /* FIXME: tricking the system by making the delegate of height 0 and invisible | ||
680 | 51 | is no good: the item in the model still exists and some things | ||
681 | 52 | such as keyboard selection break. | ||
682 | 53 | */ | ||
683 | 54 | visible: results.model.totalCount > 0 | ||
684 | 55 | height: visible ? header.height + results_layout.anchors.topMargin + results.totalHeight : 0 | ||
685 | 56 | //Behavior on height {NumberAnimation {duration: 200}} | ||
686 | 57 | |||
687 | 58 | GroupHeader { | ||
688 | 59 | id: header | ||
689 | 60 | |||
690 | 61 | availableCount: results.model.totalCount - results.cellsPerRow | ||
691 | 62 | folded: parent.folded | ||
692 | 63 | anchors.top: parent.top | ||
693 | 64 | anchors.left: parent.left | ||
694 | 65 | anchors.right: parent.right | ||
695 | 66 | height: 32 | ||
696 | 67 | icon: parent.iconHint | ||
697 | 68 | label: parent.displayName | ||
698 | 69 | |||
699 | 70 | onClicked: parent.folded = !parent.folded | ||
700 | 71 | } | ||
704 | 72 | 55 | ||
705 | 73 | Item { | 56 | Item { |
706 | 74 | id: results_layout | 57 | id: results_layout |
707 | 75 | 58 | ||
711 | 76 | anchors.top: header.bottom | 59 | anchors.fill: parent |
712 | 77 | anchors.topMargin: 22 | 60 | anchors.topMargin: 12 |
710 | 78 | anchors.left: parent.left | ||
713 | 79 | anchors.leftMargin: 2 | 61 | anchors.leftMargin: 2 |
714 | 80 | anchors.right: parent.right | ||
715 | 81 | anchors.bottom: parent.bottom | ||
716 | 82 | 62 | ||
717 | 83 | CenteredGridView { | 63 | CenteredGridView { |
718 | 84 | id: results | 64 | id: results |
719 | 85 | 65 | ||
760 | 86 | /* FIXME: this is a gross hack compensating for the lack of sections | 66 | focus: true |
761 | 87 | in GridView (see ListView.section). | 67 | |
762 | 88 | 68 | anchors.fill: parent | |
723 | 89 | We nest GridViews inside a ListView and add headers manually | ||
724 | 90 | (GroupHeader). The total height of each Group is computed | ||
725 | 91 | manually and given back to the ListView. However that size cannot | ||
726 | 92 | be used by the individual GridViews because it would make them | ||
727 | 93 | load all of their delegates at once using far too much memory and | ||
728 | 94 | processing power. Instead we constrain the height of the GridViews | ||
729 | 95 | and compute their position manually to compensate for the position | ||
730 | 96 | changes when flicking the ListView. | ||
731 | 97 | |||
732 | 98 | We assume that renderer.parentListView is the ListView we nest our | ||
733 | 99 | GridView into. | ||
734 | 100 | */ | ||
735 | 101 | property variant flickable: renderer.parentListView.contentItem | ||
736 | 102 | |||
737 | 103 | /* flickable.contentY*0 is equal to 0 but is necessary in order to | ||
738 | 104 | have the entire expression being evaluated at the right moment. | ||
739 | 105 | */ | ||
740 | 106 | property int inFlickableY: flickable.contentY*0+parent.mapToItem(flickable, 0, 0).y | ||
741 | 107 | /* note: testing for flickable.height < 0 is probably useless since it is | ||
742 | 108 | unlikely flickable.height will ever be negative. | ||
743 | 109 | */ | ||
744 | 110 | property int compensateY: inFlickableY > 0 || flickable.height < 0 || totalHeight < flickable.height ? 0 : -inFlickableY | ||
745 | 111 | |||
746 | 112 | /* Synchronise the position and content's position of the GridView | ||
747 | 113 | with the current position of flickable's visibleArea */ | ||
748 | 114 | function synchronisePosition() { | ||
749 | 115 | y = compensateY | ||
750 | 116 | contentY = compensateY | ||
751 | 117 | } | ||
752 | 118 | |||
753 | 119 | onCompensateYChanged: synchronisePosition() | ||
754 | 120 | /* Any change in content needs to trigger a synchronisation */ | ||
755 | 121 | onCountChanged: synchronisePosition() | ||
756 | 122 | onModelChanged: synchronisePosition() | ||
757 | 123 | |||
758 | 124 | width: flickable.width | ||
759 | 125 | height: Math.min(totalHeight, flickable.height) | ||
763 | 126 | 69 | ||
764 | 127 | property int totalHeight: results.cellHeight*Math.ceil(count/cellsPerRow) | 70 | property int totalHeight: results.cellHeight*Math.ceil(count/cellsPerRow) |
765 | 128 | 71 | ||
766 | @@ -134,7 +77,7 @@ | |||
767 | 134 | interactive: false | 77 | interactive: false |
768 | 135 | clip: true | 78 | clip: true |
769 | 136 | 79 | ||
771 | 137 | delegate: Item { | 80 | delegate: FocusScope { |
772 | 138 | 81 | ||
773 | 139 | width: results.cellWidth | 82 | width: results.cellWidth |
774 | 140 | height: results.cellHeight | 83 | height: results.cellHeight |
775 | @@ -155,6 +98,7 @@ | |||
776 | 155 | height: results.delegateHeight | 98 | height: results.delegateHeight |
777 | 156 | anchors.horizontalCenter: parent.horizontalCenter | 99 | anchors.horizontalCenter: parent.horizontalCenter |
778 | 157 | 100 | ||
779 | 101 | focus: true | ||
780 | 158 | sourceComponent: cellRenderer | 102 | sourceComponent: cellRenderer |
781 | 159 | onLoaded: { | 103 | onLoaded: { |
782 | 160 | item.uri = uri | 104 | item.uri = uri |
783 | @@ -162,13 +106,14 @@ | |||
784 | 162 | item.mimetype = mimetype | 106 | item.mimetype = mimetype |
785 | 163 | item.displayName = displayName | 107 | item.displayName = displayName |
786 | 164 | item.comment = comment | 108 | item.comment = comment |
787 | 109 | item.focus = true | ||
788 | 165 | } | 110 | } |
789 | 166 | } | 111 | } |
790 | 167 | } | 112 | } |
791 | 168 | 113 | ||
792 | 169 | /* Only display one line of items when folded */ | 114 | /* Only display one line of items when folded */ |
793 | 170 | model: SortFilterProxyModel { | 115 | model: SortFilterProxyModel { |
795 | 171 | model: renderer.model != undefined ? renderer.model : null | 116 | model: renderer.group_model != undefined ? renderer.group_model : null |
796 | 172 | limit: folded ? results.cellsPerRow : -1 | 117 | limit: folded ? results.cellsPerRow : -1 |
797 | 173 | } | 118 | } |
798 | 174 | } | 119 | } |
799 | 175 | 120 | ||
800 | === modified file 'places/Scrollbar.qml' | |||
801 | --- places/Scrollbar.qml 2011-06-26 15:16:43 +0000 | |||
802 | +++ places/Scrollbar.qml 2011-07-27 19:03:19 +0000 | |||
803 | @@ -83,7 +83,7 @@ | |||
804 | 83 | when: !dragMouseArea.drag.active | 83 | when: !dragMouseArea.drag.active |
805 | 84 | } | 84 | } |
806 | 85 | 85 | ||
808 | 86 | height: Math.max(minimalHeight, targetFlickable.visibleArea.heightRatio * scrollbar.height) | 86 | height: Math.min(scrollbar.height, Math.max(minimalHeight, targetFlickable.visibleArea.heightRatio * scrollbar.height)) |
809 | 87 | 87 | ||
810 | 88 | Behavior on height {NumberAnimation {duration: 200; easing.type: Easing.InOutQuad}} | 88 | Behavior on height {NumberAnimation {duration: 200; easing.type: Easing.InOutQuad}} |
811 | 89 | 89 | ||
812 | 90 | 90 | ||
813 | === modified file 'places/SearchEntry.qml' | |||
814 | --- places/SearchEntry.qml 2011-06-23 17:08:53 +0000 | |||
815 | +++ places/SearchEntry.qml 2011-07-27 19:03:19 +0000 | |||
816 | @@ -19,7 +19,7 @@ | |||
817 | 19 | import QtQuick 1.0 | 19 | import QtQuick 1.0 |
818 | 20 | import Effects 1.0 | 20 | import Effects 1.0 |
819 | 21 | 21 | ||
821 | 22 | FocusScope { | 22 | AbstractButton { |
822 | 23 | property string searchQuery | 23 | property string searchQuery |
823 | 24 | 24 | ||
824 | 25 | /* Cancels current search when the dash becomes invisible */ | 25 | /* Cancels current search when the dash becomes invisible */ |
825 | @@ -37,6 +37,8 @@ | |||
826 | 37 | /* Keys forwarded to the search entry are forwarded to the text input. */ | 37 | /* Keys forwarded to the search entry are forwarded to the text input. */ |
827 | 38 | Keys.forwardTo: [search_input] | 38 | Keys.forwardTo: [search_input] |
828 | 39 | 39 | ||
829 | 40 | opacity: state == "selected" ? 1.0 : 0.7 | ||
830 | 41 | |||
831 | 40 | BorderImage { | 42 | BorderImage { |
832 | 41 | anchors.fill: parent | 43 | anchors.fill: parent |
833 | 42 | source: "artwork/search_background.sci" | 44 | source: "artwork/search_background.sci" |
834 | 43 | 45 | ||
835 | === modified file 'places/SearchRefine.qml' | |||
836 | --- places/SearchRefine.qml 2011-06-23 17:08:53 +0000 | |||
837 | +++ places/SearchRefine.qml 2011-07-27 19:03:19 +0000 | |||
838 | @@ -26,6 +26,9 @@ | |||
839 | 26 | property int headerHeight | 26 | property int headerHeight |
840 | 27 | property variant placeEntryModel | 27 | property variant placeEntryModel |
841 | 28 | 28 | ||
842 | 29 | /* Give the focus to header when folded */ | ||
843 | 30 | onFoldedChanged: if (folded) header.focus = true | ||
844 | 31 | |||
845 | 29 | AbstractButton { | 32 | AbstractButton { |
846 | 30 | id: header | 33 | id: header |
847 | 31 | 34 | ||
848 | @@ -49,6 +52,9 @@ | |||
849 | 49 | 52 | ||
850 | 50 | focus: true | 53 | focus: true |
851 | 51 | 54 | ||
852 | 55 | /* Do not navigate down to the options if they are folded */ | ||
853 | 56 | KeyNavigation.down: !searchRefine.folded ? options : header | ||
854 | 57 | |||
855 | 52 | anchors.left: parent.left | 58 | anchors.left: parent.left |
856 | 53 | anchors.right: parent.right | 59 | anchors.right: parent.right |
857 | 54 | anchors.top: parent.top | 60 | anchors.top: parent.top |
858 | @@ -61,6 +67,7 @@ | |||
859 | 61 | text: u2d.tr("Refine search") | 67 | text: u2d.tr("Refine search") |
860 | 62 | font.bold: true | 68 | font.bold: true |
861 | 63 | font.pixelSize: 16 | 69 | font.pixelSize: 16 |
862 | 70 | font.underline: parent.state == "selected" | ||
863 | 64 | 71 | ||
864 | 65 | anchors.top: parent.top | 72 | anchors.top: parent.top |
865 | 66 | anchors.left: parent.left | 73 | anchors.left: parent.left |
866 | @@ -92,6 +99,8 @@ | |||
867 | 92 | opacity: folded ? 0.0 : 1.0 | 99 | opacity: folded ? 0.0 : 1.0 |
868 | 93 | Behavior on opacity {NumberAnimation {duration: 100; easing.type: Easing.InOutQuad}} | 100 | Behavior on opacity {NumberAnimation {duration: 100; easing.type: Easing.InOutQuad}} |
869 | 94 | 101 | ||
870 | 102 | KeyNavigation.up: header | ||
871 | 103 | |||
872 | 95 | anchors.left: parent.left | 104 | anchors.left: parent.left |
873 | 96 | anchors.right: parent.right | 105 | anchors.right: parent.right |
874 | 97 | anchors.top: header.bottom | 106 | anchors.top: header.bottom |
875 | 98 | 107 | ||
876 | === modified file 'places/SearchRefineOptionType.qml' | |||
877 | --- places/SearchRefineOptionType.qml 2011-07-26 17:05:50 +0000 | |||
878 | +++ places/SearchRefineOptionType.qml 2011-07-27 19:03:19 +0000 | |||
879 | @@ -21,9 +21,11 @@ | |||
880 | 21 | SearchRefineOption { | 21 | SearchRefineOption { |
881 | 22 | id: searchRefineOption | 22 | id: searchRefineOption |
882 | 23 | 23 | ||
884 | 24 | AbstractButton { | 24 | Item { |
885 | 25 | id: header | 25 | id: header |
886 | 26 | 26 | ||
887 | 27 | KeyNavigation.down: filters | ||
888 | 28 | |||
889 | 27 | focus: true | 29 | focus: true |
890 | 28 | anchors.top: parent.top | 30 | anchors.top: parent.top |
891 | 29 | anchors.left: parent.left | 31 | anchors.left: parent.left |
892 | @@ -39,6 +41,7 @@ | |||
893 | 39 | text: searchRefineOption.title | 41 | text: searchRefineOption.title |
894 | 40 | font.pixelSize: 16 | 42 | font.pixelSize: 16 |
895 | 41 | font.bold: true | 43 | font.bold: true |
896 | 44 | font.underline: parent.state == "selected" | ||
897 | 42 | } | 45 | } |
898 | 43 | } | 46 | } |
899 | 44 | 47 | ||
900 | @@ -63,6 +66,7 @@ | |||
901 | 63 | 66 | ||
902 | 64 | /* Make sure the first item is selected when getting the focus for the first time */ | 67 | /* Make sure the first item is selected when getting the focus for the first time */ |
903 | 65 | currentIndex: 0 | 68 | currentIndex: 0 |
904 | 69 | KeyNavigation.up: header | ||
905 | 66 | 70 | ||
906 | 67 | delegate: TickBox { | 71 | delegate: TickBox { |
907 | 68 | height: filters.cellHeight | 72 | height: filters.cellHeight |
908 | 69 | 73 | ||
909 | === modified file 'places/UnityEmptySearchRenderer.qml' | |||
910 | --- places/UnityEmptySearchRenderer.qml 2011-06-23 17:08:53 +0000 | |||
911 | +++ places/UnityEmptySearchRenderer.qml 2011-07-27 19:03:19 +0000 | |||
912 | @@ -40,7 +40,7 @@ | |||
913 | 40 | boundsBehavior: ListView.StopAtBounds | 40 | boundsBehavior: ListView.StopAtBounds |
914 | 41 | orientation: ListView.Vertical | 41 | orientation: ListView.Vertical |
915 | 42 | 42 | ||
917 | 43 | model: renderer.model | 43 | model: renderer.group_model |
918 | 44 | delegate: Button { | 44 | delegate: Button { |
919 | 45 | property string uri: column_0 | 45 | property string uri: column_0 |
920 | 46 | property string iconHint: column_1 | 46 | property string iconHint: column_1 |
921 | 47 | 47 | ||
922 | === modified file 'places/dash.qml' | |||
923 | --- places/dash.qml 2011-07-05 19:01:18 +0000 | |||
924 | +++ places/dash.qml 2011-07-27 19:03:19 +0000 | |||
925 | @@ -30,7 +30,16 @@ | |||
926 | 30 | value: (currentPage && currentPage.expanded != undefined) ? currentPage.expanded : true | 30 | value: (currentPage && currentPage.expanded != undefined) ? currentPage.expanded : true |
927 | 31 | } | 31 | } |
928 | 32 | 32 | ||
929 | 33 | /* Unload the current page when closing the dash */ | ||
930 | 34 | Connections { | ||
931 | 35 | target: dashView | ||
932 | 36 | onActiveChanged: if (!dashView.active) pageLoader.source = "" | ||
933 | 37 | } | ||
934 | 38 | |||
935 | 33 | function activatePage(page) { | 39 | function activatePage(page) { |
936 | 40 | /* Always give the focus to the search entry when switching pages */ | ||
937 | 41 | search_entry.focus = true | ||
938 | 42 | |||
939 | 34 | if (page == currentPage) { | 43 | if (page == currentPage) { |
940 | 35 | return | 44 | return |
941 | 36 | } | 45 | } |
942 | @@ -40,12 +49,6 @@ | |||
943 | 40 | } | 49 | } |
944 | 41 | currentPage = page | 50 | currentPage = page |
945 | 42 | currentPage.visible = true | 51 | currentPage.visible = true |
946 | 43 | /* FIXME: For some reason currentPage gets the focus when it becomes | ||
947 | 44 | visible. Reset the focus to the search_bar instead. | ||
948 | 45 | It could be due to Qt bug QTBUG-13380: | ||
949 | 46 | "Listview gets focus when it becomes visible" | ||
950 | 47 | */ | ||
951 | 48 | search_entry.focus = true | ||
952 | 49 | } | 52 | } |
953 | 50 | 53 | ||
954 | 51 | function activatePlaceEntry(fileName, groupName, section) { | 54 | function activatePlaceEntry(fileName, groupName, section) { |
955 | @@ -121,13 +124,19 @@ | |||
956 | 121 | /* Unhandled keys will always be forwarded to the search bar. That way | 124 | /* Unhandled keys will always be forwarded to the search bar. That way |
957 | 122 | the user can type and search from anywhere in the interface without | 125 | the user can type and search from anywhere in the interface without |
958 | 123 | necessarily focusing the search bar first. */ | 126 | necessarily focusing the search bar first. */ |
960 | 124 | Keys.forwardTo: [search_entry] | 127 | /* FIXME: deactivated because it makes the user lose the focus very often */ |
961 | 128 | //Keys.forwardTo: [search_entry] | ||
962 | 125 | 129 | ||
963 | 126 | 130 | ||
964 | 127 | SearchEntry { | 131 | SearchEntry { |
965 | 128 | id: search_entry | 132 | id: search_entry |
966 | 129 | 133 | ||
967 | 130 | focus: true | 134 | focus: true |
968 | 135 | /* FIXME: check on visible necessary; fixed in Qt Quick 1.1 | ||
969 | 136 | ref: http://bugreports.qt.nokia.com/browse/QTBUG-15862 | ||
970 | 137 | */ | ||
971 | 138 | KeyNavigation.right: refine_search.visible ? refine_search : search_entry | ||
972 | 139 | KeyNavigation.down: pageLoader | ||
973 | 131 | 140 | ||
974 | 132 | anchors.top: parent.top | 141 | anchors.top: parent.top |
975 | 133 | anchors.topMargin: 10 | 142 | anchors.topMargin: 10 |
976 | @@ -142,6 +151,8 @@ | |||
977 | 142 | SearchRefine { | 151 | SearchRefine { |
978 | 143 | id: refine_search | 152 | id: refine_search |
979 | 144 | 153 | ||
980 | 154 | KeyNavigation.left: search_entry | ||
981 | 155 | |||
982 | 145 | /* SearchRefine is only to be displayed for places, not in the home page */ | 156 | /* SearchRefine is only to be displayed for places, not in the home page */ |
983 | 146 | visible: dashView.activePlaceEntry != "" | 157 | visible: dashView.activePlaceEntry != "" |
984 | 147 | placeEntryModel: visible && currentPage != undefined ? currentPage.model : undefined | 158 | placeEntryModel: visible && currentPage != undefined ? currentPage.model : undefined |
985 | @@ -158,6 +169,12 @@ | |||
986 | 158 | Loader { | 169 | Loader { |
987 | 159 | id: pageLoader | 170 | id: pageLoader |
988 | 160 | 171 | ||
989 | 172 | /* FIXME: check on visible necessary; fixed in Qt Quick 1.1 | ||
990 | 173 | ref: http://bugreports.qt.nokia.com/browse/QTBUG-15862 | ||
991 | 174 | */ | ||
992 | 175 | KeyNavigation.right: refine_search.visible && !refine_search.folded ? refine_search : pageLoader | ||
993 | 176 | KeyNavigation.up: search_entry | ||
994 | 177 | |||
995 | 161 | anchors.top: search_entry.bottom | 178 | anchors.top: search_entry.bottom |
996 | 162 | anchors.topMargin: 2 | 179 | anchors.topMargin: 2 |
997 | 163 | anchors.bottom: parent.bottom | 180 | anchors.bottom: parent.bottom |
998 | @@ -165,6 +182,7 @@ | |||
999 | 165 | anchors.leftMargin: 20 | 182 | anchors.leftMargin: 20 |
1000 | 166 | anchors.right: !refine_search.visible || refine_search.folded ? parent.right : refine_search.left | 183 | anchors.right: !refine_search.visible || refine_search.folded ? parent.right : refine_search.left |
1001 | 167 | anchors.rightMargin: !refine_search.visible || refine_search.folded ? 0 : 15 | 184 | anchors.rightMargin: !refine_search.visible || refine_search.folded ? 0 : 15 |
1002 | 185 | onLoaded: item.focus = true | ||
1003 | 168 | } | 186 | } |
1004 | 169 | } | 187 | } |
1005 | 170 | 188 |
I've found bugs:
- if number of results less than 6, the "See more/fewer results" option is of course hidden but
is still found by the keyboard navigation. Repro: do a search which returns <6 Applications. Hit down: where did focus go? Hit down again: there it is!
- Do search which returns large list of applications, scroll down. Now return to search box &
do new search (again returning large list). New search results are still scrolled down.
- At Home, if you leave mouse highlighting one gridItem, using keyboard to navigate gives
you a second "active" looking gridItem.
- At Home, press down, down, right, right, up, up, then Esc to close it. Now
open Home again, press down, down and you appear on the third column now.
- At Search List of more than 6 rows or so, be about half-way down the applications list.
Press up, down, left, right, and see how the list moves on the left/right motion - it
shouldn't. This one is hard to reproduce - I get it searching for "e".
- At "Search Applications", at the search box, press right. Where is focus?
- Quickly type Meta, "w", down. See where focus appears - at "See Fewer Results" in the Files&Folders section
Suggestions/ Questions?
- highlighted grid item's text has underline. Correct?
- At Home, continually pressing right on gridItems should wrap around?? Instead of stopping?
Similarly for down & up? Fewer barriers?