on xenial help sign in
Introduce a GlobalShortcut QML component for handling global keyboard shortcuts

Approved by: Michael Zanetti, Michał Sawicz

commit17cb6be391f5bc5d633d5ce3f987ab02c5ff5240 authorLukáš Tinkl <lukas.tinkl@canonical.com> dateMon 7 Sep 2015, 14:05 (11 years ago) parentb0a1c58b9da988
30 files changed+817−152
debian/unity8-private.install+1 plugins/CMakeLists.txt+1 plugins/GlobalShortcut/CMakeLists.txt+10 plugins/GlobalShortcut/globalshortcut.cpp+80 plugins/GlobalShortcut/globalshortcut.h+76 plugins/GlobalShortcut/globalshortcutregistry.cpp+115 plugins/GlobalShortcut/globalshortcutregistry.h+70 plugins/GlobalShortcut/plugin.cpp+27 plugins/GlobalShortcut/plugin.h+32 plugins/GlobalShortcut/qmldir+2 plugins/Unity/Session/Session.qmltypes+34 plugins/Unity/Session/dbusunitysessionservice.cpp+38−2 plugins/Utils/easingcurve.h+2−2 po/unity8.pot+66−62 qml/Components/Dialogs.qml+62−6 qml/Components/PhysicalKeysMapper.qml+7−8 qml/Components/ScreenGrabber.qml+7 qml/Components/ShellDialog.qml+7−1 qml/Components/VolumeControl.qml+20−6 qml/OrientedShell.qml+1−5 qml/Panel/Indicators/MessageMenuItemFactory.qml+2−2 qml/Shell.qml+11−4 tests/mocks/Utils/windowstatestorage.cpp−2 tests/mocks/Utils/windowstatestorage.h+1 tests/plugins/CMakeLists.txt+1 tests/plugins/GlobalShortcut/CMakeLists.txt+14 tests/plugins/GlobalShortcut/GlobalShortcutTest.cpp+77 tests/plugins/GlobalShortcut/shortcut.qml+39 tests/qmltests/Components/tst_Dialogs.qml+13−8 tests/qmltests/Components/tst_PhysicalKeysMapper.qml+1−44
debian/unity8-private.install+1
@@ -2,6 +2,7 @@ usr/lib/*/libunity8-private.*
22usr/lib/*/unity8/libUbuntuGestures*
33usr/lib/*/unity8/qml/AccountsService
44usr/lib/*/unity8/qml/Dash
5+usr/lib/*/unity8/qml/GlobalShortcut
56usr/lib/*/unity8/qml/Greeter
67usr/lib/*/unity8/qml/IntegratedLightDM
78usr/lib/*/unity8/qml/Lights
plugins/CMakeLists.txt+1
@@ -12,6 +12,7 @@ macro(add_unity8_plugin PLUGIN VERSION PATH)
1212endmacro()
1313
1414add_subdirectory(AccountsService)
15+add_subdirectory(GlobalShortcut)
1516add_subdirectory(Greeter)
1617add_subdirectory(IntegratedLightDM)
1718add_subdirectory(Lights)
plugins/GlobalShortcut/CMakeLists.txtnew file+10
@@ -0,0 +1,10 @@
1+set(globalshortcutplugin_SRCS
2+ globalshortcut.cpp
3+ globalshortcutregistry.cpp
4+ plugin.cpp)
5+
6+add_library(GlobalShortcut-qml SHARED ${globalshortcutplugin_SRCS})
7+
8+qt5_use_modules(GlobalShortcut-qml Quick Gui)
9+
10+add_unity8_plugin(GlobalShortcut 1.0 GlobalShortcut TARGETS GlobalShortcut-qml)
plugins/GlobalShortcut/globalshortcut.cppnew file+80
@@ -0,0 +1,80 @@
1+/*
2+ * Copyright (C) 2015 Canonical, Ltd.
3+ *
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
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 General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "globalshortcut.h"
18+#include "globalshortcutregistry.h"
19+
20+#include <QDebug>
21+#include <QQuickItem>
22+
23+Q_GLOBAL_STATIC(GlobalShortcutRegistry, registry)
24+
25+GlobalShortcut::GlobalShortcut(QQuickItem *parent)
26+ : QQuickItem(parent)
27+{
28+}
29+
30+QVariant GlobalShortcut::shortcut() const
31+{
32+ return m_shortcut;
33+}
34+
35+void GlobalShortcut::setShortcut(const QVariant &shortcut)
36+{
37+ if (m_shortcut == shortcut)
38+ return;
39+
40+ m_shortcut = shortcut;
41+ registry->addShortcut(shortcut, this);
42+ Q_EMIT shortcutChanged(shortcut);
43+}
44+
45+bool GlobalShortcut::isActive() const
46+{
47+ return m_active;
48+}
49+
50+void GlobalShortcut::setActive(bool active)
51+{
52+ if (m_active == active)
53+ return;
54+
55+ m_active = active;
56+ Q_EMIT activeChanged(active);
57+}
58+
59+void GlobalShortcut::componentComplete()
60+{
61+ connect(this, &QQuickItem::windowChanged, this, &GlobalShortcut::setupFilterOnWindow);
62+}
63+
64+void GlobalShortcut::keyPressEvent(QKeyEvent * event)
65+{
66+ Q_UNUSED(event)
67+ if (m_active) {
68+ Q_EMIT triggered(m_shortcut.toString());
69+ }
70+}
71+
72+void GlobalShortcut::setupFilterOnWindow(QQuickWindow *window)
73+{
74+ if (!window) {
75+ qWarning() << Q_FUNC_INFO << "Failed to setup filter on window";
76+ return;
77+ }
78+
79+ registry->setupFilterOnWindow((qulonglong) window->winId());
80+}
plugins/GlobalShortcut/globalshortcut.hnew file+76
@@ -0,0 +1,76 @@
1+/*
2+ * Copyright (C) 2015 Canonical, Ltd.
3+ *
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
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 General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef GLOBALSHORTCUT_H
18+#define GLOBALSHORTCUT_H
19+
20+#include <QVariant>
21+#include <QQuickWindow>
22+#include <QQuickItem>
23+
24+/**
25+ * @brief The GlobalShortcut class
26+ *
27+ * QML component for registering a shortcut; the shortcut itself can be
28+ * specified either as a string ("Ctrl+Alt+L") or as an enum value
29+ * (Qt.ControlModifier|Qt.AltModifier|Qt.Key_L).
30+ *
31+ * When the shortcut is detected, the signal triggered() gets emitted.
32+ */
33+class GlobalShortcut: public QQuickItem
34+{
35+ Q_OBJECT
36+ /**
37+ * The shortcut itself
38+ */
39+ Q_PROPERTY(QVariant shortcut READ shortcut WRITE setShortcut NOTIFY shortcutChanged)
40+ /**
41+ * Whether the shortcut is active (true by default)
42+ */
43+ Q_PROPERTY(bool active READ isActive WRITE setActive NOTIFY activeChanged)
44+
45+public:
46+ GlobalShortcut(QQuickItem *parent = nullptr);
47+ ~GlobalShortcut() = default;
48+
49+ QVariant shortcut() const;
50+ void setShortcut(const QVariant &shortcut);
51+
52+ bool isActive() const;
53+ void setActive(bool active);
54+
55+protected:
56+ void componentComplete() override;
57+ void keyPressEvent(QKeyEvent * event) override;
58+
59+Q_SIGNALS:
60+ void shortcutChanged(const QVariant &shortcut);
61+ /**
62+ * Emitted when a global keypress of @p shortcut is detected
63+ */
64+ void triggered(const QString &shortcut);
65+ void activeChanged(bool active);
66+
67+private Q_SLOTS:
68+ void setupFilterOnWindow(QQuickWindow* window);
69+
70+private:
71+ QVariant m_shortcut;
72+ bool m_active = true;
73+};
74+
75+
76+#endif // GLOBALSHORTCUT_H
plugins/GlobalShortcut/globalshortcutregistry.cppnew file+115
@@ -0,0 +1,115 @@
1+/*
2+ * Copyright (C) 2015 Canonical, Ltd.
3+ *
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
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 General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include <QDebug>
18+#include <QGuiApplication>
19+#include <QKeyEvent>
20+#include <QKeySequence>
21+
22+#include "globalshortcutregistry.h"
23+
24+static qulonglong s_windowId = 0;
25+
26+GlobalShortcutRegistry::GlobalShortcutRegistry(QObject *parent)
27+ : QObject(parent)
28+{
29+}
30+
31+GlobalShortcutList GlobalShortcutRegistry::shortcuts() const
32+{
33+ return m_shortcuts;
34+}
35+
36+bool GlobalShortcutRegistry::hasShortcut(const QVariant &seq) const
37+{
38+ return m_shortcuts.keys().contains(seq);
39+}
40+
41+void GlobalShortcutRegistry::addShortcut(const QVariant &seq, GlobalShortcut *sc)
42+{
43+ if (sc) {
44+ if (!m_shortcuts.keys().contains(seq)) { // create a new entry
45+ m_shortcuts.insert(seq, {sc});
46+ } else { // append to an existing one
47+ auto shortcuts = m_shortcuts[seq];
48+ shortcuts.append(sc);
49+ m_shortcuts.insert(seq, shortcuts);
50+ }
51+
52+ connect(sc, &GlobalShortcut::destroyed, this, &GlobalShortcutRegistry::removeShortcut);
53+ }
54+}
55+
56+void GlobalShortcutRegistry::removeShortcut(QObject *obj)
57+{
58+ QMutableMapIterator<QVariant, QVector<QPointer<GlobalShortcut>>> it(m_shortcuts);
59+ while (it.hasNext()) {
60+ it.next();
61+ GlobalShortcut * scObj = static_cast<GlobalShortcut *>(obj);
62+ if (scObj && it.value().contains(scObj)) {
63+ it.value().removeAll(scObj);
64+ if (it.value().isEmpty()) {
65+ it.remove();
66+ }
67+ }
68+ }
69+}
70+
71+bool GlobalShortcutRegistry::eventFilter(QObject *obj, QEvent *event)
72+{
73+ Q_ASSERT(m_filteredWindow);
74+ Q_ASSERT(obj == static_cast<QObject*>(m_filteredWindow.data()));
75+
76+ if (event->type() == QEvent::KeyPress) {
77+ QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event);
78+ QKeySequence seq = QKeySequence(keyEvent->key() + keyEvent->modifiers());
79+ if (m_shortcuts.keys().contains(seq)) {
80+ const auto shortcuts = m_shortcuts.value(seq);
81+ Q_FOREACH(const auto &shortcut, shortcuts) {
82+ if (shortcut) {
83+ qApp->sendEvent(shortcut, keyEvent);
84+ event->accept();
85+ }
86+ }
87+ }
88+
89+ return event->isAccepted();
90+ }
91+
92+ return QObject::eventFilter(obj, event);
93+}
94+
95+void GlobalShortcutRegistry::setupFilterOnWindow(qulonglong wid)
96+{
97+ if (wid == s_windowId) {
98+ return;
99+ }
100+
101+ if (m_filteredWindow) {
102+ m_filteredWindow->removeEventFilter(this);
103+ m_filteredWindow.clear();
104+ s_windowId = 0;
105+ }
106+
107+ for(QWindow *window: qApp->allWindows()) {
108+ if (window && window->winId() == wid) {
109+ m_filteredWindow = window;
110+ window->installEventFilter(this);
111+ s_windowId = wid;
112+ break;
113+ }
114+ }
115+}
plugins/GlobalShortcut/globalshortcutregistry.hnew file+70
@@ -0,0 +1,70 @@
1+/*
2+ * Copyright (C) 2015 Canonical, Ltd.
3+ *
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
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 General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef GLOBALSHORTCUT_REGISTRY_H
18+#define GLOBALSHORTCUT_REGISTRY_H
19+
20+#include <QObject>
21+#include <QVariantList>
22+#include <QPointer>
23+#include <QWindow>
24+
25+#include "globalshortcut.h"
26+
27+typedef QMap<QVariant, QVector<QPointer<GlobalShortcut>>> GlobalShortcutList;
28+
29+/**
30+ * @brief The GlobalShortcutRegistry class
31+ *
32+ * Serves as a central point for shortcut registration.
33+ */
34+class Q_DECL_EXPORT GlobalShortcutRegistry: public QObject
35+{
36+ Q_OBJECT
37+public:
38+ GlobalShortcutRegistry(QObject * parent = nullptr);
39+ ~GlobalShortcutRegistry() = default;
40+
41+ /**
42+ * @return the list of shortcuts currently registered
43+ */
44+ GlobalShortcutList shortcuts() const;
45+ /**
46+ * @return whether shortcut @p seq is currently registered
47+ */
48+ bool hasShortcut(const QVariant &seq) const;
49+ /**
50+ * Adds a shortcut @p seq to the registry
51+ */
52+ void addShortcut(const QVariant &seq, GlobalShortcut * sc);
53+
54+ /**
55+ * Sets up key events filtering on window @p wid
56+ */
57+ void setupFilterOnWindow(qulonglong wid);
58+
59+protected:
60+ bool eventFilter(QObject *obj, QEvent *event) override;
61+
62+private Q_SLOTS:
63+ void removeShortcut(QObject *obj);
64+
65+private:
66+ GlobalShortcutList m_shortcuts;
67+ QPointer<QWindow> m_filteredWindow = nullptr;
68+};
69+
70+#endif
plugins/GlobalShortcut/plugin.cppnew file+27
@@ -0,0 +1,27 @@
1+/*
2+ * Copyright (C) 2015 Canonical, Ltd.
3+ *
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
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 General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "plugin.h"
18+#include "globalshortcut.h"
19+
20+#include <QtQml>
21+
22+void GlobalShortcutPlugin::registerTypes(const char *uri)
23+{
24+ Q_ASSERT(uri == QLatin1String("GlobalShortcut"));
25+
26+ qmlRegisterType<GlobalShortcut>(uri, 1, 0, "GlobalShortcut");
27+}
plugins/GlobalShortcut/plugin.hnew file+32
@@ -0,0 +1,32 @@
1+/*
2+ * Copyright (C) 2015 Canonical, Ltd.
3+ *
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
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 General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#ifndef GLOBALSHORTCUTPLUGIN_H
18+#define GLOBALSHORTCUTPLUGIN_H
19+
20+#include <QQmlExtensionPlugin>
21+
22+class GlobalShortcutPlugin: public QQmlExtensionPlugin
23+{
24+ Q_OBJECT
25+ Q_PLUGIN_METADATA(IID QQmlExtensionInterface_iid)
26+
27+public:
28+ void registerTypes(const char *uri) override;
29+};
30+
31+
32+#endif // GLOBALSHORTCUTPLUGIN_H
plugins/GlobalShortcut/qmldirnew file+2
@@ -0,0 +1,2 @@
1+module GlobalShortcut
2+plugin GlobalShortcut-qml
plugins/Unity/Session/Session.qmltypes+34
@@ -14,28 +14,62 @@ Module {
1414 isCreatable: false
1515 isSingleton: true
1616 exportMetaObjectRevisions: [0]
17+ Signal {
18+ name: "LogoutRequested"
19+ Parameter { name: "have_inhibitors"; type: "bool" }
20+ }
1721 Signal {
1822 name: "logoutRequested"
1923 Parameter { name: "have_inhibitors"; type: "bool" }
2024 }
25+ Signal {
26+ name: "RebootRequested"
27+ Parameter { name: "have_inhibitors"; type: "bool" }
28+ }
2129 Signal {
2230 name: "rebootRequested"
2331 Parameter { name: "have_inhibitors"; type: "bool" }
2432 }
33+ Signal {
34+ name: "ShutdownRequested"
35+ Parameter { name: "have_inhibitors"; type: "bool" }
36+ }
2537 Signal {
2638 name: "shutdownRequested"
2739 Parameter { name: "have_inhibitors"; type: "bool" }
2840 }
41+ Signal { name: "LogoutReady" }
2942 Signal { name: "logoutReady" }
43+ Signal { name: "LockRequested" }
44+ Signal { name: "lockRequested" }
45+ Signal { name: "Locked" }
46+ Signal { name: "Unlocked" }
3047 Method { name: "Logout" }
3148 Method { name: "Reboot" }
3249 Method { name: "Shutdown" }
50+ Method { name: "Suspend" }
51+ Method { name: "Hibernate" }
52+ Method { name: "HybridSleep" }
3353 Method { name: "RequestLogout" }
3454 Method { name: "RequestReboot" }
3555 Method { name: "RequestShutdown" }
56+ Method { name: "EndSession" }
57+ Method { name: "CanHibernate"; type: "bool" }
58+ Method { name: "CanSuspend"; type: "bool" }
59+ Method { name: "CanHybridSleep"; type: "bool" }
60+ Method { name: "CanReboot"; type: "bool" }
61+ Method { name: "CanShutdown"; type: "bool" }
62+ Method { name: "CanLock"; type: "bool" }
63+ Method { name: "UserName"; type: "string" }
64+ Method { name: "RealName"; type: "string" }
65+ Method { name: "HostName"; type: "string" }
66+ Method { name: "PromptLock" }
67+ Method { name: "Lock" }
68+ Method { name: "IsLocked"; type: "bool" }
3669 Method { name: "logout" }
3770 Method { name: "reboot" }
3871 Method { name: "shutdown" }
72+ Method { name: "endSession" }
3973 }
4074 Component {
4175 name: "OrientationLock"
plugins/Unity/Session/dbusunitysessionservice.cpp+38−2
@@ -28,6 +28,7 @@
2828#include <QDBusReply>
2929#include <QElapsedTimer>
3030#include <QDateTime>
31+#include <QDBusUnixFileDescriptor>
3132
3233#define LOGIN1_SERVICE QStringLiteral("org.freedesktop.login1")
3334#define LOGIN1_PATH QStringLiteral("/org/freedesktop/login1")
@@ -44,6 +45,7 @@ public:
4445 QString logindSessionPath;
4546 bool isSessionActive = true;
4647 QElapsedTimer screensaverActiveTimer;
48+ QDBusUnixFileDescriptor m_systemdInhibitFd;
4749
4850 DBusUnitySessionServicePrivate(): QObject() {
4951 init();
@@ -66,11 +68,40 @@ public:
6668 // start watching the Active property
6769 QDBusConnection::systemBus().connect(LOGIN1_SERVICE, logindSessionPath, "org.freedesktop.DBus.Properties", "PropertiesChanged",
6870 this, SLOT(onPropertiesChanged(QString,QVariantMap,QStringList)));
71+
72+ setupSystemdInhibition();
73+
74+ // re-enable the inhibition upon resume from sleep
75+ QDBusConnection::systemBus().connect(LOGIN1_SERVICE, LOGIN1_PATH, LOGIN1_IFACE, "PrepareForSleep",
76+ this, SLOT(onResuming(bool)));
6977 } else {
7078 qWarning() << "Failed to get logind session path" << reply.error().message();
7179 }
7280 }
7381
82+ void setupSystemdInhibition()
83+ {
84+ if (m_systemdInhibitFd.isValid())
85+ return;
86+
87+ // inhibit systemd handling of power/sleep/lid buttons
88+ // http://www.freedesktop.org/wiki/Software/systemd/inhibit
89+
90+ QDBusMessage msg = QDBusMessage::createMethodCall(LOGIN1_SERVICE, LOGIN1_PATH, LOGIN1_IFACE, "Inhibit");
91+ msg << "handle-power-key:handle-suspend-key:handle-hibernate-key:handle-lid-switch"; // what
92+ msg << "Unity"; // who
93+ msg << "Unity8 handles power events"; // why
94+ msg << "block"; // mode
95+ QDBusReply<QDBusUnixFileDescriptor> desc = QDBusConnection::systemBus().asyncCall(msg);
96+
97+ if (desc.isValid()) {
98+ m_systemdInhibitFd = desc.value();
99+ } else {
100+ qWarning() << "failed to inhibit systemd powersave handling";
101+ }
102+ }
103+
104+
74105 bool checkLogin1Call(const QString &method) const
75106 {
76107 QDBusMessage msg = QDBusMessage::createMethodCall(LOGIN1_SERVICE, LOGIN1_PATH, LOGIN1_IFACE, method);
@@ -105,7 +136,6 @@ public:
105136 QDBusReply<QVariant> reply = QDBusConnection::systemBus().asyncCall(msg);
106137 if (reply.isValid()) {
107138 isSessionActive = reply.value().toBool();
108− qDebug() << "Session" << logindSessionPath << "is active:" << isSessionActive;
109139 } else {
110140 qWarning() << "Failed to get Active property" << reply.error().message();
111141 }
@@ -173,6 +203,13 @@ private Q_SLOTS:
173203 }
174204 }
175205
206+ void onResuming(bool active)
207+ {
208+ if (!active) {
209+ setupSystemdInhibition();
210+ }
211+ }
212+
176213Q_SIGNALS:
177214 void screensaverActiveChanged(bool active);
178215};
@@ -287,7 +324,6 @@ void DBusUnitySessionService::Lock()
287324 sessionPath,
288325 "org.freedesktop.DisplayManager.Session",
289326 "Lock");
290− qDebug() << "Locking session" << msg.path();
291327 QDBusReply<void> reply = QDBusConnection::systemBus().asyncCall(msg);
292328 if (!reply.isValid()) {
293329 qWarning() << "Lock call failed" << reply.error().message();
plugins/Utils/easingcurve.h+2−2
@@ -28,8 +28,8 @@
2828 * This class exposes the QEasingCurve C++ API to QML.
2929 * This is useful for user interactive animations. While the QML Animation types
3030 * all require a "from", "to" and "duration", this one is based on "period" and
31− * "progress". So you can control the position of the aimation by changing the
32− * progress, also going back and forward in the animation. Depending on the type
31+ * "progress". So you can control the position of the animation by changing the
32+ * progress, also going back and forward in the aimation. Depending on the type
3333 * of the easing curve, value will return the transformed progress.
3434 */
3535
po/unity8.pot+66−62
@@ -134,83 +134,83 @@ msgid_plural "Please wait %1 minutes and then try again…"
134134msgstr[0] ""
135135msgstr[1] ""
136136
137−#: qml/Components/Dialogs.qml:69
137+#: qml/Components/Dialogs.qml:112
138138msgctxt "Title: Lock/Log out dialog"
139139msgid "Log out"
140140msgstr ""
141141
142−#: qml/Components/Dialogs.qml:70
142+#: qml/Components/Dialogs.qml:113
143143msgid "Are you sure you want to log out?"
144144msgstr ""
145145
146−#: qml/Components/Dialogs.qml:72
146+#: qml/Components/Dialogs.qml:115
147147msgctxt "Button: Lock the system"
148148msgid "Lock"
149149msgstr ""
150150
151−#: qml/Components/Dialogs.qml:79
151+#: qml/Components/Dialogs.qml:122
152152msgctxt "Button: Log out from the system"
153153msgid "Log Out"
154154msgstr ""
155155
156−#: qml/Components/Dialogs.qml:86 qml/Components/Dialogs.qml:117
157−#: qml/Components/Dialogs.qml:173
156+#: qml/Components/Dialogs.qml:129 qml/Components/Dialogs.qml:162
157+#: qml/Components/Dialogs.qml:221
158158msgid "Cancel"
159159msgstr ""
160160
161−#: qml/Components/Dialogs.qml:98
161+#: qml/Components/Dialogs.qml:141
162162msgctxt "Title: Reboot/Shut down dialog"
163163msgid "Shut down"
164164msgstr ""
165165
166−#: qml/Components/Dialogs.qml:99
166+#: qml/Components/Dialogs.qml:142
167167msgid "Are you sure you want to shut down?"
168168msgstr ""
169169
170−#: qml/Components/Dialogs.qml:101
170+#: qml/Components/Dialogs.qml:144
171171msgctxt "Button: Reboot the system"
172172msgid "Reboot"
173173msgstr ""
174174
175−#: qml/Components/Dialogs.qml:109
175+#: qml/Components/Dialogs.qml:153
176176msgctxt "Button: Shut down the system"
177177msgid "Shut down"
178178msgstr ""
179179
180−#: qml/Components/Dialogs.qml:129
180+#: qml/Components/Dialogs.qml:175
181181msgctxt "Title: Reboot dialog"
182182msgid "Reboot"
183183msgstr ""
184184
185−#: qml/Components/Dialogs.qml:130
185+#: qml/Components/Dialogs.qml:176
186186msgid "Are you sure you want to reboot?"
187187msgstr ""
188188
189−#: qml/Components/Dialogs.qml:132
189+#: qml/Components/Dialogs.qml:178
190190msgid "No"
191191msgstr ""
192192
193−#: qml/Components/Dialogs.qml:138
193+#: qml/Components/Dialogs.qml:185
194194msgid "Yes"
195195msgstr ""
196196
197−#: qml/Components/Dialogs.qml:152
197+#: qml/Components/Dialogs.qml:200
198198msgctxt "Title: Power off/Restart dialog"
199199msgid "Power"
200200msgstr ""
201201
202−#: qml/Components/Dialogs.qml:153
202+#: qml/Components/Dialogs.qml:201
203203msgid ""
204204"Are you sure you would like\n"
205205"to power off?"
206206msgstr ""
207207
208−#: qml/Components/Dialogs.qml:155
208+#: qml/Components/Dialogs.qml:203
209209msgctxt "Button: Power off the system"
210210msgid "Power off"
211211msgstr ""
212212
213−#: qml/Components/Dialogs.qml:164
213+#: qml/Components/Dialogs.qml:212
214214msgctxt "Button: Restart the system"
215215msgid "Restart"
216216msgstr ""
@@ -387,62 +387,62 @@ msgstr ""
387387msgid "Conference"
388388msgstr ""
389389
390−#: qml/Panel/Indicators/MenuItemFactory.qml:639
390+#: qml/Panel/Indicators/MenuItemFactory.qml:674
391391msgid "Nothing is playing"
392392msgstr ""
393393
394−#: qml/Panel/Indicators/MenuItemFactory.qml:768
394+#: qml/Panel/Indicators/MenuItemFactory.qml:803
395395#, qt-format
396396msgid "%1 hour"
397397msgid_plural "%1 hours"
398398msgstr[0] ""
399399msgstr[1] ""
400400
401−#: qml/Panel/Indicators/MenuItemFactory.qml:772
401+#: qml/Panel/Indicators/MenuItemFactory.qml:807
402402#, qt-format
403403msgid "%1 minute"
404404msgid_plural "%1 minutes"
405405msgstr[0] ""
406406msgstr[1] ""
407407
408−#: qml/Panel/Indicators/MenuItemFactory.qml:777
408+#: qml/Panel/Indicators/MenuItemFactory.qml:812
409409#, qt-format
410410msgid "%1 second"
411411msgid_plural "%1 seconds"
412412msgstr[0] ""
413413msgstr[1] ""
414414
415−#: qml/Panel/Indicators/MenuItemFactory.qml:780
415+#: qml/Panel/Indicators/MenuItemFactory.qml:815
416416msgid "0 seconds"
417417msgstr ""
418418
419419#. Translators: String like "1 hour, 2 minutes, 3 seconds remaining"
420−#: qml/Panel/Indicators/MenuItemFactory.qml:782
420+#: qml/Panel/Indicators/MenuItemFactory.qml:817
421421#, qt-format
422422msgid "%1 remaining"
423423msgstr ""
424424
425−#: qml/Panel/Indicators/MenuItemFactory.qml:788
425+#: qml/Panel/Indicators/MenuItemFactory.qml:823
426426msgid "In queue…"
427427msgstr ""
428428
429−#: qml/Panel/Indicators/MenuItemFactory.qml:792
429+#: qml/Panel/Indicators/MenuItemFactory.qml:827
430430msgid "Downloading"
431431msgstr ""
432432
433−#: qml/Panel/Indicators/MenuItemFactory.qml:794
433+#: qml/Panel/Indicators/MenuItemFactory.qml:829
434434msgid "Paused, tap to resume"
435435msgstr ""
436436
437−#: qml/Panel/Indicators/MenuItemFactory.qml:796
437+#: qml/Panel/Indicators/MenuItemFactory.qml:831
438438msgid "Canceled"
439439msgstr ""
440440
441−#: qml/Panel/Indicators/MenuItemFactory.qml:798
441+#: qml/Panel/Indicators/MenuItemFactory.qml:833
442442msgid "Finished"
443443msgstr ""
444444
445−#: qml/Panel/Indicators/MenuItemFactory.qml:800
445+#: qml/Panel/Indicators/MenuItemFactory.qml:835
446446msgid "Failed, tap to retry"
447447msgstr ""
448448
@@ -451,6 +451,10 @@ msgctxt "Label: Hint in message indicator line edit"
451451msgid "Reply"
452452msgstr ""
453453
454+#: qml/Panel/Indicators/MessageMenuItemFactory.qml:204
455+msgid "Call back"
456+msgstr ""
457+
454458#: qml/Panel/Indicators/ModemInfoItem.qml:105
455459msgid "Unlock SIM"
456460msgstr ""
@@ -517,6 +521,38 @@ msgctxt "Button: Go back one page in the Wizard"
517521msgid "Back"
518522msgstr ""
519523
524+#: qml/Wizard/Pages/here-terms.qml:27
525+msgid "Terms & Conditions"
526+msgstr ""
527+
528+#: qml/Wizard/Pages/passwd-confirm.qml:49
529+msgid "Confirm passphrase"
530+msgstr ""
531+
532+#: qml/Wizard/Pages/passwd-confirm.qml:50
533+msgid "Confirm passcode"
534+msgstr ""
535+
536+#: qml/Wizard/Pages/passwd-confirm.qml:53
537+#: qml/Wizard/Pages/passwd-confirm.qml:54
538+msgid "Please try again."
539+msgstr ""
540+
541+#: qml/Wizard/Pages/passwd-confirm.qml:82 qml/Wizard/Pages/passwd-set.qml:90
542+#: qml/Wizard/Pages/10-welcome.qml:88 qml/Wizard/Pages/30-passwd-type.qml:128
543+#: qml/Wizard/Pages/40-wifi.qml:215 qml/Wizard/Pages/50-location.qml:131
544+#: qml/Wizard/Pages/60-reporting.qml:50
545+msgid "Continue"
546+msgstr ""
547+
548+#: qml/Wizard/Pages/passwd-set.qml:61
549+msgid "Choose your passcode"
550+msgstr ""
551+
552+#: qml/Wizard/Pages/passwd-set.qml:68
553+msgid "Passphrase must be 4 characters long"
554+msgstr ""
555+
520556#: qml/Wizard/Pages/10-welcome.qml:27
521557msgid "Hi!"
522558msgstr ""
@@ -529,13 +565,6 @@ msgstr ""
529565msgid "Let’s get started."
530566msgstr ""
531567
532−#: qml/Wizard/Pages/10-welcome.qml:88 qml/Wizard/Pages/30-passwd-type.qml:128
533−#: qml/Wizard/Pages/40-wifi.qml:215 qml/Wizard/Pages/50-location.qml:131
534−#: qml/Wizard/Pages/60-reporting.qml:50 qml/Wizard/Pages/passwd-confirm.qml:82
535−#: qml/Wizard/Pages/passwd-set.qml:90
536−msgid "Continue"
537−msgstr ""
538−
539568#: qml/Wizard/Pages/20-sim.qml:25
540569msgid "Add a SIM card and restart your device"
541570msgstr ""
@@ -661,31 +690,6 @@ msgstr ""
661690msgid "Finish"
662691msgstr ""
663692
664−#: qml/Wizard/Pages/here-terms.qml:27
665−msgid "Terms & Conditions"
666−msgstr ""
667−
668−#: qml/Wizard/Pages/passwd-confirm.qml:49
669−msgid "Confirm passphrase"
670−msgstr ""
671−
672−#: qml/Wizard/Pages/passwd-confirm.qml:50
673−msgid "Confirm passcode"
674−msgstr ""
675−
676−#: qml/Wizard/Pages/passwd-confirm.qml:53
677−#: qml/Wizard/Pages/passwd-confirm.qml:54
678−msgid "Please try again."
679−msgstr ""
680−
681−#: qml/Wizard/Pages/passwd-set.qml:61
682−msgid "Choose your passcode"
683−msgstr ""
684−
685−#: qml/Wizard/Pages/passwd-set.qml:68
686−msgid "Passphrase must be 4 characters long"
687−msgstr ""
688−
689693#. Translators: This is the arrow for "Back" buttons
690694#: qml/Wizard/StackButton.qml:39
691695#, qt-format
qml/Components/Dialogs.qml+62−6
@@ -19,6 +19,7 @@ import QtQuick 2.0
1919import Unity.Application 0.1
2020import Unity.Session 0.1
2121import Ubuntu.Components 1.1
22+import GlobalShortcut 1.0
2223import "../Greeter"
2324
2425Item {
@@ -35,6 +36,7 @@ Item {
3536 ApplicationManager.stopApplication(app.appId);
3637 }
3738 }
39+ property string usageScenario
3840
3941 signal powerOffClicked();
4042
@@ -42,19 +44,61 @@ Item {
4244 d.showPowerDialog();
4345 }
4446
47+ GlobalShortcut { // reboot/shutdown dialog
48+ shortcut: Qt.Key_PowerDown
49+ active: root.usageScenario === "desktop"
50+ onTriggered: root.unitySessionService.RequestShutdown()
51+ }
52+
53+ GlobalShortcut { // reboot/shutdown dialog
54+ shortcut: Qt.Key_PowerOff
55+ active: root.usageScenario === "desktop"
56+ onTriggered: root.unitySessionService.RequestShutdown()
57+ }
58+
59+ GlobalShortcut { // sleep
60+ shortcut: Qt.Key_Sleep
61+ onTriggered: root.unitySessionService.Suspend()
62+ }
63+
64+ GlobalShortcut { // hibernate
65+ shortcut: Qt.Key_Hibernate
66+ onTriggered: root.unitySessionService.Hibernate()
67+ }
68+
69+ GlobalShortcut { // logout/lock dialog
70+ shortcut: Qt.Key_LogOff
71+ onTriggered: root.unitySessionService.RequestLogout()
72+ }
73+
74+ GlobalShortcut { // logout/lock dialog
75+ shortcut: Qt.ControlModifier|Qt.AltModifier|Qt.Key_Delete
76+ onTriggered: root.unitySessionService.RequestLogout()
77+ }
78+
79+ GlobalShortcut { // lock screen
80+ shortcut: Qt.Key_ScreenSaver
81+ onTriggered: lightDM.greeter.showGreeter()
82+ }
83+
84+ GlobalShortcut { // lock screen
85+ shortcut: Qt.ControlModifier|Qt.AltModifier|Qt.Key_L
86+ onTriggered: lightDM.greeter.showGreeter()
87+ }
88+
4589 QtObject {
4690 id: d // private stuff
4791 objectName: "dialogsPrivate"
4892
49−
5093 function showPowerDialog() {
5194 if (!dialogLoader.active) {
5295 dialogLoader.sourceComponent = powerDialogComponent;
96+ dialogLoader.focus = true;
5397 dialogLoader.active = true;
54− dialogLoader.item.forceActiveFocus();
5598 }
5699 }
57100 }
101+
58102 Loader {
59103 id: dialogLoader
60104 objectName: "dialogLoader"
@@ -62,7 +106,8 @@ Item {
62106 active: false
63107 }
64108
65− LightDM{id: lightDM} // Provide backend access
109+ LightDM {id: lightDM} // Provide backend access
110+
66111 Component {
67112 id: logoutDialogComponent
68113 ShellDialog {
@@ -105,6 +150,7 @@ Item {
105150 unitySessionService.reboot();
106151 shutdownDialog.hide();
107152 }
153+ color: UbuntuColors.lightGrey
108154 }
109155 Button {
110156 text: i18n.ctr("Button: Shut down the system", "Shut down")
@@ -113,12 +159,14 @@ Item {
113159 unitySessionService.shutdown();
114160 shutdownDialog.hide();
115161 }
162+ color: UbuntuColors.red
116163 }
117164 Button {
118165 text: i18n.tr("Cancel")
119166 onClicked: {
120167 shutdownDialog.hide();
121168 }
169+ color: UbuntuColors.lightGrey
122170 }
123171 }
124172 }
@@ -134,6 +182,7 @@ Item {
134182 onClicked: {
135183 rebootDialog.hide();
136184 }
185+ color: UbuntuColors.lightGrey
137186 }
138187 Button {
139188 text: i18n.tr("Yes")
@@ -142,6 +191,7 @@ Item {
142191 unitySessionService.reboot();
143192 rebootDialog.hide();
144193 }
194+ color: UbuntuColors.red
145195 }
146196 }
147197 }
@@ -168,14 +218,14 @@ Item {
168218 unitySessionService.reboot();
169219 powerDialog.hide();
170220 }
171− color: UbuntuColors.green
221+ color: UbuntuColors.lightGrey
172222 }
173223 Button {
174224 text: i18n.tr("Cancel")
175225 onClicked: {
176226 powerDialog.hide();
177227 }
178− color: UbuntuColors.coolGrey
228+ color: UbuntuColors.lightGrey
179229 }
180230 }
181231 }
@@ -187,6 +237,7 @@ Item {
187237 // Display a dialog to ask the user to confirm.
188238 if (!dialogLoader.active) {
189239 dialogLoader.sourceComponent = logoutDialogComponent;
240+ dialogLoader.focus = true;
190241 dialogLoader.active = true;
191242 }
192243 }
@@ -195,6 +246,7 @@ Item {
195246 // Display a dialog to ask the user to confirm.
196247 if (!dialogLoader.active) {
197248 dialogLoader.sourceComponent = shutdownDialogComponent;
249+ dialogLoader.focus = true;
198250 dialogLoader.active = true;
199251 }
200252 }
@@ -202,7 +254,11 @@ Item {
202254 onRebootRequested: {
203255 // Display a dialog to ask the user to confirm.
204256 if (!dialogLoader.active) {
205− dialogLoader.sourceComponent = rebootDialogComponent;
257+ // display a combined reboot/shutdown dialog, sadly the session indicator calls rather the "Reboot()" method
258+ // than shutdown when clicking on the "Shutdown..." menu item
259+ // FIXME: when/if session indicator is fixed, put the rebootDialogComponent here
260+ dialogLoader.sourceComponent = shutdownDialogComponent;
261+ dialogLoader.focus = true;
206262 dialogLoader.active = true;
207263 }
208264 }
qml/Components/PhysicalKeysMapper.qml+7−8
@@ -30,7 +30,6 @@ import Powerd 0.1
3030 * Power dialog
3131 * Volume Decreases/Increases
3232 * Screenshots
33−
3433*/
3534
3635Item {
@@ -80,15 +79,15 @@ Item {
8079 if (Powerd.status === Powerd.On) {
8180 powerKeyLongPressTimer.restart();
8281 }
83− event.accepted = true;
84− } else if ((event.key == Qt.Key_MediaTogglePlayPause || event.key == Qt.Key_MediaPlay)
85− && !event.isAutoRepeat) {
82+ } else if ((event.key == Qt.Key_MediaTogglePlayPause || event.key == Qt.Key_MediaPlay) && !event.isAutoRepeat) {
8683 event.accepted = callManager.handleMediaKey(false);
8784 } else if (event.key == Qt.Key_VolumeDown) {
8885 if (event.isAutoRepeat && !d.ignoreVolumeEvents) root.volumeDownTriggered();
8986 else if (!event.isAutoRepeat) {
9087 if (d.volumeUpKeyPressed) {
91− if (Powerd.status === Powerd.On) root.screenshotTriggered();
88+ if (Powerd.status === Powerd.On) {
89+ root.screenshotTriggered();
90+ }
9291 d.ignoreVolumeEvents = true;
9392 }
9493 d.volumeDownKeyPressed = true;
@@ -97,7 +96,9 @@ Item {
9796 if (event.isAutoRepeat && !d.ignoreVolumeEvents) root.volumeUpTriggered();
9897 else if (!event.isAutoRepeat) {
9998 if (d.volumeDownKeyPressed) {
100− if (Powerd.status === Powerd.On) root.screenshotTriggered();
99+ if (Powerd.status === Powerd.On) {
100+ root.screenshotTriggered();
101+ }
101102 d.ignoreVolumeEvents = true;
102103 }
103104 d.volumeUpKeyPressed = true;
@@ -109,8 +110,6 @@ Item {
109110 d.altTabPressed = true;
110111 event.accepted = true;
111112 }
112− } else if (event.key == Qt.Key_Print) {
113− root.screenshotTriggered();
114113 }
115114 }
116115
qml/Components/ScreenGrabber.qml+7
@@ -17,6 +17,7 @@
1717import QtQuick 2.0
1818import QtMultimedia 5.0
1919import ScreenGrabber 0.1
20+import GlobalShortcut 1.0
2021
2122Rectangle {
2223 id: root
@@ -30,6 +31,12 @@ Rectangle {
3031 objectName: "screenGrabber"
3132 }
3233
34+ GlobalShortcut {
35+ id: screenshotShortcut
36+ shortcut: Qt.Key_Print
37+ onTriggered: capture()
38+ }
39+
3340 Audio {
3441 id: shutterSound
3542 audioRole: MediaPlayer.alert
qml/Components/ShellDialog.qml+7−1
@@ -33,5 +33,11 @@ Dialog {
3333
3434 onVisibleChanged: { if (!visible) { dialogLoader.active = false; } }
3535
36− Component.onCompleted: show()
36+ Keys.onEscapePressed: hide()
37+
38+ focus: true
39+
40+ Component.onCompleted: {
41+ show();
42+ }
3743}
qml/Components/VolumeControl.qml+20−6
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2013 Canonical, Ltd.
2+ * Copyright (C) 2013-2015 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
@@ -14,8 +14,9 @@
1414 * along with this program. If not, see <http://www.gnu.org/licenses/>.
1515 */
1616
17−import QtQuick 2.0
18−import QMenuModel 0.1
17+import QtQuick 2.4
18+import QMenuModel 0.1 as QMenuModel
19+import GlobalShortcut 1.0
1920
2021Item {
2122 id: root
@@ -32,13 +33,20 @@ Item {
3233 actionGroup.indicatorsAction.updateState(root.showNotification);
3334 }
3435
35− QDBusActionGroup {
36+ GlobalShortcut {
37+ id: muteShortcut
38+ shortcut: Qt.Key_VolumeMute
39+ Keys.onPressed: toggleMute()
40+ }
41+
42+ QMenuModel.QDBusActionGroup {
3643 id: actionGroup
37− busType: 1
44+ busType: QMenuModel.DBus.SessionBus
3845 busName: "com.canonical.indicator.sound"
3946 objectPath: "/com/canonical/indicator/sound"
4047
4148 property variant actionObject: action("volume")
49+ property variant muteActionObject: action("mute")
4250 property variant indicatorsAction: action("indicator-shown")
4351 }
4452
@@ -50,5 +58,11 @@ Item {
5058 actionGroup.actionObject.activate(stepDown);
5159 }
5260
53− Component.onCompleted: actionGroup.start()
61+ function toggleMute() {
62+ actionGroup.muteActionObject.activate();
63+ }
64+
65+ Component.onCompleted: {
66+ actionGroup.start();
67+ }
5468}
qml/OrientedShell.qml+1−5
@@ -103,7 +103,7 @@ Rectangle {
103103 // TODO: Choose the closest to the current one
104104 if (supportedOrientations & Qt.PortraitOrientation) {
105105 return Screen.angleBetween(nativeOrientation, Qt.PortraitOrientation);
106− } else if (supportedOrientations & Qt.LandcscapeOrientation) {
106+ } else if (supportedOrientations & Qt.LandscapeOrientation) {
107107 return Screen.angleBetween(nativeOrientation, Qt.LandscapeOrientation);
108108 } else if (supportedOrientations & Qt.InvertedPortraitOrientation) {
109109 return Screen.angleBetween(nativeOrientation, Qt.InvertedPortraitOrientation);
@@ -120,19 +120,15 @@ Rectangle {
120120 switch (angle) {
121121 case 0:
122122 return nativeOrientation;
123− break;
124123 case 90:
125124 return nativeOrientation === Qt.PortraitOrientation ? Qt.InvertedLandscapeOrientation
126125 : Qt.PortraitOrientation;
127− break;
128126 case 180:
129127 return nativeOrientation === Qt.PortraitOrientation ? Qt.InvertedPortraitOrientation
130128 : Qt.InvertedLandscapeOrientation;
131− break;
132129 case 270:
133130 return nativeOrientation === Qt.PortraitOrientation ? Qt.LandscapeOrientation
134131 : Qt.InvertedPortraitOrientation;
135− break;
136132 default:
137133 console.warn("angleToOrientation: Invalid orientation angle: " + angle);
138134 return primaryOrientation;
qml/Panel/Indicators/MessageMenuItemFactory.qml+2−2
@@ -201,8 +201,8 @@ Loader {
201201 title: menuData && menuData.label || ""
202202 time: timeFormatter.timeString
203203 body: getExtendedProperty(extendedData, "xCanonicalText", "")
204− actionButtonText: getExtendedProperty(activateActionDescription, "label", "Call back")
205− replyButtonText: getExtendedProperty(replyActionDescription, "label", "Send")
204+ actionButtonText: getExtendedProperty(activateActionDescription, "label", i18n.tr("Call back"))
205+ replyButtonText: getExtendedProperty(replyActionDescription, "label", i18n.tr("Send"))
206206 // icons
207207 avatar: getExtendedProperty(extendedData, "icon", "image://theme/contact")
208208 icon: getExtendedProperty(extendedData, "xCanonicalAppIcon", "image://theme/missed-call")
qml/Shell.qml+11−4
@@ -25,6 +25,7 @@ import Ubuntu.Gestures 0.1
2525import Ubuntu.Telephony 0.1 as Telephony
2626import Unity.Connectivity 0.1
2727import Unity.Launcher 0.1
28+import GlobalShortcut 1.0 // has to be before Utils, because of WindowKeysFilter
2829import Utils 0.1
2930import Powerd 0.1
3031import SessionBroadcast 0.1
@@ -41,6 +42,7 @@ import Unity.Session 0.1
4142import Unity.DashCommunicator 0.1
4243import Unity.Indicators 0.1 as Indicators
4344
45+
4446Item {
4547 id: shell
4648
@@ -145,7 +147,7 @@ Item {
145147 // This is a dummy image to detect if the custom GSettings set wallpaper loads successfully.
146148 Image {
147149 id: gsImageTester
148− source: backgroundSettings.pictureUri != undefined && backgroundSettings.pictureUri.length > 0 ? backgroundSettings.pictureUri : ""
150+ source: backgroundSettings.pictureUri && backgroundSettings.pictureUri.length > 0 ? backgroundSettings.pictureUri : ""
149151 height: 0
150152 width: 0
151153 sourceSize.height: 0
@@ -181,7 +183,7 @@ Item {
181183 id: physicalKeysMapper
182184 objectName: "physicalKeysMapper"
183185
184− onPowerKeyLongPressed: dialogs.showPowerDialog()
186+ onPowerKeyLongPressed: dialogs.showPowerDialog();
185187 onVolumeDownTriggered: volumeControl.volumeDown();
186188 onVolumeUpTriggered: volumeControl.volumeUp();
187189 onScreenshotTriggered: screenGrabber.capture();
@@ -192,6 +194,10 @@ Item {
192194 z: dialogs.z + 10
193195 }
194196
197+ GlobalShortcut {
198+ // dummy shortcut to force creation of GlobalShortcutRegistry before WindowKeyFilter
199+ }
200+
195201 WindowKeysFilter {
196202 Keys.onPressed: physicalKeysMapper.onKeyPressed(event);
197203 Keys.onReleased: physicalKeysMapper.onKeyReleased(event);
@@ -413,7 +419,7 @@ Item {
413419 sourceComponent: shell.mode != "shell" ? integratedGreeter :
414420 Qt.createComponent(Qt.resolvedUrl("Greeter/ShimGreeter.qml"));
415421 onLoaded: {
416− item.objectName = "greeter"
422+ item.objectName = "greeter"
417423 }
418424 }
419425
@@ -663,6 +669,7 @@ Item {
663669 objectName: "dialogs"
664670 anchors.fill: parent
665671 z: overlay.z + 10
672+ usageScenario: shell.usageScenario
666673 onPowerOffClicked: {
667674 shutdownFadeOutRectangle.enabled = true;
668675 shutdownFadeOutRectangle.visible = true;
@@ -689,7 +696,7 @@ Item {
689696 to: 1.0
690697 onStopped: {
691698 if (shutdownFadeOutRectangle.enabled && shutdownFadeOutRectangle.visible) {
692− DBusUnitySessionService.Shutdown();
699+ DBusUnitySessionService.shutdown();
693700 }
694701 }
695702 }
tests/mocks/Utils/windowstatestorage.cpp−2
@@ -16,8 +16,6 @@
1616
1717#include "windowstatestorage.h"
1818
19−#include <QRect>
20−
2119WindowStateStorage::WindowStateStorage(QObject *parent):
2220 QObject(parent)
2321{
tests/mocks/Utils/windowstatestorage.h+1
@@ -16,6 +16,7 @@
1616
1717#include <QObject>
1818#include <QVariantMap>
19+#include <QRect>
1920
2021class WindowStateStorage: public QObject
2122{
tests/plugins/CMakeLists.txt+1
@@ -1,4 +1,5 @@
11add_subdirectory(AccountsService)
2+add_subdirectory(GlobalShortcut)
23add_subdirectory(Greeter)
34add_subdirectory(IntegratedLightDM)
45add_subdirectory(Dash)
tests/plugins/GlobalShortcut/CMakeLists.txtnew file+14
@@ -0,0 +1,14 @@
1+include_directories(
2+ ${CMAKE_SOURCE_DIR}/plugins/GlobalShortcut
3+ ${CMAKE_CURRENT_BINARY_DIR}
4+ )
5+
6+add_definitions(
7+ -DCURRENT_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}"
8+ )
9+add_definitions(-DBUILT_PLUGINS_DIR="${CMAKE_BINARY_DIR}/plugins")
10+
11+add_executable(GlobalShortcutTestExec GlobalShortcutTest.cpp ${CMAKE_SOURCE_DIR}/plugins/GlobalShortcut/globalshortcut.cpp)
12+qt5_use_modules(GlobalShortcutTestExec Test Quick)
13+target_link_libraries(GlobalShortcutTestExec ${Qt5Quick_LIBRARIES} GlobalShortcut-qml)
14+add_unity8_uitest(GlobalShortcut GlobalShortcutTestExec DEPENDS GlobalShortcut-qml)
tests/plugins/GlobalShortcut/GlobalShortcutTest.cppnew file+77
@@ -0,0 +1,77 @@
1+/*
2+ * Copyright (C) 2015 Canonical, Ltd.
3+ *
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
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 General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "globalshortcut.h"
18+
19+#include <QtTest>
20+#include <QtQuick/QQuickView>
21+#include <QtQuick/QQuickItem>
22+#include <QQmlEngine>
23+
24+class GlobalShortcutTest: public QObject
25+{
26+ Q_OBJECT
27+
28+private Q_SLOTS:
29+
30+ void initTestCase()
31+ {
32+ m_view = new QQuickView();
33+ m_view->engine()->addImportPath(BUILT_PLUGINS_DIR);
34+ m_view->setSource(QUrl::fromLocalFile(CURRENT_SOURCE_DIR "/shortcut.qml"));
35+ m_shortcut = dynamic_cast<GlobalShortcut*>(m_view->rootObject()->property("shortcut").value<QObject*>());
36+ QVERIFY(m_shortcut);
37+ m_inactiveShortcut = dynamic_cast<GlobalShortcut*>(m_view->rootObject()->property("inactiveShortcut").value<QObject*>());
38+ QVERIFY(m_inactiveShortcut);
39+ m_view->show();
40+ QTest::qWaitForWindowExposed(m_view);
41+ }
42+
43+ void cleanupTestCase()
44+ {
45+ m_view.clear();
46+ }
47+
48+ void testGlobalShortcut()
49+ {
50+ QSignalSpy shortcutSpy(m_shortcut, &GlobalShortcut::triggered);
51+ // test pressing "Mute Volume"
52+ QTest::keyClick(m_view, Qt::Key_VolumeMute);
53+ QTRY_COMPARE(shortcutSpy.count(), 1);
54+ const QVariantList args = shortcutSpy.takeFirst();
55+ // verify we got the signal back and a non-empty shortcut
56+ QCOMPARE(args.count(), 1);
57+ QVERIFY(!args.first().toString().isEmpty());
58+ }
59+
60+ void testInactiveGlobalShortcut()
61+ {
62+ QSignalSpy shortcutSpy(m_inactiveShortcut, &GlobalShortcut::triggered);
63+ // test pressing Ctrl+Alt+L
64+ QTest::keyClick(m_view, Qt::Key_L, Qt::ControlModifier|Qt::AltModifier);
65+ // verify we didn't get any signal back, shortcut is not active
66+ QTRY_COMPARE(shortcutSpy.count(), 0);
67+ }
68+
69+private:
70+ QPointer<QQuickView> m_view;
71+ GlobalShortcut *m_shortcut = nullptr;
72+ GlobalShortcut *m_inactiveShortcut = nullptr;
73+};
74+
75+QTEST_MAIN(GlobalShortcutTest)
76+
77+#include "GlobalShortcutTest.moc"
tests/plugins/GlobalShortcut/shortcut.qmlnew file+39
@@ -0,0 +1,39 @@
1+/*
2+ * Copyright 2015 Canonical Ltd.
3+ *
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
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 General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import GlobalShortcut 1.0
19+
20+Rectangle {
21+ property var shortcut: globalShortcut
22+ property var inactiveShortcut: inactiveGlobalShortcut
23+
24+ width: 100
25+ height: 100
26+
27+ GlobalShortcut {
28+ id: globalShortcut
29+ objectName: "globalShortcut"
30+ shortcut: Qt.Key_VolumeMute
31+ }
32+
33+ GlobalShortcut {
34+ id: inactiveGlobalShortcut
35+ objectName: "inactiveGlobalShortcut"
36+ shortcut: Qt.ControlModifier|Qt.AltModifier|Qt.Key_L
37+ active: false
38+ }
39+}
tests/qmltests/Components/tst_Dialogs.qml+13−8
@@ -32,6 +32,7 @@ Rectangle {
3232 signal logoutRequested
3333 signal shutdownRequested
3434 signal rebootRequested
35+ signal logoutReady
3536 function logout() {}
3637 function shutdown() {}
3738 function reboot() {}
@@ -79,15 +80,19 @@ Rectangle {
7980 Column {
8081 anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
8182 spacing: units.gu(1)
82− Button { text: "Power dialog"; onClicked: { testCase.test_showPowerDialog(); } }
83− Button { text: "Logout Requested"; onClicked: { fakeUnitySession.logoutRequested(); } }
84− Button { text: "Shutdown Requested"; onClicked: { fakeUnitySession.shutdownRequested(); } }
85− Button { text: "Reboot Requested"; onClicked: { fakeUnitySession.rebootRequested(); } }
83+ Button { text: "Power dialog"; onClicked: { testCase.test_showPowerDialog(); }
84+ activeFocusOnPress: false }
85+ Button { text: "Logout Requested"; onClicked: { fakeUnitySession.logoutRequested(); }
86+ activeFocusOnPress: false }
87+ Button { text: "Shutdown Requested"; onClicked: { fakeUnitySession.shutdownRequested(); }
88+ activeFocusOnPress: false }
89+ Button { text: "Reboot Requested"; onClicked: { fakeUnitySession.rebootRequested(); }
90+ activeFocusOnPress: false }
8691 Label { text: "Rotation:" }
87− Button { text: "0"; onClicked: {fakeShell.rotation = 0;} }
88− Button { text: "90"; onClicked: {fakeShell.rotation = 90;} }
89− Button { text: "180"; onClicked: {fakeShell.rotation = 180;} }
90− Button { text: "270"; onClicked: {fakeShell.rotation = 270;} }
92+ Button { text: "0"; onClicked: {fakeShell.rotation = 0;} activeFocusOnPress: false }
93+ Button { text: "90"; onClicked: {fakeShell.rotation = 90;} activeFocusOnPress: false }
94+ Button { text: "180"; onClicked: {fakeShell.rotation = 180;} activeFocusOnPress: false }
95+ Button { text: "270"; onClicked: {fakeShell.rotation = 270;} activeFocusOnPress: false }
9196 }
9297 }
9398
tests/qmltests/Components/tst_PhysicalKeysMapper.qml+1−44
@@ -38,18 +38,6 @@ TestCase {
3838 signalName: "powerKeyLongPressed"
3939 }
4040
41− SignalSpy {
42− id: volumeDownSpy
43− target: physicalKeysMapper
44− signalName: "volumeDownTriggered"
45− }
46−
47− SignalSpy {
48− id: volumeUpSpy
49− target: physicalKeysMapper
50− signalName: "volumeUpTriggered"
51− }
52−
5341 SignalSpy {
5442 id: screenshotSpy
5543 target: physicalKeysMapper
@@ -65,8 +53,6 @@ TestCase {
6553 function cleanup() {
6654 loader.active = false;
6755 powerSpy.clear();
68− volumeDownSpy.clear();
69− volumeUpSpy.clear();
7056 screenshotSpy.clear();
7157 }
7258
@@ -103,11 +89,6 @@ TestCase {
10389 compare(powerSpy.count, 1);
10490 }
10591
106− function test_screenshotPrtScr() {
107− physicalKeysMapper.onKeyPressed({ key: Qt.Key_Print })
108− tryCompare(screenshotSpy, "count", 1) // verify the screenshotTriggered signal has been called, exactly once
109− }
110−
11192 function test_screenshotButtons_data() {
11293 return [
11394 { tag: "UpFirst", first: Qt.Key_VolumeUp, second: Qt.Key_VolumeDown },
@@ -121,30 +102,6 @@ TestCase {
121102 screenshotSpy.wait();
122103 physicalKeysMapper.onKeyReleased({ key: data.first });
123104 physicalKeysMapper.onKeyReleased({ key: data.second });
124−
125− wait(100);
126− compare(volumeUpSpy.count, 0);
127− compare(volumeDownSpy.count, 0);
128− }
129−
130− function test_volumeButton_data() {
131− return [
132− { tag: "Down", key: Qt.Key_VolumeDown, spy: volumeDownSpy },
133− { tag: "Up", key: Qt.Key_VolumeUp, spy: volumeUpSpy },
134− ];
135− }
136−
137− function test_volumeButton(data) {
138− physicalKeysMapper.onKeyPressed({ key: data.key });
139−
140− wait(100);
141− compare(data.spy.count, 0);
142−
143− physicalKeysMapper.onKeyReleased({ key: data.key });
144− data.spy.wait();
145−
146− physicalKeysMapper.onKeyPressed({ key: data.key });
147− physicalKeysMapper.onKeyPressed({ key: data.key, isAutoRepeat: true });
148− data.spy.wait();
105+ compare(screenshotSpy.count, 1);
149106 }
150107}