on xenial help sign in
Limit tab-focus travelling on dialogs with a fence
ac6799eAlbert Astals Cid
plugins/Utils/CMakeLists.txt+1
@@ -34,6 +34,7 @@ set(QMLPLUGIN_SRC
34 deviceconfigparser.cpp
34 deviceconfigparser.cpp
35 globalfunctions.cpp
35 globalfunctions.cpp
36 URLDispatcher.cpp
36 URLDispatcher.cpp
37+ tabfocusfence.cpp
37 plugin.cpp
38 plugin.cpp
38 )
39 )
39
40
plugins/Utils/plugin.cpp+3−1
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright (C) 2012-2015 Canonical, Ltd.
2+ * Copyright (C) 2012-2017 Canonical, Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
5 * it under the terms of the GNU General Public License as published by
@@ -40,6 +40,7 @@
40#include "globalfunctions.h"
40#include "globalfunctions.h"
41#include "URLDispatcher.h"
41#include "URLDispatcher.h"
42#include "appdrawerproxymodel.h"
42#include "appdrawerproxymodel.h"
43+#include "tabfocusfence.h"
43
44
44static QObject *createWindowStateStorage(QQmlEngine *engine, QJSEngine *scriptEngine)
45static QObject *createWindowStateStorage(QQmlEngine *engine, QJSEngine *scriptEngine)
45{
46{
@@ -84,4 +85,5 @@ void UtilsPlugin::registerTypes(const char *uri)
84 qmlRegisterSingletonType<GlobalFunctions>(uri, 0, 1, "Functions", createGlobalFunctions);
85 qmlRegisterSingletonType<GlobalFunctions>(uri, 0, 1, "Functions", createGlobalFunctions);
85 qmlRegisterType<URLDispatcher>(uri, 0, 1, "URLDispatcher");
86 qmlRegisterType<URLDispatcher>(uri, 0, 1, "URLDispatcher");
86 qmlRegisterType<AppDrawerProxyModel>(uri, 0, 1, "AppDrawerProxyModel");
87 qmlRegisterType<AppDrawerProxyModel>(uri, 0, 1, "AppDrawerProxyModel");
88+ qmlRegisterType<TabFocusFenceItem>(uri, 0, 1, "TabFocusFence");
87}
89}
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 @@
1/*
1/*
2− * Copyright (C) 2014-2016 Canonical, Ltd.
2+ * Copyright (C) 2014-2017 Canonical, Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
5 * it under the terms of the GNU General Public License as published by
@@ -149,6 +149,7 @@ Item {
149 title: i18n.ctr("Title: Lock/Log out dialog", "Log out")
149 title: i18n.ctr("Title: Lock/Log out dialog", "Log out")
150 text: i18n.tr("Are you sure you want to log out?")
150 text: i18n.tr("Are you sure you want to log out?")
151 Button {
151 Button {
152+ width: parent.width
152 text: i18n.ctr("Button: Lock the system", "Lock")
153 text: i18n.ctr("Button: Lock the system", "Lock")
153 onClicked: {
154 onClicked: {
154 LightDMService.greeter.showGreeter()
155 LightDMService.greeter.showGreeter()
@@ -156,6 +157,7 @@ Item {
156 }
157 }
157 }
158 }
158 Button {
159 Button {
160+ width: parent.width
159 focus: true
161 focus: true
160 text: i18n.ctr("Button: Log out from the system", "Log Out")
162 text: i18n.ctr("Button: Log out from the system", "Log Out")
161 onClicked: {
163 onClicked: {
@@ -164,6 +166,7 @@ Item {
164 }
166 }
165 }
167 }
166 Button {
168 Button {
169+ width: parent.width
167 text: i18n.tr("Cancel")
170 text: i18n.tr("Cancel")
168 onClicked: {
171 onClicked: {
169 logoutDialog.hide();
172 logoutDialog.hide();
@@ -179,12 +182,14 @@ Item {
179 title: i18n.ctr("Title: Reboot dialog", "Reboot")
182 title: i18n.ctr("Title: Reboot dialog", "Reboot")
180 text: i18n.tr("Are you sure you want to reboot?")
183 text: i18n.tr("Are you sure you want to reboot?")
181 Button {
184 Button {
185+ width: parent.width
182 text: i18n.tr("No")
186 text: i18n.tr("No")
183 onClicked: {
187 onClicked: {
184 rebootDialog.hide();
188 rebootDialog.hide();
185 }
189 }
186 }
190 }
187 Button {
191 Button {
192+ width: parent.width
188 focus: true
193 focus: true
189 text: i18n.tr("Yes")
194 text: i18n.tr("Yes")
190 onClicked: {
195 onClicked: {
@@ -204,6 +209,7 @@ Item {
204 title: i18n.ctr("Title: Power off/Restart dialog", "Power")
209 title: i18n.ctr("Title: Power off/Restart dialog", "Power")
205 text: i18n.tr("Are you sure you would like\nto power off?")
210 text: i18n.tr("Are you sure you would like\nto power off?")
206 Button {
211 Button {
212+ width: parent.width
207 focus: true
213 focus: true
208 text: i18n.ctr("Button: Power off the system", "Power off")
214 text: i18n.ctr("Button: Power off the system", "Power off")
209 onClicked: {
215 onClicked: {
@@ -214,6 +220,7 @@ Item {
214 color: theme.palette.normal.negative
220 color: theme.palette.normal.negative
215 }
221 }
216 Button {
222 Button {
223+ width: parent.width
217 text: i18n.ctr("Button: Restart the system", "Restart")
224 text: i18n.ctr("Button: Restart the system", "Restart")
218 onClicked: {
225 onClicked: {
219 root.closeAllApps();
226 root.closeAllApps();
@@ -222,6 +229,7 @@ Item {
222 }
229 }
223 }
230 }
224 Button {
231 Button {
232+ width: parent.width
225 text: i18n.tr("Cancel")
233 text: i18n.tr("Cancel")
226 onClicked: {
234 onClicked: {
227 powerDialog.hide();
235 powerDialog.hide();
qml/Components/Lockscreen.qml+3−1
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright (C) 2013 Canonical, Ltd.
2+ * Copyright (C) 2013-2017 Canonical, Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
5 * it under the terms of the GNU General Public License as published by
@@ -240,8 +240,10 @@ Showable {
240 property var dialogLoader // dummy to satisfy ShellDialog's context dependent prop
240 property var dialogLoader // dummy to satisfy ShellDialog's context dependent prop
241
241
242 Button {
242 Button {
243+ width: parent.width
243 objectName: "infoPopupOkButton"
244 objectName: "infoPopupOkButton"
244 text: i18n.tr("OK")
245 text: i18n.tr("OK")
246+ focus: true
245 onClicked: {
247 onClicked: {
246 PopupUtils.close(dialog)
248 PopupUtils.close(dialog)
247 root.infoPopupConfirmed();
249 root.infoPopupConfirmed();
qml/Components/ModeSwitchWarningDialog.qml+8−2
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright (C) 2015 Canonical, Ltd.
2+ * Copyright (C) 2015-2017 Canonical, Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
5 * it under the terms of the GNU General Public License as published by
@@ -29,6 +29,7 @@ ShellDialog {
29 signal forceClose();
29 signal forceClose();
30
30
31 Label {
31 Label {
32+ width: parent.width
32 text: i18n.tr("Apps may have unsaved data:")
33 text: i18n.tr("Apps may have unsaved data:")
33 fontSize: "large"
34 fontSize: "large"
34 color: "#5D5D5D"
35 color: "#5D5D5D"
@@ -37,6 +38,7 @@ ShellDialog {
37 Repeater {
38 Repeater {
38 id: appRepeater
39 id: appRepeater
39 RowLayout {
40 RowLayout {
41+ width: parent.width
40 spacing: units.gu(2)
42 spacing: units.gu(2)
41 Image {
43 Image {
42 Layout.preferredHeight: units.gu(2)
44 Layout.preferredHeight: units.gu(2)
@@ -54,20 +56,23 @@ ShellDialog {
54 }
56 }
55
57
56 Label {
58 Label {
59+ width: parent.width
57 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.")
60 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.")
58 wrapMode: Text.WordWrap
61 wrapMode: Text.WordWrap
59 color: "#888888"
62 color: "#888888"
60 }
63 }
61
64
62 Label {
65 Label {
66+ width: parent.width
63 text: i18n.tr("Or force close now (unsaved data will be lost).")
67 text: i18n.tr("Or force close now (unsaved data will be lost).")
64 wrapMode: Text.WordWrap
68 wrapMode: Text.WordWrap
65 color: "#888888"
69 color: "#888888"
66 }
70 }
67
71
68− ThinDivider {}
72+ ThinDivider { width: parent.width }
69
73
70 RowLayout {
74 RowLayout {
75+ width: parent.width
71 Label {
76 Label {
72 objectName: "reconnectLabel"
77 objectName: "reconnectLabel"
73 Layout.fillWidth: true
78 Layout.fillWidth: true
@@ -84,6 +89,7 @@ ShellDialog {
84 }
89 }
85
90
86 Button {
91 Button {
92+ focus: true
87 objectName: "forceCloseButton"
93 objectName: "forceCloseButton"
88 text: i18n.tr("Close all")
94 text: i18n.tr("Close all")
89 color: theme.palette.normal.negative
95 color: theme.palette.normal.negative
qml/Components/ShellDialog.qml+15−1
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright (C) 2014 Canonical, Ltd.
2+ * Copyright (C) 2014-2017 Canonical, Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
5 * it under the terms of the GNU General Public License as published by
@@ -19,6 +19,7 @@ import QtQuick 2.4
19import Ubuntu.Components 1.3
19import Ubuntu.Components 1.3
20import Ubuntu.Components.Themes 1.3
20import Ubuntu.Components.Themes 1.3
21import Ubuntu.Components.Popups 1.3
21import Ubuntu.Components.Popups 1.3
22+import Utils 0.1
22
23
23/*
24/*
24 A Dialog configured for use as a proper in-scene Dialog
25 A Dialog configured for use as a proper in-scene Dialog
@@ -32,6 +33,8 @@ Dialog {
32 // NB: PopupBase, Dialog's superclass, will check for the existence of this property
33 // NB: PopupBase, Dialog's superclass, will check for the existence of this property
33 property bool reparentToRootItem: false
34 property bool reparentToRootItem: false
34
35
36+ default property alias columnContents: column.data
37+
35 onVisibleChanged: { if (!visible && dialogLoader) { dialogLoader.active = false; } }
38 onVisibleChanged: { if (!visible && dialogLoader) { dialogLoader.active = false; } }
36
39
37 Keys.onEscapePressed: hide()
40 Keys.onEscapePressed: hide()
@@ -50,4 +53,15 @@ Dialog {
50 __foreground.theme = themeHack
53 __foreground.theme = themeHack
51 show();
54 show();
52 }
55 }
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+ }
53}
67}
tests/mocks/Utils/CMakeLists.txt+1
@@ -25,6 +25,7 @@ set(QMLPLUGIN_SRC
25 ${CMAKE_SOURCE_DIR}/plugins/Utils/deviceconfigparser.cpp
25 ${CMAKE_SOURCE_DIR}/plugins/Utils/deviceconfigparser.cpp
26 ${CMAKE_SOURCE_DIR}/plugins/Utils/globalfunctions.cpp
26 ${CMAKE_SOURCE_DIR}/plugins/Utils/globalfunctions.cpp
27 ${CMAKE_SOURCE_DIR}/plugins/Utils/appdrawerproxymodel.cpp
27 ${CMAKE_SOURCE_DIR}/plugins/Utils/appdrawerproxymodel.cpp
28+ ${CMAKE_SOURCE_DIR}/plugins/Utils/tabfocusfence.cpp
28 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/ApplicationManagerInterface.h
29 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/ApplicationManagerInterface.h
29 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/ApplicationInfoInterface.h
30 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/ApplicationInfoInterface.h
30 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/MirSurfaceInterface.h
31 ${APPLICATION_API_INCLUDEDIR}/unity/shell/application/MirSurfaceInterface.h
tests/mocks/Utils/plugin.cpp+3−1
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright (C) 2015-2016 Canonical, Ltd.
2+ * Copyright (C) 2015-2017 Canonical, Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
5 * it under the terms of the GNU General Public License as published by
@@ -41,6 +41,7 @@
41#include <deviceconfigparser.h>
41#include <deviceconfigparser.h>
42#include <globalfunctions.h>
42#include <globalfunctions.h>
43#include <appdrawerproxymodel.h>
43#include <appdrawerproxymodel.h>
44+#include <tabfocusfence.h>
44
45
45static QObject *createWindowStateStorage(QQmlEngine *engine, QJSEngine *scriptEngine)
46static QObject *createWindowStateStorage(QQmlEngine *engine, QJSEngine *scriptEngine)
46{
47{
@@ -84,4 +85,5 @@ void FakeUtilsPlugin::registerTypes(const char *uri)
84 qmlRegisterSingletonType<GlobalFunctions>(uri, 0, 1, "Functions", createGlobalFunctions);
85 qmlRegisterSingletonType<GlobalFunctions>(uri, 0, 1, "Functions", createGlobalFunctions);
85 qmlRegisterType<URLDispatcher>(uri, 0, 1, "URLDispatcher");
86 qmlRegisterType<URLDispatcher>(uri, 0, 1, "URLDispatcher");
86 qmlRegisterType<AppDrawerProxyModel>(uri, 0, 1, "AppDrawerProxyModel");
87 qmlRegisterType<AppDrawerProxyModel>(uri, 0, 1, "AppDrawerProxyModel");
88+ qmlRegisterType<TabFocusFenceItem>(uri, 0, 1, "TabFocusFence");
87}
89}
tests/qmltests/tst_OrientedShell.qml+39−1
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright (C) 2015-2016 Canonical, Ltd.
2+ * Copyright (C) 2015-2017 Canonical, Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
5 * it under the terms of the GNU General Public License as published by
@@ -1558,5 +1558,43 @@ Rectangle {
1558 verify(surface);
1558 verify(surface);
1559 return surface.activeFocus;
1559 return surface.activeFocus;
1560 }
1560 }
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+ }
1561 }
1599 }
1562}
1600}