Merge lp:~zsombi/ubuntu-ui-toolkit/list_item_focus into lp:ubuntu-ui-toolkit/staging

Proposed by Zsombor Egri on 2016-03-01
Status: Merged
Approved by: Christian Dywan on 2016-03-02
Approved revision: 1785
Merged at revision: 1878
Proposed branch: lp:~zsombi/ubuntu-ui-toolkit/list_item_focus
Merge into: lp:ubuntu-ui-toolkit/staging
Diff against target: 1287 lines (+897/-29)
16 files modified
src/Ubuntu/Components/Themes/Ambiance/1.3/ListItemStyle.qml (+1/-0)
src/Ubuntu/Components/plugin/plugin.pri (+2/-0)
src/Ubuntu/Components/plugin/privates/listviewextensions.cpp (+151/-0)
src/Ubuntu/Components/plugin/privates/listviewextensions.h (+60/-0)
src/Ubuntu/Components/plugin/quickutils.cpp (+53/-0)
src/Ubuntu/Components/plugin/quickutils.h (+3/-0)
src/Ubuntu/Components/plugin/uclistitem.cpp (+110/-7)
src/Ubuntu/Components/plugin/uclistitem.h (+7/-0)
src/Ubuntu/Components/plugin/uclistitem_p.h (+5/-1)
src/Ubuntu/Components/plugin/ucstyleditembase.cpp (+16/-1)
src/Ubuntu/Components/plugin/ucstyleditembase.h (+2/-1)
src/Ubuntu/Components/plugin/ucstyleditembase_p.h (+1/-0)
src/Ubuntu/Components/plugin/ucviewitemsattached.cpp (+28/-10)
tests/resources/listitems/ListItemTest.qml (+10/-5)
tests/unit_x11/tst_components/tst_listitem_focus.qml (+417/-0)
tests/unit_x11/tst_components/tst_quickutils.qml (+31/-4)
To merge this branch: bzr merge lp:~zsombi/ubuntu-ui-toolkit/list_item_focus
Reviewer Review Type Date Requested Status
ubuntu-sdk-build-bot continuous-integration 2016-03-01 Approve on 2016-03-02
Christian Dywan (community) 2016-03-01 Approve on 2016-03-02
Tim Peeters 2016-03-01 Pending
PS Jenkins bot continuous-integration 2016-03-01 Pending
Review via email: mp+287648@code.launchpad.net

This proposal supersedes a proposal from 2016-02-29.

Commit message

ListItem focus navigation in ListView and other views.

Description of the change

- introduce internal isFocusScope into StyledItem to drive whether a StyledItem is a focus scope or not; FocusScopes can get the activeFocus true only the first time they are focused, in any other cases their last focused ascendant will be the activeFocus; ListItem's focus framing requires it to be activeFocus, therefore we need to turn off the focus scope in the StyledItem, and that cannot be removed once it is set.
- internal ListViewProxy added to wrap inaccessible QQuickListView API; it also overrides the default ListView navigation (up/down for vertical, left/right for horizontal orientations) to be able to notify the ListItem being focused by key events causing focus frame to be drawn.

Focus handling of ListItem:
- when in ListView, Tab/Backtab is used to enter/leave the focus of ListView. If no item is selected in ListView, it will focus and select the first available item in the ListView. When Tab/Backtab is pressed again, it will leave the ListView. ListItems are selected and focused the same time using the up/down or left/right arrows depending on the ListView direction. When a selected and focused ListItem has focusable child components (i.e. CheckBox, Switch, etc) this can be focused through the horizontal navigation keys (left/right) or vertical ones depending of the orientation.
- when a ListItem is out of the ListView, it will obey to the default Tab/Backtab navigation rules, and its child focusable elements can be focused either with horizontal keys or with the standard Tab/Backtab.

To post a comment you must log in.
ubuntu-sdk-build-bot (ubuntu-sdk-build-bot) wrote : Posted in a previous version of this proposal
review: Approve (continuous-integration)
ubuntu-sdk-build-bot (ubuntu-sdk-build-bot) wrote : Posted in a previous version of this proposal
review: Approve (continuous-integration)
ubuntu-sdk-build-bot (ubuntu-sdk-build-bot) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
ubuntu-sdk-build-bot (ubuntu-sdk-build-bot) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
ubuntu-sdk-build-bot (ubuntu-sdk-build-bot) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
ubuntu-sdk-build-bot (ubuntu-sdk-build-bot) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
ubuntu-sdk-build-bot (ubuntu-sdk-build-bot) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
ubuntu-sdk-build-bot (ubuntu-sdk-build-bot) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Christian Dywan (kalikiana) wrote : Posted in a previous version of this proposal

I feel it should be good to have separate if basic unit tests for firstFocusableChild, lasyFocusableChild, isDecendantOf, to ensure you can rely on their behavior when looking at ListItem tests.

+ // first or the last focus child is reached, so we wrap around

Do we really want wrap-around? I'm double-checking because we planned to not have it for Tab focus in general, see bug 1523821, unless that decision has been re-considered in the meantime.

review: Needs Information
Zsombor Egri (zsombi) wrote : Posted in a previous version of this proposal

> I feel it should be good to have separate if basic unit tests for
> firstFocusableChild, lasyFocusableChild, isDecendantOf, to ensure you can rely
> on their behavior when looking at ListItem tests.

Good note, I'll add those, though that needs a separate unit test as those are private APIs.

>
> + // first or the last focus child is reached, so we wrap around
>
> Do we really want wrap-around? I'm double-checking because we planned to not
> have it for Tab focus in general, see bug 1523821, unless that decision has
> been re-considered in the meantime.

Hehe, that's interesting. Last time I've chatted with Femma was that this (and even others) will have to wrap around. Now, if we must forbid wrapping around, we will be in trouble, as Qt has no option to stop that. We could do that in our implementation, however even that wouldn't be straight forward, as next tabletop element may not necessarily be the sibling or the child of the current focus, so the tab order is not that clear there. And as said, Qt functions return the next tabletop, which in case it is to jump from the last one, it'll wrap around. As every UI in the OS worlds does that. So this would be again something we want to format the user's brain...

Christian Dywan (kalikiana) wrote : Posted in a previous version of this proposal

> > + // first or the last focus child is reached, so we wrap around
> >
> > Do we really want wrap-around? I'm double-checking because we planned to not
> > have it for Tab focus in general, see bug 1523821, unless that decision has
> > been re-considered in the meantime.
>
> Hehe, that's interesting. Last time I've chatted with Femma was that this (and
> even others) will have to wrap around. Now, if we must forbid wrapping around,
> we will be in trouble, as Qt has no option to stop that. We could do that in
> our implementation, however even that wouldn't be straight forward, as next
> tabletop element may not necessarily be the sibling or the child of the
> current focus, so the tab order is not that clear there. And as said, Qt
> functions return the next tabletop, which in case it is to jump from the last
> one, it'll wrap around. As every UI in the OS worlds does that. So this would
> be again something we want to format the user's brain...

The way I see it we wouldn't do something else but we would do nothing at all once you reach the bottom (or top), so I'm not sure most users even rely on that. I agree, though, it may be tricky to implement at all.

Let's consider it part of bug 1523821 then, since you've written the code anyway, so for the moment it's consistent.

Zsombor Egri (zsombi) wrote : Posted in a previous version of this proposal

>
> The way I see it we wouldn't do something else but we would do nothing at all
> once you reach the bottom (or top), so I'm not sure most users even rely on
> that. I agree, though, it may be tricky to implement at all.

As said, even GTK apps go around with the focus, they never stop in the bottom or top. OSX, Windows does that, why would we want to format brains again? Like the easiest way for a user - if he got used to that - to reach the first before the last element while the very first one is focused is to push the Backtab twice. In our case this would require him/her to walk along the tabletops and get there finally...

>
> Let's consider it part of bug 1523821 then, since you've written the code
> anyway, so for the moment it's consistent.

ubuntu-sdk-build-bot (ubuntu-sdk-build-bot) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
ubuntu-sdk-build-bot (ubuntu-sdk-build-bot) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
ubuntu-sdk-build-bot (ubuntu-sdk-build-bot) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
ubuntu-sdk-build-bot (ubuntu-sdk-build-bot) wrote : Posted in a previous version of this proposal

