on xenial help sign in
Limit tab-focus travelling on dialogs with a fence

Approved by: Lukáš Tinkl, Unity8 CI Bot

commitac6799e838dd763fb7b34fd74af9350920c16fdf authorAlbert Astals Cid <albert.astals@canonical.com> dateTue 24 Jan 2017, 07:38 (10 years ago) parent906fa33cec096a
11 files changed+163−8
plugins/Utils/CMakeLists.txt+1 plugins/Utils/plugin.cpp+3−1 plugins/Utils/tabfocusfence.cpp+46 plugins/Utils/tabfocusfence.h+35 qml/Components/Dialogs.qml+9−1 qml/Components/Lockscreen.qml+3−1 qml/Components/ModeSwitchWarningDialog.qml+8−2 qml/Components/ShellDialog.qml+15−1 tests/mocks/Utils/CMakeLists.txt+1 tests/mocks/Utils/plugin.cpp+3−1 tests/qmltests/tst_OrientedShell.qml+39−1
plugins/Utils/CMakeLists.txt+1
@@ -34,6 +34,7 @@ set(QMLPLUGIN_SRC
3434 deviceconfigparser.cpp
3535 globalfunctions.cpp
3636 URLDispatcher.cpp
37+ tabfocusfence.cpp
3738 plugin.cpp
3839 )
3940
plugins/Utils/plugin.cpp+3−1
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2012-2015 Canonical, Ltd.
2+ * Copyright (C) 2012-2017 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -40,6 +40,7 @@
4040#include "globalfunctions.h"
4141#include "URLDispatcher.h"
4242#include "appdrawerproxymodel.h"
43+#include "tabfocusfence.h"
4344
4445static QObject *createWindowStateStorage(QQmlEngine *engine, QJSEngine *scriptEngine)
4546{
@@ -84,4 +85,5 @@ void UtilsPlugin::registerTypes(const char *uri)
8485 qmlRegisterSingletonType<GlobalFunctions>(uri, 0, 1, "Functions", createGlobalFunctions);
8586 qmlRegisterType<URLDispatcher>(uri, 0, 1, "URLDispatcher");
8687 qmlRegisterType<AppDrawerProxyModel>(uri, 0, 1, "AppDrawerProxyModel");
88+ qmlRegisterType<TabFocusFenceItem>(uri, 0, 1, "TabFocusFence");
8789}
plugins/Utils/tabfocusfence.cppnew file+46
@@ -0,0 +1,46 @@
1+/*
2+ * Copyright 2017 Canonical Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU Lesser General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+*/
16+
17+#include "tabfocusfence.h"
18+
19+#include <private/qquickitem_p.h>
20+
21+TabFocusFenceItem::TabFocusFenceItem(QQuickItem *parent) : QQuickItem(parent)
22+{
23+ QQuickItemPrivate *d = QQuickItemPrivate::get(this);
24+ d->isTabFence = true;
25+ setFlag(ItemIsFocusScope);
26+}
27+
28+void TabFocusFenceItem::keyPressEvent(QKeyEvent *event)
29+{
30+ // Needed so we eat Tab keys when there's only one item inside the fence
31+ if (event->key() == Qt::Key_Tab) {
32+ event->accept();
33+ } else {
34+ QQuickItem::keyPressEvent(event);
35+ }
36+}
37+
38+void TabFocusFenceItem::keyReleaseEvent(QKeyEvent *event)
39+{
40+ // Needed so we eat Tab keys when there's only one item inside the fence
41+ if (event->key() == Qt::Key_Tab) {
42+ event->accept();
43+ } else {
44+ QQuickItem::keyReleaseEvent(event);
45+ }
46+}
plugins/Utils/tabfocusfence.hnew file+35
@@ -0,0 +1,35 @@
1+/*
2+ * Copyright 2017 Canonical Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU Lesser General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+*/
16+
17+#ifndef TABFOCUSFENCE_H
18+#define TABFOCUSFENCE_H
19+
20+#include <QQuickItem>
21+
22+// An item that restricts focus Tab travelling
23+// to its children
24+class TabFocusFenceItem : public QQuickItem
25+{
26+Q_OBJECT
27+
28+public:
29+ TabFocusFenceItem(QQuickItem *parent = nullptr);
30+
31+ void keyPressEvent(QKeyEvent *event) override;
32+ void keyReleaseEvent(QKeyEvent *event) override;
33+};
34+
35+#endif
qml/Components/Dialogs.qml+9−1
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2014-2016 Canonical, Ltd.
2+ * Copyright (C) 2014-2017 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -149,6 +149,7 @@ Item {
149149 title: i18n.ctr("Title: Lock/Log out dialog", "Log out")
150150 text: i18n.tr("Are you sure you want to log out?")
151151 Button {
152+ width: parent.width
152153 text: i18n.ctr("Button: Lock the system", "Lock")
153154 onClicked: {
154155 LightDMService.greeter.showGreeter()
@@ -156,6 +157,7 @@ Item {
156157 }
157158 }
158159 Button {
160+ width: parent.width
159161 focus: true
160162 text: i18n.ctr("Button: Log out from the system", "Log Out")
161163 onClicked: {
@@ -164,6 +166,7 @@ Item {
164166 }
165167 }
166168 Button {
169+ width: parent.width
167170 text: i18n.tr("Cancel")
168171 onClicked: {
169172 logoutDialog.hide();
@@ -179,12 +182,14 @@ Item {
179182 title: i18n.ctr("Title: Reboot dialog", "Reboot")
180183 text: i18n.tr("Are you sure you want to reboot?")
181184 Button {
185+ width: parent.width
182186 text: i18n.tr("No")
183187 onClicked: {
184188 rebootDialog.hide();
185189 }
186190 }
187191 Button {
192+ width: parent.width
188193 focus: true
189194 text: i18n.tr("Yes")
190195 onClicked: {
@@ -204,6 +209,7 @@ Item {
204209 title: i18n.ctr("Title: Power off/Restart dialog", "Power")
205210 text: i18n.tr("Are you sure you would like\nto power off?")
206211 Button {
212+ width: parent.width
207213 focus: true
208214 text: i18n.ctr("Button: Power off the system", "Power off")
209215 onClicked: {
@@ -214,6 +220,7 @@ Item {
214220 color: theme.palette.normal.negative
215221 }
216222 Button {
223+ width: parent.width
217224 text: i18n.ctr("Button: Restart the system", "Restart")
218225 onClicked: {
219226 root.closeAllApps();
@@ -222,6 +229,7 @@ Item {
222229 }
223230 }
224231 Button {
232+ width: parent.width
225233 text: i18n.tr("Cancel")
226234 onClicked: {
227235 powerDialog.hide();
qml/Components/Lockscreen.qml+3−1
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2013 Canonical, Ltd.
2+ * Copyright (C) 2013-2017 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -240,8 +240,10 @@ Showable {
240240 property var dialogLoader // dummy to satisfy ShellDialog's context dependent prop
241241
242242 Button {
243+ width: parent.width
243244 objectName: "infoPopupOkButton"
244245 text: i18n.tr("OK")
246+ focus: true
245247 onClicked: {
246248 PopupUtils.close(dialog)
247249 root.infoPopupConfirmed();
qml/Components/ModeSwitchWarningDialog.qml+8−2
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2015 Canonical, Ltd.
2+ * Copyright (C) 2015-2017 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -29,6 +29,7 @@ ShellDialog {
2929 signal forceClose();
3030
3131 Label {
32+ width: parent.width
3233 text: i18n.tr("Apps may have unsaved data:")
3334 fontSize: "large"
3435 color: "#5D5D5D"
@@ -37,6 +38,7 @@ ShellDialog {
3738 Repeater {
3839 id: appRepeater
3940 RowLayout {
41+ width: parent.width
4042 spacing: units.gu(2)
4143 Image {
4244 Layout.preferredHeight: units.gu(2)
@@ -54,20 +56,23 @@ ShellDialog {
5456 }
5557
5658 Label {
59+ width: parent.width
5760 text: i18n.ctr("Re-dock means connect the device again to an external screen/mouse/keyboard", "Re-dock, save your work and close these apps to continue.")
5861 wrapMode: Text.WordWrap
5962 color: "#888888"
6063 }
6164
6265 Label {
66+ width: parent.width
6367 text: i18n.tr("Or force close now (unsaved data will be lost).")
6468 wrapMode: Text.WordWrap
6569 color: "#888888"
6670 }
6771
68− ThinDivider {}
72+ ThinDivider { width: parent.width }
6973
7074 RowLayout {
75+ width: parent.width
7176 Label {
7277 objectName: "reconnectLabel"
7378 Layout.fillWidth: true
@@ -84,6 +89,7 @@ ShellDialog {
8489 }
8590
8691 Button {
92+ focus: true
8793 objectName: "forceCloseButton"
8894 text: i18n.tr("Close all")
8995 color: theme.palette.normal.negative
qml/Components/ShellDialog.qml+15−1
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2014 Canonical, Ltd.
2+ * Copyright (C) 2014-2017 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -19,6 +19,7 @@ import QtQuick 2.4
1919import Ubuntu.Components 1.3
2020import Ubuntu.Components.Themes 1.3
2121import Ubuntu.Components.Popups 1.3
22+import Utils 0.1
2223
2324/*
2425 A Dialog configured for use as a proper in-scene Dialog
@@ -32,6 +33,8 @@ Dialog {
3233 // NB: PopupBase, Dialog's superclass, will check for the existence of this property
3334 property bool reparentToRootItem: false
3435
36+ default property alias columnContents: column.data
37+
3538 onVisibleChanged: { if (!visible && dialogLoader) { dialogLoader.active = false; } }
3639
3740 Keys.onEscapePressed: hide()
@@ -50,4 +53,15 @@ Dialog {
5053 __foreground.theme = themeHack
5154 show();
5255 }
56+
57+ TabFocusFence {
58+ width: parent.width
59+ height: column.height
60+ focus: true
61+ Column {
62+ id: column
63+ width: parent.width
64+ spacing: units.gu(2)
65+ }
66+ }
5367}
tests/mocks/Utils/CMakeLists.txt+1
@@ -25,6 +25,7 @@ set(QMLPLUGIN_SRC
2525 ${CMAKE_SOURCE_DIR}/plugins/Utils/deviceconfigparser.cpp
2626 ${CMAKE_SOURCE_DIR}/plugins/Utils/globalfunctions.cpp
2727 ${CMAKE_SOURCE_DIR}/plugins/Utils/appdrawerproxymodel.cpp
28+ ${CMAKE_SOURCE_DIR}/plugins/Utils/tabfocusfence.cpp
2829 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/ApplicationManagerInterface.h
2930 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/ApplicationInfoInterface.h
3031 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/MirSurfaceInterface.h
tests/mocks/Utils/plugin.cpp+3−1
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2015-2016 Canonical, Ltd.
2+ * Copyright (C) 2015-2017 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -41,6 +41,7 @@
4141#include <deviceconfigparser.h>
4242#include <globalfunctions.h>
4343#include <appdrawerproxymodel.h>
44+#include <tabfocusfence.h>
4445
4546static QObject *createWindowStateStorage(QQmlEngine *engine, QJSEngine *scriptEngine)
4647{
@@ -84,4 +85,5 @@ void FakeUtilsPlugin::registerTypes(const char *uri)
8485 qmlRegisterSingletonType<GlobalFunctions>(uri, 0, 1, "Functions", createGlobalFunctions);
8586 qmlRegisterType<URLDispatcher>(uri, 0, 1, "URLDispatcher");
8687 qmlRegisterType<AppDrawerProxyModel>(uri, 0, 1, "AppDrawerProxyModel");
88+ qmlRegisterType<TabFocusFenceItem>(uri, 0, 1, "TabFocusFence");
8789}
tests/qmltests/tst_OrientedShell.qml+39−1
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2015-2016 Canonical, Ltd.
2+ * Copyright (C) 2015-2017 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -1558,5 +1558,43 @@ Rectangle {
15581558 verify(surface);
15591559 return surface.activeFocus;
15601560 }
1561+
1562+ function test_tabCyclyingInShutdownDialog() {
1563+ loadShell("mako");
1564+
1565+ testCase.showPowerDialog();
1566+
1567+ var dialogs = findChild(orientedShell, "dialogs");
1568+ var buttons = findChildsByType(dialogs, "Button");
1569+
1570+ tryCompare(buttons[0], "activeFocus", true);
1571+
1572+ keyClick(Qt.Key_Tab);
1573+ tryCompare(buttons[1], "activeFocus", true);
1574+
1575+ keyClick(Qt.Key_Tab);
1576+ tryCompare(buttons[2], "activeFocus", true);
1577+
1578+ keyClick(Qt.Key_Tab);
1579+ tryCompare(buttons[0], "activeFocus", true);
1580+
1581+ keyClick(Qt.Key_Escape);
1582+
1583+ var dialogLoader = findChild(orientedShell, "dialogLoader");
1584+ tryCompare(dialogLoader, "item", null);
1585+ }
1586+
1587+ function test_escColosesShutdownDialog() {
1588+ loadShell("mako");
1589+
1590+ testCase.showPowerDialog();
1591+
1592+ var dialogLoader = findChild(orientedShell, "dialogLoader");
1593+ tryCompareFunction(function() { return dialogLoader.item !== null }, true);
1594+
1595+ keyClick(Qt.Key_Escape);
1596+
1597+ tryCompare(dialogLoader, "item", null);
1598+ }
15611599 }
15621600}