FAILED: Autolanding.
More details in the following jenkins job:
https://jenkins.ubuntu.com/ubuntu-sdk/job/ubuntu-ui-toolkit-autolanding/131/
Executed test runs:
    None: https://jenkins.ubuntu.com/ubuntu-sdk/job/generic-land-mp/132/console

review: Needs Fixing (continuous-integration)
Tim Peeters (tpeeters) wrote : Posted in a previous version of this proposal

We need support for cursor keys in the ListView used in SectionsStyle in https://code.launchpad.net/~tpeeters/ubuntu-ui-toolkit/60-scectionScrolling/+merge/286330 too. It is a simple horizontal ListView that can be selected using (back)tab, and navigation inside the sections should be done with left/right cursor keys.

ubuntu-sdk-build-bot (ubuntu-sdk-build-bot) wrote : Posted in a previous version of this proposal
review: Approve (continuous-integration)
ubuntu-sdk-build-bot (ubuntu-sdk-build-bot) wrote : Posted in a previous version of this proposal
review: Approve (continuous-integration)
ubuntu-sdk-build-bot (ubuntu-sdk-build-bot) wrote : Posted in a previous version of this proposal
review: Approve (continuous-integration)
Christian Dywan (kalikiana) wrote : Posted in a previous version of this proposal

I can, after using swipe to reveal left or right hand actions, use arrows to focus the actions and even activate them; however there's no visual indication of that.
I'm not sure this is wanted/ needed since the context menu would anyway achieve the same thing - maybe they should simply not take focus.

review: Needs Fixing
Christian Dywan (kalikiana) wrote : Posted in a previous version of this proposal

In the gallery I can reproduce a new bug: in the list view used to switch pages I can use the arrows to move the selection past the last item. The focus visual is still on the last item, and window.activeFocusItem doesn't change (it remains on the last item), and I'm seeing this error message:
examples/ubuntu-ui-toolkit-gallery/MainPage.qml:110: TypeError: Cannot read property 'source' of undefined

review: Needs Fixing
Tim Peeters (tpeeters) wrote : Posted in a previous version of this proposal

In the debian control file we need to update the source dependency on a Qt that fixes this:
plugin/uclistitem.cpp: In member function ‘virtual void UCListItem::itemChange(QQuickItem::ItemChange, const QQuickItem::ItemChangeData&)’:
plugin/uclistitem.cpp:1100:16: error: ‘class UCListItemPrivate’ has no member named ‘isTabFence’
             d->isTabFence = d->parentAttached->isAttachedToListView();
                ^

review: Needs Fixing
Christian Dywan (kalikiana) wrote : Posted in a previous version of this proposal

> I can, after using swipe to reveal left or right hand actions, use arrows to
> focus the actions and even activate them; however there's no visual indication
> of that.
> I'm not sure this is wanted/ needed since the context menu would anyway
> achieve the same thing - maybe they should simply not take focus.

Following up on this: Definitely not wanted, as per the spec. So disabling is the way to go.

ubuntu-sdk-build-bot (ubuntu-sdk-build-bot) wrote : Posted in a previous version of this proposal
review: Approve (continuous-integration)
ubuntu-sdk-build-bot (ubuntu-sdk-build-bot) wrote : Posted in a previous version of this proposal
review: Approve (continuous-integration)
ubuntu-sdk-build-bot (ubuntu-sdk-build-bot) wrote : Posted in a previous version of this proposal
review: Approve (continuous-integration)
ubuntu-sdk-build-bot (ubuntu-sdk-build-bot) wrote : Posted in a previous version of this proposal
review: Approve (continuous-integration)
ubuntu-sdk-build-bot (ubuntu-sdk-build-bot) wrote : Posted in a previous version of this proposal
review: Approve (continuous-integration)
ubuntu-sdk-build-bot (ubuntu-sdk-build-bot) wrote : Posted in a previous version of this proposal
review: Approve (continuous-integration)
1783. By Zsombor Egri on 2016-03-01

roll back palette changes

1784. By Zsombor Egri on 2016-03-01

origin merged

1785. By Zsombor Egri on 2016-03-01

API fixed

Christian Dywan (kalikiana) wrote :

Sweet, sweet fixes. Like the additional tests. Let's get this in!

review: Approve
review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/Ubuntu/Components/Themes/Ambiance/1.3/ListItemStyle.qml'
2--- src/Ubuntu/Components/Themes/Ambiance/1.3/ListItemStyle.qml 2016-01-27 16:42:47 +0000
3+++ src/Ubuntu/Components/Themes/Ambiance/1.3/ListItemStyle.qml 2016-03-01 15:09:23 +0000
4@@ -79,6 +79,7 @@
5 id: actionButton
6 action: modelData
7 enabled: action.enabled
8+ activeFocusOnTab: false
9 width: MathUtils.clamp(delegateLoader.item ? delegateLoader.item.width : 0, actionsRow.minItemWidth, actionsRow.maxItemWidth)
10 anchors {
11 top: parent ? parent.top : undefined
12
13=== modified file 'src/Ubuntu/Components/plugin/plugin.pri'
14--- src/Ubuntu/Components/plugin/plugin.pri 2016-02-10 18:02:18 +0000
15+++ src/Ubuntu/Components/plugin/plugin.pri 2016-03-01 15:09:23 +0000
16@@ -110,6 +110,7 @@
17 $$PWD/ucmainviewbase.h \
18 $$PWD/ucmainviewbase_p.h \
19 $$PWD/ucperformancemonitor.h \
20+ $$PWD/privates/listviewextensions.h \
21 $$PWD/privates/frame.h \
22 $$PWD/privates/ucpagewrapper.h \
23 $$PWD/privates/ucpagewrapper_p.h \
24@@ -187,6 +188,7 @@
25 $$PWD/ucpagetreenode.cpp \
26 $$PWD/ucmainviewbase.cpp \
27 $$PWD/ucperformancemonitor.cpp \
28+ $$PWD/privates/listviewextensions.cpp \
29 $$PWD/privates/frame.cpp \
30 $$PWD/privates/ucpagewrapper.cpp \
31 $$PWD/privates/ucpagewrapperincubator.cpp \
32
33=== added file 'src/Ubuntu/Components/plugin/privates/listviewextensions.cpp'
34--- src/Ubuntu/Components/plugin/privates/listviewextensions.cpp 1970-01-01 00:00:00 +0000
35+++ src/Ubuntu/Components/plugin/privates/listviewextensions.cpp 2016-03-01 15:09:23 +0000
36@@ -0,0 +1,151 @@
37+/*
38+ * Copyright 2016 Canonical Ltd.
39+ *
40+ * This program is free software; you can redistribute it and/or modify
41+ * it under the terms of the GNU Lesser General Public License as published by
42+ * the Free Software Foundation; version 3.
43+ *
44+ * This program is distributed in the hope that it will be useful,
45+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
46+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
47+ * GNU Lesser General Public License for more details.
48+ *
49+ * You should have received a copy of the GNU Lesser General Public License
50+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
51+ *
52+ * Author Zsombor Egri <zsombor.egri@canonical.com>
53+ */
54+
55+#include "listviewextensions.h"
56+#include "uclistitem_p.h"
57+#include "quickutils.h"
58+#include <QtQuick/QQuickItem>
59+#include <QtQuick/private/qquickflickable_p.h>
60+
61+ListViewProxy::ListViewProxy(QQuickFlickable *listView, QObject *parent)
62+ : QObject(parent)
63+ , listView(listView)
64+{
65+}
66+ListViewProxy::~ListViewProxy()
67+{
68+ if (isEventFilter) {
69+ listView->removeEventFilter(this);
70+ }
71+}
72+
73+// proxy methods
74+
75+Qt::Orientation ListViewProxy::orientation()
76+{
77+ return (Qt::Orientation)listView->property("orientation").toInt();
78+}
79+
80+int ListViewProxy::count()
81+{
82+ return listView->property("count").toInt();
83+}
84+
85+QQuickItem *ListViewProxy::currentItem()
86+{
87+ return listView->property("currentItem").value<QQuickItem*>();
88+}
89+
90+int ListViewProxy::currentIndex()
91+{
92+ return listView->property("currentIndex").toInt();
93+}
94+
95+void ListViewProxy::setCurrentIndex(int index)
96+{
97+ listView->setProperty("currentIndex", index);
98+}
99+
100+QVariant ListViewProxy::model()
101+{
102+ return listView->property("model");
103+}
104+
105+/*********************************************************************
106+ * Additional functionality used in different places in toolkit
107+ *********************************************************************/
108+
109+// Navigation override used by ListItems
110+void ListViewProxy::overrideItemNavigation(bool override)
111+{
112+ if (override) {
113+ listView->installEventFilter(this);
114+ } else {
115+ listView->removeEventFilter(this);
116+ }
117+ isEventFilter = override;
118+}
119+
120+bool ListViewProxy::eventFilter(QObject *, QEvent *event)
121+{
122+ switch (event->type()) {
123+ case QEvent::FocusIn:
124+ return focusInEvent(static_cast<QFocusEvent*>(event));
125+ case QEvent::KeyPress:
126+ return keyPressEvent(static_cast<QKeyEvent*>(event));
127+ default:
128+ break;
129+ }
130+
131+ return false;
132+}
133+
134+void ListViewProxy::setKeyNavigationForListView(bool value)
135+{
136+ UCListItem *listItem = qobject_cast<UCListItem*>(currentItem());
137+ if (listItem) {
138+ UCListItemPrivate::get(listItem)->setListViewKeyNavigation(value);
139+ listItem->update();
140+ }
141+}
142+
143+// grab focusIn event
144+bool ListViewProxy::focusInEvent(QFocusEvent *event)
145+{
146+ switch (event->reason()) {
147+ case Qt::TabFocusReason:
148+ case Qt::BacktabFocusReason:
149+ {
150+ QQuickItem *currentItem = this->currentItem();
151+ if (!currentItem && count() > 0) {
152+ // set the first one to be the focus
153+ setCurrentIndex(0);
154+ setKeyNavigationForListView(true);
155+ }
156+ break;
157+ }
158+ default:
159+ break;
160+ }
161+ return false;
162+}
163+
164+// override up/down key presses for ListView
165+bool ListViewProxy::keyPressEvent(QKeyEvent *event)
166+{
167+ int key = event->key();
168+ Qt::Orientation orientation = this->orientation();
169+
170+ if ((orientation == Qt::Vertical && key != Qt::Key_Up && key != Qt::Key_Down)
171+ || (orientation == Qt::Horizontal && key != Qt::Key_Left && key != Qt::Key_Right)) {
172+ return false;
173+ }
174+ // for horizontal moves take into account the layout mirroring
175+ bool isRtl = QQuickItemPrivate::get(listView)->effectiveLayoutMirror;
176+ bool forwards = (key == Qt::Key_Up || (isRtl ? key == Qt::Key_Left : key == Qt::Key_Right));
177+ int currentIndex = this->currentIndex();
178+ int count = this->count();
179+
180+ if (currentIndex >= 0 && count > 0) {
181+ currentIndex = qBound<int>(0, forwards ? currentIndex - 1 : currentIndex + 1, count - 1);
182+ setCurrentIndex(currentIndex);
183+ setKeyNavigationForListView(true);
184+ }
185+
186+ return true;
187+}
188
189=== added file 'src/Ubuntu/Components/plugin/privates/listviewextensions.h'
190--- src/Ubuntu/Components/plugin/privates/listviewextensions.h 1970-01-01 00:00:00 +0000
191+++ src/Ubuntu/Components/plugin/privates/listviewextensions.h 2016-03-01 15:09:23 +0000
192@@ -0,0 +1,60 @@
193+/*
194+ * Copyright 2016 Canonical Ltd.
195+ *
196+ * This program is free software; you can redistribute it and/or modify
197+ * it under the terms of the GNU Lesser General Public License as published by
198+ * the Free Software Foundation; version 3.
199+ *
200+ * This program is distributed in the hope that it will be useful,
201+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
202+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
203+ * GNU Lesser General Public License for more details.
204+ *
205+ * You should have received a copy of the GNU Lesser General Public License
206+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
207+ *
208+ * Author Zsombor Egri <zsombor.egri@canonical.com>
209+ */
210+
211+#ifndef LISTVIEWEXTENSIONS_H
212+#define LISTVIEWEXTENSIONS_H
213+
214+#include <QtCore/QObject>
215+
216+class QQuickFlickable;
217+class QQuickItem;
218+class QFocusEvent;
219+class QKeyEvent;
220+class ListViewProxy : public QObject
221+{
222+ Q_OBJECT
223+public:
224+ explicit ListViewProxy(QQuickFlickable *listView, QObject *parent = 0);
225+ virtual ~ListViewProxy();
226+ inline QQuickFlickable *view() const
227+ {
228+ return listView;
229+ }
230+ void overrideItemNavigation(bool override);
231+
232+
233+ // proxied methods
234+ Qt::Orientation orientation();
235+ int count();
236+ QQuickItem *currentItem();
237+ int currentIndex();
238+ void setCurrentIndex(int index);
239+ QVariant model();
240+
241+protected:
242+ bool eventFilter(QObject *, QEvent *) override;
243+
244+ bool focusInEvent(QFocusEvent *event);
245+ bool keyPressEvent(QKeyEvent *event);
246+ void setKeyNavigationForListView(bool value);
247+private:
248+ QQuickFlickable *listView;
249+ bool isEventFilter:1;
250+};
251+
252+#endif // LISTVIEWEXTENSIONS_H
253
254=== modified file 'src/Ubuntu/Components/plugin/quickutils.cpp'
255--- src/Ubuntu/Components/plugin/quickutils.cpp 2016-02-01 18:57:26 +0000
256+++ src/Ubuntu/Components/plugin/quickutils.cpp 2016-03-01 15:09:23 +0000
257@@ -214,3 +214,56 @@
258 }
259 return showWarnings == 2;
260 }
261+
262+// check whether an item is a descendant of parent
263+bool QuickUtils::descendantItemOf(QQuickItem *item, const QQuickItem *parent)
264+{
265+ while (item && parent) {
266+ if (item == parent) {
267+ return true;
268+ }
269+ item = item->parentItem();
270+ }
271+ return false;
272+}
273+
274+// returns the first key-focusable child item
275+QQuickItem *QuickUtils::firstFocusableChild(QQuickItem *item)
276+{
277+ if (!item) {
278+ return Q_NULLPTR;
279+ }
280+ const QList<QQuickItem*> &list = item->childItems();
281+ for (int i = 0; i < list.count(); i++) {
282+ QQuickItem *child = list.at(i);
283+ if (child->activeFocusOnTab()) {
284+ return child;
285+ }
286+ QQuickItem *focus = firstFocusableChild(child);
287+ if (focus) {
288+ return focus;
289+ }
290+ }
291+ return Q_NULLPTR;
292+}
293+
294+// returns the last key-focusable child item
295+QQuickItem *QuickUtils::lastFocusableChild(QQuickItem *item)
296+{
297+ if (!item) {
298+ return Q_NULLPTR;
299+ }
300+ const QList<QQuickItem*> &list = item->childItems();
301+ int i = list.count() - 1;
302+ while (i >= 0) {
303+ QQuickItem *child = list.at(i--);
304+ if (child->activeFocusOnTab()) {
305+ return child;
306+ }
307+ QQuickItem *focus = lastFocusableChild(child);
308+ if (focus) {
309+ return focus;
310+ }
311+ }
312+ return Q_NULLPTR;
313+}
314
315=== modified file 'src/Ubuntu/Components/plugin/quickutils.h'
316--- src/Ubuntu/Components/plugin/quickutils.h 2016-02-10 09:54:59 +0000
317+++ src/Ubuntu/Components/plugin/quickutils.h 2016-03-01 15:09:23 +0000
318@@ -55,6 +55,9 @@
319 Q_REVISION(1) Q_INVOKABLE static bool inherits(QObject *object, const QString &fromClass);
320 QObject* createQmlObject(const QUrl &url, QQmlEngine *engine);
321 static bool showDeprecationWarnings();
322+ static bool descendantItemOf(QQuickItem *item, const QQuickItem *parent);
323+ Q_INVOKABLE static QQuickItem *firstFocusableChild(QQuickItem *item);
324+ Q_INVOKABLE static QQuickItem *lastFocusableChild(QQuickItem *item);
325
326 bool mouseAttached()
327 {
328
329=== modified file 'src/Ubuntu/Components/plugin/uclistitem.cpp'
330--- src/Ubuntu/Components/plugin/uclistitem.cpp 2016-02-08 11:00:27 +0000
331+++ src/Ubuntu/Components/plugin/uclistitem.cpp 2016-03-01 15:09:23 +0000
332@@ -28,6 +28,7 @@
333 #include "quickutils.h"
334 #include "ucaction.h"
335 #include "ucnamespace.h"
336+#include "privates/listviewextensions.h"
337 #include <QtQml/QQmlInfo>
338 #include <QtQuick/private/qquickitem_p.h>
339 #include <QtQuick/private/qquickflickable_p.h>
340@@ -210,7 +211,10 @@
341 , suppressClick(false)
342 , ready(false)
343 , customColor(false)
344+ , listViewKeyNavigation(false)
345 {
346+ // the ListItem is not a focus scope
347+ isFocusScope = false;
348 }
349 UCListItemPrivate::~UCListItemPrivate()
350 {
351@@ -220,10 +224,10 @@
352 {
353 Q_Q(UCListItem);
354 contentItem->setObjectName("ListItemHolder");
355+ divider->init(q);
356 QQml_setParent_noEvent(contentItem, q);
357 contentItem->setParentItem(q);
358 contentItem->setClip(true);
359- divider->init(q);
360 // content will be redirected to the contentItem, therefore we must report
361 // children changes as it would come from the main component
362 QObject::connect(contentItem, &QQuickItem::childrenChanged,
363@@ -978,6 +982,23 @@
364 {
365 }
366
367+// override keyNavigationFocus getter
368+bool UCListItem::keyNavigationFocus() const
369+{
370+ Q_D(const UCListItem);
371+ return d->keyNavigationFocus ||d->listViewKeyNavigation;
372+}
373+
374+void UCListItemPrivate::setListViewKeyNavigation(bool value)
375+{
376+ Q_Q(UCListItem);
377+ bool prevKeyNav = q->keyNavigationFocus();
378+ listViewKeyNavigation = value;
379+ if (prevKeyNav != q->keyNavigationFocus()) {
380+ Q_EMIT q->keyNavigationFocusChanged();
381+ }
382+}
383+
384 QObject *UCListItem::attachedViewItems(QObject *object, bool create)
385 {
386 return qmlAttachedPropertiesObject<UCViewItemsAttached>(object, create);
387@@ -1074,6 +1095,9 @@
388 d->selection->attachToViewItems(d->parentAttached.data());
389 connect(d->parentAttached.data(), SIGNAL(expandedIndicesChanged(QList<int>)),
390 this, SLOT(_q_updateExpansion(QList<int>)), Qt::DirectConnection);
391+ // if the ViewItems is attached to a ListView, disable tab stops on the ListItem
392+ setActiveFocusOnTab(!d->parentAttached->isAttachedToListView());
393+ d->isTabFence = d->parentAttached->isAttachedToListView();
394 }
395
396 if (parentAttachee) {
397@@ -1101,17 +1125,36 @@
398 if (!rectNode) {
399 rectNode = QQuickItemPrivate::get(this)->sceneGraphContext()->createRectangleNode();
400 }
401+ bool updateNode = false;
402+
403+ // focus frame
404+ bool paintFocus = hasActiveFocus() && keyNavigationFocus();
405+ rectNode->setPenWidth(paintFocus ? UCUnits::instance()->dp(1) : 0);
406+ if (paintFocus) {
407+ QColor penColor;
408+ if (getTheme()) {
409+ penColor = getTheme()->getPaletteColor(isEnabled() ? "normal" : "disabled", "focus");
410+ }
411+ rectNode->setPenColor(penColor);
412+ rectNode->setColor(Qt::transparent);
413+ updateNode = true;
414+ }
415+ QRectF rect(boundingRect());
416+ rect -= QMarginsF(0, 0, UCUnits::instance()->dp(1), 0);
417+ d->divider->setOpacity(paintFocus ? 0.0 : 1.0);
418+ rectNode->setRect(rect);
419+
420+ // highlight color
421 if (color.alphaF() >= (1.0f / 255.0f)) {
422 rectNode->setColor(color);
423- // cover only the area of the contentItem, removing divider's thickness
424- QRectF rect(boundingRect());
425- if (d->divider->isVisible()) {
426- rect -= QMarginsF(0, 0, 0, d->divider->height());
427- }
428- rectNode->setRect(rect);
429 rectNode->setGradientStops(QGradientStops());
430 rectNode->setAntialiasing(true);
431 rectNode->setAntialiasing(false);
432+ updateNode = true;
433+ }
434+
435+ // update
436+ if (updateNode) {
437 rectNode->update();
438 } else {
439 // delete node, this will delete the divider node as well
440@@ -1426,6 +1469,66 @@
441 }
442 }
443
444+void UCListItem::focusInEvent(QFocusEvent *event)
445+{
446+ Q_D(UCListItem);
447+ UCStyledItemBase::focusInEvent(event);
448+ if (event->reason() == Qt::MouseFocusReason) {
449+ d_func()->setListViewKeyNavigation(false);
450+ }
451+ update();
452+}
453+
454+void UCListItem::focusOutEvent(QFocusEvent *event)
455+{
456+ UCStyledItemBase::focusOutEvent(event);
457+ d_func()->setListViewKeyNavigation(false);
458+ update();
459+}
460+
461+// handle horizontal keys to navigate between focusable slots
462+void UCListItem::keyPressEvent(QKeyEvent *event)
463+{
464+ UCStyledItemBase::keyPressEvent(event);
465+ Q_D(UCListItem);
466+ int key = event->key();
467+ if (key != Qt::Key_Left && key != Qt::Key_Right) {
468+ return;
469+ }
470+
471+ bool forwards = (d->effectiveLayoutMirror ? key == Qt::Key_Left : key == Qt::Key_Right);
472+ // we must check whether the ListItem has any key navigation focusable child
473+ // this is needed due to the Qt bug https://bugreports.qt.io/browse/QTBUG-50516
474+ if (!QuickUtils::firstFocusableChild(this)) {
475+ return;
476+ }
477+
478+ // get the next focusable relative to the active focus item
479+ QQuickItem *activeFocus = isFocusScope() ? scopedFocusItem() : window()->activeFocusItem();
480+ if (!activeFocus) {
481+ return;
482+ }
483+
484+ Qt::FocusReason reason = forwards ? Qt::TabFocusReason : Qt::BacktabFocusReason;
485+ if ((activeFocus == QuickUtils::firstFocusableChild(this) && !forwards) ||
486+ (activeFocus == QuickUtils::lastFocusableChild(this) && forwards)) {
487+ // first or the last focus child is reached, so we wrap around
488+ // but for that we must set the activeFocus to false in order to
489+ // be able to focus the ListItem, especially when the ListItem is a Tab fence
490+ activeFocus->setFocus(false);
491+ forceActiveFocus(reason);
492+ } else if (activeFocus == this) {
493+ // get the first or last focusable item, depending on the direction
494+ QQuickItem *nextFocus = forwards
495+ ? QuickUtils::firstFocusableChild(this)
496+ : QuickUtils::lastFocusableChild(this);
497+ nextFocus->forceActiveFocus(reason);
498+ } else {
499+ // in case the ListItem is in ListView, we can freely proceed with the focusing
500+ QQuickItemPrivate::focusNextPrev(activeFocus, forwards);
501+ }
502+}
503+
504 /*!
505 * \qmlproperty ListItemActions ListItem::leadingActions
506 *
507
508=== modified file 'src/Ubuntu/Components/plugin/uclistitem.h'
509--- src/Ubuntu/Components/plugin/uclistitem.h 2015-11-16 16:24:42 +0000
510+++ src/Ubuntu/Components/plugin/uclistitem.h 2016-03-01 15:09:23 +0000
511@@ -53,6 +53,9 @@
512 explicit UCListItem(QQuickItem *parent = 0);
513 ~UCListItem();
514
515+ // overrides
516+ bool keyNavigationFocus() const override;
517+
518 QQuickItem *contentItem() const;
519 UCListItemDivider *divider() const;
520 UCListItemActions *leadingActions() const;
521@@ -83,6 +86,9 @@
522 bool childMouseEventFilter(QQuickItem *child, QEvent *event);
523 bool eventFilter(QObject *, QEvent *);
524 void timerEvent(QTimerEvent *event);
525+ void focusInEvent(QFocusEvent *event) override;
526+ void focusOutEvent(QFocusEvent *event) override;
527+ void keyPressEvent(QKeyEvent *event) override;
528
529 Q_SIGNALS:
530 void leadingActionsChanged();
531@@ -177,6 +183,7 @@
532 static UCViewItemsAttached *qmlAttachedProperties(QObject *owner);
533
534 bool listenToRebind(UCListItem *item, bool listen);
535+ bool isAttachedToListView();
536 bool isMoving();
537 bool isBoundTo(UCListItem *item);
538
539
540=== modified file 'src/Ubuntu/Components/plugin/uclistitem_p.h'
541--- src/Ubuntu/Components/plugin/uclistitem_p.h 2015-12-14 09:16:41 +0000
542+++ src/Ubuntu/Components/plugin/uclistitem_p.h 2016-03-01 15:09:23 +0000
543@@ -102,6 +102,7 @@
544 bool suppressClick:1;
545 bool ready:1;
546 bool customColor:1;
547+ bool listViewKeyNavigation:1;
548
549 // getters/setters
550 QQmlListProperty<QObject> data();
551@@ -119,6 +120,7 @@
552 void setSelectMode(bool selectable);
553 UCAction *action() const;
554 void setAction(UCAction *action);
555+ void setListViewKeyNavigation(bool value);
556
557 virtual void postThemeChanged();
558 inline UCListItemStyle *listItemStyle() const;
559@@ -130,12 +132,14 @@
560
561 class PropertyChange;
562 class ListItemDragArea;
563+class ListViewProxy;
564 class UCViewItemsAttachedPrivate : public QObjectPrivate
565 {
566 Q_DECLARE_PUBLIC(UCViewItemsAttached)
567 public:
568 UCViewItemsAttachedPrivate();
569 ~UCViewItemsAttachedPrivate();
570+ void init();
571
572 static UCViewItemsAttachedPrivate *get(UCViewItemsAttached *item)
573 {
574@@ -162,7 +166,7 @@
575 QMap<int, QPointer<UCListItem> > expansionList;
576 QList< QPointer<QQuickFlickable> > flickables;
577 QPointer<UCListItem> boundItem;
578- QQuickFlickable *listView;
579+ ListViewProxy *listView;
580 ListItemDragArea *dragArea;
581 UCViewItemsAttached::ExpansionFlags expansionFlags;
582 bool selectable:1;
583
584=== modified file 'src/Ubuntu/Components/plugin/ucstyleditembase.cpp'
585--- src/Ubuntu/Components/plugin/ucstyleditembase.cpp 2016-02-25 16:41:51 +0000
586+++ src/Ubuntu/Components/plugin/ucstyleditembase.cpp 2016-03-01 15:09:23 +0000
587@@ -33,6 +33,7 @@
588 , keyNavigationFocus(false)
589 , activeFocusOnPress(false)
590 , wasStyleLoaded(false)
591+ , isFocusScope(true)
592 {
593 }
594
595@@ -58,7 +59,6 @@
596 void UCStyledItemBasePrivate::init()
597 {
598 Q_Q(UCStyledItemBase);
599- q->setFlag(QQuickItem::ItemIsFocusScope);
600 QObject::connect(q, &QQuickItem::activeFocusOnTabChanged, q, &UCStyledItemBase::activeFocusOnTabChanged2);
601 }
602
603@@ -501,6 +501,21 @@
604 loadStyleItem(false);
605 }
606
607+void UCStyledItemBase::classBegin()
608+{
609+ /* Some items require not to be focus scopes (like ListItem), however for
610+ * backwards compatibility we must keep setting the generic styled items
611+ * as focus scopes. The flag once set cannot be cleared, therefore we must
612+ * use an additional boolean member isFocusScope to drive this request.
613+ * The member defaults to true. Additionally, the focus scope flag must
614+ * be set before the parentItem is set and child items are added.
615+ */
616+ if (d_func()->isFocusScope) {
617+ setFlag(QQuickItem::ItemIsFocusScope);
618+ }
619+ QQuickItem::classBegin();
620+}
621+
622 void UCStyledItemBase::componentComplete()
623 {
624 QQuickItem::componentComplete();
625
626=== modified file 'src/Ubuntu/Components/plugin/ucstyleditembase.h'
627--- src/Ubuntu/Components/plugin/ucstyleditembase.h 2015-12-16 08:05:44 +0000
628+++ src/Ubuntu/Components/plugin/ucstyleditembase.h 2016-03-01 15:09:23 +0000
629@@ -48,7 +48,7 @@
630 public:
631 explicit UCStyledItemBase(QQuickItem *parent = 0);
632
633- bool keyNavigationFocus() const;
634+ virtual bool keyNavigationFocus() const;
635 bool activefocusOnPress() const;
636 void setActiveFocusOnPress(bool value);
637 bool activeFocusOnTab2() const;
638@@ -73,6 +73,7 @@
639 virtual void preThemeChanged();
640 virtual void postThemeChanged();
641
642+ void classBegin();
643 void componentComplete();
644 void itemChange(ItemChange change, const ItemChangeData &data);
645 void focusInEvent(QFocusEvent *key);
646
647=== modified file 'src/Ubuntu/Components/plugin/ucstyleditembase_p.h'
648--- src/Ubuntu/Components/plugin/ucstyleditembase_p.h 2015-12-15 19:17:11 +0000
649+++ src/Ubuntu/Components/plugin/ucstyleditembase_p.h 2016-03-01 15:09:23 +0000
650@@ -72,6 +72,7 @@
651 bool keyNavigationFocus:1;
652 bool activeFocusOnPress:1;
653 bool wasStyleLoaded:1;
654+ bool isFocusScope:1;
655
656 protected:
657
658
659=== modified file 'src/Ubuntu/Components/plugin/ucviewitemsattached.cpp'
660--- src/Ubuntu/Components/plugin/ucviewitemsattached.cpp 2016-01-30 12:03:31 +0000
661+++ src/Ubuntu/Components/plugin/ucviewitemsattached.cpp 2016-03-01 15:09:23 +0000
662@@ -1,5 +1,5 @@
663 /*
664- * Copyright 2014-2015 Canonical Ltd.
665+ * Copyright 2014-2016 Canonical Ltd.
666 *
667 * This program is free software; you can redistribute it and/or modify
668 * it under the terms of the GNU Lesser General Public License as published by
669@@ -23,6 +23,7 @@
670 #include "i18n.h"
671 #include "uclistitemstyle.h"
672 #include "privates/listitemdragarea.h"
673+#include "privates/listviewextensions.h"
674 #include <QtQuick/private/qquickflickable_p.h>
675 #include <QtQml/private/qqmlcomponentattached_p.h>
676 #include <QtQml/QQmlInfo>
677@@ -116,6 +117,22 @@
678 clearFlickablesList();
679 }
680
681+void UCViewItemsAttachedPrivate::init()
682+{
683+ Q_Q(UCViewItemsAttached);
684+ if (parent->inherits("QQuickListView")) {
685+ listView = new ListViewProxy(static_cast<QQuickFlickable*>(parent), q);
686+
687+ // ListView focus handling
688+ listView->view()->setActiveFocusOnTab(true);
689+ // filter ListView events to override up/down focus handling
690+ listView->overrideItemNavigation(true);
691+ }
692+ // listen readyness
693+ QQmlComponentAttached *attached = QQmlComponent::qmlAttachedProperties(parent);
694+ QObject::connect(attached, &QQmlComponentAttached::completed, q, &UCViewItemsAttached::completed);
695+}
696+
697 // disconnect all flickables
698 void UCViewItemsAttachedPrivate::clearFlickablesList()
699 {
700@@ -167,12 +184,7 @@
701 UCViewItemsAttached::UCViewItemsAttached(QObject *owner)
702 : QObject(*(new UCViewItemsAttachedPrivate()), owner)
703 {
704- if (owner->inherits("QQuickListView")) {
705- d_func()->listView = static_cast<QQuickFlickable*>(owner);
706- }
707- // listen readyness
708- QQmlComponentAttached *attached = QQmlComponent::qmlAttachedProperties(owner);
709- connect(attached, &QQmlComponentAttached::completed, this, &UCViewItemsAttached::completed);
710+ d_func()->init();
711 }
712
713 UCViewItemsAttached::~UCViewItemsAttached()
714@@ -204,6 +216,12 @@
715 return result;
716 }
717
718+// reports whether the ViewItems is attached to ListView
719+bool UCViewItemsAttached::isAttachedToListView()
720+{
721+ return (d_func()->listView != Q_NULLPTR);
722+}
723+
724 // reports true if any of the ascendant flickables is moving
725 bool UCViewItemsAttached::isMoving()
726 {
727@@ -462,7 +480,7 @@
728 qmlInfo(parent()) << QStringLiteral("Dragging mode requires ListView");
729 return;
730 }
731- QVariant model = d->listView->property("model");
732+ QVariant model = d->listView->model();
733 // warn if the model is anything else but Instance model (ObjectModel or DelegateModel)
734 // or a derivate of QAbstractItemModel
735 QString warning = QStringLiteral("Dragging is only supported when using a QAbstractItemModel, ListModel or list.");
736@@ -492,7 +510,7 @@
737 dragArea->reset();
738 return;
739 }
740- dragArea = new ListItemDragArea(listView);
741+ dragArea = new ListItemDragArea(listView->view());
742 dragArea->init(q_func());
743 }
744
745@@ -515,7 +533,7 @@
746 // updates the selected indices list in ViewAttached which is changed due to dragging
747 void UCViewItemsAttachedPrivate::updateSelectedIndices(int fromIndex, int toIndex)
748 {
749- if (selectedList.count() == listView->property("count").toInt()) {
750+ if (selectedList.count() == listView->count()) {
751 // all indices selected, no need to reorder
752 return;
753 }
754
755=== modified file 'tests/resources/listitems/ListItemTest.qml'
756--- tests/resources/listitems/ListItemTest.qml 2015-09-18 08:36:23 +0000
757+++ tests/resources/listitems/ListItemTest.qml 2016-03-01 15:09:23 +0000
758@@ -15,8 +15,8 @@
759 */
760
761 import QtQuick 2.4
762-import Ubuntu.Components 1.2
763-import Ubuntu.Components.Styles 1.2
764+import Ubuntu.Components 1.3
765+import Ubuntu.Components.Styles 1.3
766 import QtQuick.Layouts 1.1
767
768 MainView {
769@@ -207,6 +207,7 @@
770 title.text: "This is one Label split in two lines.\n" +
771 "The second line - item #" + modelData
772 }
773+ CheckBox {}
774 Button {
775 text: "Pressme..."
776 }
777@@ -245,7 +246,7 @@
778 model: 10
779 ListItem {
780 objectName: "InFlickable"+index
781- color: UbuntuColors.red
782+ color: UbuntuColors.silk
783 highlightColor: "lime"
784 divider.colorFrom: UbuntuColors.green
785
786@@ -263,9 +264,13 @@
787 Label {
788 text: modelData + " Flickable item"
789 }
790- Button {
791- text: "Pressme..."
792+ Row {
793 anchors.centerIn: parent
794+ spacing: units.dp(4)
795+ Button {
796+ text: "Pressme..."
797+ }
798+ Switch {}
799 }
800
801 onClicked: divider.visible = !divider.visible
802
803=== added file 'tests/unit_x11/tst_components/tst_listitem_focus.qml'
804--- tests/unit_x11/tst_components/tst_listitem_focus.qml 1970-01-01 00:00:00 +0000
805+++ tests/unit_x11/tst_components/tst_listitem_focus.qml 2016-03-01 15:09:23 +0000
806@@ -0,0 +1,417 @@
807+/*
808+ * Copyright 2016 Canonical Ltd.
809+ *
810+ * This program is free software; you can redistribute it and/or modify
811+ * it under the terms of the GNU Lesser General Public License as published by
812+ * the Free Software Foundation; version 3.
813+ *
814+ * This program is distributed in the hope that it will be useful,
815+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
816+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
817+ * GNU Lesser General Public License for more details.
818+ *
819+ * You should have received a copy of the GNU Lesser General Public License
820+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
821+ */
822+
823+import QtQuick 2.4
824+import QtTest 1.0
825+import Ubuntu.Test 1.0
826+import Ubuntu.Components 1.3
827+import QtQuick.Window 2.2
828+
829+Item {
830+ id: main
831+ width: units.gu(40)
832+ height: units.gu(71)
833+
834+ property Item activeFocusItem: Window.activeFocusItem
835+
836+ Rectangle {
837+ id: topItem
838+ objectName: "topItem"
839+ activeFocusOnTab: true
840+ width: parent.width
841+ height: units.gu(2)
842+ }
843+
844+ Loader {
845+ id: testLoader
846+ anchors {
847+ fill: parent
848+ topMargin: topItem.height
849+ }
850+ }
851+
852+ Component {
853+ id: simpleListItem
854+ ListItem {
855+ objectName: "simple" + index
856+ property int itemIndex: index
857+ }
858+ }
859+
860+ Component {
861+ id: listItemWithContent
862+ ListItem {
863+ id: listItem
864+ objectName: "withContent" + index
865+ property int itemIndex: index
866+ Row {
867+ spacing: units.gu(1)
868+ CheckBox { objectName: "checkbox" + listItem.itemIndex }
869+ Switch { objectName: "switch" + listItem.itemIndex }
870+ Button { objectName: "button" + listItem.itemIndex; text: "test" }
871+ }
872+ leadingActions: ListItemActions {
873+ actions: Action {
874+ iconName: "delete"
875+ }
876+ }
877+ trailingActions: ListItemActions {
878+ actions: Action {
879+ iconName: "edit"
880+ }
881+ }
882+ }
883+ }
884+
885+ Component {
886+ id: listView
887+ ListView {
888+ model: 10
889+ }
890+ }
891+
892+ Component {
893+ id: generic
894+ Column {
895+ spacing: units.gu(1)
896+ Repeater {
897+ model: 2
898+ delegate: listItemWithContent
899+ }
900+ Repeater {
901+ model: 2
902+ delegate: simpleListItem
903+ }
904+ }
905+ }
906+
907+ ListItemTestCase13 {
908+ name: "ListItemFocus"
909+ when: windowShown
910+
911+ function loadTest(component) {
912+ testLoader.sourceComponent = component;
913+ tryCompare(testLoader, "status", Loader.Ready);
914+ return testLoader.item;
915+ }
916+
917+ function cleanup() {
918+ testLoader.sourceComponent = null;
919+ wait(200);
920+ }
921+ function init() {
922+ topItem.forceActiveFocus(Qt.TabFocusReason);
923+ }
924+
925+ function initTestCase() {
926+ TestExtras.registerTouchDevice();
927+ }
928+
929+ // Tab/Backtab focuses the First ListItem in a ListView
930+ function test_focusing_listview_focuses_first_item_data() {
931+ return [
932+ {tag: "Tab, no content", preFocus: topItem, delegate: simpleListItem, key: Qt.Key_Tab, focusItem: "simple0"},
933+ {tag: "Tab, with content", preFocus: topItem, delegate: listItemWithContent, key: Qt.Key_Tab, focusItem: "withContent0"},
934+ {tag: "Backtab, no content", preFocus: topItem, delegate: simpleListItem, key: Qt.Key_Backtab, focusItem: "simple0"},
935+ {tag: "Backtab, with content", preFocus: topItem, delegate: listItemWithContent, key: Qt.Key_Backtab, focusItem: "withContent0"},
936+ ];
937+ }
938+ function test_focusing_listview_focuses_first_item(data) {
939+ var test = loadTest(listView);
940+ test.delegate = data.delegate;
941+ waitForRendering(test, 500);
942+ data.preFocus.forceActiveFocus();
943+ verify(data.preFocus.activeFocus);
944+ keyClick(data.key);
945+ var listItem = findChild(test, data.focusItem);
946+ verify(listItem);
947+ tryCompare(listItem, "activeFocus", true, 500, "Focus hasn't been gained by the ListItem");
948+ verify(listItem.keyNavigationFocus);
949+ }
950+
951+ // vertical key navigation in ListView between ListItems
952+ function test_focus_and_navigate_in_listview_data() {
953+ return [
954+ {tag: "No content", delegate: simpleListItem, key: Qt.Key_Down, keyTimes: 3, focusItems: ["simple1", "simple2", "simple3"]},
955+ {tag: "With content", delegate: listItemWithContent, key: Qt.Key_Down, keyTimes: 3, focusItems: ["withContent1", "withContent2", "withContent3"]},
956+ ];
957+ }
958+ function test_focus_and_navigate_in_listview(data) {
959+ var test = loadTest(listView);
960+ test.delegate = data.delegate;
961+ waitForRendering(test, 500);
962+ keyClick(Qt.Key_Tab);
963+ for (var i = 0; i < data.keyTimes; i++) {
964+ keyClick(data.key);
965+ var item = findChild(test, data.focusItems[i]);
966+ verify(item);
967+ tryCompare(item, "activeFocus", true, 500, "Focus hasn't been gained by the ListItem");
968+ verify(item.keyNavigationFocus, "failure on key navigation on " + data.focusItems[i]);
969+ }
970+ }
971+
972+ // vertical navigation updates ListView.currentItem (as well as currentIndex)
973+ function test_focus_and_navigate_in_listview_updates_currentItem_data() {
974+ return [
975+ {tag: "No content", delegate: simpleListItem, key: Qt.Key_Down, keyTimes: 3, focusItems: ["simple1", "simple2", "simple3"]},
976+ {tag: "With content", delegate: listItemWithContent, key: Qt.Key_Down, keyTimes: 3, focusItems: ["withContent1", "withContent2", "withContent3"]},
977+ ];
978+ }
979+ function test_focus_and_navigate_in_listview_updates_currentItem(data) {
980+ var test = loadTest(listView);
981+ test.delegate = data.delegate;
982+ waitForRendering(test, 500);
983+ keyClick(Qt.Key_Tab);
984+ for (var i = 0; i < data.keyTimes; i++) {
985+ keyClick(data.key);
986+ var item = findChild(test, data.focusItems[i]);
987+ verify(item);
988+ tryCompare(item, "activeFocus", true, 500, "Focus hasn't been gained by the ListItem");
989+ verify(item.keyNavigationFocus, "failure on key navigation on " + data.focusItems[i]);
990+ compare(test.currentItem, item);
991+ compare(test.currentIndex, item.itemIndex);
992+ }
993+ }
994+
995+ // re-focusing ListView will focus on the last focused item
996+ function test_refocus_listview_on_last_focused_item_data() {
997+ return [
998+ {tag: "No content", delegate: simpleListItem, key: Qt.Key_Down, keyTimes: 3, focusItems: ["simple1", "simple2", "simple3"]},
999+ {tag: "With content", delegate: listItemWithContent, key: Qt.Key_Down, keyTimes: 3, focusItems: ["withContent1", "withContent2", "withContent3"]},
1000+ ];
1001+ }
1002+ function test_refocus_listview_on_last_focused_item(data) {
1003+ var test = loadTest(listView);
1004+ test.delegate = data.delegate;
1005+ waitForRendering(test, 500);
1006+ // focus on ListView and focus the 3rd item
1007+ test.forceActiveFocus();
1008+ test.currentIndex = 2;
1009+ waitForRendering(test, 400);
1010+ verify(!test.currentItem.keyNavigationFocus, "Focus frame shown for the item");
1011+ // focus away
1012+ keyClick(Qt.Key_Tab);
1013+ waitForRendering(test, 400);
1014+ // then focus back
1015+ keyClick(Qt.Key_Backtab);
1016+ waitForRendering(test, 400);
1017+ tryCompare(test.currentItem, "activeFocus", true, 500, "Focus hasn't been gained by the ListItem");
1018+ verify(test.currentItem.keyNavigationFocus, "Focus frame not shown for the item");
1019+ }
1020+
1021+ // Tab/Backtab focuses, next Tab/Backtab focuses out of ListItem in a ListView
1022+ function test_tab_backtab_navigates_away_of_listview_data() {
1023+ return [
1024+ {tag: "Tab, simple", preFocus: topItem, delegate: simpleListItem, key: Qt.Key_Tab},
1025+ {tag: "Tab, with content", preFocus: topItem, delegate: listItemWithContent, key: Qt.Key_Tab},
1026+ {tag: "BackTab, simple", preFocus: topItem, delegate: simpleListItem, key: Qt.Key_Backtab},
1027+ {tag: "BackTab, with content", preFocus: topItem, delegate: listItemWithContent, key: Qt.Key_Backtab},
1028+ ];
1029+ }
1030+ function test_tab_backtab_navigates_away_of_listview(data) {
1031+ var test = loadTest(listView);
1032+ test.delegate = data.delegate;
1033+ waitForRendering(test, 500);
1034+ data.preFocus.forceActiveFocus();
1035+ verify(data.preFocus.activeFocus);
1036+ // the first tab focuses the ListView and its first child
1037+ keyClick(data.key);
1038+ tryCompare(test, "activeFocus", true, 500, "Focus hasn't been gained bythe ListItem");
1039+
1040+ // the second tab should leave the ListView
1041+ keyClick(data.key);
1042+ tryCompare(test, "activeFocus", false, 500, "Focus hasn't been lost by the ListItem");
1043+ }
1044+
1045+ // testing Tab/Backtab navigation when in a generic item
1046+ function test_tab_navigation_when_not_in_listview_data() {
1047+ return [
1048+ {tag: "Tabs", firstFocus: topItem, key: Qt.Key_Tab,
1049+ focusItems: ["withContent0", "checkbox0", "switch0", "button0"
1050+ , "withContent1", "checkbox1", "switch1", "button1"]},
1051+ {tag: "Backtabs", firstFocus: topItem, key: Qt.Key_Backtab,
1052+ focusItems: ["simple1", "simple0"
1053+ , "button1", "switch1", "checkbox1", "withContent1"]},
1054+ ];
1055+ }
1056+ function test_tab_navigation_when_not_in_listview(data) {
1057+ var test = loadTest(generic);
1058+ data.firstFocus.forceActiveFocus();
1059+ for (var i = 0; i < data.focusItems.length; i++) {
1060+ keyClick(data.key);
1061+ compare(main.activeFocusItem.objectName, data.focusItems[i], "Unexpected focused item");
1062+ }
1063+ }
1064+
1065+ // focus frame should not be shown
1066+ function test_mouse_or_tap_focus_doesnt_show_focusframe_data() {
1067+ return [
1068+ {tag: "Focus with mouse", mouse: true},
1069+ {tag: "Focus with touch", mouse: false},
1070+ ];
1071+ }
1072+ function test_mouse_or_tap_focus_doesnt_show_focusframe(data) {
1073+ var test = loadTest(listView);
1074+ test.delegate = simpleListItem;
1075+ waitForRendering(test, 500);
1076+ var item = findChild(test, "simple3");
1077+ verify(item);
1078+ if (data.mouse) {
1079+ mouseClick(item, centerOf(item).x, centerOf(item).y);
1080+ } else {
1081+ TestExtras.touchClick(0, item, centerOf(item));
1082+ }
1083+ verify(!item.keyNavigationFocus, "Focus frame must not be shown!");
1084+ }
1085+
1086+ // focus with mouse, then press Tab/Backtab, then mouse
1087+ function test_focus_with_mouse_and_tab() {
1088+ var test = loadTest(listView);
1089+ test.delegate = simpleListItem;
1090+ waitForRendering(test, 500);
1091+ var listItem0 = findChild(test, "simple0");
1092+ verify(listItem0);
1093+ var listItem1 = findChild(test, "simple1");
1094+ verify(listItem1);
1095+ var listItem2 = findChild(test, "simple2");
1096+ verify(listItem2);
1097+
1098+ // click on first
1099+ mouseClick(listItem0, centerOf(listItem0).x, centerOf(listItem0).y);
1100+ verify(listItem0.activeFocus, "Not focused");
1101+ }
1102+
1103+ function test_horizontal_navigation_between_listitem_children_with_tabstop_data() {
1104+ return [
1105+ {tag: "in ListView, rightwards", test: listView, focusItem: "withContent1",
1106+ key: Qt.Key_Right, focus: ["checkbox1", "switch1", "button1", "withContent1"]},
1107+ {tag: "in ListView, leftwards", test: listView, focusItem: "withContent1",
1108+ key: Qt.Key_Left, focus: ["button1", "switch1", "checkbox1", "withContent1"]},
1109+ {tag: "in generic, rightwards", test: generic, focusItem: "withContent1",
1110+ key: Qt.Key_Right, focus: ["checkbox1", "switch1", "button1", "withContent1"]},
1111+ {tag: "in generic, leftwards", test: listView, focusItem: "withContent1",
1112+ key: Qt.Key_Left, focus: ["button1", "switch1", "checkbox1", "withContent1"]},
1113+ ];
1114+ }
1115+ function test_horizontal_navigation_between_listitem_children_with_tabstop(data) {
1116+ var test = loadTest(data.test);
1117+ if (test.hasOwnProperty("delegate")) {
1118+ test.delegate = listItemWithContent;
1119+ waitForRendering(test, 500);
1120+ }
1121+ var item = findChild(test, data.focusItem);
1122+ verify(item);
1123+ item.forceActiveFocus();
1124+ tryCompare(item, "activeFocus", true, 500, "Focus hasn't been gained by the ListItem");
1125+ for (var i = 0; i < data.focus.length; i++) {
1126+ keyClick(data.key);
1127+ tryCompare(main.activeFocusItem, "objectName", data.focus[i]);
1128+ }
1129+ }
1130+
1131+ // executes a combination of tab/navigation keys/backtab sequence
1132+ function test_pattern_data() {
1133+ return [
1134+ {tag: "Tabs in ListView", test: listView, delegate: listItemWithContent, testPlan: [
1135+ {key: Qt.Key_Tab, focus: "withContent0"},
1136+ {key: Qt.Key_Tab, focus: "topItem"},
1137+ {key: Qt.Key_Backtab, focus: "withContent0"},
1138+ ]},
1139+ {tag: "Tab and navigate in ListView", test: listView, delegate: listItemWithContent, testPlan: [
1140+ {key: Qt.Key_Tab, focus: "withContent0"},
1141+ {key: Qt.Key_Down, focus: "withContent1"},
1142+ {key: Qt.Key_Left, focus: "button1"},
1143+ {key: Qt.Key_Left, focus: "switch1"},
1144+ {key: Qt.Key_Down, focus: "withContent2"},
1145+ {key: Qt.Key_Right, focus: "checkbox2"},
1146+ {key: Qt.Key_Right, focus: "switch2"},
1147+ {key: Qt.Key_Down, focus: "withContent3"},
1148+ {key: Qt.Key_Down, focus: "withContent4"},
1149+ {key: Qt.Key_Backtab, focus: "topItem"},
1150+ {key: Qt.Key_Tab, focus: "withContent4"},
1151+ ]},
1152+ {tag: "Tab and navigate in generic", test: generic, testPlan: [
1153+ {key: Qt.Key_Tab, focus: "withContent0"},
1154+ {key: Qt.Key_Down, focus: "withContent0"},
1155+ {key: Qt.Key_Left, focus: "button0"},
1156+ {key: Qt.Key_Left, focus: "switch0"},
1157+ {key: Qt.Key_Down, focus: "switch0"},
1158+ {key: Qt.Key_Right, focus: "button0"},
1159+ {key: Qt.Key_Right, focus: "withContent0"},
1160+ {key: Qt.Key_Down, focus: "withContent0"},
1161+ {key: Qt.Key_Down, focus: "withContent0"},
1162+ {key: Qt.Key_Backtab, focus: "topItem"},
1163+ {key: Qt.Key_Tab, focus: "withContent0"},
1164+ ]},
1165+ {tag: "Mixed Tab and navigate keys in generic", test: generic, testPlan: [
1166+ {key: Qt.Key_Tab, focus: "withContent0"},
1167+ {key: Qt.Key_Tab, focus: "checkbox0"},
1168+ {key: Qt.Key_Tab, focus: "switch0"},
1169+ {key: Qt.Key_Right, focus: "button0"},
1170+ {key: Qt.Key_Right, focus: "withContent0"},
1171+ {key: Qt.Key_Tab, focus: "checkbox0"},
1172+ {key: Qt.Key_Left, focus: "withContent0"},
1173+ {key: Qt.Key_Left, focus: "button0"},
1174+ {key: Qt.Key_Tab, focus: "withContent1"},
1175+ ]},
1176+ ];
1177+ }
1178+ function test_pattern(data) {
1179+ var test = loadTest(data.test);
1180+ if (test.hasOwnProperty("delegate") && data.delegate) {
1181+ test.delegate = data.delegate;
1182+ waitForRendering(test, 500);
1183+ }
1184+ for (var i = 0; i < data.testPlan.length; i++) {
1185+ var plan = data.testPlan[i];
1186+ keyClick(plan.key);
1187+ tryCompare(main.activeFocusItem, "activeFocus", true, 200, "Focus not set for " + plan.focus);
1188+ compare(main.activeFocusItem.objectName, plan.focus);
1189+ if (main.activeFocusItem.hasOwnProperty("keyNavigationFocus")) {
1190+ verify(main.activeFocusItem.keyNavigationFocus);
1191+ }
1192+ }
1193+ }
1194+
1195+ function test_do_not_focus_on_actions_data() {
1196+ return [
1197+ {tag: "leading actions revealed", test: listView, focusItem: "withContent1", leading: true, swipeDx: units.gu(10),
1198+ key: Qt.Key_Left, focus: ["button1", "switch1", "checkbox1", "withContent1"]},
1199+ {tag: "trailing actions revealed", test: listView, focusItem: "withContent1", leading: false, swipeDx: -units.gu(10),
1200+ key: Qt.Key_Left, focus: ["button1", "switch1", "checkbox1", "withContent1"]},
1201+ ]
1202+ }
1203+ function test_do_not_focus_on_actions(data) {
1204+ var test = loadTest(data.test);
1205+ if (test.hasOwnProperty("delegate")) {
1206+ test.delegate = listItemWithContent;
1207+ waitForRendering(test, 500);
1208+ }
1209+ var item = findChild(test, data.focusItem);
1210+ verify(item);
1211+ item.forceActiveFocus();
1212+ tryCompare(item, "activeFocus", true, 500, "Focus hasn't been gained by the ListItem");
1213+ // swipe in
1214+ swipe(item, data.leading ? 1 : item.width - 1, centerOf(item).y, data.swipeDx, 0);
1215+ // compare
1216+ for (var i = 0; i < data.focus.length; i++) {
1217+ keyClick(data.key);
1218+ tryCompare(main.activeFocusItem, "objectName", data.focus[i]);
1219+ }
1220+ }
1221+ }
1222+}
1223+
1224
1225=== modified file 'tests/unit_x11/tst_components/tst_quickutils.qml'
1226--- tests/unit_x11/tst_components/tst_quickutils.qml 2015-03-03 13:20:06 +0000
1227+++ tests/unit_x11/tst_components/tst_quickutils.qml 2016-03-01 15:09:23 +0000
1228@@ -1,5 +1,5 @@
1229 /*
1230- * Copyright 2012 Canonical Ltd.
1231+ * Copyright 2012-2016 Canonical Ltd.
1232 *
1233 * This program is free software; you can redistribute it and/or modify
1234 * it under the terms of the GNU Lesser General Public License as published by
1235@@ -14,16 +14,33 @@
1236 * along with this program. If not, see <http://www.gnu.org/licenses/>.
1237 */
1238
1239-import QtQuick 2.0
1240+import QtQuick 2.4
1241 import QtTest 1.0
1242-import Ubuntu.Components 1.1
1243-import Ubuntu.Components.ListItems 1.0
1244+import Ubuntu.Components 1.3
1245
1246 Item {
1247 id: root
1248 width: units.gu(40)
1249 height: units.gu(40)
1250
1251+ Column {
1252+ id: focusGroup
1253+ Item {
1254+ objectName: "unfocusableFirst"
1255+ }
1256+
1257+ Repeater {
1258+ model: 5
1259+ Item {
1260+ objectName: index == 0 ? "first" : (index == 4 ? "last" : "item" + index)
1261+ activeFocusOnTab: true
1262+ }
1263+ }
1264+ Item {
1265+ objectName: "unfocusableLast"
1266+ }
1267+ }
1268+
1269 TestCase {
1270 id: test
1271 name: "QuickUtilsAPI"
1272@@ -39,5 +56,15 @@
1273 compare(QuickUtils.className(test), "TestCase", "className for TestCase");
1274 compare(QuickUtils.className(root), "QQuickItem", "className for Item");
1275 }
1276+
1277+ function test_firstFocusableChild()
1278+ {
1279+ compare(QuickUtils.firstFocusableChild(focusGroup).objectName, "first");
1280+ }
1281+
1282+ function test_lastFocusableChild()
1283+ {
1284+ compare(QuickUtils.lastFocusableChild(focusGroup).objectName, "last");
1285+ }
1286 }
1287 }

Subscribers

People subscribed via source and target branches