on xenial help sign in
merge silo 2272
commite8b0d117b2af2b12f07e6b9a2d82d8d5f3801ebb authorLukáš Tinkl <lukas.tinkl@canonical.com> dateTue 24 Jan 2017, 13:57 (10 years ago) parent529061ebd29f3d
135 files changed+3753−2114
CMakeLists.txt+1−1 cmake/modules/QmlTest.cmake+8−3 data/com.canonical.Unity8.gschema.xml+5 debian/changelog+51 debian/control+1−1 debian/rules+1−1 plugins/LightDM/CMakeLists.txt+1−1 plugins/LightDM/DBusGreeterList.cpp+6−6 plugins/LightDM/DBusGreeterList.h+2−2 plugins/LightDM/Greeter.cpp+137−23 plugins/LightDM/Greeter.h+17−13 plugins/LightDM/GreeterPrivate.h+13−8 plugins/LightDM/IntegratedLightDM/liblightdm/Greeter.cpp+7−4 plugins/LightDM/IntegratedLightDM/liblightdm/GreeterPrivate.cpp+27−22 plugins/LightDM/IntegratedLightDM/liblightdm/GreeterPrivate.h+1 plugins/LightDM/PromptsModel.cpp+97 plugins/LightDM/PromptsModel.h+70 plugins/LightDM/SessionsModel.cpp+1−2 plugins/LightDM/SessionsModel.h+4−5 plugins/LightDM/UsersModel.cpp+171−9 plugins/LightDM/UsersModel.h+6−8 plugins/LightDM/plugin.cpp+12−4 plugins/Unity/Launcher/launchermodel.cpp−1 plugins/Utils/CMakeLists.txt+1 plugins/Utils/appdrawerproxymodel.cpp+1−1 plugins/Utils/plugin.cpp+3−1 plugins/Utils/tabfocusfence.cpp+72 plugins/Utils/tabfocusfence.h+38 po/unity8.pot+67−39 qml/ApplicationMenus/MenuBar.qml+14 qml/ApplicationMenus/MenuPopup.qml+162−123 qml/Components/Dialogs.qml+42−3 qml/Components/KeyboardShortcutsOverlay.qml+1−1 qml/Components/KeymapSwitcher.qml+15−7 qml/Components/Lockscreen.qml+3−1 qml/Components/ModeSwitchWarningDialog.qml+8−2 qml/Components/ShellDialog.qml+21−3 qml/Greeter/Circle.qml+1−1 qml/Greeter/FullLightDMImpl.qml+1 qml/Greeter/Greeter.qml+27−57 qml/Greeter/GreeterPrompt.qml+32−31 qml/Greeter/IntegratedLightDMImpl.qml+1 qml/Greeter/LightDMService.qml+4−4 qml/Greeter/LoginList.qml+42−111 qml/Greeter/NarrowView.qml+11−21 qml/Greeter/PromptList.qml+148 qml/Greeter/WideView.qml+13−22 qml/Launcher/Drawer.qml+86−4 qml/Launcher/DrawerGridView.qml+3−1 qml/Launcher/DrawerListView.qml+6 qml/Launcher/Launcher.qml+11−8 qml/Launcher/LauncherDelegate.qml+8−9 qml/Launcher/LauncherPanel.qml+11−6 qml/Launcher/MoreAppsHeader.qml+12−5 qml/Launcher/graphics/launcher-app-focus-ring.svg−12 qml/OrientedShell.qml+2−1 qml/Panel/PanelBar.qml+1 qml/Panel/PanelMenu.qml+14 qml/Shell.qml+49−5 qml/Stage/DecoratedWindow.qml+36−43 qml/Stage/WindowDecoration.qml+6−2 src/MouseTouchAdaptor.cpp+28−1 src/MouseTouchAdaptor.h+1 tests/CMakeLists.txt+5−3 tests/autopilot/unity8/fixture_setup.py+1−1 tests/autopilot/unity8/shell/tests/__init__.py+1−1 tests/mocks/AccountsService/AccountsService.cpp+2−3 tests/mocks/AccountsService/AccountsService.h+3−6 tests/mocks/AccountsService/CMakeLists.txt+3−2 tests/mocks/CMakeLists.txt+2−1 tests/mocks/GSettings.1.0/fake_gsettings.cpp+14 tests/mocks/GSettings.1.0/fake_gsettings.h+7 tests/mocks/LightDM/CMakeLists.txt−1 tests/mocks/LightDM/IntegratedLightDM/CMakeLists.txt−50 tests/mocks/LightDM/IntegratedLightDM/MockGreeter.cpp−51 tests/mocks/LightDM/IntegratedLightDM/MockGreeter.h−42 tests/mocks/LightDM/IntegratedLightDM/MockSessionsModel.cpp−66 tests/mocks/LightDM/IntegratedLightDM/MockSessionsModel.h−44 tests/mocks/LightDM/IntegratedLightDM/MockUsersModel.cpp−44 tests/mocks/LightDM/IntegratedLightDM/QLightDM/Greeter−17 tests/mocks/LightDM/IntegratedLightDM/QLightDM/SessionsModel−17 tests/mocks/LightDM/IntegratedLightDM/QLightDM/UsersModel−17 tests/mocks/LightDM/IntegratedLightDM/liblightdm/CMakeLists.txt−21 tests/mocks/LightDM/IntegratedLightDM/liblightdm/Greeter.cpp−201 tests/mocks/LightDM/IntegratedLightDM/liblightdm/UsersModel.cpp−121 tests/mocks/LightDM/IntegratedLightDM/plugin.cpp−79 tests/mocks/LightDM/IntegratedLightDM/qmldir−2 tests/mocks/LightDMController/CMakeLists.txt+15 tests/mocks/LightDM/IntegratedLightDM/MockUsersModel.h → tests/mocks/LightDMController/plugin.cpp+16−20 tests/mocks/LightDM/IntegratedLightDM/plugin.h → tests/mocks/LightDMController/plugin.h+5−8 tests/mocks/LightDMController/qmldir+2 tests/mocks/Unity/Application/MirMock.cpp+13 tests/mocks/Unity/Application/MirMock.h+4 tests/mocks/Unity/Launcher/MockAppDrawerModel.cpp−1 tests/mocks/Utils/CMakeLists.txt+1 tests/mocks/Utils/plugin.cpp+3−1 tests/mocks/liblightdm/CMakeLists.txt+18 tests/mocks/LightDM/IntegratedLightDM/liblightdm/GreeterPrivate.cpp → tests/mocks/liblightdm/GreeterPrivate.cpp tests/mocks/LightDM/IntegratedLightDM/liblightdm/GreeterPrivate.h → tests/mocks/liblightdm/GreeterPrivate.h tests/mocks/liblightdm/MockController.cpp+191 tests/mocks/liblightdm/MockController.h+106 tests/mocks/liblightdm/MockGreeter.cpp+316 tests/mocks/LightDM/IntegratedLightDM/liblightdm/Greeter.h → tests/mocks/liblightdm/MockGreeter.h+8−21 tests/mocks/LightDM/IntegratedLightDM/liblightdm/SessionsModel.cpp → tests/mocks/liblightdm/MockSessionsModel.cpp+40−42 tests/mocks/LightDM/IntegratedLightDM/liblightdm/SessionsModel.h → tests/mocks/liblightdm/MockSessionsModel.h+9−17 tests/mocks/liblightdm/MockUsersModel.cpp+219 tests/mocks/LightDM/IntegratedLightDM/liblightdm/UsersModel.h → tests/mocks/liblightdm/MockUsersModel.h+8−16 tests/mocks/LightDM/IntegratedLightDM/liblightdm/SessionsModelPrivate.cpp → tests/mocks/liblightdm/SessionsModelPrivate.cpp tests/mocks/LightDM/IntegratedLightDM/liblightdm/SessionsModelPrivate.h → tests/mocks/liblightdm/SessionsModelPrivate.h tests/mocks/LightDM/IntegratedLightDM/liblightdm/UsersModelPrivate.cpp → tests/mocks/liblightdm/UsersModelPrivate.cpp tests/mocks/LightDM/IntegratedLightDM/liblightdm/UsersModelPrivate.h → tests/mocks/liblightdm/UsersModelPrivate.h tests/plugins/LightDM/IntegratedLightDM/CMakeLists.txt+34−6 tests/plugins/LightDM/IntegratedLightDM/dbus.cpp+7−11 tests/plugins/LightDM/IntegratedLightDM/greeter.qml+2−7 tests/plugins/LightDM/IntegratedLightDM/promptsmodel.cpp+175 tests/plugins/LightDM/IntegratedLightDM/sessionsmodel.cpp+15−13 tests/plugins/LightDM/IntegratedLightDM/usersmodel.cpp+87−16 tests/plugins/Utils/CMakeLists.txt+4−1 tests/plugins/Utils/WindowStateStorageTest.cpp+64 tests/qmltests/ApplicationMenuDataLoader.qml+13−5 tests/qmltests/ApplicationMenus/tst_MenuBar.qml+18−5 tests/qmltests/ApplicationMenus/tst_MenuPopup.qml+148−37 tests/qmltests/Dash/tst_DashShell.qml+8−40 tests/qmltests/Greeter/TestView.qml+7−16 tests/qmltests/Greeter/tst_Greeter.qml+62−110 tests/qmltests/Greeter/tst_Infographics.qml+1−11 tests/qmltests/Greeter/tst_NarrowView.qml+20−43 tests/qmltests/Greeter/tst_WideView.qml+125−121 tests/qmltests/Launcher/tst_Drawer.qml+58−7 tests/qmltests/Launcher/tst_Launcher.qml+15−6 tests/qmltests/Panel/tst_Panel.qml+36 tests/qmltests/Tutorial/tst_Tutorial.qml+3−3 tests/qmltests/tst_OrientedShell.qml+90−8 tests/qmltests/tst_Shell.qml+86−80 tests/qmltests/tst_ShellWithPin.qml+22−13
CMakeLists.txt+1−1
@@ -70,7 +70,7 @@ find_package(Qt5DBus 5.6 REQUIRED)
7070find_package(Qt5Concurrent 5.6 REQUIRED)
7171find_package(Qt5Sql 5.6 REQUIRED)
7272
73−pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application=23)
73+pkg_check_modules(APPLICATION_API REQUIRED unity-shell-application=25)
7474pkg_check_modules(GEONAMES REQUIRED geonames>=0.2)
7575pkg_check_modules(GIO REQUIRED gio-2.0>=2.32)
7676pkg_check_modules(GLIB REQUIRED glib-2.0>=2.32)
cmake/modules/QmlTest.cmake+8−3
@@ -264,12 +264,16 @@ endfunction()
264264# installed system.
265265
266266function(add_meta_test TARGET_NAME)
267− cmake_parse_arguments(TEST "" "" "DEPENDS" ${ARGN})
267+ cmake_parse_arguments(TEST "SERIAL" "" "DEPENDS" ${ARGN})
268268
269269 add_custom_target(${TARGET_NAME})
270270
271271 set(filename "${CMAKE_BINARY_DIR}/tests/scripts/${TARGET_NAME}.sh")
272− file(WRITE "${filename}" "#!/usr/bin/parallel --shebang --no-notice\n\n")
272+ if(TEST_SERIAL)
273+ file(WRITE "${filename}" "#!/bin/sh\n\n")
274+ else()
275+ file(WRITE "${filename}" "#!/usr/bin/parallel --shebang --no-notice\n\n")
276+ endif()
273277
274278 add_meta_dependencies(${TARGET_NAME} DEPENDS ${TEST_DEPENDS})
275279 # else we will write the rest of the script as we add cmake targets
@@ -307,6 +311,7 @@ function(install_test_script TARGET_NAME)
307311 foreach(ONE_CMD ${TEST_COMMAND})
308312 set(script "${script}'${ONE_CMD}' ")
309313 endforeach()
314+ set(script "${script}\"\$@\"") # Allow passing arguments if desired
310315
311316 set(filename "${CMAKE_BINARY_DIR}/tests/scripts/${TARGET_NAME}.sh")
312317
@@ -370,7 +375,7 @@ function(add_meta_dependencies UPSTREAM_TARGET)
370375 # add depend to the meta test script that we will install on system
371376 set(filename "${CMAKE_BINARY_DIR}/tests/scripts/${UPSTREAM_TARGET}.sh")
372377 if (EXISTS "${filename}")
373− file(APPEND "${filename}" "${CMAKE_INSTALL_PREFIX}/${SHELL_PRIVATE_LIBDIR}/tests/scripts/${depend}.sh\n")
378+ file(APPEND "${filename}" "${CMAKE_INSTALL_PREFIX}/${SHELL_PRIVATE_LIBDIR}/tests/scripts/${depend}.sh \"\$@\" 2>&1\n")
374379 endif()
375380 endforeach()
376381endfunction()
data/com.canonical.Unity8.gschema.xml+5
@@ -53,6 +53,11 @@
5353 <summary>Enable or disable the indicator pull down menus</summary>
5454 <description>Toggle the availability of the indicator pull down menus</description>
5555 </key>
56+ <key type="s" name="appstore-uri">
57+ <default>'scope://com.canonical.scopes.clickstore'</default>
58+ <summary>The uri to the app store</summary>
59+ <description>This will be used whenever the user triggers an action to open the app store.</description>
60+ </key>
5661 </schema>
5762
5863 <schema path="/com/canonical/unity8/greeter/" id="com.canonical.Unity8.Greeter" gettext-domain="unity8">
debian/changelog+51
@@ -1,3 +1,54 @@
1+unity8 (8.15+17.04.20170124-0ubuntu1) zesty; urgency=medium
2+
3+ [ Albert Astals Cid ]
4+ * Limit tab-focus travelling on dialogs with a fence
5+ * Restore focus to where it was when our ShellDialogs get unloaded
6+ * Update current session after changing the user
7+ * Add keyboard navigation for Indicators
8+ * a window -> the current window
9+ * There's no spreadDelegate_ anymore
10+
11+ [ Daniel d'Andrada ]
12+ * Simplify DecoratedWindow
13+ * Remove unnecessary warning message
14+
15+ [ Josh Arenson ]
16+ * Add a test for the session chooser icon in the greeter's sessions
17+ list
18+
19+ [ Lukáš Tinkl ]
20+ * Fix keymap not being applied on the shell itself (LP: #1626435)
21+ * Shell dialog improvements (kbd focus, mouse eater)
22+ * Start searching directly as you type, w/o having to first
23+ focus/click the search field.
24+ * Add a test for the real implementation of WindowStateStorage
25+ * Use a four finger gesture to open the drawer, much like in u7
26+
27+ [ Michael Terry ]
28+ * Simplify the lightdm mock to make future greeter improvements easier
29+ to test.
30+ * Add support for guest sessions in unity8-greeter.
31+ * Use a model for PAM prompts, supporting more possible interactions.
32+ * Fix grouping of autopkg output and allow optionally passing
33+ arguments to installed test scripts.
34+ * Add support for LightDM hints for manual logins and hiding normal
35+ users.
36+
37+ [ Michael Zanetti ]
38+ * hint the launcher to indicate a successful size change to the user
39+ (LP: #1646457)
40+ * Improvements for the appdrawer (LP: #1648173)
41+ * Adjust home key to still focus the dash instead of messing with the
42+ drawer
43+ * allow 4 finger simulation with mousetouchadaptor
44+
45+ [ Nick Dedekind ]
46+ * Added Alt+F10 shortcut to open app menus. (LP: #1656896)
47+ * Fixed menu layout width calculations. (LP: #1657050)
48+ * Skip Panel::test_drag_indicator_item_down_shows_menu
49+
50+ -- Michał Sawicz <michal.sawicz@canonical.com> Tue, 24 Jan 2017 07:46:58 +0000
51+
152unity8 (8.15+17.04.20170110.4-0ubuntu1) zesty; urgency=medium
253
354 [ Albert Astals Cid ]
debian/control+1−1
@@ -38,7 +38,7 @@ Build-Depends: cmake,
3838 libubuntugestures5-private-dev (>= 1.3.2030),
3939 libudev-dev,
4040 libudm-common-dev,
41− libunity-api-dev (>= 8.0),
41+ libunity-api-dev (>= 8.1),
4242 libusermetricsoutput1-dev,
4343# Need those X11 libs touch emulation from mouse events in manual QML tests on a X11 desktop
4444 libx11-dev[!arm64 !armhf],
debian/rules+1−1
@@ -40,4 +40,4 @@ override_dh_makeshlibs:
4040override_dh_shlibdeps:
4141 # Some mock libraries link against liblightdm-qt5-3.so which we want to
4242 # avoid, since we only really link against our mock one, not the system one.
43− dh_shlibdeps -XlibMockLightDM-qml.so -XlibMockAccountsService-qml.so -Lunity8-private
43+ dh_shlibdeps -XlibMockAccountsService-qml.so -Lunity8-private
plugins/LightDM/CMakeLists.txt+1−1
@@ -7,7 +7,6 @@ include_directories(
77 ${CMAKE_CURRENT_SOURCE_DIR}
88 ${CMAKE_CURRENT_BINARY_DIR}
99 ${CMAKE_SOURCE_DIR}/plugins/Utils
10− ${CMAKE_BINARY_DIR}/tests/mocks/LightDM/IntegratedLightDM
1110 ${libunity8-private_SOURCE_DIR}
1211 ${LIBUSERMETRICSOUTPUT_INCLUDE_DIRS}
1312)
@@ -18,6 +17,7 @@ set(QMLPLUGIN_SRC
1817 DBusGreeterList.cpp
1918 Greeter.cpp
2019 plugin.cpp
20+ PromptsModel.cpp
2121 SessionsModel.cpp
2222 UsersModel.cpp
2323 )
plugins/LightDM/DBusGreeterList.cpp+6−6
@@ -25,7 +25,7 @@ DBusGreeterList::DBusGreeterList(Greeter *greeter, const QString &path)
2525 m_greeter(greeter)
2626{
2727 connect(m_greeter, &Greeter::authenticationUserChanged, this, &DBusGreeterList::authenticationUserChangedHandler);
28− connect(m_greeter, &Greeter::promptlessChanged, this, &DBusGreeterList::promptlessChangedHandler);
28+ connect(m_greeter, &Greeter::isAuthenticatedChanged, this, &DBusGreeterList::authenticatedChangedHandler);
2929}
3030
3131QString DBusGreeterList::GetActiveEntry() const
@@ -40,16 +40,16 @@ void DBusGreeterList::SetActiveEntry(const QString &entry)
4040
4141bool DBusGreeterList::entryIsLocked() const
4242{
43− return !m_greeter->promptless();
43+ return !m_greeter->isAuthenticated();
4444}
4545
46−void DBusGreeterList::authenticationUserChangedHandler(const QString &user)
46+void DBusGreeterList::authenticationUserChangedHandler()
4747{
48− notifyPropertyChanged(QStringLiteral("ActiveEntry"), user);
49− Q_EMIT EntrySelected(user);
48+ notifyPropertyChanged(QStringLiteral("ActiveEntry"), m_greeter->authenticationUser());
49+ Q_EMIT EntrySelected(m_greeter->authenticationUser());
5050}
5151
52−void DBusGreeterList::promptlessChangedHandler()
52+void DBusGreeterList::authenticatedChangedHandler()
5353{
5454 notifyPropertyChanged(QStringLiteral("EntryIsLocked"), entryIsLocked());
5555 Q_EMIT entryIsLockedChanged();
plugins/LightDM/DBusGreeterList.h+2−2
@@ -47,8 +47,8 @@ Q_SIGNALS:
4747 void entryIsLockedChanged();
4848
4949private Q_SLOTS:
50− void authenticationUserChangedHandler(const QString &user);
51− void promptlessChangedHandler();
50+ void authenticationUserChangedHandler();
51+ void authenticatedChangedHandler();
5252
5353private:
5454 Greeter *m_greeter;
plugins/LightDM/Greeter.cpp+137−23
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2013, 2015 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
@@ -17,13 +17,16 @@
1717
1818#include "Greeter.h"
1919#include "GreeterPrivate.h"
20+#include <QCoreApplication>
2021#include <libintl.h>
2122
23+static Greeter *singleton = nullptr;
24+
2225GreeterPrivate::GreeterPrivate(Greeter* parent)
2326 : m_greeter(new QLightDM::Greeter(parent)),
2427 m_active(false),
25− wasPrompted(false),
26− promptless(false),
28+ responded(false),
29+ everResponded(false),
2730 q_ptr(parent)
2831{
2932}
@@ -41,9 +44,32 @@ Greeter::Greeter(QObject* parent)
4144 connect(d->m_greeter, &QLightDM::Greeter::authenticationComplete,
4245 this, &Greeter::authenticationCompleteFilter);
4346
47+ // Don't get stuck waiting for PAM as we shut down.
48+ connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit,
49+ d->m_greeter, &QLightDM::Greeter::cancelAuthentication);
50+
4451 d->m_greeter->connectSync();
4552}
4653
54+Greeter::~Greeter()
55+{
56+ singleton = nullptr;
57+}
58+
59+Greeter *Greeter::instance()
60+{
61+ if (!singleton) {
62+ singleton = new Greeter;
63+ }
64+ return singleton;
65+}
66+
67+PromptsModel *Greeter::promptsModel()
68+{
69+ Q_D(Greeter);
70+ return &d->prompts;
71+}
72+
4773bool Greeter::isActive() const
4874{
4975 Q_D(const Greeter);
@@ -68,7 +94,16 @@ bool Greeter::isAuthenticated() const
6894QString Greeter::authenticationUser() const
6995{
7096 Q_D(const Greeter);
71− return d->m_greeter->authenticationUser();
97+ return d->cachedAuthUser;
98+}
99+
100+void Greeter::checkAuthenticationUser()
101+{
102+ Q_D(Greeter);
103+ if (d->cachedAuthUser != d->m_greeter->authenticationUser()) {
104+ d->cachedAuthUser = d->m_greeter->authenticationUser();
105+ Q_EMIT authenticationUserChanged();
106+ }
72107}
73108
74109QString Greeter::defaultSessionHint() const
@@ -77,35 +112,64 @@ QString Greeter::defaultSessionHint() const
77112 return d->m_greeter->defaultSessionHint();
78113}
79114
80−bool Greeter::promptless() const
115+QString Greeter::selectUser() const
81116{
82117 Q_D(const Greeter);
83− return d->promptless;
118+ if (hasGuestAccount() && d->m_greeter->selectGuestHint()) {
119+ return QStringLiteral("*guest");
120+ } else {
121+ return d->m_greeter->selectUserHint();
122+ }
84123}
85124
86−QString Greeter::selectUser() const
125+bool Greeter::hasGuestAccount() const
126+{
127+ Q_D(const Greeter);
128+ return d->m_greeter->hasGuestAccountHint();
129+}
130+
131+bool Greeter::showManualLoginHint() const
87132{
88133 Q_D(const Greeter);
89− return d->m_greeter->selectUserHint();
134+ return d->m_greeter->showManualLoginHint();
135+}
136+
137+bool Greeter::hideUsersHint() const
138+{
139+ Q_D(const Greeter);
140+ return d->m_greeter->hideUsersHint();
90141}
91142
92143void Greeter::authenticate(const QString &username)
93144{
94145 Q_D(Greeter);
95− d->wasPrompted = false;
96− if (d->promptless) {
97− d->promptless = false;
98− Q_EMIT promptlessChanged();
146+ d->prompts.clear();
147+ d->responded = false;
148+ d->everResponded = false;
149+
150+ if (authenticationUser() == username) {
151+ d->prompts = d->leftovers;
99152 }
153+ d->leftovers.clear();
100154
101− d->m_greeter->authenticate(username);
155+ if (username == QStringLiteral("*guest")) {
156+ d->m_greeter->authenticateAsGuest();
157+ } else if (username == QStringLiteral("*other")) {
158+ d->m_greeter->authenticate(nullptr);
159+ } else {
160+ d->m_greeter->authenticate(username);
161+ }
162+
163+ Q_EMIT authenticationStarted();
102164 Q_EMIT isAuthenticatedChanged();
103− Q_EMIT authenticationUserChanged(username);
165+ checkAuthenticationUser();
104166}
105167
106168void Greeter::respond(const QString &response)
107169{
108170 Q_D(Greeter);
171+ d->responded = true;
172+ d->everResponded = true;
109173 d->m_greeter->respond(response);
110174}
111175
@@ -118,32 +182,82 @@ bool Greeter::startSessionSync(const QString &session)
118182void Greeter::showPromptFilter(const QString &text, QLightDM::Greeter::PromptType type)
119183{
120184 Q_D(Greeter);
121− d->wasPrompted = true;
185+
186+ checkAuthenticationUser(); // may have changed in liblightdm
122187
123188 bool isDefaultPrompt = (text == dgettext("Linux-PAM", "Password: "));
189+ bool isSecret = type == QLightDM::Greeter::PromptTypeSecret;
190+
191+ QString trimmedText;
192+ if (!isDefaultPrompt)
193+ trimmedText = text.trimmed();
124194
125195 // Strip prompt of any colons at the end
126− QString trimmedText = text.trimmed();
127196 if (trimmedText.endsWith(':') || trimmedText.endsWith(QStringLiteral(":"))) {
128197 trimmedText.chop(1);
129198 }
130199
131− Q_EMIT showPrompt(trimmedText, type == QLightDM::Greeter::PromptTypeSecret, isDefaultPrompt);
200+ if (trimmedText == "login") {
201+ // 'login' is provided untranslated by LightDM when asking for a manual
202+ // login username.
203+ trimmedText = gettext("Username");
204+ }
205+
206+ if (d->responded) {
207+ d->prompts.clear();
208+ d->responded = false;
209+ }
210+
211+ d->prompts.append(trimmedText, isSecret ? PromptsModel::Secret : PromptsModel::Question);
132212}
133213
134214void Greeter::showMessageFilter(const QString &text, QLightDM::Greeter::MessageType type)
135215{
136− Q_EMIT showMessage(text, type == QLightDM::Greeter::MessageTypeError);
216+ Q_D(Greeter);
217+
218+ checkAuthenticationUser(); // may have changed in liblightdm
219+
220+ bool isError = type == QLightDM::Greeter::MessageTypeError;
221+
222+ if (d->responded) {
223+ d->prompts.clear();
224+ d->responded = false;
225+ }
226+ d->prompts.append(text, isError? PromptsModel::Error : PromptsModel::Message);
137227}
138228
139229void Greeter::authenticationCompleteFilter()
140230{
141231 Q_D(Greeter);
142− if (!d->wasPrompted) {
143− d->promptless = true;
144− Q_EMIT promptlessChanged();
145− }
146232
147233 Q_EMIT isAuthenticatedChanged();
148− Q_EMIT authenticationComplete();
234+
235+ bool automatic = !d->everResponded;
236+ bool pamHasLeftoverMessages = !d->prompts.hasPrompt() && d->prompts.rowCount() > 0;
237+
238+ if (!isAuthenticated()) {
239+ if (pamHasLeftoverMessages) {
240+ d->leftovers = d->prompts; // Prefer PAM's messages
241+ } else if (automatic) {
242+ d->leftovers.append(gettext("Failed to authenticate"), PromptsModel::Error);
243+ } else {
244+ d->leftovers.append(gettext("Invalid password, please try again"), PromptsModel::Error);
245+ }
246+ } else if (pamHasLeftoverMessages) {
247+ automatic = true; // treat this successful login as automatic, so user sees message
248+ d->leftovers = d->prompts;
249+ }
250+
251+ if (automatic) {
252+ d->prompts = d->leftovers; // OK, we'll just use these now
253+ d->leftovers.clear();
254+ d->prompts.append(isAuthenticated() ? gettext("Log In") : gettext("Retry"),
255+ PromptsModel::Button);
256+ }
257+
258+ if (isAuthenticated()) {
259+ Q_EMIT loginSuccess(automatic);
260+ } else {
261+ Q_EMIT loginError(automatic);
262+ }
149263}
plugins/LightDM/Greeter.h+17−13
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2012,2013,2015 Canonical, Ltd.
2+ * Copyright (C) 2012-2016 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
@@ -21,13 +21,13 @@
2121 such edits in the future, and by inserting ourselves here, we have more
2222 control. */
2323
24−#ifndef UNITY_GREETER_H
25−#define UNITY_GREETER_H
24+#pragma once
2625
2726#include <QLightDM/Greeter>
2827#include <QtCore/QObject>
2928
3029class GreeterPrivate;
30+class PromptsModel;
3131
3232class Greeter : public QObject
3333{
@@ -37,18 +37,22 @@ class Greeter : public QObject
3737 Q_PROPERTY(bool authenticated READ isAuthenticated NOTIFY isAuthenticatedChanged)
3838 Q_PROPERTY(QString authenticationUser READ authenticationUser NOTIFY authenticationUserChanged)
3939 Q_PROPERTY(QString defaultSession READ defaultSessionHint CONSTANT)
40− Q_PROPERTY(bool promptless READ promptless NOTIFY promptlessChanged)
4140 Q_PROPERTY(QString selectUser READ selectUser CONSTANT)
4241
4342public:
44− explicit Greeter(QObject* parent=0);
43+ static Greeter *instance();
44+ virtual ~Greeter();
4545
4646 bool isActive() const;
4747 bool isAuthenticated() const;
4848 QString authenticationUser() const;
4949 QString defaultSessionHint() const;
50− bool promptless() const;
5150 QString selectUser() const;
51+ bool hasGuestAccount() const;
52+ bool showManualLoginHint() const;
53+ bool hideUsersHint() const;
54+
55+ PromptsModel *promptsModel();
5256
5357public Q_SLOTS:
5458 void authenticate(const QString &username=QString());
@@ -57,21 +61,22 @@ public Q_SLOTS:
5761 void setIsActive(bool isActive);
5862
5963Q_SIGNALS:
60− void showMessage(const QString &text, bool isError);
61− void showPrompt(const QString &text, bool isSecret, bool isDefaultPrompt);
62− void authenticationComplete();
63− void authenticationUserChanged(const QString &user);
64+ void authenticationUserChanged();
6465 void isActiveChanged();
6566 void isAuthenticatedChanged();
66− void promptlessChanged();
6767 void showGreeter();
6868 void hideGreeter();
69+ void loginError(bool automatic);
70+ void loginSuccess(bool automatic);
71+ void authenticationStarted(); // useful for testing
6972
7073 // This signal is emitted by external agents like indicators, and the UI
7174 // should switch to this user if possible.
7275 void requestAuthenticationUser(const QString &user);
7376
7477protected:
78+ explicit Greeter(QObject* parent=0);
79+
7580 GreeterPrivate * const d_ptr;
7681
7782 Q_DECLARE_PRIVATE(Greeter)
@@ -80,6 +85,5 @@ private Q_SLOTS:
8085 void showMessageFilter(const QString &text, QLightDM::Greeter::MessageType type);
8186 void showPromptFilter(const QString &text, QLightDM::Greeter::PromptType type);
8287 void authenticationCompleteFilter();
88+ void checkAuthenticationUser();
8389};
84−
85−#endif
plugins/LightDM/GreeterPrivate.h+13−8
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2014 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
@@ -14,10 +14,14 @@
1414 * along with this program. If not, see <http://www.gnu.org/licenses/>.
1515 */
1616
17−#ifndef UNITY_GREETER_PRIVATE_H
18−#define UNITY_GREETER_PRIVATE_H
17+#pragma once
1918
20−#include <QLightDM/Greeter>
19+#include "PromptsModel.h"
20+#include <QObject>
21+
22+namespace QLightDM {
23+ class Greeter;
24+}
2125
2226class GreeterPrivate
2327{
@@ -26,8 +30,11 @@ public:
2630
2731 QLightDM::Greeter *m_greeter;
2832 bool m_active;
29− bool wasPrompted;
30− bool promptless;
33+ PromptsModel prompts;
34+ PromptsModel leftovers; // prompts to show during next auth for same user
35+ bool responded;
36+ bool everResponded;
37+ QString cachedAuthUser;
3138
3239protected:
3340 Greeter * const q_ptr;
@@ -35,5 +42,3 @@ protected:
3542private:
3643 Q_DECLARE_PUBLIC(Greeter)
3744};
38−
39−#endif // UNITY_GREETER_PRIVATE_H
plugins/LightDM/IntegratedLightDM/liblightdm/Greeter.cpp+7−4
@@ -49,7 +49,7 @@ QString Greeter::authenticationUser() const
4949
5050bool Greeter::hasGuestAccountHint() const
5151{
52− return true;
52+ return false;
5353}
5454
5555QString Greeter::getHint(const QString &name) const
@@ -70,12 +70,12 @@ bool Greeter::hideUsersHint() const
7070
7171bool Greeter::showManualLoginHint() const
7272{
73− return true;
73+ return false;
7474}
7575
7676bool Greeter::showRemoteLoginHint() const
7777{
78− return true;
78+ return false;
7979}
8080
8181QString Greeter::selectUserHint() const
@@ -146,7 +146,10 @@ void Greeter::authenticateRemote(const QString &session, const QString &username
146146}
147147
148148void Greeter::cancelAuthentication()
149−{}
149+{
150+ Q_D(Greeter);
151+ d->cancelAuthentication();
152+}
150153
151154void Greeter::setLanguage (const QString &language)
152155{
plugins/LightDM/IntegratedLightDM/liblightdm/GreeterPrivate.cpp+27−22
@@ -206,6 +206,28 @@ public Q_SLOTS:
206206 }
207207 }
208208
209+ void cancelPam()
210+ {
211+ if (pamHandle != nullptr) {
212+ QFuture<int> pamFuture = futureWatcher.future();
213+ pam_handle *handle = pamHandle;
214+ pamHandle = nullptr; // to disable normal finishPam() handling
215+ pamFuture.cancel();
216+
217+ // Note the empty loop, we just want to clear the futures queue.
218+ // Any further prompts from the pam thread will be immediately
219+ // responded to/dismissed in handlePrompt().
220+ while (respond(QString()));
221+
222+ // Now let signal/slot handling happen so the thread can finish
223+ while (!pamFuture.isFinished()) {
224+ QCoreApplication::processEvents();
225+ }
226+
227+ pam_end(handle, PAM_CONV_ERR);
228+ }
229+ }
230+
209231Q_SIGNALS:
210232 void showMessage(pam_handle *handle, QString text, QLightDM::Greeter::MessageType type);
211233 void showPrompt(pam_handle *handle, QString text, QLightDM::Greeter::PromptType type, QLightDM::GreeterImpl::ResponseFuture response);
@@ -247,28 +269,6 @@ private Q_SLOTS:
247269 }
248270
249271private:
250− void cancelPam()
251− {
252− if (pamHandle != nullptr) {
253− QFuture<int> pamFuture = futureWatcher.future();
254− pam_handle *handle = pamHandle;
255− pamHandle = nullptr; // to disable normal finishPam() handling
256− pamFuture.cancel();
257−
258− // Note the empty loop, we just want to clear the futures queue.
259− // Any further prompts from the pam thread will be immediately
260− // responded to/dismissed in handlePrompt().
261− while (respond(QString()));
262−
263− // Now let signal/slot handling happen so the thread can finish
264− while (!pamFuture.isFinished()) {
265− QCoreApplication::processEvents();
266− }
267−
268− pam_end(handle, PAM_CONV_ERR);
269− }
270− }
271−
272272 Greeter *greeter;
273273 GreeterPrivate *greeterPrivate;
274274 pam_handle* pamHandle;
@@ -299,6 +299,11 @@ void GreeterPrivate::handleRespond(const QString &response)
299299 m_impl->respond(response);
300300}
301301
302+void GreeterPrivate::cancelAuthentication()
303+{
304+ m_impl->cancelPam();
305+}
306+
302307}
303308
304309#include "GreeterPrivate.moc"
plugins/LightDM/IntegratedLightDM/liblightdm/GreeterPrivate.h+1
@@ -38,6 +38,7 @@ public:
3838
3939 void handleAuthenticate();
4040 void handleRespond(const QString &response);
41+ void cancelAuthentication();
4142
4243protected:
4344 GreeterImpl *m_impl; // if the backend needs more private data
plugins/LightDM/PromptsModel.cppnew file+97
@@ -0,0 +1,97 @@
1+/*
2+ * Copyright (C) 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 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+
18+#include "PromptsModel.h"
19+
20+PromptsModel::PromptsModel(QObject* parent)
21+ : QAbstractListModel(parent)
22+{
23+ m_roleNames[TypeRole] = "type";
24+ m_roleNames[TextRole] = "text";
25+}
26+
27+PromptsModel& PromptsModel::operator=(const PromptsModel &other)
28+{
29+ beginResetModel();
30+ m_prompts = other.m_prompts;
31+ endResetModel();
32+ Q_EMIT countChanged();
33+ return *this;
34+}
35+
36+int PromptsModel::rowCount(const QModelIndex &parent) const
37+{
38+ if (parent.isValid())
39+ return 0;
40+
41+ return m_prompts.size();
42+}
43+
44+QVariant PromptsModel::data(const QModelIndex &index, int role) const
45+{
46+ if (!index.isValid() || index.column() > 0 || index.row() >= m_prompts.size())
47+ return QVariant();
48+
49+ switch (role) {
50+ case Qt::DisplayRole: // fallthrough
51+ case TextRole: return m_prompts[index.row()].prompt;
52+ case TypeRole: return m_prompts[index.row()].type;
53+ default: return QVariant();
54+ }
55+}
56+
57+QHash<int, QByteArray> PromptsModel::roleNames() const
58+{
59+ return m_roleNames;
60+}
61+
62+void PromptsModel::prepend(const QString &text, PromptType type)
63+{
64+ beginInsertRows(QModelIndex(), 0, 0);
65+ m_prompts.prepend(PromptInfo{text, type});
66+ endInsertRows();
67+
68+ Q_EMIT countChanged();
69+}
70+
71+void PromptsModel::append(const QString &text, PromptType type)
72+{
73+ beginInsertRows(QModelIndex(), m_prompts.size(), m_prompts.size());
74+ m_prompts.append(PromptInfo{text, type});
75+ endInsertRows();
76+
77+ Q_EMIT countChanged();
78+}
79+
80+void PromptsModel::clear()
81+{
82+ beginResetModel();
83+ m_prompts.clear();
84+ endResetModel();
85+
86+ Q_EMIT countChanged();
87+}
88+
89+bool PromptsModel::hasPrompt() const
90+{
91+ Q_FOREACH(const PromptInfo &info, m_prompts) {
92+ if (info.type == PromptType::Secret || info.type == PromptType::Question) {
93+ return true;
94+ }
95+ }
96+ return false;
97+}
plugins/LightDM/PromptsModel.hnew file+70
@@ -0,0 +1,70 @@
1+/*
2+ * Copyright (C) 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 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+
18+#pragma once
19+
20+#include <QAbstractListModel>
21+
22+class PromptsModel : public QAbstractListModel
23+{
24+ Q_OBJECT
25+
26+ Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
27+
28+public:
29+ enum PromptsModelRoles {
30+ TypeRole = Qt::UserRole,
31+ TextRole,
32+ };
33+ Q_ENUM(PromptsModelRoles)
34+
35+ enum PromptType {
36+ Message,
37+ Error,
38+ Secret,
39+ Question,
40+ Button,
41+ };
42+ Q_ENUM(PromptType)
43+
44+ explicit PromptsModel(QObject* parent=0);
45+
46+ PromptsModel& operator=(const PromptsModel &other);
47+
48+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
49+ QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
50+ QHash<int, QByteArray> roleNames() const override;
51+
52+ Q_INVOKABLE void prepend(const QString &text, PromptType type);
53+ Q_INVOKABLE void append(const QString &text, PromptType type);
54+
55+ void clear();
56+
57+ bool hasPrompt() const;
58+
59+Q_SIGNALS:
60+ void countChanged();
61+
62+private:
63+ struct PromptInfo {
64+ QString prompt;
65+ PromptType type;
66+ };
67+
68+ QList<PromptInfo> m_prompts;
69+ QHash<int, QByteArray> m_roleNames;
70+};
plugins/LightDM/SessionsModel.cpp+1−2
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2015 Canonical, Ltd.
2+ * Copyright (C) 2015-2016 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
@@ -128,7 +128,6 @@ SessionsModel::SessionsModel(QObject* parent)
128128 }
129129
130130 setModel(m_model);
131− setSourceModel(m_model);
132131 setSortCaseSensitivity(Qt::CaseInsensitive);
133132 setSortLocaleAware(true);
134133 setSortRole(Qt::DisplayRole);
plugins/LightDM/SessionsModel.h+4−5
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2015 Canonical, Ltd.
2+ * Copyright (C) 2015-2016 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
@@ -15,8 +15,7 @@
1515 *
1616 */
1717
18−#ifndef UNITY_SESSIONSMODEL_H
19−#define UNITY_SESSIONSMODEL_H
18+#pragma once
2019
2120#include <unitysortfilterproxymodelqml.h>
2221#include <QLightDM/SessionsModel>
@@ -30,6 +29,7 @@ class SessionsModel : public UnitySortFilterProxyModelQML
3029
3130 Q_PROPERTY(QList<QUrl> iconSearchDirectories READ iconSearchDirectories
3231 WRITE setIconSearchDirectories NOTIFY iconSearchDirectoriesChanged)
32+
3333Q_SIGNALS:
3434 void iconSearchDirectoriesChanged();
3535
@@ -54,6 +54,7 @@ public:
5454 Q_INVOKABLE QUrl iconUrl(const QString sessionName) const;
5555
5656 void setIconSearchDirectories(const QList<QUrl> searchDirectories);
57+
5758private:
5859 QLightDM::SessionsModel* m_model;
5960 QHash<int, QByteArray> m_roleNames;
@@ -63,5 +64,3 @@ private:
6364 QUrl("/usr/share/unity-greeter/")};
6465
6566};
66−
67−#endif // UNITY_SESSIONSMODEL_H
plugins/LightDM/UsersModel.cpp+171−9
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2013 Canonical, Ltd.
2+ * Copyright (C) 2013,2015-2016 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
@@ -12,20 +12,22 @@
1212 *
1313 * You should have received a copy of the GNU General Public License
1414 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− *
16− * Author: Michael Terry <michael.terry@canonical.com>
1715 */
1816
17+#include "Greeter.h"
1918#include "UsersModel.h"
19+#include <QIdentityProxyModel>
2020#include <QLightDM/UsersModel>
21−#include <QtCore/QSortFilterProxyModel>
21+
22+#include <libintl.h>
2223
2324// First, we define an internal class that wraps LightDM's UsersModel. This
2425// class will modify some of the data coming from LightDM. For example, we
25−// modify any empty Real Names into just normal Names.
26+// modify any empty Real Names into just normal Names. We also add optional
27+// rows, depending on configuration.
2628// (We can't modify the data directly in UsersModel below because it won't sort
2729// using the modified data.)
28−class MangleModel : public QSortFilterProxyModel
30+class MangleModel : public QIdentityProxyModel
2931{
3032 Q_OBJECT
3133
@@ -33,21 +35,75 @@ public:
3335 explicit MangleModel(QObject* parent=0);
3436
3537 QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
38+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
39+ QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
40+
41+private:
42+ struct CustomRow {
43+ QString name;
44+ QString realName;
45+ };
46+
47+ int sourceRowCount() const;
48+
49+ void updateGuestRow();
50+ void updateManualRow();
51+ void updateCustomRows();
52+
53+ void addCustomRow(const CustomRow &newRow);
54+ void removeCustomRow(const QString &rowName);
55+
56+ QList<CustomRow> m_customRows;
57+ bool m_updatingCustomRows;
3658};
3759
3860MangleModel::MangleModel(QObject* parent)
39− : QSortFilterProxyModel(parent)
61+ : QIdentityProxyModel(parent)
62+ , m_updatingCustomRows(false)
4063{
4164 setSourceModel(new QLightDM::UsersModel(this));
65+
66+ updateCustomRows();
67+
68+ // Would be nice if there were a rowCountChanged signal in the base class.
69+ // We redo all custom rows on any row count change, because (A) some of
70+ // custom rows (manual login) use row count information and (B) when
71+ // testing, we use a modelReset signal as a way to indicate that a custom
72+ // row has been toggled off or on.
73+ connect(this, &QIdentityProxyModel::modelReset,
74+ this, &MangleModel::updateCustomRows);
75+ connect(this, &QIdentityProxyModel::rowsInserted,
76+ this, &MangleModel::updateCustomRows);
77+ connect(this, &QIdentityProxyModel::rowsRemoved,
78+ this, &MangleModel::updateCustomRows);
4279}
4380
4481QVariant MangleModel::data(const QModelIndex &index, int role) const
4582{
46− QVariant variantData = QSortFilterProxyModel::data(index, role);
83+ QVariant variantData;
84+
85+ if (index.row() >= rowCount())
86+ return QVariant();
87+
88+ bool isCustomRow = index.row() >= sourceRowCount();
89+ if (isCustomRow && index.column() == 0) {
90+ int customIndex = index.row() - sourceRowCount();
91+ if (role == QLightDM::UsersModel::NameRole) {
92+ variantData = m_customRows[customIndex].name;
93+ } else if (role == QLightDM::UsersModel::RealNameRole) {
94+ variantData = m_customRows[customIndex].realName;
95+ } else if (role == QLightDM::UsersModel::LoggedInRole) {
96+ variantData = false;
97+ } else if (role == QLightDM::UsersModel::SessionRole) {
98+ variantData = Greeter::instance()->defaultSessionHint();
99+ }
100+ } else {
101+ variantData = QIdentityProxyModel::data(index, role);
102+ }
47103
48104 // If user's real name is empty, switch to unix name
49105 if (role == QLightDM::UsersModel::RealNameRole && variantData.toString().isEmpty()) {
50− variantData = QSortFilterProxyModel::data(index, QLightDM::UsersModel::NameRole);
106+ variantData = data(index, QLightDM::UsersModel::NameRole);
51107 } else if (role == QLightDM::UsersModel::BackgroundPathRole && variantData.toString().startsWith('#')) {
52108 const QString stringData = "data:image/svg+xml,<svg><rect width='100%' height='100%' fill='" + variantData.toString() + "'/></svg>";
53109 variantData = stringData;
@@ -56,6 +112,95 @@ QVariant MangleModel::data(const QModelIndex &index, int role) const
56112 return variantData;
57113}
58114
115+void MangleModel::addCustomRow(const CustomRow &newRow)
116+{
117+ for (int i = 0; i < m_customRows.size(); i++) {
118+ if (m_customRows[i].name == newRow.name) {
119+ return; // we don't have custom rows that change content yet
120+ }
121+ }
122+
123+ beginInsertRows(QModelIndex(), rowCount(), rowCount());
124+ m_customRows << newRow;
125+ endInsertRows();
126+}
127+
128+void MangleModel::removeCustomRow(const QString &rowName)
129+{
130+ for (int i = 0; i < m_customRows.size(); i++) {
131+ if (m_customRows[i].name == rowName) {
132+ int rowNum = sourceRowCount() + i;
133+ beginRemoveRows(QModelIndex(), rowNum, rowNum);
134+ m_customRows.removeAt(i);
135+ endRemoveRows();
136+ break;
137+ }
138+ }
139+}
140+
141+void MangleModel::updateManualRow()
142+{
143+ bool hasAnotherEntry = sourceRowCount() > 0;
144+ for (int i = 0; !hasAnotherEntry && i < m_customRows.size(); i++) {
145+ if (m_customRows[i].name != QStringLiteral("*other")) {
146+ hasAnotherEntry = true;
147+ }
148+ }
149+
150+ // Show manual login if we are asked to OR if no other entry exists
151+ if (Greeter::instance()->showManualLoginHint() || !hasAnotherEntry)
152+ addCustomRow({QStringLiteral("*other"), gettext("Login")});
153+ else
154+ removeCustomRow(QStringLiteral("*other"));
155+}
156+
157+void MangleModel::updateGuestRow()
158+{
159+ if (Greeter::instance()->hasGuestAccount())
160+ addCustomRow({QStringLiteral("*guest"), gettext("Guest Session")});
161+ else
162+ removeCustomRow(QStringLiteral("*guest"));
163+}
164+
165+void MangleModel::updateCustomRows()
166+{
167+ // We update when rowCount changes, but we also insert/remove rows here.
168+ // So guard this function to avoid recursion.
169+ if (m_updatingCustomRows)
170+ return;
171+
172+ m_updatingCustomRows = true;
173+ updateGuestRow();
174+ updateManualRow();
175+ m_updatingCustomRows = false;
176+}
177+
178+int MangleModel::rowCount(const QModelIndex &parent) const
179+{
180+ if (parent.isValid())
181+ return 0;
182+ else
183+ return sourceRowCount() + m_customRows.size();
184+}
185+
186+int MangleModel::sourceRowCount() const
187+{
188+ return Greeter::instance()->hideUsersHint() ? 0 : sourceModel()->rowCount();
189+}
190+
191+QModelIndex MangleModel::index(int row, int column, const QModelIndex &parent) const
192+{
193+ if (row >= rowCount())
194+ return QModelIndex();
195+
196+ bool isCustomRow = row >= sourceRowCount();
197+ if (isCustomRow && !parent.isValid()) {
198+ return createIndex(row, column);
199+ } else {
200+ return QIdentityProxyModel::index(row, column, parent);
201+ }
202+}
203+
59204// **** Now we continue with actual UsersModel class ****
60205
61206UsersModel::UsersModel(QObject* parent)
@@ -68,4 +213,21 @@ UsersModel::UsersModel(QObject* parent)
68213 sort(0);
69214}
70215
216+bool UsersModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const
217+{
218+ auto leftName = source_left.data(QLightDM::UsersModel::NameRole);
219+ auto rightName = source_right.data(QLightDM::UsersModel::NameRole);
220+
221+ if (leftName == QStringLiteral("*guest"))
222+ return false;
223+ if (rightName == QStringLiteral("*guest"))
224+ return true;
225+ if (leftName == QStringLiteral("*other"))
226+ return false;
227+ if (rightName == QStringLiteral("*other"))
228+ return true;
229+
230+ return UnitySortFilterProxyModelQML::lessThan(source_left, source_right);
231+}
232+
71233#include "UsersModel.moc"
plugins/LightDM/UsersModel.h+6−8
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2012,2013 Canonical, Ltd.
2+ * Copyright (C) 2012-2013,2015-2016 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
@@ -12,19 +12,16 @@
1212 *
1313 * You should have received a copy of the GNU General Public License
1414 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− *
16− * Authors: Michael Terry <michael.terry@canonical.com>
1715 */
1816
1917/* This class is a really tiny filter around QLightDM::UsersModel. There are
2018 some operations that we want to edit a bit for the benefit of Qml.
2119 Specifically, we want to sort users according to realName. */
2220
23−#ifndef UNITY_USERSMODEL_H
24−#define UNITY_USERSMODEL_H
21+#pragma once
2522
2623#include <unitysortfilterproxymodelqml.h>
27−#include <QtCore/QObject>
24+#include <QObject>
2825
2926class UsersModel : public UnitySortFilterProxyModelQML
3027{
@@ -32,6 +29,7 @@ class UsersModel : public UnitySortFilterProxyModelQML
3229
3330public:
3431 explicit UsersModel(QObject* parent=0);
35−};
3632
37−#endif
33+protected:
34+ bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override;
35+};
plugins/LightDM/plugin.cpp+12−4
@@ -19,6 +19,7 @@
1919#include "DBusGreeter.h"
2020#include "DBusGreeterList.h"
2121#include "Greeter.h"
22+#include "PromptsModel.h"
2223#include "SessionsModel.h"
2324#include "UsersModel.h"
2425#include <libusermetricsoutput/ColorTheme.h>
@@ -32,16 +33,24 @@
3233
3334static QObject *greeter_provider(QQmlEngine *engine, QJSEngine *scriptEngine)
3435{
35− Q_UNUSED(engine)
3636 Q_UNUSED(scriptEngine)
3737
38− Greeter *greeter = new Greeter();
38+ Greeter *greeter = Greeter::instance();
3939 new DBusGreeter(greeter, QStringLiteral("/"));
4040 new DBusGreeterList(greeter, QStringLiteral("/list"));
4141
42+ engine->setObjectOwnership(greeter, QQmlEngine::CppOwnership);
43+
4244 return greeter;
4345}
4446
47+static QObject *prompts_provider(QQmlEngine *engine, QJSEngine *)
48+{
49+ auto model = Greeter::instance()->promptsModel();
50+ engine->setObjectOwnership(model, QQmlEngine::CppOwnership);
51+ return model;
52+}
53+
4554static QObject *sessions_provider(QQmlEngine *engine, QJSEngine *scriptEngine)
4655{
4756 Q_UNUSED(engine)
@@ -78,8 +87,7 @@ void PLUGIN_CLASSNAME::registerTypes(const char *uri)
7887 #error No library defined in LightDM plugin
7988#endif
8089
81− qRegisterMetaType<QLightDM::Greeter::MessageType>("QLightDM::Greeter::MessageType");
82− qRegisterMetaType<QLightDM::Greeter::PromptType>("QLightDM::Greeter::PromptType");
90+ qmlRegisterSingletonType<PromptsModel>(uri, 0, 1, "Prompts", prompts_provider);
8391
8492 qmlRegisterSingletonType<SessionsModel>(uri, 0, 1, "Sessions", sessions_provider);
8593 qmlRegisterUncreatableType<QLightDM::SessionsModel>(uri, 0, 1, "SessionRoles", QStringLiteral("Type is not instantiable"));
plugins/Unity/Launcher/launchermodel.cpp−1
@@ -214,7 +214,6 @@ void LauncherModel::quickListActionInvoked(const QString &appId, int actionIndex
214214void LauncherModel::setUser(const QString &username)
215215{
216216 Q_UNUSED(username)
217− qWarning() << "This backend doesn't support multiple users";
218217}
219218
220219QString LauncherModel::getUrlForAppId(const QString &appId) const
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/appdrawerproxymodel.cpp+1−1
@@ -185,5 +185,5 @@ QString AppDrawerProxyModel::appId(int index) const
185185 return adpm->appId(sourceIndex.row());
186186 }
187187 }
188− return nullptr;
188+ return QString();
189189}
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+72
@@ -0,0 +1,72 @@
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+bool TabFocusFenceItem::focusNext()
29+{
30+ QQuickItem * current = scopedFocusItem();
31+ if (current) {
32+ QQuickItem * next = current->nextItemInFocusChain(true);
33+ if (next) {
34+ next->setFocus(true, Qt::TabFocusReason);
35+ return true;
36+ }
37+ }
38+ return false;
39+}
40+
41+bool TabFocusFenceItem::focusPrev()
42+{
43+ QQuickItem * current = scopedFocusItem();
44+ if (current) {
45+ QQuickItem * prev = current->nextItemInFocusChain(false);
46+ if (prev) {
47+ prev->setFocus(true, Qt::BacktabFocusReason);
48+ return true;
49+ }
50+ }
51+ return false;
52+}
53+
54+void TabFocusFenceItem::keyPressEvent(QKeyEvent *event)
55+{
56+ // Needed so we eat Tab keys when there's only one item inside the fence
57+ if (event->key() == Qt::Key_Tab) {
58+ event->accept();
59+ } else {
60+ QQuickItem::keyPressEvent(event);
61+ }
62+}
63+
64+void TabFocusFenceItem::keyReleaseEvent(QKeyEvent *event)
65+{
66+ // Needed so we eat Tab keys when there's only one item inside the fence
67+ if (event->key() == Qt::Key_Tab) {
68+ event->accept();
69+ } else {
70+ QQuickItem::keyReleaseEvent(event);
71+ }
72+}
plugins/Utils/tabfocusfence.hnew file+38
@@ -0,0 +1,38 @@
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+ Q_INVOKABLE bool focusNext();
32+ Q_INVOKABLE bool focusPrev();
33+
34+ void keyPressEvent(QKeyEvent *event) override;
35+ void keyReleaseEvent(QKeyEvent *event) override;
36+};
37+
38+#endif
po/unity8.pot+67−39
@@ -8,7 +8,7 @@ msgid ""
88msgstr ""
99"Project-Id-Version: unity8\n"
1010"Report-Msgid-Bugs-To: \n"
11−"POT-Creation-Date: 2017-01-10 14:48+0000\n"
11+"POT-Creation-Date: 2017-01-24 07:46+0000\n"
1212"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
1313"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
1414"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -18,10 +18,38 @@ msgstr ""
1818"Content-Transfer-Encoding: 8bit\n"
1919"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
2020
21−#: plugins/LightDM/Greeter.cpp:123
21+#: plugins/LightDM/Greeter.cpp:188
2222msgid "Password: "
2323msgstr ""
2424
25+#: plugins/LightDM/Greeter.cpp:203
26+msgid "Username"
27+msgstr ""
28+
29+#: plugins/LightDM/Greeter.cpp:242
30+msgid "Failed to authenticate"
31+msgstr ""
32+
33+#: plugins/LightDM/Greeter.cpp:244
34+msgid "Invalid password, please try again"
35+msgstr ""
36+
37+#: plugins/LightDM/Greeter.cpp:254
38+msgid "Log In"
39+msgstr ""
40+
41+#: plugins/LightDM/Greeter.cpp:254
42+msgid "Retry"
43+msgstr ""
44+
45+#: plugins/LightDM/UsersModel.cpp:152
46+msgid "Login"
47+msgstr ""
48+
49+#: plugins/LightDM/UsersModel.cpp:160
50+msgid "Guest Session"
51+msgstr ""
52+
2553#: plugins/Unity/Launcher/launcheritem.cpp:50
2654#: plugins/Unity/Launcher/launcheritem.cpp:122
2755msgid "Pin shortcut"
@@ -35,66 +63,66 @@ msgstr ""
3563msgid "Unpin shortcut"
3664msgstr ""
3765
38−#: qml/Components/Dialogs.qml:149
66+#: qml/Components/Dialogs.qml:177
3967msgctxt "Title: Lock/Log out dialog"
4068msgid "Log out"
4169msgstr ""
4270
43−#: qml/Components/Dialogs.qml:150
71+#: qml/Components/Dialogs.qml:178
4472msgid "Are you sure you want to log out?"
4573msgstr ""
4674
47−#: qml/Components/Dialogs.qml:152
75+#: qml/Components/Dialogs.qml:181
4876msgctxt "Button: Lock the system"
4977msgid "Lock"
5078msgstr ""
5179
52−#: qml/Components/Dialogs.qml:160
80+#: qml/Components/Dialogs.qml:191
5381msgctxt "Button: Log out from the system"
5482msgid "Log Out"
5583msgstr ""
5684
57−#: qml/Components/Dialogs.qml:167 qml/Components/Dialogs.qml:225
58−#: qml/Dash/DashPageHeader.qml:324 qml/Greeter/NarrowView.qml:232
85+#: qml/Components/Dialogs.qml:199 qml/Components/Dialogs.qml:264
86+#: qml/Dash/DashPageHeader.qml:324 qml/Greeter/NarrowView.qml:222
5987#: qml/Wizard/Pages/passcode-confirm.qml:32
6088#: qml/Wizard/Pages/passcode-set.qml:32
6189msgid "Cancel"
6290msgstr ""
6391
64−#: qml/Components/Dialogs.qml:179
92+#: qml/Components/Dialogs.qml:211
6593msgctxt "Title: Reboot dialog"
6694msgid "Reboot"
6795msgstr ""
6896
69−#: qml/Components/Dialogs.qml:180
97+#: qml/Components/Dialogs.qml:212
7098msgid "Are you sure you want to reboot?"
7199msgstr ""
72100
73−#: qml/Components/Dialogs.qml:182
101+#: qml/Components/Dialogs.qml:215
74102msgid "No"
75103msgstr ""
76104
77−#: qml/Components/Dialogs.qml:189
105+#: qml/Components/Dialogs.qml:223
78106msgid "Yes"
79107msgstr ""
80108
81−#: qml/Components/Dialogs.qml:204
109+#: qml/Components/Dialogs.qml:239
82110msgctxt "Title: Power off/Restart dialog"
83111msgid "Power"
84112msgstr ""
85113
86−#: qml/Components/Dialogs.qml:205
114+#: qml/Components/Dialogs.qml:240
87115msgid ""
88116"Are you sure you would like\n"
89117"to power off?"
90118msgstr ""
91119
92−#: qml/Components/Dialogs.qml:208
120+#: qml/Components/Dialogs.qml:244
93121msgctxt "Button: Power off the system"
94122msgid "Power off"
95123msgstr ""
96124
97−#: qml/Components/Dialogs.qml:217
125+#: qml/Components/Dialogs.qml:255
98126msgctxt "Button: Restart the system"
99127msgid "Restart"
100128msgstr ""
@@ -120,7 +148,7 @@ msgid "Alt + PrtScr"
120148msgstr ""
121149
122150#: qml/Components/KeyboardShortcutsOverlay.qml:80
123−msgid "Takes a screenshot of a window."
151+msgid "Takes a screenshot of the current window."
124152msgstr ""
125153
126154#: qml/Components/KeyboardShortcutsOverlay.qml:88
@@ -267,7 +295,7 @@ msgstr ""
267295msgid "Closes the current window."
268296msgstr ""
269297
270−#: qml/Components/Lockscreen.qml:212 qml/Greeter/NarrowView.qml:252
298+#: qml/Components/Lockscreen.qml:212 qml/Greeter/NarrowView.qml:242
271299msgid "Return to Call"
272300msgstr ""
273301
@@ -275,7 +303,7 @@ msgstr ""
275303msgid "Emergency Call"
276304msgstr ""
277305
278−#: qml/Components/Lockscreen.qml:244
306+#: qml/Components/Lockscreen.qml:245
279307msgid "OK"
280308msgstr ""
281309
@@ -289,29 +317,29 @@ msgstr ""
289317msgid "%1:%2"
290318msgstr ""
291319
292−#: qml/Components/ModeSwitchWarningDialog.qml:32
320+#: qml/Components/ModeSwitchWarningDialog.qml:33
293321msgid "Apps may have unsaved data:"
294322msgstr ""
295323
296−#: qml/Components/ModeSwitchWarningDialog.qml:57
324+#: qml/Components/ModeSwitchWarningDialog.qml:60
297325msgctxt ""
298326"Re-dock means connect the device again to an external screen/mouse/keyboard"
299327msgid "Re-dock, save your work and close these apps to continue."
300328msgstr ""
301329
302−#: qml/Components/ModeSwitchWarningDialog.qml:63
330+#: qml/Components/ModeSwitchWarningDialog.qml:67
303331msgid "Or force close now (unsaved data will be lost)."
304332msgstr ""
305333
306−#: qml/Components/ModeSwitchWarningDialog.qml:75
334+#: qml/Components/ModeSwitchWarningDialog.qml:80
307335msgid "OK, I will reconnect"
308336msgstr ""
309337
310−#: qml/Components/ModeSwitchWarningDialog.qml:76
338+#: qml/Components/ModeSwitchWarningDialog.qml:81
311339msgid "Reconnect now!"
312340msgstr ""
313341
314−#: qml/Components/ModeSwitchWarningDialog.qml:88
342+#: qml/Components/ModeSwitchWarningDialog.qml:94
315343msgid "Close all"
316344msgstr ""
317345
@@ -461,44 +489,44 @@ msgid_plural "Please wait %1 minutes and then try again…"
461489msgstr[0] ""
462490msgstr[1] ""
463491
464−#: qml/Greeter/Greeter.qml:592
492+#: qml/Greeter/Greeter.qml:561
465493msgid "Try again"
466494msgstr ""
467495
468−#: qml/Greeter/LoginList.qml:70
469−msgid "Passphrase"
496+#: qml/Greeter/Greeter.qml:562
497+msgid "Enter passphrase to unlock"
470498msgstr ""
471499
472−#: qml/Greeter/LoginList.qml:71
473−msgid "Passcode"
500+#: qml/Greeter/Greeter.qml:563
501+msgid "Enter passcode to unlock"
474502msgstr ""
475503
476−#: qml/Greeter/LoginList.qml:98
477−msgid "Retry"
504+#: qml/Greeter/NarrowView.qml:242
505+msgid "Emergency"
478506msgstr ""
479507
480−#: qml/Greeter/LoginList.qml:99
481−msgid "Log In"
508+#: qml/Greeter/PromptList.qml:126
509+msgid "Passphrase"
482510msgstr ""
483511
484−#: qml/Greeter/NarrowView.qml:252
485−msgid "Emergency"
512+#: qml/Greeter/PromptList.qml:126
513+msgid "Passcode"
486514msgstr ""
487515
488516#: qml/Greeter/SessionsList.qml:122
489517msgid "Select desktop environment"
490518msgstr ""
491519
492−#: qml/Launcher/Drawer.qml:80
520+#: qml/Launcher/Drawer.qml:92
493521msgid "Search…"
494522msgstr ""
495523
496−#: qml/Launcher/Drawer.qml:101
524+#: qml/Launcher/Drawer.qml:123
497525msgctxt "Apps sorted alphabetically"
498526msgid "A-Z"
499527msgstr ""
500528
501−#: qml/Launcher/MoreAppsHeader.qml:32
529+#: qml/Launcher/MoreAppsHeader.qml:39
502530msgid "More apps in the store"
503531msgstr ""
504532
qml/ApplicationMenus/MenuBar.qml+14
@@ -57,6 +57,20 @@ Item {
5757 onReleased: d.stopSHortcutTimer()
5858 }
5959
60+ GlobalShortcut {
61+ shortcut: Qt.AltModifier | Qt.Key_F10
62+ active: enableKeyFilter && d.currentItem == null
63+ onTriggered: {
64+ for (var i = 0; i < rowRepeater.count; i++) {
65+ var item = rowRepeater.itemAt(i);
66+ if (item.enabled) {
67+ item.show();
68+ break;
69+ }
70+ }
71+ }
72+ }
73+
6074 InverseMouseArea {
6175 acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
6276 anchors.fill: parent
qml/ApplicationMenus/MenuPopup.qml+162−123
@@ -26,19 +26,7 @@ UbuntuShape {
2626 objectName: "menu"
2727 backgroundColor: theme.palette.normal.overlay
2828
29− property alias unityMenuModel: listView.model
30−
31− readonly property real __ajustedMinimumHeight: {
32− if (listView.contentHeight > __minimumHeight) {
33− return units.gu(30);
34− }
35− return Math.max(listView.contentHeight, units.gu(2));
36− }
37−
38− readonly property real __minimumWidth: units.gu(20)
39− readonly property real __minimumHeight: units.gu(30)
40− readonly property real __maximumWidth: Screen.width * 0.7
41− readonly property real __maximumHeight: Screen.height * 0.7
29+ property alias unityMenuModel: repeater.model
4230
4331 function show() {
4432 visible = true;
@@ -63,18 +51,23 @@ UbuntuShape {
6351 d.dismissAll();
6452 }
6553
66− implicitWidth: container.width
67− implicitHeight: MathUtils.clamp(listView.contentHeight, __ajustedMinimumHeight, __maximumHeight)
54+ implicitWidth: focusScope.width
55+ implicitHeight: focusScope.height
6856
6957 MenuNavigator {
7058 id: d
7159 objectName: "d"
72− itemView: listView
60+ itemView: repeater
7361
7462 property Item currentItem: null
7563 property Item hoveredItem: null
7664 readonly property int currentIndex: currentItem ? currentItem.__ownIndex : -1
7765
66+ property real __minimumWidth: units.gu(20)
67+ property real __maximumWidth: Screen.width * 0.7
68+ property real __minimumHeight: units.gu(2)
69+ property real __maximumHeight: Screen.height * 0.7
70+
7871 signal dismissAll()
7972
8073 onCurrentItemChanged: {
@@ -86,13 +79,26 @@ UbuntuShape {
8679 }
8780
8881 onSelect: {
89− currentItem = listView.itemAt(index);
82+ currentItem = repeater.itemAt(index);
83+ if (currentItem) {
84+ if (currentItem.y < listView.contentY) {
85+ listView.contentY = currentItem.y;
86+ } else if (currentItem.y + currentItem.height > listView.contentY + listView.height) {
87+ listView.contentY = currentItem.y + currentItem.height - listView.height;
88+ }
89+ }
9090 }
9191 }
9292
93+ MouseArea {
94+ // Eat events.
95+ anchors.fill: parent
96+ }
97+
9398 Item {
9499 id: focusScope
95− anchors.fill: parent
100+ width: container.width
101+ height: container.height
96102 focus: visible
97103
98104 Keys.onUpPressed: d.selectPrevious(d.currentIndex)
@@ -108,17 +114,17 @@ UbuntuShape {
108114 id: container
109115 objectName: "container"
110116
111− width: listView.contentWidth
112− height: parent.height
117+ height: MathUtils.clamp(listView.contentHeight, d.__minimumHeight, d.__maximumHeight)
118+ width: menuColumn.width
113119 spacing: 0
114120
115− // FIXME use ListView.header - tried but was flaky with positionViewAtIndex.
121+ // Header - scroll up
116122 Item {
117− Layout.fillWidth: true;
118− Layout.maximumHeight: units.gu(3)
119− Layout.minimumHeight: units.gu(3)
123+ Layout.fillWidth: true
124+ height: units.gu(3)
120125 visible: listView.contentHeight > root.height
121126 enabled: !listView.atYBeginning
127+ z: 1
122128
123129 Rectangle {
124130 color: enabled ? theme.palette.normal.overlayText :
@@ -143,47 +149,35 @@ UbuntuShape {
143149 MouseArea {
144150 anchors.fill: parent
145151 onPressed: {
146− var index = listView.indexAt(0, listView.contentY);
147− listView.positionViewAtIndex(index-1, ListView.Beginning);
152+ var item = menuColumn.childAt(0, listView.contentY);
153+ if (item) {
154+ var previousItem = item;
155+ do {
156+ previousItem = repeater.itemAt(previousItem.__ownIndex-1);
157+ if (!previousItem) {
158+ listView.contentY = 0;
159+ return;
160+ }
161+ } while (previousItem.__isSeparator);
162+
163+ listView.contentY = previousItem.y
164+ }
148165 }
149166 }
150167 }
151168
152− ListView {
169+ // Menu Items
170+ Flickable {
153171 id: listView
154− objectName: "listView"
155− Layout.fillHeight: true
156− Layout.fillWidth: true
157− contentWidth: MathUtils.clamp(contentItem.childrenRect.width,
158− __minimumWidth,
159− __maximumWidth)
160−
161− orientation: Qt.Vertical
162− interactive: contentHeight > height
163172 clip: interactive
164− highlightFollowsCurrentItem: false
165−
166− highlight: Rectangle {
167− color: "transparent"
168− border.width: units.dp(1)
169− border.color: UbuntuColors.orange
170− z: 1
171173
172− width: listView.width
173− height: d.currentItem ? d.currentItem.height : 0
174− y: d.currentItem ? d.currentItem.y : 0
175− visible: d.currentItem
176− }
177−
178− function itemAt(index) {
179− if (index > count || index < 0) return null;
180− currentIndex = index;
181− return currentItem;
182− }
174+ Layout.fillHeight: true
175+ Layout.fillWidth: true
176+ contentHeight: menuColumn.height
177+ interactive: height < contentHeight
183178
184179 MouseArea {
185− id: menuMouseArea
186− anchors.fill: listView
180+ anchors.fill: parent
187181 hoverEnabled: true
188182 propagateComposedEvents: true // propogate events so we send clicks to children.
189183 z: 1 // on top so we override any other hovers
@@ -195,7 +189,7 @@ UbuntuShape {
195189
196190 if (!d.hoveredItem || !d.currentItem ||
197191 !d.hoveredItem.contains(Qt.point(pos.x - d.currentItem.x, pos.y - d.currentItem.y))) {
198− d.hoveredItem = listView.itemAt(listView.indexAt(pos.x, pos.y));
192+ d.hoveredItem = menuColumn.childAt(pos.x, pos.y)
199193 if (!d.hoveredItem || !d.hoveredItem.enabled)
200194 return false;
201195 d.currentItem = d.hoveredItem;
@@ -216,89 +210,123 @@ UbuntuShape {
216210 }
217211 }
218212
219− delegate: Loader {
220− id: loader
221− objectName: root.objectName + "-item" + __ownIndex
213+ ColumnLayout {
214+ id: menuColumn
215+ spacing: 0
222216
223− property int __ownIndex: index
217+ width: MathUtils.clamp(implicitWidth, d.__minimumWidth, d.__maximumWidth)
224218
225− width: root.width
226− enabled: model.isSeparator ? false : model.sensitive
219+ Repeater {
220+ id: repeater
227221
228− sourceComponent: {
229− if (model.isSeparator) {
230− return separatorComponent;
231− }
232− return menuItemComponent;
233− }
222+ Loader {
223+ id: loader
224+ objectName: root.objectName + "-item" + __ownIndex
234225
235− property Item popup: null
236−
237− Component {
238− id: menuItemComponent
239− MenuItem {
240− id: menuItem
241− menuData: model
242− objectName: loader.objectName + "-actionItem"
243−
244− action.onTriggered: {
245− d.currentItem = loader;
246−
247− if (hasSubmenu) {
248− if (!popup) {
249− var model = root.unityMenuModel.submenu(__ownIndex);
250− popup = submenuComponent.createObject(focusScope, {
251− objectName: loader.objectName + "-",
252− unityMenuModel: model,
253− x: Qt.binding(function() { return root.width }),
254− y: Qt.binding(function() { return loader.y })
255− });
256− } else if (popup) {
257− popup.visible = true;
258− }
259− popup.retreat.connect(function() {
260− popup.destroy();
261− popup = null;
262− menuItem.forceActiveFocus();
263− })
264− } else {
265− root.unityMenuModel.activate(__ownIndex);
226+ property int __ownIndex: index
227+ property bool __isSeparator: model.isSeparator
228+
229+ enabled: __isSeparator ? false : model.sensitive
230+
231+ sourceComponent: {
232+ if (model.isSeparator) {
233+ return separatorComponent;
266234 }
235+ return menuItemComponent;
267236 }
268237
269− Connections {
270− target: d
271− onCurrentIndexChanged: {
272− if (popup && d.currentIndex != __ownIndex) {
273− popup.visible = false;
238+ property Item popup: null
239+
240+ Layout.fillWidth: true
241+
242+ Component {
243+ id: menuItemComponent
244+ MenuItem {
245+ id: menuItem
246+ menuData: model
247+ objectName: loader.objectName + "-actionItem"
248+
249+ width: MathUtils.clamp(implicitWidth, d.__minimumWidth, d.__maximumWidth)
250+
251+ action.onTriggered: {
252+ d.currentItem = loader;
253+
254+ if (hasSubmenu) {
255+ if (!popup) {
256+ var model = root.unityMenuModel.submenu(__ownIndex);
257+ popup = submenuComponent.createObject(focusScope, {
258+ objectName: loader.objectName + "-",
259+ unityMenuModel: model,
260+ x: Qt.binding(function() { return root.width }),
261+ y: Qt.binding(function() {
262+ var dummy = listView.contentY; // force a recalc on contentY change.
263+ return mapToItem(container, 0, y).y;
264+ })
265+ });
266+ } else if (popup) {
267+ popup.visible = true;
268+ }
269+ popup.retreat.connect(function() {
270+ popup.destroy();
271+ popup = null;
272+ menuItem.forceActiveFocus();
273+ })
274+ } else {
275+ root.unityMenuModel.activate(__ownIndex);
276+ }
274277 }
275− }
276− onDismissAll: {
277− if (popup) {
278− popup.destroy();
279− popup = null;
278+
279+ Connections {
280+ target: d
281+ onCurrentIndexChanged: {
282+ if (popup && d.currentIndex != __ownIndex) {
283+ popup.visible = false;
284+ }
285+ }
286+ onDismissAll: {
287+ if (popup) {
288+ popup.destroy();
289+ popup = null;
290+ }
291+ }
280292 }
281293 }
282294 }
283− }
284− }
285295
286− Component {
287− id: separatorComponent
288− ListItems.ThinDivider {
289− objectName: loader.objectName + "-separator"
296+ Component {
297+ id: separatorComponent
298+ ListItems.ThinDivider {
299+ objectName: loader.objectName + "-separator"
300+ implicitHeight: units.dp(2)
301+ }
302+ }
290303 }
304+
291305 }
292306 }
293− } // ListView
294307
295− // FIXME use ListView.footer - tried but was flaky with positionViewAtIndex.
308+ // Highlight
309+ Rectangle {
310+ color: "transparent"
311+ border.width: units.dp(1)
312+ border.color: UbuntuColors.orange
313+ z: 1
314+
315+ width: listView.width
316+ height: d.currentItem ? d.currentItem.height : 0
317+ y: d.currentItem ? d.currentItem.y : 0
318+ visible: d.currentItem
319+ }
320+
321+ } // Flickable
322+
323+ // Header - scroll down
296324 Item {
297− Layout.fillWidth: true;
298− Layout.maximumHeight: units.gu(3)
299− Layout.minimumHeight: units.gu(3)
325+ Layout.fillWidth: true
326+ height: units.gu(3)
300327 visible: listView.contentHeight > root.height
301328 enabled: !listView.atYEnd
329+ z: 1
302330
303331 Rectangle {
304332 color: enabled ? theme.palette.normal.overlayText :
@@ -323,8 +351,19 @@ UbuntuShape {
323351 MouseArea {
324352 anchors.fill: parent
325353 onPressed: {
326− var index = listView.indexAt(0, listView.contentY);
327− listView.positionViewAtIndex(index+1, ListView.Beginning);
354+ var item = menuColumn.childAt(0, listView.contentY + listView.height);
355+ if (item) {
356+ var nextItem = item;
357+ do {
358+ nextItem = repeater.itemAt(nextItem.__ownIndex+1);
359+ if (!nextItem) {
360+ listView.contentY = listView.contentHeight - listView.height;
361+ return;
362+ }
363+ } while (nextItem.__isSeparator);
364+
365+ listView.contentY = nextItem.y - listView.height
366+ }
328367 }
329368 }
330369 }
qml/Components/Dialogs.qml+42−3
@@ -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
@@ -24,10 +24,13 @@ import Unity.Platform 1.0
2424import Utils 0.1
2525import "../Greeter"
2626
27−Item {
27+MouseArea {
2828 id: root
29+ acceptedButtons: Qt.AllButtons
30+ hoverEnabled: true
31+ onWheel: wheel.accepted = true
2932
30− readonly property alias hasActiveDialog: dialogLoader.active
33+ readonly property bool hasActiveDialog: dialogLoader.active || d.modeSwitchWarningPopup
3134
3235 // to be set from outside, useful mostly for testing purposes
3336 property var unitySessionService: DBusUnitySessionService
@@ -42,6 +45,7 @@ Item {
4245 }
4346 property string usageScenario
4447 property size screenSize: Qt.size(Screen.width, Screen.height)
48+ property bool hasKeyboard: false
4549
4650 signal powerOffClicked();
4751
@@ -120,6 +124,11 @@ Item {
120124 onTriggered: LightDMService.greeter.showGreeter()
121125 }
122126
127+ GlobalShortcut { // lock screen
128+ shortcut: Qt.MetaModifier|Qt.Key_L
129+ onTriggered: LightDMService.greeter.showGreeter()
130+ }
131+
123132 QtObject {
124133 id: d // private stuff
125134 objectName: "dialogsPrivate"
@@ -140,6 +149,25 @@ Item {
140149 objectName: "dialogLoader"
141150 anchors.fill: parent
142151 active: false
152+ onActiveChanged: {
153+ if (!active) {
154+ if (previousFocusedItem) {
155+ previousFocusedItem.forceActiveFocus(Qt.OtherFocusReason);
156+ previousFocusedItem = undefined;
157+ }
158+ previousSourceComponent = undefined;
159+ sourceComponent = undefined;
160+ }
161+ }
162+ onSourceComponentChanged: {
163+ if (previousSourceComponent !== sourceComponent) {
164+ previousSourceComponent = sourceComponent;
165+ previousFocusedItem = window.activeFocusItem;
166+ }
167+ }
168+
169+ property var previousSourceComponent: undefined
170+ property var previousFocusedItem: undefined
143171 }
144172
145173 Component {
@@ -149,13 +177,16 @@ Item {
149177 title: i18n.ctr("Title: Lock/Log out dialog", "Log out")
150178 text: i18n.tr("Are you sure you want to log out?")
151179 Button {
180+ width: parent.width
152181 text: i18n.ctr("Button: Lock the system", "Lock")
153182 onClicked: {
154183 LightDMService.greeter.showGreeter()
155184 logoutDialog.hide();
156185 }
186+ Component.onCompleted: if (root.hasKeyboard) forceActiveFocus(Qt.TabFocusReason)
157187 }
158188 Button {
189+ width: parent.width
159190 focus: true
160191 text: i18n.ctr("Button: Log out from the system", "Log Out")
161192 onClicked: {
@@ -164,6 +195,7 @@ Item {
164195 }
165196 }
166197 Button {
198+ width: parent.width
167199 text: i18n.tr("Cancel")
168200 onClicked: {
169201 logoutDialog.hide();
@@ -179,12 +211,14 @@ Item {
179211 title: i18n.ctr("Title: Reboot dialog", "Reboot")
180212 text: i18n.tr("Are you sure you want to reboot?")
181213 Button {
214+ width: parent.width
182215 text: i18n.tr("No")
183216 onClicked: {
184217 rebootDialog.hide();
185218 }
186219 }
187220 Button {
221+ width: parent.width
188222 focus: true
189223 text: i18n.tr("Yes")
190224 onClicked: {
@@ -193,6 +227,7 @@ Item {
193227 rebootDialog.hide();
194228 }
195229 color: theme.palette.normal.negative
230+ Component.onCompleted: if (root.hasKeyboard) forceActiveFocus(Qt.TabFocusReason)
196231 }
197232 }
198233 }
@@ -204,6 +239,7 @@ Item {
204239 title: i18n.ctr("Title: Power off/Restart dialog", "Power")
205240 text: i18n.tr("Are you sure you would like\nto power off?")
206241 Button {
242+ width: parent.width
207243 focus: true
208244 text: i18n.ctr("Button: Power off the system", "Power off")
209245 onClicked: {
@@ -212,8 +248,10 @@ Item {
212248 root.powerOffClicked();
213249 }
214250 color: theme.palette.normal.negative
251+ Component.onCompleted: if (root.hasKeyboard) forceActiveFocus(Qt.TabFocusReason)
215252 }
216253 Button {
254+ width: parent.width
217255 text: i18n.ctr("Button: Restart the system", "Restart")
218256 onClicked: {
219257 root.closeAllApps();
@@ -222,6 +260,7 @@ Item {
222260 }
223261 }
224262 Button {
263+ width: parent.width
225264 text: i18n.tr("Cancel")
226265 onClicked: {
227266 powerDialog.hide();
qml/Components/KeyboardShortcutsOverlay.qml+1−1
@@ -77,7 +77,7 @@ Rectangle {
7777 font.weight: Font.Medium
7878 }
7979 Label {
80− text: i18n.tr("Takes a screenshot of a window.")
80+ text: i18n.tr("Takes a screenshot of the current window.")
8181 fontSize: "small"
8282 font.weight: Font.Light
8383 wrapMode: Text.Wrap
qml/Components/KeymapSwitcher.qml+15−7
@@ -51,6 +51,9 @@ QtObject {
5151 nextIndex = currentKeymapIndex + 1;
5252 }
5353 currentKeymapIndex = nextIndex;
54+ if (actionGroup.currentAction.valid) {
55+ actionGroup.currentAction.updateState(currentKeymapIndex);
56+ }
5457 }
5558
5659 function previousKeymap() {
@@ -60,30 +63,35 @@ QtObject {
6063 prevIndex = currentKeymapIndex - 1;
6164 }
6265 currentKeymapIndex = prevIndex;
66+ if (actionGroup.currentAction.valid) {
67+ actionGroup.currentAction.updateState(currentKeymapIndex);
68+ }
6369 }
6470
65− property Binding surfaceKeymapBinding: Binding {
71+ property Binding surfaceKeymapBinding: Binding { // NB: needed mainly for xmir & libertine apps
6672 target: root.focusedSurface
6773 property: "keymap"
6874 value: root.currentKeymap
6975 }
7076
77+ property Binding unityKeymapBinding: Binding {
78+ target: Mir
79+ property: "currentKeymap"
80+ value: root.currentKeymap
81+ }
82+
7183 // indicator
7284 property QDBusActionGroup actionGroup: QDBusActionGroup {
7385 busType: DBus.SessionBus
7486 busName: "com.canonical.indicator.keyboard"
7587 objectPath: "/com/canonical/indicator/keyboard"
7688
77− property variant currentAction: action("current")
78− property variant activeAction: action("active")
89+ property variant currentAction: action("current") // the one that's checked by the indicator
90+ property variant activeAction: action("active") // the one that we clicked
7991
8092 Component.onCompleted: actionGroup.start();
8193 }
8294
83− onCurrentKeymapIndexChanged: {
84− actionGroup.currentAction.updateState(currentKeymapIndex);
85− }
86−
8795 readonly property int activeActionState: actionGroup.activeAction.valid ? actionGroup.activeAction.state : -1
8896
8997 onActiveActionStateChanged: {
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+21−3
@@ -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,9 +33,9 @@ Dialog {
3233 // NB: PopupBase, Dialog's superclass, will check for the existence of this property
3334 property bool reparentToRootItem: false
3435
35− onVisibleChanged: { if (!visible && dialogLoader) { dialogLoader.active = false; } }
36+ default property alias columnContents: column.data
3637
37− Keys.onEscapePressed: hide()
38+ onVisibleChanged: { if (!visible && dialogLoader) { dialogLoader.active = false; } }
3839
3940 focus: true
4041
@@ -50,4 +51,21 @@ Dialog {
5051 __foreground.theme = themeHack
5152 show();
5253 }
54+
55+ TabFocusFence {
56+ width: parent.width
57+ height: column.height
58+ focus: true
59+ Column {
60+ id: column
61+ width: parent.width
62+ spacing: units.gu(2)
63+ }
64+ Keys.onDownPressed: {
65+ event.accepted = focusNext();
66+ }
67+ Keys.onUpPressed: {
68+ event.accepted = focusPrev();
69+ }
70+ }
5371}
qml/Greeter/Circle.qml+1−1
@@ -27,7 +27,7 @@ Canvas {
2727 onCenterCircleChanged: requestPaint()
2828
2929 onPaint: {
30− if (circleScale <= 0) {
30+ if (circleScale <= 0 || width <= 0 || height <= 0) {
3131 return;
3232 }
3333
qml/Greeter/FullLightDMImpl.qml+1
@@ -22,6 +22,7 @@ Item{
2222
2323 property var greeter: LightDM.Greeter
2424 property var infographic: LightDM.Infographic
25+ property var prompts: LightDM.Prompts
2526 property var sessions: LightDM.Sessions
2627 property var sessionRoles: LightDM.SessionRoles
2728 property var users: LightDM.Users
qml/Greeter/Greeter.qml+27−57
@@ -74,11 +74,11 @@ Showable {
7474 forcedUnlock = false;
7575 if (required) {
7676 if (loader.item) {
77− loader.item.reset(true /* forceShow */);
77+ loader.item.forceShow();
7878 }
7979 // Normally loader.onLoaded will select a user, but if we're
8080 // already shown, do it manually.
81− d.selectUser(d.currentIndex, false);
81+ d.selectUser(d.currentIndex);
8282 }
8383
8484 // Even though we may already be shown, we want to call show() for its
@@ -154,7 +154,7 @@ Showable {
154154 readonly property bool multiUser: LightDMService.users.count > 1
155155 readonly property int selectUserIndex: d.getUserIndex(LightDMService.greeter.selectUser)
156156 property int currentIndex: Math.max(selectUserIndex, 0)
157− property bool waiting
157+ readonly property bool waiting: LightDMService.prompts.count == 0 && !root.forcedUnlock
158158 property bool isLockscreen // true when we are locking an active session, rather than first user login
159159 readonly property bool secureFingerprint: isLockscreen &&
160160 AccountsService.failedFingerprintLogins <
@@ -189,13 +189,9 @@ Showable {
189189 return -1;
190190 }
191191
192− function selectUser(index, reset) {
192+ function selectUser(index) {
193193 if (index < 0 || index >= LightDMService.users.count)
194194 return;
195− d.waiting = true;
196− if (reset) {
197− loader.item.reset(false /* forceShow */);
198− }
199195 currentIndex = index;
200196 var user = LightDMService.users.data(index, LightDMService.userRoles.NameRole);
201197 AccountsService.user = user;
@@ -206,20 +202,17 @@ Showable {
206202 function hideView() {
207203 if (loader.item) {
208204 loader.item.enabled = false; // drop OSK and prevent interaction
209− loader.item.notifyAuthenticationSucceeded(false /* showFakePassword */);
210205 loader.item.hide();
211206 }
212207 }
213208
214209 function login() {
215− d.waiting = true;
216210 if (LightDMService.greeter.startSessionSync(root.sessionToStart())) {
217211 sessionStarted();
218212 hideView();
219213 } else if (loader.item) {
220214 loader.item.notifyAuthenticationFailed();
221215 }
222− d.waiting = false;
223216 }
224217
225218 function startUnlock(toTheRight) {
@@ -239,27 +232,13 @@ Showable {
239232 }
240233 }
241234
242− function showPromptMessage(text, isError) {
243− // inefficient, but we only rarely deal with messages
244− var html = text.replace(/&/g, "&amp;")
245− .replace(/</g, "&lt;")
246− .replace(/>/g, "&gt;")
247− .replace(/\n/g, "<br>");
248− if (isError) {
249− html = "<font color=\"#df382c\">" + html + "</font>";
250− }
251−
252− if (loader.item) {
253− loader.item.showMessage(html);
254− }
255− }
256−
257235 function showFingerprintMessage(msg) {
236+ d.selectUser(d.currentIndex);
237+ LightDMService.prompts.prepend(msg, LightDMService.prompts.Error);
258238 if (loader.item) {
259− loader.item.reset(false /* forceShow */);
260239 loader.item.showErrorMessage(msg);
240+ loader.item.notifyAuthenticationFailed();
261241 }
262− showPromptMessage(msg, true);
263242 }
264243 }
265244
@@ -285,7 +264,6 @@ Showable {
285264
286265 onRequiredChanged: {
287266 if (required) {
288− d.waiting = true;
289267 lockedApp = "";
290268 }
291269 }
@@ -377,14 +355,14 @@ Showable {
377355 onLoaded: {
378356 root.lockedApp = "";
379357 item.forceActiveFocus();
380− d.selectUser(d.currentIndex, true);
358+ d.selectUser(d.currentIndex);
381359 LightDMService.infographic.readyForDataChange();
382360 }
383361
384362 Connections {
385363 target: loader.item
386364 onSelected: {
387− d.selectUser(index, true);
365+ d.selectUser(index);
388366 }
389367 onResponded: {
390368 if (root.locked) {
@@ -481,27 +459,15 @@ Showable {
481459 onShowGreeter: root.forceShow()
482460 onHideGreeter: root.forcedUnlock = true
483461
484− onShowMessage: d.showPromptMessage(text, isError)
485−
486− onShowPrompt: {
487− if (loader.item) {
488− loader.item.showPrompt(text, isSecret, isDefaultPrompt);
462+ onLoginError: {
463+ if (!loader.item) {
464+ return;
489465 }
490466
491− d.waiting = false;
492− }
493−
494− onAuthenticationComplete: {
495− d.waiting = false;
467+ loader.item.notifyAuthenticationFailed();
496468
497− if (LightDMService.greeter.authenticated) {
498− if (!LightDMService.greeter.promptless) {
499− d.login();
500− }
501− } else {
502− if (!LightDMService.greeter.promptless) {
503− AccountsService.failedLogins++;
504− }
469+ if (!automatic) {
470+ AccountsService.failedLogins++;
505471
506472 // Check if we should initiate a factory reset
507473 if (maxFailedLogins >= 2) { // require at least a warning
@@ -519,14 +485,17 @@ Showable {
519485 forcedDelayTimer.forceDelay();
520486 }
521487
522− loader.item.notifyAuthenticationFailed();
523− if (!LightDMService.greeter.promptless) {
524− d.selectUser(d.currentIndex, false);
525− }
488+ d.selectUser(d.currentIndex);
489+ }
490+ }
491+
492+ onLoginSuccess: {
493+ if (!automatic) {
494+ d.login();
526495 }
527496 }
528497
529− onRequestAuthenticationUser: d.selectUser(d.getUserIndex(user), true)
498+ onRequestAuthenticationUser: d.selectUser(d.getUserIndex(user))
530499 }
531500
532501 Connections {
@@ -589,7 +558,9 @@ Showable {
589558 if (!d.secureFingerprint) {
590559 d.startUnlock(false /* toTheRight */); // use normal login instead
591560 }
592− var msg = d.secureFingerprint ? i18n.tr("Try again") : "";
561+ var msg = d.secureFingerprint ? i18n.tr("Try again") :
562+ d.alphanumeric ? i18n.tr("Enter passphrase to unlock") :
563+ i18n.tr("Enter passcode to unlock");
593564 d.showFingerprintMessage(msg);
594565 }
595566
@@ -609,8 +580,7 @@ Showable {
609580 }
610581 console.log("Identified user by fingerprint:", result);
611582 if (loader.item) {
612− loader.item.enabled = false;
613− loader.item.notifyAuthenticationSucceeded(true /* showFakePassword */);
583+ loader.item.showFakePassword();
614584 }
615585 if (root.active)
616586 root.forcedUnlock = true;
qml/Greeter/GreeterPrompt.qml+32−31
@@ -27,31 +27,25 @@ FocusScope {
2727 property bool isAlphanumeric
2828 property string text
2929 property bool isSecret
30+ property bool interactive: true
31+ readonly property alias enteredText: passwordInput.text
3032
3133 signal clicked()
3234 signal canceled()
33− signal responded(string text)
34−
35− function reset() {
36− passwordInput.text = "";
37− fakeLabel.text = "";
38− d.enabled = true;
39− }
35+ signal accepted()
4036
4137 function showFakePassword() {
4238 // Just a silly hack for looking like 4 pin numbers got entered, if
4339 // a fingerprint was used and we happen to be using a pin. This was
4440 // a request from Design.
4541 if (isSecret && isPrompt && !isAlphanumeric) {
46− d.enabled = false;
47− text = "...."; // actual text doesn't matter
42+ passwordInput.text = "...."; // actual text doesn't matter
4843 }
4944 }
5045
5146 StyledItem {
5247 id: d
5348
54− property bool enabled: true
5549 readonly property color textColor: passwordInput.enabled ? theme.palette.normal.raisedText
5650 : theme.palette.disabled.raisedText
5751 readonly property color selectedColor: passwordInput.enabled ? theme.palette.normal.raised
@@ -60,12 +54,6 @@ FocusScope {
6054 : theme.palette.disabled.raisedSecondaryText
6155 readonly property color errorColor: passwordInput.enabled ? theme.palette.normal.negative
6256 : theme.palette.disabled.negative
63−
64− onEnabledChanged: {
65− if (!enabled) {
66− fakeLabel.text = passwordInput.displayText;
67− }
68− }
6957 }
7058
7159 Rectangle {
@@ -86,15 +74,17 @@ FocusScope {
8674 }
8775 }
8876
89− Rectangle {
77+ StyledItem {
9078 id: promptButton
9179 objectName: "promptButton"
9280 anchors.fill: parent
9381 visible: !root.isPrompt
82+ activeFocusOnTab: true
83+
84+ styleName: "FocusShape"
9485
9586 function triggered() {
96− if (d.enabled) {
97− d.enabled = false;
87+ if (root.interactive) {
9888 root.clicked();
9989 }
10090 }
@@ -109,6 +99,7 @@ FocusScope {
10999 }
110100 }
111101
102+ Keys.onSpacePressed: triggered();
112103 Keys.onReturnPressed: triggered();
113104 Keys.onEnterPressed: triggered();
114105 MouseArea {
@@ -129,6 +120,7 @@ FocusScope {
129120 anchors.fill: parent
130121 visible: root.isPrompt
131122 opacity: fakeLabel.visible ? 0 : 1
123+ activeFocusOnTab: true
132124
133125 validator: RegExpValidator {
134126 regExp: root.isAlphanumeric ? /^.*$/ : /^\d{4}$/
@@ -142,15 +134,23 @@ FocusScope {
142134
143135 readonly property real frameSpacing: units.gu(0.5)
144136
145− style: Item {
146− property color color: d.textColor
147− property color selectedTextColor: d.selectedColor
148− property color selectionColor: d.textColor
149− property color borderColor: "transparent"
150− property color backgroundColor: "transparent"
151− property color errorColor: d.errorColor
152− property real frameSpacing: passwordInput.frameSpacing
137+ style: StyledItem {
153138 anchors.fill: parent
139+ styleName: "FocusShape"
140+
141+ // Properties needed by TextField
142+ readonly property color color: d.textColor
143+ readonly property color selectedTextColor: d.selectedColor
144+ readonly property color selectionColor: d.textColor
145+ readonly property color borderColor: "transparent"
146+ readonly property color backgroundColor: "transparent"
147+ readonly property color errorColor: d.errorColor
148+ readonly property real frameSpacing: styledItem.frameSpacing
149+
150+ // Properties needed by FocusShape
151+ readonly property bool enabled: styledItem.enabled
152+ readonly property bool keyNavigationFocus: styledItem.keyNavigationFocus
153+ property bool activeFocusOnTab
154154 }
155155
156156 secondaryItem: [
@@ -178,9 +178,8 @@ FocusScope {
178178 onAccepted: respond()
179179
180180 function respond() {
181− if (d.enabled) {
182− d.enabled = false;
183− root.responded(text);
181+ if (root.interactive) {
182+ root.accepted();
184183 }
185184 }
186185
@@ -194,6 +193,7 @@ FocusScope {
194193 // palette color, whereas we want raisedSecondaryText.
195194 Label {
196195 id: hint
196+ objectName: "promptHint"
197197 anchors {
198198 left: parent.left
199199 right: parent.right
@@ -223,6 +223,7 @@ FocusScope {
223223 anchors.leftMargin: passwordInput.frameSpacing * 2
224224 anchors.rightMargin: passwordInput.frameSpacing * 2 + capsIcon.visibleWidth
225225 color: d.drawColor
226− visible: root.isPrompt && !d.enabled
226+ text: passwordInput.displayText
227+ visible: root.isPrompt && !root.interactive
227228 }
228229}
qml/Greeter/IntegratedLightDMImpl.qml+1
@@ -22,6 +22,7 @@ Item{
2222
2323 property var greeter: LightDM.Greeter
2424 property var infographic: LightDM.Infographic
25+ property var prompts: LightDM.Prompts
2526 property var sessions: LightDM.Sessions
2627 property var sessionRoles: LightDM.SessionRoles
2728 property var users: LightDM.Users
qml/Greeter/LightDMService.qml+4−4
@@ -27,6 +27,7 @@ Loader {
2727
2828 property var greeter: d.valid ? loader.item.greeter : null
2929 property var infographic: d.valid ? loader.item.infographic : null
30+ property var prompts: d.valid ? loader.item.prompts : null
3031 property var sessions: d.valid ? loader.item.sessions : null
3132 property var sessionRoles: d.valid ? loader.item.sessionRoles : null
3233 property var users: d.valid ? loader.item.users : null
@@ -35,10 +36,9 @@ Loader {
3536 // This trickery handles cases where applicationArguments aren't provided
3637 // such as during testing
3738 property var fullLightDM: {
38− if (typeof applicationArguments !== "undefined") {
39− if (applicationArguments.mode === "greeter") {
40− return true;
41− }
39+ if (typeof applicationArguments === "undefined" ||
40+ applicationArguments.mode === "greeter") {
41+ return true;
4242 }
4343 return false;
4444 }
qml/Greeter/LoginList.qml+42−111
@@ -25,95 +25,50 @@ StyledItem {
2525 focus: true
2626
2727 property alias model: userList.model
28− property bool alphanumeric: true
28+ property alias alphanumeric: promptList.alphanumeric
2929 property int currentIndex
3030 property bool locked
3131 property bool waiting
3232 property alias boxVerticalOffset: highlightItem.y
3333
34− readonly property alias passwordInput: passwordInput
3534 readonly property int numAboveBelow: 4
3635 readonly property int cellHeight: units.gu(5)
37− readonly property int highlightedHeight: units.gu(15)
36+ readonly property int highlightedHeight: highlightItem.height
3837 readonly property int moveDuration: UbuntuAnimation.FastDuration
39− property string selectedSession
4038 property string currentSession
4139 readonly property string currentUser: userList.currentItem.username
42− property bool wasPrompted: false
4340
44− signal loginListSessionChanged(string session)
4541 signal responded(string response)
4642 signal selected(int index)
4743 signal sessionChooserButtonClicked()
4844
4945 function tryToUnlock() {
50− if (wasPrompted) {
51− passwordInput.forceActiveFocus();
52− } else {
53− if (root.locked) {
54− root.selected(currentIndex);
55− } else {
56− root.responded("");
57− }
58− }
59− }
60−
61− function showMessage(html) {
62− if (infoLabel.text === "") {
63− infoLabel.text = html;
64− } else {
65− infoLabel.text += "<br>" + html;
66− }
67− }
68−
69− function showPrompt(text, isSecret, isDefaultPrompt) {
70− passwordInput.text = isDefaultPrompt ? alphanumeric ? i18n.tr("Passphrase")
71− : i18n.tr("Passcode")
72− : text;
73− passwordInput.isPrompt = true;
74− passwordInput.isSecret = isSecret;
75− passwordInput.reset();
76− wasPrompted = true;
46+ promptList.forceActiveFocus();
7747 }
7848
7949 function showError() {
8050 wrongPasswordAnimation.start();
81− root.resetAuthentication();
82− }
83−
84− function reset() {
85− root.resetAuthentication();
8651 }
8752
8853 function showFakePassword() {
89− passwordInput.showFakePassword();
90− }
91−
92− QtObject {
93− id: d
94−
95− function checkIfPromptless() {
96− if (!waiting && !wasPrompted) {
97− passwordInput.isPrompt = false;
98− passwordInput.text = root.locked ? i18n.tr("Retry")
99− : i18n.tr("Log In")
100− }
101− }
54+ promptList.interactive = false;
55+ promptList.showFakePassword();
10256 }
10357
104− onWaitingChanged: d.checkIfPromptless()
105− onLockedChanged: d.checkIfPromptless()
106−
10758 theme: ThemeSettings {
10859 name: "Ubuntu.Components.Themes.Ambiance"
10960 }
11061
11162 Keys.onUpPressed: {
112− selected(currentIndex - 1);
63+ if (currentIndex > 0) {
64+ selected(currentIndex - 1);
65+ }
11366 event.accepted = true;
11467 }
11568 Keys.onDownPressed: {
116− selected(currentIndex + 1);
69+ if (currentIndex + 1 < model.count) {
70+ selected(currentIndex + 1);
71+ }
11772 event.accepted = true;
11873 }
11974 Keys.onEscapePressed: {
@@ -135,7 +90,8 @@ StyledItem {
13590 rightMargin: units.gu(2)
13691 }
13792
138− height: root.highlightedHeight
93+ height: Math.max(units.gu(15), promptList.height + units.gu(8))
94+ Behavior on height { NumberAnimation { duration: root.moveDuration; easing.type: Easing.InOutQuad; } }
13995 }
14096
14197 ListView {
@@ -153,14 +109,8 @@ StyledItem {
153109 interactive: count > 1
154110
155111 readonly property bool movingInternally: moveTimer.running || userList.moving
156− onMovingInternallyChanged: {
157− if (!movingInternally) {
158− root.selected(currentIndex);
159− }
160− }
161112
162113 onCurrentIndexChanged: {
163− root.resetAuthentication();
164114 moveTimer.start();
165115 }
166116
@@ -203,7 +153,10 @@ StyledItem {
203153 // Add an offset to bottomMargin for any items below the highlight
204154 bottomMargin: -(units.gu(4) + (parent.belowHighlight ? parent.belowOffset : 0))
205155 }
206− text: realName
156+ text: userList.currentIndex === index
157+ && name === "*other"
158+ && LightDMService.greeter.authenticationUser !== ""
159+ ? LightDMService.greeter.authenticationUser : realName
207160 color: userList.currentIndex !== index ? theme.palette.normal.raised
208161 : theme.palette.normal.raisedText
209162
@@ -309,64 +262,42 @@ StyledItem {
309262 }
310263 }
311264
312− FadingLabel {
313− id: infoLabel
314− objectName: "infoLabel"
315− anchors {
316− bottom: passwordInput.top
317− left: highlightItem.left
318− topMargin: units.gu(1)
319− bottomMargin: units.gu(1)
320− leftMargin: units.gu(2)
321− rightMargin: units.gu(1)
322− }
323−
324− color: theme.palette.normal.raisedText
325− width: root.width - anchors.leftMargin - anchors.rightMargin
326− fontSize: "small"
327− textFormat: Text.StyledText
328−
329− opacity: (userList.movingInternally || text == "") ? 0 : 1
330− Behavior on opacity {
331− NumberAnimation { duration: 100 }
332− }
333− }
334−
335− GreeterPrompt {
336− id: passwordInput
337− objectName: "passwordInput"
265+ PromptList {
266+ id: promptList
267+ objectName: "promptList"
338268 anchors {
339269 bottom: highlightItem.bottom
340270 horizontalCenter: highlightItem.horizontalCenter
341271 margins: units.gu(2)
342272 }
343273 width: highlightItem.width - anchors.margins * 2
344− opacity: userList.movingInternally ? 0 : 1
345−
346− activeFocusOnTab: true
347− isAlphanumeric: root.alphanumeric
348274
349− onClicked: root.tryToUnlock()
350− onResponded: root.responded(text)
351− onCanceled: root.selected(currentIndex)
352−
353− Behavior on opacity {
354− NumberAnimation { duration: 100 }
275+ onClicked: {
276+ interactive = false;
277+ if (root.locked) {
278+ root.selected(currentIndex);
279+ } else {
280+ root.responded("");
281+ }
282+ }
283+ onResponded: {
284+ interactive = false;
285+ root.responded(text);
286+ }
287+ onCanceled: {
288+ interactive = false;
289+ root.selected(currentIndex);
355290 }
356291
357− WrongPasswordAnimation {
358− id: wrongPasswordAnimation
359− objectName: "wrongPasswordAnimation"
360− target: passwordInput
292+ Connections {
293+ target: LightDMService.prompts
294+ onModelReset: promptList.interactive = true
361295 }
362296 }
363297
364− function resetAuthentication() {
365− if (!userList.currentItem) {
366− return;
367− }
368− infoLabel.text = "";
369− passwordInput.reset();
370− root.wasPrompted = false;
298+ WrongPasswordAnimation {
299+ id: wrongPasswordAnimation
300+ objectName: "wrongPasswordAnimation"
301+ target: promptList
371302 }
372303}
qml/Greeter/NarrowView.qml+11−21
@@ -48,14 +48,6 @@ FocusScope {
4848 signal tease()
4949 signal emergencyCall()
5050
51− function showMessage(html) {
52− loginList.showMessage(html);
53− }
54−
55− function showPrompt(text, isSecret, isDefaultPrompt) {
56− loginList.showPrompt(text, isSecret, isDefaultPrompt);
57− }
58−
5951 function showLastChance() {
6052 /* TODO: when we finish support for resetting device after too many
6153 failed logins, we should re-add this popup.
@@ -75,10 +67,8 @@ FocusScope {
7567 coverPage.hide();
7668 }
7769
78− function notifyAuthenticationSucceeded(showFakePassword) {
79− if (showFakePassword) {
80− loginList.showFakePassword();
81− }
70+ function showFakePassword() {
71+ loginList.showFakePassword();
8272 }
8373
8474 function notifyAuthenticationFailed() {
@@ -89,11 +79,8 @@ FocusScope {
8979 coverPage.showErrorMessage(msg);
9080 }
9181
92− function reset(forceShow) {
93− loginList.reset();
94− if (forceShow) {
95− coverPage.show();
96− }
82+ function forceShow() {
83+ coverPage.show();
9784 }
9885
9986 function tryToUnlock(toTheRight) {
@@ -120,6 +107,7 @@ FocusScope {
120107 objectName: "lockscreen"
121108 anchors.fill: parent
122109 shown: false
110+ opacity: 0
123111
124112 showAnimation: StandardAnimation { property: "opacity"; to: 1 }
125113 hideAnimation: StandardAnimation { property: "opacity"; to: 0 }
@@ -195,10 +183,12 @@ FocusScope {
195183 onClicked: hide()
196184
197185 onShowProgressChanged: {
198− if (showProgress === 1) {
199− loginList.reset();
200− } else if (showProgress === 0) {
201− loginList.tryToUnlock();
186+ if (showProgress === 0) {
187+ if (lockscreen.shown) {
188+ loginList.tryToUnlock();
189+ } else {
190+ root.responded("");
191+ }
202192 }
203193 }
204194
qml/Greeter/PromptList.qmlnew file+148
@@ -0,0 +1,148 @@
1+/*
2+ * Copyright (C) 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 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 Ubuntu.Components 1.3
19+import "../Components"
20+import "." 0.1
21+
22+FocusScope {
23+ id: root
24+ height: childrenRect.height
25+
26+ property bool alphanumeric: true
27+ property bool interactive: true
28+
29+ signal responded(string text)
30+ signal clicked()
31+ signal canceled()
32+
33+ function showFakePassword() {
34+ for (var i = 0; i < repeater.count; i++) {
35+ var item = repeater.itemAt(i).item;
36+ if (item.isPrompt) {
37+ item.showFakePassword();
38+ }
39+ }
40+ }
41+
42+ QtObject {
43+ id: d
44+
45+ function sendResponse() {
46+ for (var i = 0; i < repeater.count; i++) {
47+ var item = repeater.itemAt(i).item;
48+ if (item.isPrompt) {
49+ root.responded(item.enteredText);
50+ }
51+ }
52+ }
53+ }
54+
55+ Column {
56+ width: parent.width
57+ spacing: units.gu(0.5)
58+
59+ Repeater {
60+ id: repeater
61+ model: LightDMService.prompts
62+
63+ delegate: Loader {
64+ id: loader
65+
66+ readonly property bool isLabel: model.type == LightDMService.prompts.Message ||
67+ model.type == LightDMService.prompts.Error
68+ readonly property var modelData: model
69+
70+ sourceComponent: isLabel ? infoLabel : greeterPrompt
71+
72+ onLoaded: {
73+ for (var i = 0; i < repeater.count; i++) {
74+ var item = repeater.itemAt(i);
75+ if (item && !item.isLabel) {
76+ item.focus = true;
77+ break;
78+ }
79+ }
80+ loader.item.opacity = 1;
81+ }
82+
83+ Binding {
84+ target: loader.item
85+ property: "model"
86+ value: loader.modelData
87+ }
88+ }
89+ }
90+ }
91+
92+ Component {
93+ id: infoLabel
94+
95+ FadingLabel {
96+ objectName: "infoLabel" + model.index
97+ width: root.width
98+
99+ property var model
100+ readonly property bool isPrompt: false
101+
102+ color: model.type === LightDMService.prompts.Message ? theme.palette.normal.raisedText
103+ : theme.palette.normal.negative
104+ fontSize: "small"
105+ textFormat: Text.PlainText
106+ text: model.text
107+
108+ Behavior on opacity { UbuntuNumberAnimation {} }
109+ opacity: 0
110+ }
111+ }
112+
113+ Component {
114+ id: greeterPrompt
115+
116+ GreeterPrompt {
117+ objectName: "greeterPrompt" + model.index
118+ width: root.width
119+
120+ property var model
121+
122+ interactive: root.interactive
123+ isAlphanumeric: model.text !== "" || root.alphanumeric
124+ isPrompt: model.type !== LightDMService.prompts.Button
125+ isSecret: model.type === LightDMService.prompts.Secret
126+ text: model.text ? model.text : (isAlphanumeric ? i18n.tr("Passphrase") : i18n.tr("Passcode"))
127+
128+ onClicked: root.clicked()
129+ onAccepted: {
130+ // If there is another GreeterPrompt, focus it.
131+ for (var i = model.index + 1; i < repeater.count; i++) {
132+ var item = repeater.itemAt(i).item;
133+ if (item.isPrompt) {
134+ item.forceActiveFocus();
135+ return;
136+ }
137+ }
138+
139+ // Nope we're the last one; just send our response.
140+ d.sendResponse();
141+ }
142+ onCanceled: root.canceled()
143+
144+ Behavior on opacity { UbuntuNumberAnimation {} }
145+ opacity: 0
146+ }
147+ }
148+}
qml/Greeter/WideView.qml+13−22
@@ -52,16 +52,8 @@ FocusScope {
5252 loginList.showError();
5353 }
5454
55− function reset(forceShow) {
56− loginList.reset();
57− }
58−
59− function showMessage(html) {
60− loginList.showMessage(html);
61− }
62−
63− function showPrompt(text, isSecret, isDefaultPrompt) {
64− loginList.showPrompt(text, isSecret, isDefaultPrompt);
55+ function forceShow() {
56+ // Nothing to do, we are always fully shown
6557 }
6658
6759 function tryToUnlock(toTheRight) {
@@ -84,10 +76,8 @@ FocusScope {
8476 coverPage.hide();
8577 }
8678
87− function notifyAuthenticationSucceeded(showFakePassword) {
88− if (showFakePassword) {
89− loginList.showFakePassword();
90− }
79+ function showFakePassword() {
80+ loginList.showFakePassword();
9181 }
9282
9383 function showLastChance() {
@@ -125,8 +115,6 @@ FocusScope {
125115 id: loginList
126116 objectName: "loginList"
127117
128− property int selectedUserIndex: 0
129−
130118 width: units.gu(40)
131119 anchors {
132120 left: parent.left
@@ -141,15 +129,18 @@ FocusScope {
141129 Behavior on boxVerticalOffset { UbuntuNumberAnimation {} }
142130
143131 model: root.userModel
144− currentSession: LightDMService.users.data(selectedUserIndex, LightDMService.userRoles.SessionRole);
145132 onResponded: root.responded(response)
146− onSelected: {
147− root.selected(index)
148− loginList.selectedUserIndex = index;
149− }
133+ onSelected: root.selected(index)
150134 onSessionChooserButtonClicked: parent.state = "SessionsList"
135+ onCurrentIndexChanged: setCurrentSession()
151136
152137 Keys.forwardTo: [sessionChooserLoader.item]
138+
139+ Component.onCompleted: setCurrentSession()
140+
141+ function setCurrentSession() {
142+ currentSession = LightDMService.users.data(currentIndex, LightDMService.userRoles.SessionRole);
143+ }
153144 }
154145
155146 Loader {
@@ -177,7 +168,7 @@ FocusScope {
177168 onSessionSelected: loginList.currentSession = sessionKey
178169 onShowLoginList: {
179170 coverPage.state = "LoginList"
180− loginList.passwordInput.forceActiveFocus();
171+ loginList.tryToUnlock();
181172 }
182173 ignoreUnknownSignals: true
183174 }
qml/Launcher/Drawer.qml+86−4
@@ -20,6 +20,7 @@ import Unity.Launcher 0.1
2020import Utils 0.1
2121import "../Components"
2222import Qt.labs.settings 1.0
23+import GSettings 1.0
2324
2425FocusScope {
2526 id: root
@@ -43,6 +44,16 @@ FocusScope {
4344 searchField.focus = true;
4445 }
4546
47+ Keys.onPressed: {
48+ if (event.text.trim() !== "") {
49+ focusInput();
50+ searchField.text = event.text;
51+ }
52+ // Catch all presses here in case the navigation lets something through
53+ // We never want to end up in the launcher with focus
54+ event.accepted = true;
55+ }
56+
4657 Settings {
4758 property alias selectedTab: sections.selectedIndex
4859 }
@@ -76,9 +87,13 @@ FocusScope {
7687
7788 TextField {
7889 id: searchField
90+ objectName: "searchField"
7991 anchors { left: parent.left; top: parent.top; right: parent.right; margins: units.gu(1) }
8092 placeholderText: i18n.tr("Search…")
8193 focus: true
94+
95+ KeyNavigation.down: sections
96+
8297 onAccepted: {
8398 if (searchField.displayText != "" && listLoader.item && listLoader.item.currentItem) {
8499 root.applicationSelected(listLoader.item.getFirstAppId());
@@ -95,7 +110,14 @@ FocusScope {
95110
96111 Sections {
97112 id: sections
113+ objectName: "drawerSections"
98114 width: parent.width
115+
116+ KeyNavigation.up: searchField
117+ KeyNavigation.down: headerFocusScope
118+ KeyNavigation.backtab: searchField
119+ KeyNavigation.tab: headerFocusScope
120+
99121 actions: [
100122 Action {
101123 text: i18n.ctr("Apps sorted alphabetically", "A-Z")
@@ -115,9 +137,45 @@ FocusScope {
115137 }
116138 }
117139
140+ FocusScope {
141+ id: headerFocusScope
142+ objectName: "headerFocusScope"
143+ KeyNavigation.up: sections
144+ KeyNavigation.down: listLoader.item
145+ KeyNavigation.backtab: sections
146+ KeyNavigation.tab: listLoader.item
147+ activeFocusOnTab: true
148+
149+ GSettings {
150+ id: settings
151+ schema.id: "com.canonical.Unity8"
152+ }
153+
154+ Keys.onPressed: {
155+ switch (event.key) {
156+ case Qt.Key_Return:
157+ case Qt.Key_Enter:
158+ case Qt.Key_Space:
159+ trigger();
160+ event.accepted = true;
161+ }
162+ }
163+
164+ function trigger() {
165+ Qt.openUrlExternally(settings.appstoreUri)
166+ }
167+ }
168+
118169 Loader {
119170 id: listLoader
120− anchors { left: parent.left; top: sectionsContainer.bottom; right: parent.right; bottom: parent.bottom; leftMargin: units.gu(1); rightMargin: units.gu(1) }
171+ objectName: "drawerListLoader"
172+ anchors { left: parent.left; top: sectionsContainer.bottom; right: parent.right; bottom: parent.bottom }
173+
174+ KeyNavigation.up: headerFocusScope
175+ KeyNavigation.down: searchField
176+ KeyNavigation.backtab: headerFocusScope
177+ KeyNavigation.tab: searchField
178+
121179 sourceComponent: {
122180 switch (sections.selectedIndex) {
123181 case 0: return aToZComponent;
@@ -167,10 +225,13 @@ FocusScope {
167225 Component {
168226 id: mostUsedComponent
169227 DrawerListView {
228+ id: mostUsedListView
170229
171230 header: MoreAppsHeader {
172231 width: parent.width
173232 height: units.gu(6)
233+ highlighted: headerFocusScope.activeFocus
234+ onClicked: headerFocusScope.trigger();
174235 }
175236
176237 model: AppDrawerProxyModel {
@@ -180,7 +241,8 @@ FocusScope {
180241 }
181242
182243 delegate: UbuntuShape {
183− width: parent.width
244+ width: parent.width - units.gu(2)
245+ anchors.horizontalCenter: parent.horizontalCenter
184246 color: "#20ffffff"
185247 aspect: UbuntuShape.Flat
186248 // NOTE: Cannot use gridView.rows here as it would evaluate to 0 at first and only update later,
@@ -196,6 +258,9 @@ FocusScope {
196258 bottomMargin: units.gu(1)
197259 clip: true
198260
261+ interactive: true
262+ focus: index == mostUsedListView.currentIndex
263+
199264 model: sortProxyModel
200265
201266 delegateWidth: units.gu(8)
@@ -209,10 +274,13 @@ FocusScope {
209274 Component {
210275 id: aToZComponent
211276 DrawerListView {
277+ id: aToZListView
212278
213279 header: MoreAppsHeader {
214280 width: parent.width
215281 height: units.gu(6)
282+ highlighted: headerFocusScope.activeFocus
283+ onClicked: headerFocusScope.trigger();
216284 }
217285
218286 model: AppDrawerProxyModel {
@@ -222,7 +290,8 @@ FocusScope {
222290 }
223291
224292 delegate: UbuntuShape {
225− width: parent.width
293+ width: parent.width - units.gu(2)
294+ anchors.horizontalCenter: parent.horizontalCenter
226295 color: "#20ffffff"
227296 aspect: UbuntuShape.Flat
228297
@@ -244,7 +313,8 @@ FocusScope {
244313 anchors { left: parent.left; top: categoryNameLabel.bottom; right: parent.right; topMargin: units.gu(1) }
245314 height: rows * delegateHeight
246315
247− interactive: false
316+ interactive: true
317+ focus: index == aToZListView.currentIndex
248318
249319 model: AppDrawerProxyModel {
250320 id: categoryModel
@@ -263,10 +333,13 @@ FocusScope {
263333 Component {
264334 id: drawerDelegateComponent
265335 AbstractButton {
336+ id: drawerDelegate
266337 width: GridView.view.cellWidth
267338 height: units.gu(10)
268339 objectName: "drawerItem_" + model.appId
269340
341+ readonly property bool focused: index === GridView.view.currentIndex && GridView.view.activeFocus
342+
270343 onClicked: root.applicationSelected(model.appId)
271344
272345 Column {
@@ -290,6 +363,15 @@ FocusScope {
290363 source: model.icon
291364 }
292365 sourceFillMode: UbuntuShape.PreserveAspectCrop
366+
367+ StyledItem {
368+ styleName: "FocusShape"
369+ anchors.fill: parent
370+ StyleHints {
371+ visible: drawerDelegate.focused
372+ radius: units.gu(2.55)
373+ }
374+ }
293375 }
294376
295377 Label {
qml/Launcher/DrawerGridView.qml+3−1
@@ -17,7 +17,7 @@
1717import QtQuick 2.4
1818import "../Components"
1919
20−Item {
20+FocusScope {
2121 id: root
2222
2323 property int delegateWidth: units.gu(10)
@@ -25,6 +25,7 @@ Item {
2525 property alias delegate: gridView.delegate
2626 property alias model: gridView.model
2727 property alias interactive: gridView.interactive
28+ property alias currentIndex: gridView.currentIndex
2829
2930 property alias header: gridView.header
3031 property alias topMargin: gridView.topMargin
@@ -37,6 +38,7 @@ Item {
3738 id: gridView
3839 anchors.fill: parent
3940 leftMargin: spacing
41+ focus: true
4042
4143 readonly property int overflow: width - (root.columns * root.delegateWidth)
4244 readonly property real spacing: overflow / (root.columns)
qml/Launcher/DrawerListView.qml+6
@@ -25,6 +25,12 @@ ListView {
2525 bottomMargin: units.gu(1)
2626 spacing: units.gu(1)
2727 clip: true
28+ focus: true
29+
30+ onActiveFocusChanged: {
31+ currentIndex = -1;
32+ currentIndex = 0;
33+ }
2834
2935 function getFirstAppId() {
3036 return model.appId(0);
qml/Launcher/Launcher.qml+11−8
@@ -74,7 +74,7 @@ FocusScope {
7474 } else {
7575 superPressTimer.stop();
7676 superLongPressTimer.stop();
77− launcher.switchToNextState("");
77+ switchToNextState("");
7878 panel.shortcutHintsShown = false;
7979 }
8080 }
@@ -87,14 +87,14 @@ FocusScope {
8787 superPressTimer.stop();
8888 superLongPressTimer.stop();
8989 } else {
90+ switchToNextState("");
91+ root.focus = false;
9092 if (panel.highlightIndex == -1) {
91− showDashHome();
93+ root.showDashHome();
9294 } else if (panel.highlightIndex >= 0){
9395 launcherApplicationSelected(LauncherModel.get(panel.highlightIndex).appId);
9496 }
9597 panel.highlightIndex = -2;
96− switchToNextState("");
97− root.focus = false;
9898 }
9999 }
100100
@@ -108,6 +108,10 @@ FocusScope {
108108 }
109109 }
110110
111+ onPanelWidthChanged: {
112+ hint();
113+ }
114+
111115 function hide(flags) {
112116 if ((flags & ignoreHideIfMouseOverLauncher) && Utils.Functions.itemUnderMouse(panel)) {
113117 return;
@@ -206,7 +210,7 @@ FocusScope {
206210 case Qt.Key_Return:
207211 case Qt.Key_Space:
208212 if (panel.highlightIndex == -1) {
209− showDashHome();
213+ root.showDashHome();
210214 } else if (panel.highlightIndex >= 0) {
211215 launcherApplicationSelected(LauncherModel.get(panel.highlightIndex).appId);
212216 }
@@ -469,7 +473,7 @@ FocusScope {
469473 }
470474 onPassed: {
471475 if (root.drawerEnabled) {
472− root.switchToNextState("drawer");
476+ root.openDrawer()
473477 }
474478 }
475479
@@ -545,8 +549,7 @@ FocusScope {
545549 if (!dragging) {
546550 if (distance > panel.width / 2) {
547551 if (root.drawerEnabled && distance > panel.width * 3 && dragDirection() !== "left") {
548− root.switchToNextState("drawer");
549− root.focus = true;
552+ root.openDrawer(false)
550553 } else {
551554 root.switchToNextState("visible");
552555 }
qml/Launcher/LauncherDelegate.qml+8−9
@@ -124,15 +124,14 @@ Item {
124124 height: parent.itemHeight + units.gu(1)
125125 anchors.centerIn: parent
126126
127− Image {
128− objectName: "focusRing"
129− anchors.centerIn: iconShape
130− height: width * 15 / 16
131− width: iconShape.width + units.gu(1)
132− source: "graphics/launcher-app-focus-ring.svg"
133− sourceSize.width: width
134− sourceSize.height: height
135− visible: root.highlighted
127+ StyledItem {
128+ styleName: "FocusShape"
129+ anchors.fill: iconShape
130+ activeFocusOnTab: true
131+ StyleHints {
132+ visible: root.highlighted
133+ radius: units.gu(2.55)
134+ }
136135 }
137136
138137 ProportionalShape {
qml/Launcher/LauncherPanel.qml+11−6
@@ -80,6 +80,7 @@ Rectangle {
8080 }
8181
8282 Rectangle {
83+ id: bfb
8384 objectName: "buttonShowDashHome"
8485 width: parent.width
8586 height: width * .9
@@ -100,13 +101,15 @@ Rectangle {
100101 activeFocusOnPress: false
101102 onClicked: root.showDashHome()
102103 }
103− Rectangle {
104− objectName: "bfbFocusHighlight"
104+
105+ StyledItem {
106+ styleName: "FocusShape"
105107 anchors.fill: parent
106− border.color: "white"
107− border.width: units.dp(1)
108− color: "transparent"
109− visible: parent.highlighted
108+ anchors.margins: units.gu(.5)
109+ StyleHints {
110+ visible: bfb.highlighted
111+ radius: 0
112+ }
110113 }
111114 }
112115
@@ -788,6 +791,8 @@ Rectangle {
788791 quickList.model = launcherListView.model.get(index).quickList;
789792 quickList.appId = launcherListView.model.get(index).appId;
790793 quickList.state = "open";
794+ root.highlightIndex = index;
795+ quickList.forceActiveFocus();
791796 }
792797
793798 Item {
qml/Launcher/MoreAppsHeader.qml+12−5
@@ -4,17 +4,24 @@ import Ubuntu.Components 1.3
44AbstractButton {
55 id: root
66
7− onClicked: {
8− // TODO: Make this point to the snappy store as soon as we stop landing to vivid
9− Qt.openUrlExternally("scope://com.canonical.scopes.clickstore")
10− }
7+ property bool highlighted: false
118
129 UbuntuShape {
13− width: parent.width
10+ width: parent.width - units.gu(2)
11+ anchors.horizontalCenter: parent.horizontalCenter
1412 height: parent.height - units.gu(1)
1513 color: "#20ffffff"
1614 aspect: UbuntuShape.Flat
1715
16+ StyledItem {
17+ styleName: "FocusShape"
18+ anchors.fill: parent
19+ activeFocusOnTab: true
20+ StyleHints {
21+ visible: root.highlighted
22+ }
23+ }
24+
1825 Row {
1926 anchors.fill: parent
2027 anchors.margins: units.gu(1)
qml/Launcher/graphics/launcher-app-focus-ring.svgdeleted−12
@@ -1,12 +0,0 @@
1−<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2−<svg width="172px" height="163px" viewBox="0 0 172 163" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
3− <!-- Generator: Sketch 3.4.4 (17249) - http://www.bohemiancoding.com/sketch -->
4− <title>Shape</title>
5− <desc>Created with Sketch.</desc>
6− <defs></defs>
7− <g id="•-Launcher" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
8− <g id="Artboard-9" sketch:type="MSArtboardGroup" transform="translate(-163.000000, -1436.000000)" fill="#E95420">
9− <path d="M221.983432,1440 L221.983432,1440 C195.6127,1440 184.708233,1442.4723 177.107949,1450.10734 C169.476819,1457.77336 167,1468.79245 167,1495.3481 L167,1538.9019 C167,1565.45755 169.476819,1576.47664 177.107949,1584.14266 C184.708233,1591.7777 195.6127,1594.25 221.983432,1594.25 L276.016868,1594.25 C302.387595,1594.25 313.291998,1591.77771 320.892221,1584.14264 C328.523252,1576.47663 331,1565.45769 331,1538.9019 L331,1495.3481 C331,1468.79231 328.523252,1457.77337 320.892221,1450.10736 C313.291998,1442.47229 302.387595,1440 276.016868,1440 L221.983432,1440 Z M221.983432,1436 L276.016868,1436 C302.345315,1436 314.848953,1438.36655 323.727108,1447.2854 C332.633306,1456.23243 335,1468.85167 335,1495.3481 L335,1538.9019 C335,1565.39833 332.633306,1578.01757 323.727108,1586.9646 C314.848953,1595.88345 302.345315,1598.25 276.016868,1598.25 L221.983432,1598.25 C195.654985,1598.25 183.151291,1595.88345 174.273077,1586.96463 C165.366772,1578.0176 163,1565.39822 163,1538.9019 L163,1495.3481 C163,1468.85178 165.366772,1456.2324 174.273077,1447.28537 C183.151291,1438.36655 195.654985,1436 221.983432,1436 L221.983432,1436 Z" id="Shape" sketch:type="MSShapeGroup"></path>
10− </g>
11− </g>
12−</svg>
qml/OrientedShell.qml+2−1
@@ -266,7 +266,8 @@ Item {
266266 nativeWidth: root.width
267267 nativeHeight: root.height
268268 mode: applicationArguments.mode
269− hasMouse: miceModel.count + touchPadModel.count > 0
269+ hasMouse: pointerInputDevices > 0
270+ hasKeyboard: keyboardsModel.count > 0
270271 // TODO: Factor in if the current screen is a touch screen and if the user wants to
271272 // have multiple keyboards around. For now we only enable one keyboard at a time
272273 // thus hiding it here if there is a physical one around or if we have a second
qml/Panel/PanelBar.qml+1
@@ -49,6 +49,7 @@ Item {
4949 row.resetCurrentItem();
5050 }
5151 row.setCurrentItemIndex(index);
52+ d.alignIndicators();
5253 }
5354
5455 function addScrollOffset(scrollAmmout) {
qml/Panel/PanelMenu.qml+14
@@ -140,6 +140,19 @@ Showable {
140140 visible: !root.fullyClosed
141141 }
142142
143+ Keys.onPressed: {
144+ if (event.key === Qt.Key_Left) {
145+ bar.setCurrentItemIndex(bar.currentItemIndex - 1);
146+ event.accepted = true;
147+ } else if (event.key === Qt.Key_Right) {
148+ bar.setCurrentItemIndex(bar.currentItemIndex + 1);
149+ event.accepted = true;
150+ } else if (event.key === Qt.Key_Escape) {
151+ root.hide();
152+ event.accepted = true;
153+ }
154+ }
155+
143156 PanelBar {
144157 id: bar
145158 objectName: "indicatorsBar"
@@ -368,6 +381,7 @@ Showable {
368381 State {
369382 name: "commit"
370383 extend: "locked"
384+ PropertyChanges { target: root; focus: true }
371385 PropertyChanges { target: bar; interactive: true }
372386 PropertyChanges {
373387 target: d;
qml/Shell.qml+49−5
@@ -68,6 +68,7 @@ StyledItem {
6868 stage.updateFocusedAppOrientationAnimated();
6969 }
7070 property bool hasMouse: false
71+ property bool hasKeyboard: false
7172
7273 // to be read from outside
7374 readonly property int mainAppWindowOrientationAngle: stage.mainAppWindowOrientationAngle
@@ -240,7 +241,7 @@ StyledItem {
240241 // Ignore when greeter is active, to avoid pocket presses
241242 if (!greeter.active) {
242243 launcher.fadeOut();
243− shell.showHome();
244+ ApplicationManager.requestFocusApplication("unity8-dash");
244245 }
245246 }
246247 onTouchBegun: { cursor.opacity = 0; }
@@ -319,6 +320,35 @@ StyledItem {
319320 panel.applicationMenus.hide();
320321 }
321322 }
323+
324+ TouchGestureArea {
325+ anchors.fill: stage
326+
327+ minimumTouchPoints: 4
328+ maximumTouchPoints: minimumTouchPoints
329+
330+ readonly property bool recognisedPress: status == TouchGestureArea.Recognized &&
331+ touchPoints.length >= minimumTouchPoints &&
332+ touchPoints.length <= maximumTouchPoints
333+ property bool wasPressed: false
334+
335+ onRecognisedPressChanged: {
336+ if (recognisedPress) {
337+ wasPressed = true;
338+ }
339+ }
340+
341+ onStatusChanged: {
342+ if (status !== TouchGestureArea.Recognized) {
343+ if (status === TouchGestureArea.WaitingForTouch) {
344+ if (wasPressed && !dragging) {
345+ launcher.openDrawer(true);
346+ }
347+ }
348+ wasPressed = false;
349+ }
350+ }
351+ }
322352 }
323353
324354 InputMethod {
@@ -343,6 +373,16 @@ StyledItem {
343373 onLoaded: {
344374 item.objectName = "greeter"
345375 }
376+ property bool openDrawerAfterUnlock: false
377+ Connections {
378+ target: greeter
379+ onActiveChanged: {
380+ if (!greeter.active && greeterLoader.openDrawerAfterUnlock) {
381+ launcher.openDrawer(false);
382+ greeterLoader.openDrawerAfterUnlock = false;
383+ }
384+ }
385+ }
346386 }
347387
348388 Component {
@@ -434,9 +474,11 @@ StyledItem {
434474 if (shell.mode === "greeter") {
435475 SessionBroadcast.requestHomeShown(AccountsService.user);
436476 } else {
437− var animate = !LightDMService.greeter.active && !stages.shown;
438− dash.setCurrentScope(0, animate, false);
439− ApplicationManager.requestFocusApplication("unity8-dash");
477+ if (!greeter.active) {
478+ launcher.openDrawer(false);
479+ } else {
480+ greeterLoader.openDrawerAfterUnlock = true;
481+ }
440482 }
441483 }
442484
@@ -512,7 +554,7 @@ StyledItem {
512554 lockedVisible: shell.usageScenario == "desktop" && !settings.autohideLauncher && !panel.fullscreenMode
513555 blurSource: greeter.shown ? greeter : stages
514556 topPanelHeight: panel.panelHeight
515− drawerEnabled: !greeter.shown
557+ drawerEnabled: !greeter.active
516558
517559 onShowDashHome: showHome()
518560 onLauncherApplicationSelected: {
@@ -657,8 +699,10 @@ StyledItem {
657699 id: dialogs
658700 objectName: "dialogs"
659701 anchors.fill: parent
702+ visible: hasActiveDialog
660703 z: overlay.z + 10
661704 usageScenario: shell.usageScenario
705+ hasKeyboard: shell.hasKeyboard
662706 onPowerOffClicked: {
663707 shutdownFadeOutRectangle.enabled = true;
664708 shutdownFadeOutRectangle.visible = true;
qml/Stage/DecoratedWindow.qml+36−43
@@ -196,18 +196,19 @@ FocusScope {
196196 ]
197197 }
198198
199− MouseArea {
199+ WindowDecoration {
200+ id: decoration
201+ closeButtonVisible: root.application.appId !== "unity8-dash"
202+ objectName: "appWindowDecoration"
203+
200204 anchors { left: parent.left; top: parent.top; right: parent.right }
201205 height: units.gu(3)
202206
203− opacity: root.hasDecoration ? Math.min(1, root.showDecoration) : 0
207+ title: applicationWindow.title
204208
209+ opacity: root.hasDecoration ? Math.min(1, root.showDecoration) : 0
205210 Behavior on opacity { UbuntuNumberAnimation { } }
206211
207− drag.target: Item {}
208− drag.filterChildren: true
209− drag.threshold: 0
210−
211212 onPressed: root.decorationPressed();
212213 onPressedChanged: moveHandler.handlePressedChanged(pressed, pressedButtons, mouseX, mouseY)
213214 onPositionChanged: moveHandler.handlePositionChanged(mouse)
@@ -216,47 +217,39 @@ FocusScope {
216217 moveHandler.handleReleased();
217218 }
218219
219− WindowDecoration {
220− id: decoration
221− closeButtonVisible: root.application.appId !== "unity8-dash"
222− objectName: "appWindowDecoration"
223− anchors.fill: parent
224− title: applicationWindow.title
225−
226− onCloseClicked: root.closeClicked();
227− onMaximizeClicked: { root.decorationPressed(); root.maximizeClicked(); }
228− onMaximizeHorizontallyClicked: { root.decorationPressed(); root.maximizeHorizontallyClicked(); }
229− onMaximizeVerticallyClicked: { root.decorationPressed(); root.maximizeVerticallyClicked(); }
230− onMinimizeClicked: root.minimizeClicked();
231−
232− enableMenus: {
233− return active &&
234− surface &&
235− (PanelState.focusedPersistentSurfaceId === surface.persistentId && !PanelState.decorationsVisible)
236− }
237− menu: sharedAppModel.model
220+ onCloseClicked: root.closeClicked();
221+ onMaximizeClicked: { root.decorationPressed(); root.maximizeClicked(); }
222+ onMaximizeHorizontallyClicked: { root.decorationPressed(); root.maximizeHorizontallyClicked(); }
223+ onMaximizeVerticallyClicked: { root.decorationPressed(); root.maximizeVerticallyClicked(); }
224+ onMinimizeClicked: root.minimizeClicked();
238225
239− Indicators.SharedUnityMenuModel {
240− id: sharedAppModel
241− property var menus: surface ? ApplicationMenuRegistry.getMenusForSurface(surface.persistentId) : []
242− property var menuService: menus.length > 0 ? menus[0] : undefined
226+ enableMenus: {
227+ return active &&
228+ surface &&
229+ (PanelState.focusedPersistentSurfaceId === surface.persistentId && !PanelState.decorationsVisible)
230+ }
231+ menu: sharedAppModel.model
243232
244− busName: menuService ? menuService.service : ""
245− menuObjectPath: menuService && menuService.menuPath ? menuService.menuPath : ""
246− actions: menuService && menuService.actionPath ? { "unity": menuService.actionPath } : {}
247− }
233+ Indicators.SharedUnityMenuModel {
234+ id: sharedAppModel
235+ property var menus: surface ? ApplicationMenuRegistry.getMenusForSurface(surface.persistentId) : []
236+ property var menuService: menus.length > 0 ? menus[0] : undefined
248237
249− Connections {
250− target: ApplicationMenuRegistry
251− onSurfaceMenuRegistered: {
252− if (surface && surfaceId === surface.persistentId) {
253− sharedAppModel.menus = Qt.binding(function() { return surface ? ApplicationMenuRegistry.getMenusForSurface(surface.persistentId) : [] });
254− }
238+ busName: menuService ? menuService.service : ""
239+ menuObjectPath: menuService && menuService.menuPath ? menuService.menuPath : ""
240+ actions: menuService && menuService.actionPath ? { "unity": menuService.actionPath } : {}
241+ }
242+
243+ Connections {
244+ target: ApplicationMenuRegistry
245+ onSurfaceMenuRegistered: {
246+ if (surface && surfaceId === surface.persistentId) {
247+ sharedAppModel.menus = Qt.binding(function() { return surface ? ApplicationMenuRegistry.getMenusForSurface(surface.persistentId) : [] });
255248 }
256− onSurfaceMenuUnregistered: {
257− if (surface && surfaceId === surface.persistentId) {
258− sharedAppModel.menus = Qt.binding(function() { return surface ? ApplicationMenuRegistry.getMenusForSurface(surface.persistentId) : [] });
259− }
249+ }
250+ onSurfaceMenuUnregistered: {
251+ if (surface && surfaceId === surface.persistentId) {
252+ sharedAppModel.menus = Qt.binding(function() { return surface ? ApplicationMenuRegistry.getMenusForSurface(surface.persistentId) : [] });
260253 }
261254 }
262255 }
qml/Stage/WindowDecoration.qml+6−2
@@ -38,6 +38,10 @@ MouseArea {
3838 acceptedButtons: Qt.AllButtons // prevent leaking unhandled mouse events
3939 hoverEnabled: true
4040
41+ drag.target: Item {}
42+ drag.filterChildren: true
43+ drag.threshold: 0
44+
4145 signal closeClicked()
4246 signal minimizeClicked()
4347 signal maximizeClicked()
@@ -64,8 +68,8 @@ MouseArea {
6468 (menuBar.showRequested || root.containsMouse)
6569 }
6670
67− // We dont want touch events to fall through to parent,
68− // otherwise the containsMouse will not work.
71+ // We dont want touch events to fall through to parent as it expect some child MouseArea to have them
72+ // If not some MouseArea in the menu bar, it will be this one.
6973 MouseArea {
7074 anchors.fill: parent
7175 propagateComposedEvents: true
src/MouseTouchAdaptor.cpp+28−1
@@ -72,6 +72,7 @@ namespace {
7272MouseTouchAdaptor *g_instance = nullptr;
7373
7474const Qt::KeyboardModifiers TRI_PRESS_MODIFIER = Qt::ShiftModifier|Qt::ControlModifier|Qt::AltModifier;
75+const Qt::KeyboardModifiers QUAD_PRESS_MODIFIER = TRI_PRESS_MODIFIER|Qt::MetaModifier;
7576
7677Qt::MouseButton translateMouseButton(xcb_button_t detail)
7778{
@@ -91,7 +92,7 @@ Qt::KeyboardModifiers translateMofidier(uint32_t mod)
9192 if (mod & 0x01) qtMod |= Qt::ShiftModifier;
9293 if (mod & 0x04) qtMod |= Qt::ControlModifier;
9394 if (mod & 0x08) qtMod |= Qt::AltModifier;
94− if (mod & 0x80) qtMod |= Qt::MetaModifier;
95+ if (mod & 0x40) qtMod |= Qt::MetaModifier;
9596
9697 return qtMod;
9798}
@@ -101,6 +102,7 @@ MouseTouchAdaptor::MouseTouchAdaptor()
101102 : QObject(nullptr)
102103 , m_leftButtonIsPressed(false)
103104 , m_triPressModifier(false)
105+ , m_quadPressModifier(false)
104106 , m_enabled(true)
105107{
106108 QCoreApplication::instance()->installNativeEventFilter(this);
@@ -297,6 +299,13 @@ bool MouseTouchAdaptor::handleButtonPress(WId windowId, uint32_t detail, uint32_
297299 touchEvent.press(2, windowPos);
298300 m_triPressModifier = true;
299301 }
302+ if (qtMod == QUAD_PRESS_MODIFIER) {
303+ touchEvent.press(1, windowPos);
304+ touchEvent.press(2, windowPos);
305+ touchEvent.press(3, windowPos);
306+ m_quadPressModifier = true;
307+ }
308+
300309 touchEvent.commit(false /* processEvents */);
301310
302311 m_leftButtonIsPressed = true;
@@ -322,10 +331,16 @@ bool MouseTouchAdaptor::handleButtonRelease(WId windowId, uint32_t detail, uint3
322331 touchEvent.release(1, windowPos);
323332 touchEvent.release(2, windowPos);
324333 }
334+ if (m_quadPressModifier) {
335+ touchEvent.release(1, windowPos);
336+ touchEvent.release(2, windowPos);
337+ touchEvent.release(3, windowPos);
338+ }
325339 touchEvent.commit(false /* processEvents */);
326340
327341 m_leftButtonIsPressed = false;
328342 m_triPressModifier = false;
343+ m_quadPressModifier = false;
329344 return true;
330345}
331346
@@ -354,6 +369,18 @@ bool MouseTouchAdaptor::handleMotionNotify(WId windowId, uint32_t modifiers, int
354369 m_triPressModifier = false;
355370 }
356371 }
372+ if (m_quadPressModifier) {
373+ if (qtMod == QUAD_PRESS_MODIFIER) {
374+ touchEvent.move(1, windowPos);
375+ touchEvent.move(2, windowPos);
376+ touchEvent.move(3, windowPos);
377+ } else {
378+ touchEvent.release(1, windowPos);
379+ touchEvent.release(2, windowPos);
380+ touchEvent.release(3, windowPos);
381+ m_quadPressModifier = false;
382+ }
383+ }
357384 touchEvent.commit(false /* processEvents */);
358385
359386 return true;
src/MouseTouchAdaptor.h+1
@@ -59,6 +59,7 @@ private:
5959 QTouchDevice *m_touchDevice;
6060 bool m_leftButtonIsPressed;
6161 bool m_triPressModifier;
62+ bool m_quadPressModifier;
6263
6364
6465 bool m_enabled;
tests/CMakeLists.txt+5−3
@@ -7,8 +7,10 @@ add_meta_test(unittests)
77add_meta_test(uitests)
88add_meta_test(xvfbuitests)
99
10−add_meta_test(alltests DEPENDS unittests uitests)
11−add_meta_test(xvfballtests DEPENDS unittests xvfbuitests)
10+# Run our meta-meta tests serially because we don't need to nest
11+# parallelized tests.
12+add_meta_test(alltests SERIAL DEPENDS unittests uitests)
13+add_meta_test(xvfballtests SERIAL DEPENDS unittests xvfbuitests)
1214
1315# Support libraries and plugins
1416add_subdirectory(mocks)
@@ -42,8 +44,8 @@ macro(unity8_parse_arguments)
4244
4345 set(ld_paths)
4446 list(APPEND ld_paths
47+ ${UNITY_MOCKPATH}/liblightdm
4548 ${UNITY_MOCKPATH}/libusermetrics
46− ${UNITY_MOCKPATH}/LightDM/IntegratedLightDM/liblightdm
4749 )
4850
4951 string(REPLACE ";" ":" ld_library_path "${ld_paths}")
tests/autopilot/unity8/fixture_setup.py+1−1
@@ -74,7 +74,7 @@ class LaunchUnityWithFakeSensors(fixtures.Fixture):
7474 def _get_lightdm_mock_path(self):
7575 lib_path = get_mocks_library_path()
7676 lightdm_mock_path = os.path.abspath(
77− os.path.join(lib_path, "LightDM" ,"IntegratedLightDM", "liblightdm")
77+ os.path.join(lib_path, "liblightdm")
7878 )
7979
8080 if not os.path.exists(lightdm_mock_path):
tests/autopilot/unity8/shell/tests/__init__.py+1−1
@@ -264,7 +264,7 @@ class UnityTestCase(AutopilotTestCase):
264264 def _get_lightdm_mock_path(self):
265265 lib_path = get_mocks_library_path()
266266 lightdm_mock_path = os.path.abspath(
267− os.path.join(lib_path, "LightDM" ,"IntegratedLightDM", "liblightdm")
267+ os.path.join(lib_path, "liblightdm")
268268 )
269269
270270 if not os.path.exists(lightdm_mock_path):
tests/mocks/AccountsService/AccountsService.cpp+2−3
@@ -15,7 +15,7 @@
1515 */
1616
1717#include "AccountsService.h"
18−#include "MockUsersModel.h"
18+#include "UsersModel.h"
1919
2020#include <QLightDM/UsersModel>
2121#include <paths.h>
@@ -33,9 +33,8 @@ AccountsService::AccountsService(QObject* parent)
3333 m_demoEdgesCompleted(),
3434 m_hereEnabled(false),
3535 m_hereLicensePath(""),
36− m_usersModel(new MockUsersModel(this))
36+ m_usersModel(new UsersModel(this))
3737{
38− m_usersModel->setMockMode("full");
3938}
4039
4140QString AccountsService::user() const
tests/mocks/AccountsService/AccountsService.h+3−6
@@ -14,15 +14,14 @@
1414 * along with this program. If not, see <http://www.gnu.org/licenses/>.
1515 */
1616
17−#ifndef UNITY_MOCK_ACCOUNTSSERVICE_H
18−#define UNITY_MOCK_ACCOUNTSSERVICE_H
17+#pragma once
1918
2019#include <QObject>
2120#include <QString>
2221#include <QStringList>
2322#include <QVariant>
2423
25−class MockUsersModel;
24+class UsersModel;
2625
2726class AccountsService: public QObject
2827{
@@ -165,7 +164,5 @@ private:
165164 QString m_realName;
166165 QStringList m_kbdMap;
167166 QString m_email;
168− MockUsersModel *m_usersModel;
167+ UsersModel *m_usersModel;
169168};
170−
171−#endif
tests/mocks/AccountsService/CMakeLists.txt+3−2
@@ -1,14 +1,15 @@
11include_directories(
22 ${CMAKE_CURRENT_BINARY_DIR}
33 ${CMAKE_SOURCE_DIR}/plugins/LightDM
4+ ${CMAKE_SOURCE_DIR}/plugins/LightDM/IntegratedLightDM
45 ${CMAKE_SOURCE_DIR}/plugins/Utils
5− ${CMAKE_SOURCE_DIR}/tests/mocks/LightDM/IntegratedLightDM
66 )
77
88add_library(MockAccountsService-qml MODULE
9+ ${CMAKE_SOURCE_DIR}/plugins/LightDM/Greeter.cpp
10+ ${CMAKE_SOURCE_DIR}/plugins/LightDM/PromptsModel.cpp
911 ${CMAKE_SOURCE_DIR}/plugins/LightDM/UsersModel.cpp
1012 ${CMAKE_SOURCE_DIR}/plugins/Utils/unitysortfilterproxymodelqml.cpp
11− ${CMAKE_SOURCE_DIR}/tests/mocks/LightDM/IntegratedLightDM/MockUsersModel.cpp
1213 AccountsService.cpp
1314 plugin.cpp
1415 )
tests/mocks/CMakeLists.txt+2−1
@@ -32,8 +32,9 @@ add_subdirectory(Biometryd)
3232add_subdirectory(Cursor)
3333add_subdirectory(GSettings.1.0)
3434add_subdirectory(indicator-service)
35+add_subdirectory(liblightdm)
3536add_subdirectory(libusermetrics)
36−add_subdirectory(LightDM)
37+add_subdirectory(LightDMController)
3738add_subdirectory(Lights)
3839add_subdirectory(MeeGo)
3940add_subdirectory(Powerd)
tests/mocks/GSettings.1.0/fake_gsettings.cpp+14
@@ -29,6 +29,7 @@ GSettingsControllerQml::GSettingsControllerQml()
2929 , m_edgeDragWidth(2)
3030 , m_enableLauncher(true)
3131 , m_enableIndicatorMenu(true)
32+ , m_appstoreUri("http://uappexplorer.com")
3233{
3334}
3435
@@ -173,6 +174,11 @@ void GSettingsControllerQml::setEnableIndicatorMenu(bool enableIndicatorMenu)
173174 }
174175}
175176
177+QString GSettingsControllerQml::appstoreUri() const
178+{
179+ return m_appstoreUri;
180+}
181+
176182GSettingsSchemaQml::GSettingsSchemaQml(QObject *parent): QObject(parent) {
177183}
178184
@@ -376,6 +382,14 @@ QVariant GSettingsQml::enableIndicatorMenu() const
376382 }
377383}
378384
385+QVariant GSettingsQml::appstoreUri() const
386+{
387+ if (m_valid && m_schema->id() == "com.canonical.Unity8") {
388+ return GSettingsControllerQml::instance()->appstoreUri();
389+ }
390+ return QVariant();
391+}
392+
379393void GSettingsQml::setLifecycleExemptAppids(const QVariant &appIds)
380394{
381395 if (m_valid && m_schema->id() == "com.canonical.qtmir") {
tests/mocks/GSettings.1.0/fake_gsettings.h+7
@@ -59,6 +59,7 @@ class GSettingsQml: public QObject, public QQmlParserStatus
5959 Q_PROPERTY(QVariant edgeDragWidth READ edgeDragWidth WRITE setEdgeDragWidth NOTIFY edgeDragWidthChanged)
6060 Q_PROPERTY(QVariant enableLauncher READ enableLauncher WRITE setEnableLauncher NOTIFY enableLauncherChanged)
6161 Q_PROPERTY(QVariant enableIndicatorMenu READ enableIndicatorMenu WRITE setEnableIndicatorMenu NOTIFY enableIndicatorMenuChanged)
62+ Q_PROPERTY(QVariant appstoreUri READ appstoreUri NOTIFY appstoreUriChanged)
6263
6364public:
6465 GSettingsQml(QObject *parent = nullptr);
@@ -77,6 +78,7 @@ public:
7778 QVariant edgeDragWidth() const;
7879 QVariant enableLauncher() const;
7980 QVariant enableIndicatorMenu() const;
81+ QVariant appstoreUri() const;
8082
8183 void setDisableHeight(const QVariant &val);
8284 void setPictureUri(const QVariant &str);
@@ -101,6 +103,7 @@ Q_SIGNALS:
101103 void edgeDragWidthChanged();
102104 void enableLauncherChanged();
103105 void enableIndicatorMenuChanged();
106+ void appstoreUriChanged();
104107
105108private:
106109 GSettingsSchemaQml* m_schema;
@@ -147,6 +150,8 @@ public:
147150 bool enableIndicatorMenu() const;
148151 Q_INVOKABLE void setEnableIndicatorMenu(bool enableIndicatorMenu);
149152
153+ QString appstoreUri() const;
154+
150155Q_SIGNALS:
151156 void disableHeightChanged();
152157 void pictureUriChanged(const QString&);
@@ -158,6 +163,7 @@ Q_SIGNALS:
158163 void edgeDragWidthChanged(uint edgeDragWidth);
159164 void enableLauncherChanged(bool enableLauncher);
160165 void enableIndicatorMenuChanged(bool enableIndicatorMenu);
166+ void appstoreUriChanged(const QString &appstoreUri);
161167
162168private:
163169 GSettingsControllerQml();
@@ -172,6 +178,7 @@ private:
172178 uint m_edgeDragWidth;
173179 bool m_enableLauncher;
174180 bool m_enableIndicatorMenu;
181+ QString m_appstoreUri;
175182
176183 static GSettingsControllerQml* s_controllerInstance;
177184 QList<GSettingsQml *> m_registeredGSettings;
tests/mocks/LightDM/CMakeLists.txtdeleted−1
@@ -1 +0,0 @@
1−add_subdirectory(IntegratedLightDM)
tests/mocks/LightDM/IntegratedLightDM/CMakeLists.txtdeleted−50
@@ -1,50 +0,0 @@
1−# This is a copy of the normal LightDM plugin, but instead of statically
2−# linking in the lightdm bits, this one uses shared libraries so we can swap
3−# out different sets of users for different tests. When we finally switch to
4−# actually using the system liblightdm in the normal plugin, this version can
5−# be deleted.
6−
7−add_subdirectory(liblightdm)
8−
9−include_directories(
10− ${CMAKE_CURRENT_SOURCE_DIR}
11− ${CMAKE_CURRENT_BINARY_DIR}
12− ${CMAKE_SOURCE_DIR}/plugins/Utils
13− ${CMAKE_SOURCE_DIR}/plugins/LightDM
14− ${CMAKE_SOURCE_DIR}/tests/mocks/libusermetrics
15− ${libunity8-private_SOURCE_DIR}
16−)
17−
18−set(QMLPLUGIN_SRC
19− ${CMAKE_SOURCE_DIR}/plugins/LightDM/DBusGreeter.cpp
20− ${CMAKE_SOURCE_DIR}/plugins/LightDM/DBusGreeterList.cpp
21− ${CMAKE_SOURCE_DIR}/plugins/LightDM/Greeter.cpp
22− ${CMAKE_SOURCE_DIR}/plugins/LightDM/SessionsModel.cpp
23− ${CMAKE_SOURCE_DIR}/plugins/LightDM/UsersModel.cpp
24− ${CMAKE_SOURCE_DIR}/plugins/Utils/unitysortfilterproxymodelqml.cpp
25− MockGreeter.cpp
26− MockSessionsModel.cpp
27− MockUsersModel.cpp
28− plugin.cpp
29− )
30−
31−add_library(MockLightDM-qml MODULE
32− ${QMLPLUGIN_SRC}
33− )
34−
35−# We want to link to liblightdm-qt5-3, but we don't want to depend on it being
36−# installed on the system. So we make sure we link to our full fake version
37−# At run time, we can point to whichever version we happen to be using via
38−# LD_LIBRARY_PATH.
39−target_link_libraries(MockLightDM-qml
40− MockLightDM
41− MockUserMetrics
42− unity8-private
43− )
44−
45−qt5_use_modules(MockLightDM-qml DBus Gui Qml)
46−
47−add_unity8_mock(LightDM.IntegratedLightDM 0.1 LightDM/IntegratedLightDM
48− TARGETS MockLightDM-qml
49− ENVIRONMENT "LD_LIBRARY_PATH=${CMAKE_CURRENT_BINARY_DIR}/liblightdm"
50−)
tests/mocks/LightDM/IntegratedLightDM/MockGreeter.cppdeleted−51
@@ -1,51 +0,0 @@
1−/*
2− * Copyright (C) 2014 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−
18−#include "MockGreeter.h"
19−#include <GreeterPrivate.h>
20−
21−QString MockGreeter::mockMode() const
22−{
23− Q_D(const Greeter);
24− return d->m_greeter->mockMode();
25−}
26−
27−void MockGreeter::setMockMode(QString mockMode)
28−{
29− Q_D(Greeter);
30−
31− if (d->m_greeter->mockMode() != mockMode) {
32− d->m_greeter->setMockMode(mockMode);
33− Q_EMIT mockModeChanged(mockMode);
34− }
35−}
36−
37−QString MockGreeter::selectUserHint() const
38−{
39− Q_D(const Greeter);
40− return d->m_greeter->selectUserHint();
41−}
42−
43−void MockGreeter::setSelectUserHint(const QString &selectUserHint)
44−{
45− Q_D(Greeter);
46−
47− if (d->m_greeter->selectUserHint() != selectUserHint) {
48− d->m_greeter->setSelectUserHint(selectUserHint);
49− Q_EMIT selectUserHintChanged();
50− }
51−}
tests/mocks/LightDM/IntegratedLightDM/MockGreeter.hdeleted−42
@@ -1,42 +0,0 @@
1−/*
2− * Copyright (C) 2014 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−
18−// The real, production, Greeter
19−#include <Greeter.h>
20−
21−#ifndef MOCK_UNITY_GREETER_H
22−#define MOCK_UNITY_GREETER_H
23−
24−class MockGreeter : public Greeter {
25− Q_OBJECT
26−
27− Q_PROPERTY(QString mockMode READ mockMode WRITE setMockMode NOTIFY mockModeChanged)
28− Q_PROPERTY(QString selectUser READ selectUserHint WRITE setSelectUserHint NOTIFY selectUserHintChanged)
29−
30−public:
31− QString mockMode() const;
32− void setMockMode(QString mockMode);
33−
34− QString selectUserHint() const;
35− void setSelectUserHint(const QString &selectUserHint);
36−
37−Q_SIGNALS:
38− void mockModeChanged(QString mode);
39− void selectUserHintChanged();
40−};
41−
42−#endif // MOCK_UNITY_GREETER_H
tests/mocks/LightDM/IntegratedLightDM/MockSessionsModel.cppdeleted−66
@@ -1,66 +0,0 @@
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−
18−#include "MockSessionsModel.h"
19−#include <QLightDM/SessionsModel>
20−
21−
22−int MockSessionsModel::numSessions() const
23−{
24− QLightDM::SessionsModel* qSessionsModel =
25− static_cast<QLightDM::SessionsModel*>(sourceModel());
26−
27− return qSessionsModel->numSessions();
28−}
29−
30−int MockSessionsModel::numAvailableSessions() const
31−{
32− QLightDM::SessionsModel* qSessionsModel =
33− static_cast<QLightDM::SessionsModel*>(sourceModel());
34−
35− return qSessionsModel->numAvailableSessions();
36−}
37−
38−QString MockSessionsModel::testScenario() const
39−{
40− QLightDM::SessionsModel* qSessionsModel =
41− static_cast<QLightDM::SessionsModel*>(sourceModel());
42−
43− return qSessionsModel->testScenario();
44−}
45−
46−void MockSessionsModel::setNumSessions(const int numSessions)
47−{
48− QLightDM::SessionsModel* qSessionsModel =
49− static_cast<QLightDM::SessionsModel*>(sourceModel());
50−
51− if (qSessionsModel->numSessions() != numSessions) {
52− qSessionsModel->setNumSessions(numSessions);
53− Q_EMIT numSessionsChanged();
54− }
55−}
56−
57−void MockSessionsModel::setTestScenario(const QString testScenario)
58−{
59− QLightDM::SessionsModel* qSessionsModel =
60− static_cast<QLightDM::SessionsModel*>(sourceModel());
61−
62− if (qSessionsModel->testScenario() != testScenario) {
63− qSessionsModel->setTestScenario(testScenario);
64− Q_EMIT testScenarioChanged();
65− }
66−}
tests/mocks/LightDM/IntegratedLightDM/MockSessionsModel.hdeleted−44
@@ -1,44 +0,0 @@
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−
18−#ifndef MOCK_UNITY_SESSIONSMODEL_H
19−#define MOCK_UNITY_SESSIONSMODEL_H
20−
21−#include <SessionsModel.h>
22−
23−class MockSessionsModel : public SessionsModel
24−{
25− Q_OBJECT
26−
27− Q_PROPERTY(int numAvailableSessions READ numAvailableSessions CONSTANT)
28− Q_PROPERTY(int numSessions READ numSessions WRITE setNumSessions NOTIFY numSessionsChanged)
29− Q_PROPERTY(QString testScenario READ testScenario WRITE setTestScenario NOTIFY testScenarioChanged)
30−
31−public:
32− int numAvailableSessions() const;
33− int numSessions() const;
34− QString testScenario() const;
35− void setNumSessions(const int numSessions);
36− void setTestScenario(const QString testScenario);
37−
38−Q_SIGNALS:
39− void numSessionsChanged();
40− void testScenarioChanged();
41−
42−};
43−
44−#endif // MOCK_UNITY_SESSIONSMODEL_H
tests/mocks/LightDM/IntegratedLightDM/MockUsersModel.cppdeleted−44
@@ -1,44 +0,0 @@
1−/*
2− * Copyright (C) 2014 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−
18−#include "MockUsersModel.h"
19−#include <QLightDM/UsersModel>
20−#include <QSortFilterProxyModel>
21−
22−MockUsersModel::MockUsersModel(QObject* parent)
23− : UsersModel(parent)
24−{
25−}
26−
27−QString MockUsersModel::mockMode() const
28−{
29− QLightDM::UsersModel* qUsersModel =
30− static_cast<QLightDM::UsersModel*>(static_cast<QSortFilterProxyModel*>(sourceModel())->sourceModel());
31−
32− return qUsersModel->mockMode();
33−}
34−
35−void MockUsersModel::setMockMode(QString mockMode)
36−{
37− QLightDM::UsersModel* qUsersModel =
38− static_cast<QLightDM::UsersModel*>(static_cast<QSortFilterProxyModel*>(sourceModel())->sourceModel());
39−
40− if (qUsersModel->mockMode() != mockMode) {
41− qUsersModel->setMockMode(mockMode);
42− Q_EMIT mockModeChanged(mockMode);
43− }
44−}
tests/mocks/LightDM/IntegratedLightDM/QLightDM/Greeterdeleted−17
@@ -1,17 +0,0 @@
1−/*
2− * Copyright (C) 2014 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 "../liblightdm/Greeter.h"
tests/mocks/LightDM/IntegratedLightDM/QLightDM/SessionsModeldeleted−17
@@ -1,17 +0,0 @@
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 "../liblightdm/SessionsModel.h"
tests/mocks/LightDM/IntegratedLightDM/QLightDM/UsersModeldeleted−17
@@ -1,17 +0,0 @@
1−/*
2− * Copyright (C) 2014 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 "../liblightdm/UsersModel.h"
tests/mocks/LightDM/IntegratedLightDM/liblightdm/CMakeLists.txtdeleted−21
@@ -1,21 +0,0 @@
1−set(LibLightDM_SOURCES
2− Greeter.cpp
3− SessionsModel.cpp
4− UsersModel.cpp
5− GreeterPrivate.cpp
6− SessionsModelPrivate.cpp
7− UsersModelPrivate.cpp
8− ${CMAKE_SOURCE_DIR}/plugins/Utils/qvariantlistmodel.cpp
9− )
10−
11−add_library(MockLightDM SHARED ${LibLightDM_SOURCES})
12−
13−qt5_use_modules(MockLightDM Gui)
14−
15−set_target_properties(MockLightDM PROPERTIES
16− OUTPUT_NAME lightdm-qt5-3
17− SOVERSION 0)
18−
19−install(TARGETS MockLightDM
20− DESTINATION ${SHELL_INSTALL_QML}/mocks/LightDM/IntegratedLightDM/liblightdm
21− )
tests/mocks/LightDM/IntegratedLightDM/liblightdm/Greeter.cppdeleted−201
@@ -1,201 +0,0 @@
1−/*
2− * Copyright (C) 2014 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 "Greeter.h"
18−#include "GreeterPrivate.h"
19−#include <QtCore/QCoreApplication>
20−#include <QTimer>
21−
22−namespace QLightDM
23−{
24−
25−Greeter::Greeter(QObject *parent)
26− : QObject(parent),
27− d_ptr(new GreeterPrivate(this))
28−{
29−}
30−
31−Greeter::~Greeter()
32−{
33−}
34−
35−QString Greeter::authenticationUser() const
36−{
37− Q_D(const Greeter);
38− return d->authenticationUser;
39−}
40−
41−bool Greeter::hasGuestAccountHint() const
42−{
43− return true;
44−}
45−
46−QString Greeter::getHint(const QString &name) const
47−{
48− Q_UNUSED(name)
49− return "";
50−}
51−
52−QString Greeter::defaultSessionHint() const
53−{
54− return QStringLiteral("ubuntu");
55−}
56−
57−bool Greeter::hideUsersHint() const
58−{
59− return false;
60−}
61−
62−bool Greeter::showManualLoginHint() const
63−{
64− return true;
65−}
66−
67−bool Greeter::showRemoteLoginHint() const
68−{
69− return true;
70−}
71−
72−QString Greeter::selectUserHint() const
73−{
74− Q_D(const Greeter);
75− return d->selectUserHint;
76−}
77−
78−void Greeter::setSelectUserHint(const QString &selectUserHint)
79−{
80− Q_D(Greeter);
81− d->selectUserHint = selectUserHint;
82−}
83−
84−bool Greeter::selectGuestHint() const
85−{
86− return false;
87−}
88−
89−QString Greeter::autologinUserHint() const
90−{
91− return "";
92−}
93−
94−bool Greeter::autologinGuestHint() const
95−{
96− return false;
97−}
98−
99−int Greeter::autologinTimeoutHint() const
100−{
101− return 0;
102−}
103−
104−bool Greeter::inAuthentication() const
105−{
106− return false;
107−}
108−
109−QString Greeter::hostname() const
110−{
111− return "hostname1";
112−}
113−
114−bool Greeter::isAuthenticated() const
115−{
116− Q_D(const Greeter);
117− return d->authenticated;
118−}
119−
120−bool Greeter::connectSync()
121−{
122− return true;
123−}
124−
125−void Greeter::authenticate(const QString &username)
126−{
127− Q_D(Greeter);
128−
129− d->authenticated = false;
130− d->authenticationUser = username;
131− d->twoFactorDone = false;
132− QTimer::singleShot(0, this, &Greeter::delayedAuthentication);
133−}
134−
135−void Greeter::delayedAuthentication()
136−{
137− Q_D(Greeter);
138− d->handleAuthenticate();
139−}
140−
141−void Greeter::authenticateAsGuest()
142−{}
143−
144−void Greeter::authenticateAutologin()
145−{}
146−
147−void Greeter::authenticateRemote(const QString &session, const QString &username)
148−{
149− Q_UNUSED(session)
150− Q_UNUSED(username)
151−}
152−
153−void Greeter::cancelAuthentication()
154−{}
155−
156−void Greeter::setLanguage (const QString &language)
157−{
158− Q_UNUSED(language)
159−}
160−
161−bool Greeter::startSessionSync(const QString &session)
162−{
163− Q_UNUSED(session)
164− return true;
165−}
166−
167−void Greeter::respond(const QString &response)
168−{
169− Q_D(Greeter);
170−
171− d->handleRespond(response);
172−}
173−
174−void Greeter::sendAuthenticationComplete()
175−{
176− if (qEnvironmentVariableIsEmpty("UNITY_TESTING")) {
177− // simulate PAM's delay
178− QTimer::singleShot(1000, this, &Greeter::authenticationComplete);
179− } else {
180− Q_EMIT authenticationComplete();
181− }
182−}
183−
184−QString Greeter::mockMode() const
185−{
186− Q_D(const Greeter);
187− return d->mockMode;
188−}
189−
190−
191−void Greeter::setMockMode(QString mockMode)
192−{
193− Q_D(Greeter);
194−
195− if (d->mockMode != mockMode) {
196− d->mockMode = mockMode;
197− Q_EMIT mockModeChanged(mockMode);
198− }
199−}
200−
201−}
tests/mocks/LightDM/IntegratedLightDM/liblightdm/UsersModel.cppdeleted−121
@@ -1,121 +0,0 @@
1−/*
2− * Copyright (C) 2013-2016 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− * Author: Michael Terry <michael.terry@canonical.com>
17− */
18−
19−// LightDM currently is Qt4 compatible, and so doesn't define setRoleNames.
20−// To use the same method of setting role name that it does, we
21−// set our compatibility to Qt4 here too.
22−#define QT_DISABLE_DEPRECATED_BEFORE QT_VERSION_CHECK(4, 0, 0)
23−
24−#include "UsersModel.h"
25−#include "UsersModelPrivate.h"
26−#include <QtCore/QDir>
27−#include <QtCore/QString>
28−#include <QtGui/QIcon>
29−
30−namespace QLightDM
31−{
32−
33−UsersModel::UsersModel(QObject *parent) :
34− QAbstractListModel(parent),
35− d_ptr(new UsersModelPrivate(this))
36−{
37− // Extend roleNames (we want to keep the "display" role)
38− QHash<int, QByteArray> roles = roleNames();
39− roles[NameRole] = "name";
40− roles[RealNameRole] = "realName";
41− roles[LoggedInRole] = "loggedIn";
42− roles[BackgroundRole] = "background";
43− roles[BackgroundPathRole] = "backgroundPath";
44− roles[SessionRole] = "session";
45− roles[HasMessagesRole] = "hasMessages";
46− roles[ImagePathRole] = "imagePath";
47− roles[UidRole] = "uid";
48− setRoleNames(roles);
49−}
50−
51−UsersModel::~UsersModel()
52−{
53− delete d_ptr;
54−}
55−
56−int UsersModel::rowCount(const QModelIndex &parent) const
57−{
58− Q_D(const UsersModel);
59−
60− if (parent.isValid()) {
61− return 0;
62− } else { // parent is root
63− return d->entries.size();
64− }
65−}
66−
67−QVariant UsersModel::data(const QModelIndex &index, int role) const
68−{
69− Q_D(const UsersModel);
70−
71− if (!index.isValid()) {
72− return QVariant();
73− }
74−
75− int row = index.row();
76− switch (role) {
77− case Qt::DisplayRole:
78− return d->entries[row].real_name;
79− case Qt::DecorationRole:
80− return QIcon();
81− case UsersModel::NameRole:
82− return d->entries[row].username;
83− case UsersModel::RealNameRole:
84− return d->entries[row].real_name;
85− case UsersModel::SessionRole:
86− return d->entries[row].session;
87− case UsersModel::LoggedInRole:
88− return d->entries[row].is_active;
89− case UsersModel::BackgroundRole:
90− return QPixmap(d->entries[row].background);
91− case UsersModel::BackgroundPathRole:
92− return d->entries[row].background;
93− case UsersModel::HasMessagesRole:
94− return d->entries[row].has_messages;
95− case UsersModel::ImagePathRole:
96− return "";
97− case UsersModel::UidRole:
98− return d->entries[row].uid;
99− default:
100− return QVariant();
101− }
102−}
103−
104−QString UsersModel::mockMode() const
105−{
106− Q_D(const UsersModel);
107− return d->mockMode;
108−}
109−
110−void UsersModel::setMockMode(QString mockMode)
111−{
112− Q_D(UsersModel);
113−
114− if (d->mockMode != mockMode) {
115− d->mockMode = mockMode;
116− Q_EMIT mockModeChanged(mockMode);
117− d->resetEntries();
118− }
119−}
120−
121−}
tests/mocks/LightDM/IntegratedLightDM/plugin.cppdeleted−79
@@ -1,79 +0,0 @@
1−/*
2− * Copyright (C) 2014, 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 <DBusGreeter.h>
19−#include <DBusGreeterList.h>
20−#include "MockGreeter.h"
21−#include "MockSessionsModel.h"
22−#include "MockUsersModel.h"
23−#include <QLightDM/SessionsModel>
24−#include "ColorTheme.h"
25−#include "UserMetrics.h"
26−#include <QLightDM/UsersModel>
27−
28−#include <QAbstractItemModel>
29−#include <QDBusConnection>
30−#include <QtQml/qqml.h>
31−
32−static QObject *greeter_provider(QQmlEngine *engine, QJSEngine *scriptEngine)
33−{
34− Q_UNUSED(engine)
35− Q_UNUSED(scriptEngine)
36−
37− MockGreeter *greeter = new MockGreeter;
38− new DBusGreeter(greeter, "/");
39− new DBusGreeterList(greeter, "/list");
40−
41− return greeter;
42−}
43−
44−static QObject *sessions_provider(QQmlEngine *engine, QJSEngine *scriptEngine)
45−{
46− Q_UNUSED(engine)
47− Q_UNUSED(scriptEngine)
48− return new MockSessionsModel;
49−}
50−
51−static QObject *users_provider(QQmlEngine *engine, QJSEngine *scriptEngine)
52−{
53− Q_UNUSED(engine)
54− Q_UNUSED(scriptEngine)
55− return new MockUsersModel;
56−}
57−
58−static QObject *infographic_provider(QQmlEngine *engine, QJSEngine *scriptEngine)
59−{
60− Q_UNUSED(engine)
61− Q_UNUSED(scriptEngine)
62− return UserMetricsOutput::UserMetrics::getInstance();
63−}
64−
65−void LightDMPlugin::registerTypes(const char *uri)
66−{
67− qmlRegisterType<QAbstractItemModel>();
68− qmlRegisterType<UserMetricsOutput::ColorTheme>();
69−
70− Q_ASSERT(uri == QLatin1String("LightDM.IntegratedLightDM"));
71− qRegisterMetaType<QLightDM::Greeter::MessageType>("QLightDM::Greeter::MessageType");
72− qRegisterMetaType<QLightDM::Greeter::PromptType>("QLightDM::Greeter::PromptType");
73− qmlRegisterSingletonType<MockGreeter>(uri, 0, 1, "Greeter", greeter_provider);
74− qmlRegisterSingletonType<MockSessionsModel>(uri, 0, 1, "Sessions", sessions_provider);
75− qmlRegisterUncreatableType<QLightDM::SessionsModel>(uri, 0, 1, "SessionRoles", "Type is not instantiable");
76− qmlRegisterSingletonType<MockUsersModel>(uri, 0, 1, "Users", users_provider);
77− qmlRegisterUncreatableType<QLightDM::UsersModel>(uri, 0, 1, "UserRoles", "Type is not instantiable");
78− qmlRegisterSingletonType<UserMetricsOutput::UserMetrics>(uri, 0, 1, "Infographic", infographic_provider);
79−}
tests/mocks/LightDM/IntegratedLightDM/qmldirdeleted−2
@@ -1,2 +0,0 @@
1−module LightDM.IntegratedLightDM
2−plugin MockLightDM-qml
tests/mocks/LightDMController/CMakeLists.txtnew file+15
@@ -0,0 +1,15 @@
1+add_library(LightDMController MODULE
2+ plugin.cpp
3+ )
4+
5+target_include_directories(LightDMController PUBLIC
6+ ${CMAKE_SOURCE_DIR}/tests/mocks/liblightdm/
7+ )
8+
9+target_link_libraries(LightDMController
10+ MockLightDM
11+ )
12+
13+qt5_use_modules(LightDMController Qml)
14+
15+add_unity8_mock(LightDMController 0.1 LightDMController TARGETS LightDMController)
tests/mocks/LightDM/IntegratedLightDM/MockUsersModel.h → tests/mocks/LightDMController/plugin.cpprenamed+16−20
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2014 Canonical, Ltd.
2+ * Copyright (C) 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
@@ -12,28 +12,24 @@
1212 *
1313 * You should have received a copy of the GNU General Public License
1414 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− *
1615 */
1716
18−#ifndef MOCK_UNITY_USERSMODEL_H
19−#define MOCK_UNITY_USERSMODEL_H
20−
21−#include <UsersModel.h>
22−
23−class MockUsersModel : public UsersModel
24−{
25− Q_OBJECT
17+#include "plugin.h"
2618
27− Q_PROPERTY(QString mockMode READ mockMode WRITE setMockMode NOTIFY mockModeChanged)
19+#include "MockController.h"
2820
29−public:
30− explicit MockUsersModel(QObject* parent=0);
21+#include <QtQml/qqml.h>
3122
32− QString mockMode() const;
33− void setMockMode(QString mockMode);
34−
35−Q_SIGNALS:
36− void mockModeChanged(QString mode);
37−};
23+static QObject *mock_provider(QQmlEngine *engine, QJSEngine *scriptEngine)
24+{
25+ Q_UNUSED(scriptEngine)
26+ auto controller = QLightDM::MockController::instance();
27+ engine->setObjectOwnership(controller, QQmlEngine::CppOwnership);
28+ return controller;
29+}
3830
39−#endif // MOCK_UNITY_USERSMODEL_H
31+void LightDMControllerPlugin::registerTypes(const char *uri)
32+{
33+ Q_ASSERT(uri == QLatin1String("LightDMController"));
34+ qmlRegisterSingletonType<QLightDM::MockController>(uri, 0, 1, "LightDMController", mock_provider);
35+}
tests/mocks/LightDM/IntegratedLightDM/plugin.h → tests/mocks/LightDMController/plugin.hrenamed+5−8
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2014, 2015 Canonical, Ltd.
2+ * Copyright (C) 2016 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,13 +14,12 @@
1414 * along with this program. If not, see <http://www.gnu.org/licenses/>.
1515 */
1616
17−#ifndef MOCK_LIGHTDM_PLUGIN_H
18−#define MOCK_LIGHTDM_PLUGIN_H
17+#pragma once
1918
20−#include <QtQml/QQmlEngine>
21−#include <QtQml/QQmlExtensionPlugin>
19+#include <QQmlEngine>
20+#include <QQmlExtensionPlugin>
2221
23−class LightDMPlugin : public QQmlExtensionPlugin
22+class LightDMControllerPlugin : public QQmlExtensionPlugin
2423{
2524 Q_OBJECT
2625 Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface")
@@ -28,5 +27,3 @@ class LightDMPlugin : public QQmlExtensionPlugin
2827public:
2928 void registerTypes(const char *uri) override;
3029};
31−
32−#endif
tests/mocks/LightDMController/qmldirnew file+2
@@ -0,0 +1,2 @@
1+module LightDMController
2+plugin LightDMController
tests/mocks/Unity/Application/MirMock.cpp+13
@@ -47,3 +47,16 @@ QString MirMock::cursorName() const
4747{
4848 return m_cursorName;
4949}
50+
51+QString MirMock::currentKeymap() const
52+{
53+ return m_keymap;
54+}
55+
56+void MirMock::setCurrentKeymap(const QString &keymap)
57+{
58+ if (keymap != m_keymap) {
59+ m_keymap = keymap;
60+ Q_EMIT currentKeymapChanged(m_keymap);
61+ }
62+}
tests/mocks/Unity/Application/MirMock.h+4
@@ -31,9 +31,13 @@ public:
3131 void setCursorName(const QString &cursorName) override;
3232 QString cursorName() const override;
3333
34+ QString currentKeymap() const override;
35+ void setCurrentKeymap(const QString &keymap) override;
36+
3437private:
3538 static MirMock *the_mir;
3639 QString m_cursorName;
40+ QString m_keymap;
3741};
3842
3943#endif
tests/mocks/Unity/Launcher/MockAppDrawerModel.cpp−1
@@ -48,7 +48,6 @@ MockAppDrawerModel::MockAppDrawerModel(QObject *parent):
4848 m_list.append(item);
4949 item = new MockLauncherItem("libreoffice", "/usr/share/applications/libreoffice.desktop","Libre Office", "libreoffice", this);
5050 m_list.append(item);
51− qDebug() << "mock model created";
5251
5352 qsrand(QDateTime::currentMSecsSinceEpoch() / 1000);
5453}
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/mocks/liblightdm/CMakeLists.txtnew file+18
@@ -0,0 +1,18 @@
1+set(MockLightDM_SOURCES
2+ MockController.cpp
3+ MockGreeter.cpp
4+ MockSessionsModel.cpp
5+ MockUsersModel.cpp
6+ )
7+
8+add_library(MockLightDM SHARED ${MockLightDM_SOURCES})
9+
10+qt5_use_modules(MockLightDM DBus Gui)
11+
12+set_target_properties(MockLightDM PROPERTIES
13+ OUTPUT_NAME lightdm-qt5-3
14+ SOVERSION 0)
15+
16+install(TARGETS MockLightDM
17+ DESTINATION ${SHELL_INSTALL_QML}/mocks/liblightdm
18+ )
tests/mocks/LightDM/IntegratedLightDM/liblightdm/GreeterPrivate.cpp → tests/mocks/liblightdm/GreeterPrivate.cpprenamed

The content is the same.

tests/mocks/LightDM/IntegratedLightDM/liblightdm/GreeterPrivate.h → tests/mocks/liblightdm/GreeterPrivate.hrenamed

The content is the same.

tests/mocks/liblightdm/MockController.cppnew file+191
@@ -0,0 +1,191 @@
1+/*
2+ * Copyright (C) 2016 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 "MockController.h"
18+
19+static QLightDM::MockController *m_instance = nullptr;
20+
21+namespace QLightDM
22+{
23+
24+MockController::MockController(QObject *parent)
25+ : QObject(parent)
26+ , m_selectGuestHint(false)
27+ , m_hasGuestAccountHint(false)
28+ , m_fullSessions(
29+ {
30+ {"ubuntu", "Ubuntu"},
31+ {"ubuntu-2d", "Ubuntu 2D"},
32+ {"gnome", "GNOME"},
33+ {"gnome-classic", "GNOME Classic"},
34+ {"gnome-flashback-compiz", "GNOME Flashback (Compiz)"},
35+ {"gnome-flashback-metacity", "GNOME Flashback (Metacity)"},
36+ {"gnome-wayland", "GNOME on Wayland"},
37+ {"plasma", "Plasma"},
38+ {"kde", "KDE" },
39+ {"xterm", "Recovery Console"},
40+ {"", "Unknown?"}
41+ })
42+ , m_numSessions(0)
43+{
44+ reset();
45+}
46+
47+MockController::~MockController()
48+{
49+ m_instance = nullptr;
50+}
51+
52+MockController *MockController::instance()
53+{
54+ if (!m_instance) {
55+ m_instance = new MockController;
56+ }
57+ return m_instance;
58+}
59+
60+void MockController::reset()
61+{
62+ auto userMode = qgetenv("LIBLIGHTDM_MOCK_MODE");
63+ if (userMode.isEmpty()) {
64+ userMode = "full";
65+ }
66+
67+ setSelectUserHint("");
68+ setSelectGuestHint(false);
69+ setHasGuestAccountHint(false);
70+ setUserMode(userMode);
71+ setSessionMode("full");
72+ setNumSessions(numFullSessions());
73+ setShowManualLoginHint(false);
74+ setHideUsersHint(false);
75+}
76+
77+QString MockController::selectUserHint() const
78+{
79+ return m_selectUserHint;
80+}
81+
82+void MockController::setSelectUserHint(const QString &selectUserHint)
83+{
84+ if (m_selectUserHint != selectUserHint) {
85+ m_selectUserHint = selectUserHint;
86+ Q_EMIT selectUserHintChanged();
87+ }
88+}
89+
90+bool MockController::selectGuestHint() const
91+{
92+ return m_selectGuestHint;
93+}
94+
95+void MockController::setSelectGuestHint(bool selectGuestHint)
96+{
97+ if (m_selectGuestHint != selectGuestHint) {
98+ m_selectGuestHint = selectGuestHint;
99+ Q_EMIT selectGuestHintChanged();
100+ }
101+}
102+
103+bool MockController::hasGuestAccountHint() const
104+{
105+ return m_hasGuestAccountHint;
106+}
107+
108+void MockController::setHasGuestAccountHint(bool hasGuestAccountHint)
109+{
110+ if (m_hasGuestAccountHint != hasGuestAccountHint) {
111+ m_hasGuestAccountHint = hasGuestAccountHint;
112+ Q_EMIT hasGuestAccountHintChanged();
113+ }
114+}
115+
116+bool MockController::showManualLoginHint() const
117+{
118+ return m_showManualLoginHint;
119+}
120+
121+void MockController::setShowManualLoginHint(bool showManualLoginHint)
122+{
123+ if (m_showManualLoginHint != showManualLoginHint) {
124+ m_showManualLoginHint = showManualLoginHint;
125+ Q_EMIT showManualLoginHintChanged();
126+ }
127+}
128+
129+bool MockController::hideUsersHint() const
130+{
131+ return m_hideUsersHint;
132+}
133+
134+void MockController::setHideUsersHint(bool hideUsersHint)
135+{
136+ if (m_hideUsersHint != hideUsersHint) {
137+ m_hideUsersHint = hideUsersHint;
138+ Q_EMIT hideUsersHintChanged();
139+ }
140+}
141+
142+QString MockController::userMode() const
143+{
144+ return m_userMode;
145+}
146+
147+void MockController::setUserMode(const QString &userMode)
148+{
149+ if (m_userMode != userMode) {
150+ m_userMode = userMode;
151+ Q_EMIT userModeChanged();
152+ }
153+}
154+
155+QString MockController::sessionMode() const
156+{
157+ return m_sessionMode;
158+}
159+
160+void MockController::setSessionMode(const QString &sessionMode)
161+{
162+ if (m_sessionMode != sessionMode) {
163+ m_sessionMode = sessionMode;
164+ Q_EMIT sessionModeChanged();
165+ }
166+}
167+
168+const QList<MockController::SessionItem> &MockController::fullSessionItems() const
169+{
170+ return m_fullSessions;
171+}
172+
173+int MockController::numFullSessions() const
174+{
175+ return m_fullSessions.size();
176+}
177+
178+int MockController::numSessions() const
179+{
180+ return m_numSessions;
181+}
182+
183+void MockController::setNumSessions(int numSessions)
184+{
185+ if (m_numSessions != numSessions) {
186+ m_numSessions = numSessions;
187+ Q_EMIT numSessionsChanged();
188+ }
189+}
190+
191+}
tests/mocks/liblightdm/MockController.hnew file+106
@@ -0,0 +1,106 @@
1+/*
2+ * Copyright (C) 2016 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+#pragma once
18+
19+#include <QObject>
20+#include <QString>
21+
22+
23+namespace QLightDM
24+{
25+class Q_DECL_EXPORT MockController : public QObject
26+{
27+ Q_OBJECT
28+
29+ Q_PROPERTY(QString selectUserHint READ selectUserHint WRITE setSelectUserHint NOTIFY selectUserHintChanged)
30+ Q_PROPERTY(bool selectGuestHint READ selectGuestHint WRITE setSelectGuestHint NOTIFY selectGuestHintChanged)
31+ Q_PROPERTY(bool hasGuestAccountHint READ hasGuestAccountHint WRITE setHasGuestAccountHint NOTIFY hasGuestAccountHintChanged)
32+ Q_PROPERTY(bool showManualLoginHint READ showManualLoginHint WRITE setShowManualLoginHint NOTIFY showManualLoginHintChanged)
33+ Q_PROPERTY(bool hideUsersHint READ hideUsersHint WRITE setHideUsersHint NOTIFY hideUsersHintChanged)
34+
35+ // single, single-pin, single-passphrase, full
36+ Q_PROPERTY(QString userMode READ userMode WRITE setUserMode NOTIFY userModeChanged)
37+
38+ // single, none, full
39+ Q_PROPERTY(QString sessionMode READ sessionMode WRITE setSessionMode NOTIFY sessionModeChanged)
40+
41+ Q_PROPERTY(int numAvailableSessions READ numFullSessions CONSTANT)
42+ Q_PROPERTY(int numSessions READ numSessions WRITE setNumSessions NOTIFY numSessionsChanged)
43+
44+public:
45+ static MockController *instance();
46+ virtual ~MockController();
47+
48+ Q_INVOKABLE void reset();
49+
50+ QString selectUserHint() const;
51+ void setSelectUserHint(const QString &selectUserHint);
52+
53+ bool selectGuestHint() const;
54+ void setSelectGuestHint(bool selectGuestHint);
55+
56+ bool hasGuestAccountHint() const;
57+ void setHasGuestAccountHint(bool hasGuestAccountHint);
58+
59+ bool showManualLoginHint() const;
60+ void setShowManualLoginHint(bool showManualLoginHint);
61+
62+ bool hideUsersHint() const;
63+ void setHideUsersHint(bool hideUsersHint);
64+
65+ QString userMode() const;
66+ void setUserMode(const QString &userMode);
67+
68+ QString sessionMode() const;
69+ void setSessionMode(const QString &sessionMode);
70+
71+ class SessionItem
72+ {
73+ public:
74+ QString key;
75+ QString name;
76+ };
77+ int numFullSessions() const;
78+ const QList<SessionItem> &fullSessionItems() const;
79+
80+ int numSessions() const;
81+ void setNumSessions(int numSessions);
82+
83+Q_SIGNALS:
84+ void selectUserHintChanged();
85+ void selectGuestHintChanged();
86+ void hasGuestAccountHintChanged();
87+ void showManualLoginHintChanged();
88+ void hideUsersHintChanged();
89+ void userModeChanged();
90+ void sessionModeChanged();
91+ void numSessionsChanged();
92+
93+private:
94+ explicit MockController(QObject* parent=0);
95+
96+ QString m_selectUserHint;
97+ bool m_selectGuestHint;
98+ bool m_hasGuestAccountHint;
99+ bool m_showManualLoginHint;
100+ bool m_hideUsersHint;
101+ QString m_userMode;
102+ QString m_sessionMode;
103+ QList<SessionItem> m_fullSessions;
104+ int m_numSessions;
105+};
106+}
tests/mocks/liblightdm/MockGreeter.cppnew file+316
@@ -0,0 +1,316 @@
1+/*
2+ * Copyright (C) 2014-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 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 "MockController.h"
18+#include "MockGreeter.h"
19+#include <QDBusInterface>
20+#include <QDBusPendingCall>
21+#include <QTimer>
22+
23+namespace QLightDM
24+{
25+
26+class GreeterPrivate
27+{
28+public:
29+ bool authenticated = false;
30+ QString authenticationUser;
31+ bool twoFactorDone = false;
32+ QTimer authCallbackTimer;
33+};
34+
35+Greeter::Greeter(QObject *parent)
36+ : QObject(parent)
37+ , d_ptr(new GreeterPrivate)
38+{
39+ Q_D(Greeter);
40+
41+ d->authCallbackTimer.setSingleShot(true);
42+ connect(&d->authCallbackTimer, &QTimer::timeout,
43+ this, &QLightDM::Greeter::handleAuthenticate);
44+}
45+
46+Greeter::~Greeter()
47+{
48+ delete d_ptr;
49+}
50+
51+QString Greeter::authenticationUser() const
52+{
53+ Q_D(const Greeter);
54+
55+ return d->authenticationUser;
56+}
57+
58+bool Greeter::hasGuestAccountHint() const
59+{
60+ return MockController::instance()->hasGuestAccountHint();
61+}
62+
63+QString Greeter::getHint(const QString &name) const
64+{
65+ Q_UNUSED(name)
66+ return "";
67+}
68+
69+QString Greeter::defaultSessionHint() const
70+{
71+ return QStringLiteral("ubuntu");
72+}
73+
74+bool Greeter::hideUsersHint() const
75+{
76+ return MockController::instance()->hideUsersHint();
77+}
78+
79+bool Greeter::showManualLoginHint() const
80+{
81+ return MockController::instance()->showManualLoginHint();
82+}
83+
84+bool Greeter::showRemoteLoginHint() const
85+{
86+ return false;
87+}
88+
89+QString Greeter::selectUserHint() const
90+{
91+ return MockController::instance()->selectUserHint();
92+}
93+
94+bool Greeter::selectGuestHint() const
95+{
96+ return MockController::instance()->selectGuestHint();
97+}
98+
99+QString Greeter::autologinUserHint() const
100+{
101+ return "";
102+}
103+
104+bool Greeter::autologinGuestHint() const
105+{
106+ return false;
107+}
108+
109+int Greeter::autologinTimeoutHint() const
110+{
111+ return 0;
112+}
113+
114+bool Greeter::inAuthentication() const
115+{
116+ return false;
117+}
118+
119+QString Greeter::hostname() const
120+{
121+ return "hostname1";
122+}
123+
124+bool Greeter::isAuthenticated() const
125+{
126+ Q_D(const Greeter);
127+
128+ return d->authenticated;
129+}
130+
131+bool Greeter::connectSync()
132+{
133+ return true;
134+}
135+
136+void Greeter::authenticate(const QString &username)
137+{
138+ Q_D(Greeter);
139+
140+ d->authenticated = false;
141+ d->authenticationUser = username;
142+ d->twoFactorDone = false;
143+ d->authCallbackTimer.start();
144+}
145+
146+void Greeter::handleAuthenticate()
147+{
148+ Q_D(Greeter);
149+
150+ if (d->authenticated) {
151+ // This can happen from the guest authentication flow
152+ Q_EMIT authenticationComplete();
153+ return;
154+ }
155+
156+ if (d->authenticationUser.isEmpty()) {
157+ Q_EMIT showPrompt("Username: ", Greeter::PromptTypeQuestion);
158+ return;
159+ }
160+
161+ // Send out any messages we need to
162+ if (d->authenticationUser == "info-prompt")
163+ Q_EMIT showMessage("Welcome to Unity Greeter", Greeter::MessageTypeInfo);
164+ else if (d->authenticationUser == "wide-info-prompt")
165+ Q_EMIT showMessage("Welcome to Unity Greeter, the greeteriest greeter that ever did appear in these fine lands", Greeter::MessageTypeInfo);
166+ else if (d->authenticationUser == "html-info-prompt")
167+ Q_EMIT showMessage("<b>&</b>", Greeter::MessageTypeInfo);
168+ else if (d->authenticationUser == "long-info-prompt")
169+ Q_EMIT showMessage("Welcome to Unity Greeter\n\nWe like to annoy you with super ridiculously long messages.\nLike this one\n\nThis is the last line of a multiple line message.", Greeter::MessageTypeInfo);
170+ else if (d->authenticationUser == "multi-info-prompt") {
171+ Q_EMIT showMessage("Welcome to Unity Greeter", Greeter::MessageTypeInfo);
172+ Q_EMIT showMessage("This is an error", Greeter::MessageTypeError);
173+ Q_EMIT showMessage("You should have seen three messages", Greeter::MessageTypeInfo);
174+ }
175+
176+ // OK, now actually do the prompt
177+ if (d->authenticationUser == "no-password") {
178+ d->authenticated = true;
179+ Q_EMIT authenticationComplete();
180+ } else if (d->authenticationUser == "has-pin") {
181+ Q_EMIT showPrompt("Password: ", Greeter::PromptTypeSecret);
182+ } else if (d->authenticationUser == "auth-error") {
183+ d->authenticated = false;
184+ Q_EMIT authenticationComplete();
185+ } else if (d->authenticationUser == "different-prompt") {
186+ Q_EMIT showPrompt("Secret word: ", Greeter::PromptTypeSecret);
187+ } else if (d->authenticationUser == "question-prompt") {
188+ Q_EMIT showPrompt("Favorite Color (blue): ", Greeter::PromptTypeQuestion);
189+ } else if (d->authenticationUser == "two-prompts") {
190+ Q_EMIT showPrompt("Favorite Color (blue):", Greeter::PromptTypeQuestion);
191+ Q_EMIT showPrompt("Password: ", Greeter::PromptTypeSecret);
192+ } else if (d->authenticationUser == "wacky-prompts") {
193+ Q_EMIT showMessage("First message", Greeter::MessageTypeInfo);
194+ Q_EMIT showPrompt("Favorite Color (blue)", Greeter::PromptTypeQuestion);
195+ Q_EMIT showMessage("Second message", Greeter::MessageTypeError);
196+ Q_EMIT showPrompt("Password: ", Greeter::PromptTypeSecret);
197+ Q_EMIT showMessage("Last message", Greeter::MessageTypeInfo);
198+ } else {
199+ Q_EMIT showPrompt("Password: ", Greeter::PromptTypeSecret);
200+ }
201+}
202+
203+void Greeter::authenticateAsGuest()
204+{
205+ Q_D(Greeter);
206+
207+ d->authenticated = true;
208+ d->authenticationUser = QString(); // this is what the real liblightdm does
209+ d->twoFactorDone = false;
210+ d->authCallbackTimer.start();
211+}
212+
213+void Greeter::authenticateAutologin()
214+{}
215+
216+void Greeter::authenticateRemote(const QString &session, const QString &username)
217+{
218+ Q_UNUSED(session)
219+ Q_UNUSED(username)
220+}
221+
222+void Greeter::cancelAuthentication()
223+{
224+ Q_D(Greeter);
225+ d->authCallbackTimer.stop();
226+}
227+
228+void Greeter::setLanguage (const QString &language)
229+{
230+ Q_UNUSED(language)
231+}
232+
233+bool Greeter::startSessionSync(const QString &session)
234+{
235+ Q_UNUSED(session)
236+
237+ // Send a request to hide the greeter. This is normally done by logind,
238+ // but when testing, we don't want the bother of mocking that out. Instead,
239+ // just send the request directly ourselves.
240+ QDBusInterface iface("com.canonical.UnityGreeter",
241+ "/",
242+ "com.canonical.UnityGreeter",
243+ QDBusConnection::sessionBus());
244+ iface.asyncCall("HideGreeter");
245+
246+ return true;
247+}
248+
249+void Greeter::respond(const QString &response)
250+{
251+ Q_D(Greeter);
252+
253+ if (d->authenticationUser.isEmpty()) {
254+ // A manual login, our first question was which username
255+ d->authenticationUser = response;
256+ handleAuthenticate();
257+ return;
258+ }
259+
260+ if (d->authenticationUser == "no-response") {
261+ return;
262+ } else if (d->authenticationUser == "locked") {
263+ Q_EMIT showMessage("Account is locked", Greeter::MessageTypeError);
264+ sendAuthenticationComplete();
265+ return;
266+ } else if (d->authenticationUser == "two-factor") {
267+ if (!d->twoFactorDone) {
268+ if (response == "password") {
269+ d->twoFactorDone = true;
270+ Q_EMIT showPrompt("otp", Greeter::PromptTypeQuestion);
271+ } else {
272+ d->authenticated = false;
273+ sendAuthenticationComplete();
274+ }
275+ } else {
276+ d->authenticated = (response == "otp");
277+ sendAuthenticationComplete();
278+ }
279+ return;
280+ } else if (d->authenticationUser == "two-prompts" || d->authenticationUser == "wacky-prompts") {
281+ if (!d->twoFactorDone) {
282+ d->authenticated = (response == "blue");
283+ d->twoFactorDone = true;
284+ } else {
285+ d->authenticated = d->authenticated && (response == "password");
286+ sendAuthenticationComplete();
287+ }
288+ return;
289+ }
290+
291+ if (d->authenticationUser == "has-pin") {
292+ d->authenticated = (response == "1234");
293+ } else if (d->authenticationUser == "question-prompt") {
294+ d->authenticated = (response == "blue");
295+ } else {
296+ d->authenticated = (response == "password");
297+ }
298+
299+ if (d->authenticationUser == "info-after-login" && d->authenticated) {
300+ Q_EMIT showMessage("Congratulations on logging in!", Greeter::MessageTypeInfo);
301+ }
302+
303+ sendAuthenticationComplete();
304+}
305+
306+void Greeter::sendAuthenticationComplete()
307+{
308+ if (qEnvironmentVariableIsEmpty("UNITY_TESTING")) {
309+ // simulate PAM's delay
310+ QTimer::singleShot(1000, this, &Greeter::authenticationComplete);
311+ } else {
312+ Q_EMIT authenticationComplete();
313+ }
314+}
315+
316+}
tests/mocks/LightDM/IntegratedLightDM/liblightdm/Greeter.h → tests/mocks/liblightdm/MockGreeter.hrenamed+8−21
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2013 Canonical, Ltd.
2+ * Copyright (C) 2013-2016 Canonical, Ltd.
33 * Copyright (C) 2010-2011 David Edmundson.
44 * Copyright (C) 2010-2011 Robert Ancell
55 *
@@ -18,16 +18,14 @@
1818 * Author: David Edmundson <kde@davidedmundson.co.uk>
1919 */
2020
21−#ifndef UNITY_MOCK_GREETER_H
22−#define UNITY_MOCK_GREETER_H
23−
24−#include <QtCore/QObject>
25−#include <QtCore/QVariant>
21+#pragma once
2622
23+#include <QObject>
24+#include <QVariant>
2725
2826namespace QLightDM
2927{
30− class GreeterPrivate;
28+class GreeterPrivate;
3129
3230class Q_DECL_EXPORT Greeter : public QObject
3331{
@@ -42,9 +40,6 @@ class Q_DECL_EXPORT Greeter : public QObject
4240 Q_PROPERTY(QString hostname READ hostname CONSTANT)
4341 Q_PROPERTY(bool hasGuestAccount READ hasGuestAccountHint CONSTANT)
4442
45− //Mock-only API for testing purposes
46− Q_PROPERTY(QString mockMode READ mockMode WRITE setMockMode NOTIFY mockModeChanged)
47−
4843public:
4944 enum PromptType {
5045 PromptTypeQuestion,
@@ -68,7 +63,6 @@ public:
6863 bool showRemoteLoginHint() const;
6964 bool hasGuestAccountHint() const;
7065 QString selectUserHint() const;
71− void setSelectUserHint(const QString &selectUserHint); // only in mock
7266 bool selectGuestHint() const;
7367 QString autologinUserHint() const;
7468 bool autologinGuestHint() const;
@@ -79,9 +73,6 @@ public:
7973 QString authenticationUser() const;
8074 QString hostname() const;
8175
82− QString mockMode() const;
83− void setMockMode(QString mockMode);
84−
8576public Q_SLOTS:
8677 bool connectSync();
8778 void authenticate(const QString &username=QString());
@@ -98,18 +89,14 @@ Q_SIGNALS:
9889 void showPrompt(QString text, QLightDM::Greeter::PromptType type);
9990 void authenticationComplete();
10091 void autologinTimerExpired();
101− void mockModeChanged(QString mode);
102−
103−protected:
104− void sendAuthenticationComplete();
10592
10693private Q_SLOTS:
107− void delayedAuthentication();
94+ void handleAuthenticate();
10895
10996private:
97+ void sendAuthenticationComplete();
98+
11099 GreeterPrivate *d_ptr;
111100 Q_DECLARE_PRIVATE(Greeter)
112101};
113102}
114−
115−#endif // UNITY_MOCK_GREETER_H
tests/mocks/LightDM/IntegratedLightDM/liblightdm/SessionsModel.cpp → tests/mocks/liblightdm/MockSessionsModel.cpprenamed+40−42
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2015 Canonical, Ltd.
2+ * Copyright (C) 2015-2016 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
@@ -21,21 +21,35 @@
2121// set our compatibility to Qt4 here too.
2222#define QT_DISABLE_DEPRECATED_BEFORE QT_VERSION_CHECK(4, 0, 0)
2323
24−#include "SessionsModel.h"
25−#include "SessionsModelPrivate.h"
26−#include <QtCore/QDir>
27−#include <QtCore/QString>
24+#include "MockController.h"
25+#include "MockSessionsModel.h"
2826
2927namespace QLightDM
3028{
3129
32−SessionsModel::SessionsModel(QObject* parent) :
33− QAbstractListModel(parent),
34− d_ptr(new SessionsModelPrivate(this))
30+class SessionsModelPrivate
3531{
36− m_roleNames = QAbstractListModel::roleNames();
37− m_roleNames[KeyRole] = "key";
38− m_roleNames[TypeRole] = "type";
32+public:
33+ QHash<int, QByteArray> roleNames;
34+ QList<MockController::SessionItem> sessionItems;
35+};
36+
37+SessionsModel::SessionsModel(QObject* parent)
38+ : QAbstractListModel(parent)
39+ , d_ptr(new SessionsModelPrivate)
40+{
41+ Q_D(SessionsModel);
42+
43+ d->roleNames = QAbstractListModel::roleNames();
44+ d->roleNames[KeyRole] = "key";
45+ d->roleNames[TypeRole] = "type";
46+ setRoleNames(d->roleNames);
47+
48+ connect(MockController::instance(), &MockController::sessionModeChanged,
49+ this, &SessionsModel::resetEntries);
50+ connect(MockController::instance(), &MockController::numSessionsChanged,
51+ this, &SessionsModel::resetEntries);
52+ resetEntries();
3953}
4054
4155SessionsModel::~SessionsModel()
@@ -65,7 +79,9 @@ QVariant SessionsModel::data(const QModelIndex& index, int role) const
6579
6680QHash<int, QByteArray> SessionsModel::roleNames() const
6781{
68− return m_roleNames;
82+ Q_D(const SessionsModel);
83+
84+ return d->roleNames;
6985}
7086
7187int SessionsModel::rowCount(const QModelIndex& parent) const
@@ -79,42 +95,24 @@ int SessionsModel::rowCount(const QModelIndex& parent) const
7995 }
8096}
8197
82−int SessionsModel::numSessions() const
83−{
84− Q_D(const SessionsModel);
85− return d->numSessions;
86−}
87−
88−int SessionsModel::numAvailableSessions() const
89−{
90− Q_D(const SessionsModel);
91− return d->numAvailableSessions();
92−}
93−
94−QString SessionsModel::testScenario() const
95−{
96− Q_D(const SessionsModel);
97− return d->testScenario;
98−}
99−
100−void SessionsModel::setNumSessions(int numSessions)
98+void SessionsModel::resetEntries()
10199{
102100 Q_D(SessionsModel);
103101
104− if (d->numSessions != numSessions) {
105− d->numSessions = numSessions;
106− d->resetEntries();
107− }
108−}
102+ beginResetModel();
109103
110−void SessionsModel::setTestScenario(QString testScenario)
111−{
112− Q_D(SessionsModel);
104+ QString sessionMode = MockController::instance()->sessionMode();
113105
114− if (d->testScenario != testScenario) {
115− d->testScenario = testScenario;
116− d->resetEntries();
106+ if (sessionMode == "full") {
107+ d->sessionItems = MockController::instance()->fullSessionItems();
108+ d->sessionItems = d->sessionItems.mid(0, MockController::instance()->numSessions());
109+ } else if (sessionMode == "single") {
110+ d->sessionItems = {MockController::instance()->fullSessionItems()[0]};
111+ } else {
112+ d->sessionItems = {};
117113 }
114+
115+ endResetModel();
118116}
119117
120118} // namespace QLightDM
tests/mocks/LightDM/IntegratedLightDM/liblightdm/SessionsModel.h → tests/mocks/liblightdm/MockSessionsModel.hrenamed+9−17
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2015 Canonical, Ltd.
2+ * Copyright (C) 2015-2016 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
@@ -15,11 +15,12 @@
1515 *
1616 */
1717
18−#ifndef UNITY_MOCK_SESSIONSMODEL_H
19−#define UNITY_MOCK_SESSIONSMODEL_H
18+#pragma once
2019
21−#include <QtCore/QAbstractListModel>
22−#include <QtCore/QString>
20+#include <QAbstractListModel>
21+#include <QByteArray>
22+#include <QHash>
23+#include <QString>
2324
2425namespace QLightDM
2526{
@@ -30,7 +31,6 @@ class Q_DECL_EXPORT SessionsModel : public QAbstractListModel
3031 Q_OBJECT
3132
3233 public:
33−
3434 enum SessionModelRoles {
3535 //name is exposed as Qt::DisplayRole
3636 //comment is exposed as Qt::TooltipRole
@@ -54,19 +54,11 @@ class Q_DECL_EXPORT SessionsModel : public QAbstractListModel
5454 int rowCount(const QModelIndex& parent) const override;
5555 QVariant data(const QModelIndex& index, int role) const override;
5656
57− int numSessions() const;
58− int numAvailableSessions() const;
59− QString testScenario() const;
60− void setNumSessions(int numSessions);
61− void setTestScenario(QString testScenario);
62−
63− protected:
64− SessionsModelPrivate* const d_ptr;
57+ private Q_SLOTS:
58+ void resetEntries();
6559
6660 private:
67− QHash<int, QByteArray> m_roleNames;
61+ SessionsModelPrivate *d_ptr;
6862 Q_DECLARE_PRIVATE(SessionsModel)
6963 };
7064}
71−
72−#endif // UNITY_MOCK_SESSIONSMODEL_H
tests/mocks/liblightdm/MockUsersModel.cppnew file+219
@@ -0,0 +1,219 @@
1+/*
2+ * Copyright (C) 2013-2016 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+ * Author: Michael Terry <michael.terry@canonical.com>
17+ */
18+
19+// LightDM currently is Qt4 compatible, and so doesn't define setRoleNames.
20+// To use the same method of setting role name that it does, we
21+// set our compatibility to Qt4 here too.
22+#define QT_DISABLE_DEPRECATED_BEFORE QT_VERSION_CHECK(4, 0, 0)
23+
24+#include "MockController.h"
25+#include "MockUsersModel.h"
26+#include <QDir>
27+#include <QIcon>
28+
29+namespace QLightDM
30+{
31+
32+class Entry
33+{
34+public:
35+ QString username;
36+ QString real_name;
37+ QString background;
38+ QString layouts;
39+ bool is_active;
40+ bool has_messages;
41+ QString session;
42+ uid_t uid;
43+};
44+
45+class UsersModelPrivate
46+{
47+public:
48+ QList<Entry> entries;
49+};
50+
51+UsersModel::UsersModel(QObject *parent)
52+ : QAbstractListModel(parent)
53+ , d_ptr(new UsersModelPrivate)
54+{
55+ // Extend roleNames (we want to keep the "display" role)
56+ QHash<int, QByteArray> roles = roleNames();
57+ roles[NameRole] = "name";
58+ roles[RealNameRole] = "realName";
59+ roles[LoggedInRole] = "loggedIn";
60+ roles[BackgroundRole] = "background";
61+ roles[BackgroundPathRole] = "backgroundPath";
62+ roles[SessionRole] = "session";
63+ roles[HasMessagesRole] = "hasMessages";
64+ roles[ImagePathRole] = "imagePath";
65+ roles[UidRole] = "uid";
66+ setRoleNames(roles);
67+
68+ connect(MockController::instance(), &MockController::hasGuestAccountHintChanged,
69+ this, &UsersModel::resetEntries);
70+ connect(MockController::instance(), &MockController::showManualLoginHintChanged,
71+ this, &UsersModel::resetEntries);
72+ connect(MockController::instance(), &MockController::hideUsersHintChanged,
73+ this, &UsersModel::resetEntries);
74+ connect(MockController::instance(), &MockController::userModeChanged,
75+ this, &UsersModel::resetEntries);
76+ resetEntries();
77+}
78+
79+UsersModel::~UsersModel()
80+{
81+ delete d_ptr;
82+}
83+
84+void UsersModel::setCurrentSessionName(const QString &sessionName, const QString &username)
85+{
86+ Q_D(UsersModel);
87+
88+ int usersIndex = 0;
89+ for (auto &entry : d->entries) {
90+ if (username == entry.username) {
91+ entry.session = sessionName;
92+ break;
93+ }
94+ usersIndex++;
95+ }
96+
97+ dataChanged(index(usersIndex, 0), index(usersIndex, 0), QVector<int>(SessionRole));
98+}
99+
100+int UsersModel::rowCount(const QModelIndex &parent) const
101+{
102+ Q_D(const UsersModel);
103+
104+ if (parent.isValid()) {
105+ return 0;
106+ } else { // parent is root
107+ return d->entries.size();
108+ }
109+}
110+
111+QVariant UsersModel::data(const QModelIndex &index, int role) const
112+{
113+ Q_D(const UsersModel);
114+
115+ if (!index.isValid()) {
116+ return QVariant();
117+ }
118+
119+ int row = index.row();
120+ switch (role) {
121+ case Qt::DisplayRole:
122+ return d->entries[row].real_name;
123+ case Qt::DecorationRole:
124+ return QIcon();
125+ case UsersModel::NameRole:
126+ return d->entries[row].username;
127+ case UsersModel::RealNameRole:
128+ return d->entries[row].real_name;
129+ case UsersModel::SessionRole:
130+ return d->entries[row].session;
131+ case UsersModel::LoggedInRole:
132+ return d->entries[row].is_active;
133+ case UsersModel::BackgroundRole:
134+ return QPixmap(d->entries[row].background);
135+ case UsersModel::BackgroundPathRole:
136+ return d->entries[row].background;
137+ case UsersModel::HasMessagesRole:
138+ return d->entries[row].has_messages;
139+ case UsersModel::ImagePathRole:
140+ return "";
141+ case UsersModel::UidRole:
142+ return d->entries[row].uid;
143+ default:
144+ return QVariant();
145+ }
146+}
147+
148+void UsersModel::resetEntries()
149+{
150+ Q_D(UsersModel);
151+
152+ beginResetModel();
153+
154+ QString userMode = MockController::instance()->userMode();
155+
156+ if (userMode == "single") {
157+ d->entries = {{"no-password", "No Password", "", 0, false, false, "ubuntu", 0}};
158+ } else if (userMode == "single-passphrase") {
159+ d->entries = {{"has-password", "Has Password", "", 0, false, false, "ubuntu", 0}};
160+ } else if (userMode == "single-pin") {
161+ d->entries = {{"has-pin", "Has PIN", "", 0, false, false, "ubuntu", 0}};
162+ } else if (userMode == "full") {
163+ /*
164+ * Since the real model sorts these entries alphabetically, the model indices
165+ * don't line up correctly. The easiest way to fix this is to ensure that this
166+ * list of entries REMAINS IN ALPHABETICAL ORDER at all times.
167+ */
168+ d->entries = {
169+ { "active", "Active Account", 0, 0, true, false, "ubuntu", 0 },
170+ { "auth-error", "Auth Error", 0, 0, false, false, "ubuntu", 0 },
171+ { "black-background", "Black Background", "#000000", 0, false, false, "ubuntu", 0 },
172+ { "color-background", "Color Background", "#E95420", 0, false, false, "ubuntu", 0 },
173+ { "different-prompt", "Different Prompt", 0, 0, false, false, "ubuntu", 0 },
174+ { "empty-name", "", 0, 0, false, false, "ubuntu", 0 },
175+ { "has-password", "Has Password", 0, 0, false, false, "ubuntu", 0 },
176+ { "has-pin", "Has PIN", 0, 0, false, false, "ubuntu", 0 },
177+ { "html-info-prompt", "HTML Info Prompt", 0, 0, false, false, "ubuntu", 0 },
178+ { "info-after-login", "Info After Login", 0, 0, false, false, "ubuntu", 0 },
179+ { "info-prompt", "Info Prompt", 0, 0, false, false, "ubuntu", 0 },
180+ { "wacky-prompts", "Interspersed Prompts", 0, 0, false, false, "ubuntu", 0 },
181+ { "invalid-session", "Invalid Session", 0, 0, false, false, "invalid", 0 },
182+ { "locked", "Locked", 0, 0, false, false, "ubuntu", 0 },
183+ { "long-info-prompt", "Long Info Prompt", 0, 0, false, false, "ubuntu", 0 },
184+ { "long-name", "Long name (far far too long to fit, seriously this would never fit on the screen, you will never see this part of the name)", 0, 0, false, false, "ubuntu", 0 },
185+ { "multi-info-prompt", "Multi Info Prompt", 0, 0, false, false, "ubuntu", 0 },
186+ { "no-background", "No Background", "", 0, false, false, "ubuntu", 0 },
187+ { "no-password", "No Password", 0, 0, false, false, "ubuntu", 0 },
188+ { "no-response", "No Response", 0, 0, false, false, "ubuntu", 0 },
189+ { "question-prompt", "Question Prompt", 0, 0, false, false, "ubuntu", 0 },
190+ { "two-factor", "Two Factor", 0, 0, false, false, "ubuntu", 0 },
191+ { "two-prompts", "Two Prompts", 0, 0, false, false, "ubuntu", 0 },
192+ // white and black are a bit redundant, but useful for manually testing if UI is still readable
193+ { "white-background", "White Background", "#ffffff", 0, false, false, "ubuntu", 0 },
194+ { "wide-info-prompt", "Wide Info Prompt", 0, 0, false, false, "ubuntu", 0 },
195+ { "unicode", "가나다라마", 0, 0, false, false, "ubuntu", 0 }
196+ };
197+ }
198+
199+ // Assign uids in a loop, just to avoid having to muck with them when
200+ // adding or removing test users.
201+ for (int i = 0; i < d->entries.size(); i++) {
202+ d->entries[i].uid = i + 1;
203+ }
204+
205+ // Assign backgrounds
206+ QDir backgroundDir("/usr/share/backgrounds");
207+ QStringList backgrounds = backgroundDir.entryList(QDir::Files);
208+ if (!backgrounds.empty()) {
209+ for (int i = 0; i < d->entries.size(); i++) {
210+ if (d->entries[i].background.isNull()) {
211+ d->entries[i].background = backgroundDir.filePath(backgrounds[i % backgrounds.size()]);
212+ }
213+ }
214+ }
215+
216+ endResetModel();
217+}
218+
219+}
tests/mocks/LightDM/IntegratedLightDM/liblightdm/UsersModel.h → tests/mocks/liblightdm/MockUsersModel.hrenamed+8−16
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2013-2014 Canonical, Ltd.
2+ * Copyright (C) 2013-2016 Canonical, Ltd.
33 * Copyright (C) 2010-2011 David Edmundson.
44 *
55 * This program is free software; you can redistribute it and/or modify
@@ -15,13 +15,11 @@
1515 * along with this program. If not, see <http://www.gnu.org/licenses/>.
1616 */
1717
18−#ifndef UNITY_MOCK_USERSMODEL_H
19−#define UNITY_MOCK_USERSMODEL_H
18+#pragma once
2019
21−#include <QtCore/QString>
22−#include <QtCore/QSharedDataPointer>
2320#include <QAbstractListModel>
24−
21+#include <QList>
22+#include <QString>
2523
2624namespace QLightDM
2725{
@@ -31,12 +29,6 @@ class Q_DECL_EXPORT UsersModel : public QAbstractListModel
3129{
3230 Q_OBJECT
3331
34− //Mock-only API for testing purposes
35− Q_PROPERTY(QString mockMode READ mockMode WRITE setMockMode NOTIFY mockModeChanged)
36−
37−Q_SIGNALS:
38− void mockModeChanged(QString mode);
39−
4032public:
4133 explicit UsersModel(QObject *parent = 0);
4234 virtual ~UsersModel();
@@ -56,8 +48,10 @@ public:
5648 int rowCount(const QModelIndex &parent) const override;
5749 QVariant data(const QModelIndex &index, int role) const override;
5850
59− QString mockMode() const;
60− void setMockMode(QString mockMode);
51+ void setCurrentSessionName(const QString &sessionName, const QString &username);
52+
53+private Q_SLOTS:
54+ void resetEntries();
6155
6256private:
6357 UsersModelPrivate * const d_ptr;
@@ -65,5 +59,3 @@ private:
6559};
6660
6761}
68−
69−#endif // UNITY_MOCK_USERSMODEL_H
tests/mocks/LightDM/IntegratedLightDM/liblightdm/SessionsModelPrivate.cpp → tests/mocks/liblightdm/SessionsModelPrivate.cpprenamed

The content is the same.

tests/mocks/LightDM/IntegratedLightDM/liblightdm/SessionsModelPrivate.h → tests/mocks/liblightdm/SessionsModelPrivate.hrenamed

The content is the same.

tests/mocks/LightDM/IntegratedLightDM/liblightdm/UsersModelPrivate.cpp → tests/mocks/liblightdm/UsersModelPrivate.cpprenamed

The content is the same.

tests/mocks/LightDM/IntegratedLightDM/liblightdm/UsersModelPrivate.h → tests/mocks/liblightdm/UsersModelPrivate.hrenamed

The content is the same.

tests/plugins/LightDM/IntegratedLightDM/CMakeLists.txt+34−6
@@ -1,11 +1,13 @@
11include_directories(
22 ${GLIB_INCLUDE_DIRS}
33 ${CMAKE_CURRENT_BINARY_DIR}
4+ ${CMAKE_SOURCE_DIR}/plugins/LightDM/IntegratedLightDM
45 )
56
67add_executable(GreeterDBusTestExec
78 dbus.cpp
89 ${CMAKE_SOURCE_DIR}/plugins/LightDM/Greeter.cpp
10+ ${CMAKE_SOURCE_DIR}/plugins/LightDM/PromptsModel.cpp
911 )
1012# This add_dependencies is needed since we're linking with with -L and -l below
1113# make seems to work but ninja breaks without it
@@ -16,7 +18,6 @@ target_link_libraries(GreeterDBusTestExec
1618 )
1719target_include_directories(GreeterDBusTestExec PUBLIC
1820 ${CMAKE_SOURCE_DIR}/plugins/LightDM
19− ${CMAKE_SOURCE_DIR}/tests/mocks/LightDM/IntegratedLightDM
2021 )
2122set_target_properties(GreeterDBusTestExec PROPERTIES INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/${SHELL_INSTALL_QML}/mocks/IntegratedLightDM/liblightdm")
2223install(TARGETS GreeterDBusTestExec
@@ -28,7 +29,7 @@ install(FILES greeter.qml
2829add_unity8_uitest(GreeterDBus dbus-test-runner
2930 ARG_PREFIX "--parameter"
3031 ARGS --task $<TARGET_FILE:GreeterDBusTestExec>
31− DEPENDS MockLightDM MockLightDM-qml
32+ DEPENDS MockLightDM
3233 )
3334
3435add_executable(GreeterPamTestExec
@@ -63,19 +64,20 @@ target_link_libraries(GreeterSessionsModelTestExec
6364target_include_directories(GreeterSessionsModelTestExec PUBLIC
6465 ${CMAKE_SOURCE_DIR}/plugins/LightDM
6566 ${CMAKE_SOURCE_DIR}/plugins/Utils
66− ${CMAKE_SOURCE_DIR}/tests/mocks/LightDM/IntegratedLightDM
67+ ${CMAKE_SOURCE_DIR}/tests/mocks/liblightdm/
6768 )
6869install(TARGETS GreeterSessionsModelTestExec
6970 DESTINATION "${SHELL_PRIVATE_LIBDIR}/tests/plugins/LightDM/IntegratedLightDM"
7071 )
7172add_unity8_uitest(GreeterSessions GreeterSessionsModelTestExec
72− ENVIRONMENT LIBLIGHTDM_MOCK_MODE=full
7373 DEPENDS MockLightDM
7474 )
7575
7676# UsersModelTest
7777add_executable(GreeterUsersModelTestExec
7878 usersmodel.cpp
79+ ${CMAKE_SOURCE_DIR}/plugins/LightDM/Greeter.cpp
80+ ${CMAKE_SOURCE_DIR}/plugins/LightDM/PromptsModel.cpp
7981 ${CMAKE_SOURCE_DIR}/plugins/LightDM/UsersModel.cpp
8082 ${CMAKE_SOURCE_DIR}/plugins/Utils/unitysortfilterproxymodelqml.cpp
8183 )
@@ -89,17 +91,43 @@ target_link_libraries(GreeterUsersModelTestExec
8991target_include_directories(GreeterUsersModelTestExec PUBLIC
9092 ${CMAKE_SOURCE_DIR}/plugins/LightDM
9193 ${CMAKE_SOURCE_DIR}/plugins/Utils
92− ${CMAKE_SOURCE_DIR}/tests/mocks/LightDM/IntegratedLightDM
94+ ${CMAKE_SOURCE_DIR}/tests/mocks/liblightdm
9395 )
9496set_target_properties(GreeterUsersModelTestExec PROPERTIES INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/${SHELL_INSTALL_QML}/mocks/IntegratedLightDM/liblightdm")
9597install(TARGETS GreeterUsersModelTestExec
9698 DESTINATION "${SHELL_PRIVATE_LIBDIR}/tests/plugins/LightDM/IntegratedLightDM"
9799 )
98100add_unity8_uitest(GreeterUsers GreeterUsersModelTestExec
99− ENVIRONMENT LIBLIGHTDM_MOCK_MODE=full
100101 DEPENDS MockLightDM
101102 )
102103
104+# PromptsModelTest
105+add_executable(GreeterPromptsModelTestExec
106+ promptsmodel.cpp
107+ ${CMAKE_SOURCE_DIR}/plugins/LightDM/Greeter.cpp
108+ ${CMAKE_SOURCE_DIR}/plugins/LightDM/PromptsModel.cpp
109+ ${CMAKE_SOURCE_DIR}/plugins/Utils/unitysortfilterproxymodelqml.cpp
110+ )
111+# This add_dependencies is needed since we're linking with with -L and -l below
112+# make seems to work but ninja breaks without it
113+add_dependencies(GreeterPromptsModelTestExec MockLightDM)
114+qt5_use_modules(GreeterPromptsModelTestExec Core Test)
115+target_link_libraries(GreeterPromptsModelTestExec
116+ MockLightDM
117+ )
118+target_include_directories(GreeterPromptsModelTestExec PUBLIC
119+ ${CMAKE_SOURCE_DIR}/plugins/LightDM
120+ ${CMAKE_SOURCE_DIR}/plugins/Utils
121+ ${CMAKE_SOURCE_DIR}/tests/mocks/liblightdm
122+ )
123+install(TARGETS GreeterPromptsModelTestExec
124+ DESTINATION "${SHELL_PRIVATE_LIBDIR}/tests/plugins/LightDM/IntegratedLightDM"
125+ )
126+add_unity8_uitest(GreeterPrompts GreeterPromptsModelTestExec
127+ DEPENDS MockLightDM
128+ )
129+
130+# IntegratedTest
103131add_executable(GreeterIntegratedTestExec
104132 integrated.cpp
105133 )
tests/plugins/LightDM/IntegratedLightDM/dbus.cpp+7−11
@@ -82,6 +82,7 @@ private Q_SLOTS:
8282 void cleanup()
8383 {
8484 delete view;
85+ delete greeter; // reset singleton
8586 }
8687
8788 void testGetActiveEntry()
@@ -137,10 +138,10 @@ private Q_SLOTS:
137138 {
138139 QSignalSpy spy(this, &GreeterDBusTest::PropertiesChangedRelay);
139140 greeter->authenticate("has-password");
140− spy.wait();
141141
142− QVERIFY(spy.count() > 0);
143− QList<QVariant> arguments = spy.takeFirst();
142+ QTRY_COMPARE(spy.count(), 2); // EntryIsLocked then ActiveEntry
143+
144+ QList<QVariant> arguments = spy[1];
144145 QVERIFY(arguments.at(0).toString() == "com.canonical.UnityGreeter.List");
145146 QVERIFY(arguments.at(1).toMap().contains("ActiveEntry"));
146147 QVERIFY(arguments.at(1).toMap()["ActiveEntry"] == "has-password");
@@ -162,15 +163,10 @@ private Q_SLOTS:
162163 QSignalSpy spy(this, &GreeterDBusTest::PropertiesChangedRelay);
163164 greeter->authenticate("no-password");
164165
165− // Two property changed signals will be emitted, one for the IsLocked
166− // property, one for the ActiveEntry; the first will be IsLocked.
167− spy.wait();
168− if (spy.count() < 2) {
169− spy.wait();
170− }
171− QCOMPARE(spy.count(), 2);
166+ // EntryIsLocked=true, ActiveEntry, then EntryIsLocked=false
167+ QTRY_COMPARE(spy.count(), 3);
172168
173− QList<QVariant> arguments = spy.takeLast();
169+ QList<QVariant> arguments = spy[2];
174170 QVERIFY(arguments.at(0).toString() == "com.canonical.UnityGreeter.List");
175171 QVERIFY(arguments.at(1).toMap().contains("EntryIsLocked"));
176172 QVERIFY(arguments.at(1).toMap()["EntryIsLocked"] == false);
tests/plugins/LightDM/IntegratedLightDM/greeter.qml+2−7
@@ -1,5 +1,5 @@
11/*
2− * Copyright 2013 Canonical Ltd.
2+ * Copyright 2013,2015-2016 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
@@ -15,13 +15,8 @@
1515 */
1616
1717import QtQuick 2.4
18−import LightDM.IntegratedLightDM 0.1 as LightDM
18+import LightDM.FullLightDM 0.1 as LightDM
1919
2020Item {
2121 property var greeter: LightDM.Greeter
22− Binding {
23− target: LightDM.Greeter
24− property: "mockMode"
25− value: "full"
26− }
2722}
tests/plugins/LightDM/IntegratedLightDM/promptsmodel.cppnew file+175
@@ -0,0 +1,175 @@
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 "Greeter.h"
18+#include "PromptsModel.h"
19+
20+#include <QtTest>
21+
22+class GreeterPromptsModelTest : public QObject
23+{
24+ Q_OBJECT
25+
26+private:
27+ class PromptInfo {
28+ public:
29+ PromptInfo(PromptsModel::PromptType t, const QString &p)
30+ : type(t), text(p) {}
31+ PromptsModel::PromptType type;
32+ QString text;
33+ };
34+
35+ void selectUser(const QString &user)
36+ {
37+ Greeter::instance()->authenticate(user);
38+ QCOMPARE(Greeter::instance()->authenticationUser(), user);
39+ }
40+
41+ void respond(const QString &response, bool authenticated)
42+ {
43+ Greeter::instance()->respond(response);
44+ QCOMPARE(Greeter::instance()->isAuthenticated(), authenticated);
45+ }
46+
47+ void comparePrompts(const QList<PromptInfo> &expected)
48+ {
49+ QTRY_COMPARE(prompts->rowCount(), expected.size());
50+ for (int i = 0; i < prompts->rowCount(); i++) {
51+ QCOMPARE(prompts->data(prompts->index(i, 0), PromptsModel::TypeRole).toInt(), (int)expected[i].type);
52+ QCOMPARE(prompts->data(prompts->index(i, 0), PromptsModel::TextRole).toString(), expected[i].text);
53+ }
54+ }
55+
56+private Q_SLOTS:
57+
58+ void initTestCase()
59+ {
60+ prompts = Greeter::instance()->promptsModel();
61+ QVERIFY(prompts);
62+ }
63+
64+ void init()
65+ {
66+ QCOMPARE(prompts->rowCount(), 0);
67+ }
68+
69+ void cleanup()
70+ {
71+ prompts->clear();
72+ }
73+
74+ void testSimpleFailure()
75+ {
76+ selectUser("has-password");
77+ comparePrompts({{PromptsModel::Secret, ""}});
78+ respond("nope", false);
79+ comparePrompts({{PromptsModel::Secret, ""}}); // prompts don't change immediately
80+ selectUser("has-password");
81+ comparePrompts({{PromptsModel::Error, "Invalid password, please try again"},
82+ {PromptsModel::Secret, ""}});
83+ }
84+
85+ void testSimpleSuccess()
86+ {
87+ selectUser("has-password");
88+ comparePrompts({{PromptsModel::Secret, ""}});
89+ respond("password", true);
90+ comparePrompts({{PromptsModel::Secret, ""}}); // prompts don't change
91+ }
92+
93+ void testHasPassword()
94+ {
95+ selectUser("has-password");
96+ comparePrompts({{PromptsModel::Secret, ""}});
97+ }
98+
99+ void testDifferentPrompt()
100+ {
101+ selectUser("different-prompt");
102+ comparePrompts({{PromptsModel::Secret, "Secret word"}});
103+ }
104+
105+ void testNoPassword()
106+ {
107+ selectUser("no-password");
108+ comparePrompts({{PromptsModel::Button, "Log In"}});
109+ }
110+
111+ void testAuthError()
112+ {
113+ selectUser("auth-error");
114+ comparePrompts({{PromptsModel::Error, "Failed to authenticate"},
115+ {PromptsModel::Button, "Retry"}});
116+ }
117+
118+ void testTwoFactor()
119+ {
120+ selectUser("two-factor");
121+ comparePrompts({{PromptsModel::Secret, ""}});
122+ respond("password", false);
123+ comparePrompts({{PromptsModel::Question, "otp"}});
124+ }
125+
126+ void testTwoPrompts()
127+ {
128+ selectUser("two-prompts");
129+ comparePrompts({{PromptsModel::Question, "Favorite Color (blue)"},
130+ {PromptsModel::Secret, ""}});
131+ }
132+
133+ void testWackyPrompts()
134+ {
135+ selectUser("wacky-prompts");
136+ comparePrompts({{PromptsModel::Message, "First message"},
137+ {PromptsModel::Question, "Favorite Color (blue)"},
138+ {PromptsModel::Error, "Second message"},
139+ {PromptsModel::Secret, ""},
140+ {PromptsModel::Message, "Last message"}});
141+ }
142+
143+ void testHtmlInfoPrompts()
144+ {
145+ selectUser("html-info-prompt");
146+ comparePrompts({{PromptsModel::Message, "<b>&</b>"},
147+ {PromptsModel::Secret, ""}});
148+ }
149+
150+ void testInfoAfterLogin()
151+ {
152+ selectUser("info-after-login");
153+ comparePrompts({{PromptsModel::Secret, ""}});
154+ respond("password", true);
155+ comparePrompts({{PromptsModel::Message, "Congratulations on logging in!"},
156+ {PromptsModel::Button, "Log In"}});
157+ }
158+
159+ void testLocked()
160+ {
161+ selectUser("locked");
162+ comparePrompts({{PromptsModel::Secret, ""}});
163+ respond("nope", false);
164+ selectUser("locked");
165+ comparePrompts({{PromptsModel::Error, "Account is locked"},
166+ {PromptsModel::Secret, ""}});
167+ }
168+
169+private:
170+ PromptsModel *prompts;
171+};
172+
173+QTEST_GUILESS_MAIN(GreeterPromptsModelTest)
174+
175+#include "promptsmodel.moc"
tests/plugins/LightDM/IntegratedLightDM/sessionsmodel.cpp+15−13
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2015 Canonical, Ltd.
2+ * Copyright (C) 2015-2016 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
@@ -16,6 +16,8 @@
1616
1717#include "SessionsModel.h"
1818
19+#include "MockController.h"
20+
1921#include <QLightDM/SessionsModel>
2022#include <QtCore/QModelIndex>
2123#include <QtTest>
@@ -31,14 +33,14 @@ private Q_SLOTS:
3133 {
3234 model = new SessionsModel();
3335 QVERIFY(model);
34− sourceModel = new QLightDM::SessionsModel();
35− QVERIFY(sourceModel);
36+ mock = QLightDM::MockController::instance();
37+ QVERIFY(mock);
3638 }
3739
3840 void cleanup()
3941 {
4042 delete model;
41− delete sourceModel;
43+ // mock will be a singleton, doesn't need to be cleaned
4244 }
4345
4446 static QModelIndex findByKey(QAbstractItemModel *model, const QString& key)
@@ -93,34 +95,34 @@ private Q_SLOTS:
9395
9496 void testMultipleSessionsCountIsCorrect()
9597 {
96− sourceModel->setTestScenario("multipleSessions");
97− QVERIFY(sourceModel->rowCount(QModelIndex()) > 1);
98+ mock->setProperty("sessionMode", "full");
99+ QVERIFY(model->rowCount(QModelIndex()) > 1);
98100 }
99101
100102 void testNoSessionsCountIsCorrect()
101103 {
102− sourceModel->setTestScenario("noSessions");
103− QVERIFY(sourceModel->rowCount(QModelIndex()) == 0);
104+ mock->setProperty("sessionMode", "none");
105+ QVERIFY(model->rowCount(QModelIndex()) == 0);
104106 }
105107
106108 void testSingleSessionCountIsCorrect()
107109 {
108− sourceModel->setTestScenario("singleSession");
109− QVERIFY(sourceModel->rowCount(QModelIndex()) == 1);
110+ mock->setProperty("sessionMode", "single");
111+ QVERIFY(model->rowCount(QModelIndex()) == 1);
110112 }
111113
112114 void testSessionNameIsCorrect()
113115 {
114116 // This is testing the lookup, not the correctness of the strings,
115117 // so one test should be sufficient
116− sourceModel->setTestScenario("multipleSessions");
117− QVERIFY(model->data(findByKey(sourceModel, "ubuntu"),
118+ mock->setProperty("sessionMode", "full");
119+ QVERIFY(model->data(findByKey(model, "ubuntu"),
118120 Qt::DisplayRole).toString() == "Ubuntu");
119121 }
120122
121123private:
122124 SessionsModel *model;
123− QLightDM::SessionsModel *sourceModel;
125+ QObject *mock;
124126};
125127
126128QTEST_MAIN(GreeterSessionsModelTest)
tests/plugins/LightDM/IntegratedLightDM/usersmodel.cpp+87−16
@@ -14,6 +14,8 @@
1414 * along with this program. If not, see <http://www.gnu.org/licenses/>.
1515 */
1616
17+#include "Greeter.h"
18+#include "MockController.h"
1719#include "UsersModel.h"
1820
1921#include <QLightDM/UsersModel>
@@ -40,43 +42,112 @@ private:
4042 return model->data(model->index(i, 0), role).toString();
4143 }
4244
45+ void recreateModel()
46+ {
47+ delete model;
48+ model = new UsersModel();
49+ QVERIFY(model);
50+ }
51+
4352private Q_SLOTS:
4453
4554 void init()
4655 {
47− model = new UsersModel();
48− QVERIFY(model);
49− sourceModel = new QLightDM::UsersModel();
50− QVERIFY(sourceModel);
56+ recreateModel();
5157 }
5258
5359 void cleanup()
5460 {
55− delete model;
56− delete sourceModel;
61+ QLightDM::MockController::instance()->reset();
5762 }
5863
5964 void testMangleColor()
6065 {
61− QString background = getStringValue(sourceModel, "color-background", QLightDM::UsersModel::BackgroundPathRole);
62− QVERIFY(background == "#E95420");
63−
64− background = getStringValue(model, "color-background", QLightDM::UsersModel::BackgroundPathRole);
66+ auto background = getStringValue(model, "color-background", QLightDM::UsersModel::BackgroundPathRole);
6567 QVERIFY(background == "data:image/svg+xml,<svg><rect width='100%' height='100%' fill='#E95420'/></svg>");
6668 }
6769
6870 void testMangleEmptyName()
6971 {
70− QString name = getStringValue(sourceModel, "empty-name", QLightDM::UsersModel::RealNameRole);
71− QVERIFY(name == "");
72−
73− name = getStringValue(model, "empty-name", QLightDM::UsersModel::RealNameRole);
72+ auto name = getStringValue(model, "empty-name", QLightDM::UsersModel::RealNameRole);
7473 QVERIFY(name == "empty-name");
7574 }
7675
76+ void testHasGuest()
77+ {
78+ // sanity check that it doesn't start already present
79+ QCOMPARE(findName(model, QStringLiteral("*guest")), -1);
80+
81+ QLightDM::MockController::instance()->setHasGuestAccountHint(true);
82+
83+ int i = findName(model, QStringLiteral("*guest"));
84+ QVERIFY(i >= 0);
85+
86+ auto realName = model->data(i, QLightDM::UsersModel::RealNameRole).toString();
87+ QCOMPARE(realName, QStringLiteral("Guest Session"));
88+
89+ auto loggedIn = model->data(i, QLightDM::UsersModel::LoggedInRole).toBool();
90+ QVERIFY(!loggedIn);
91+
92+ auto session = model->data(i, QLightDM::UsersModel::SessionRole).toString();
93+ QCOMPARE(session, Greeter::instance()->defaultSessionHint());
94+ }
95+
96+ void testHasManual()
97+ {
98+ // sanity check that it doesn't start already present
99+ QCOMPARE(findName(model, QStringLiteral("*other")), -1);
100+
101+ QLightDM::MockController::instance()->setShowManualLoginHint(true);
102+
103+ int i = findName(model, QStringLiteral("*other"));
104+ QVERIFY(i >= 0);
105+
106+ auto realName = model->data(i, QLightDM::UsersModel::RealNameRole).toString();
107+ QCOMPARE(realName, QStringLiteral("Login"));
108+
109+ auto loggedIn = model->data(i, QLightDM::UsersModel::LoggedInRole).toBool();
110+ QVERIFY(!loggedIn);
111+
112+ auto session = model->data(i, QLightDM::UsersModel::SessionRole).toString();
113+ QCOMPARE(session, Greeter::instance()->defaultSessionHint());
114+ }
115+
116+ void testHideUsers()
117+ {
118+ QLightDM::MockController::instance()->setHideUsersHint(true);
119+
120+ QCOMPARE(model->count(), 1);
121+ auto name = model->data(0, QLightDM::UsersModel::NameRole).toString();
122+ QCOMPARE(name, QStringLiteral("*other"));
123+ }
124+
125+ void testHideUsersWithGuest()
126+ {
127+ QLightDM::MockController::instance()->setHideUsersHint(true);
128+ QLightDM::MockController::instance()->setHasGuestAccountHint(true);
129+
130+ QCOMPARE(model->count(), 1);
131+ auto name = model->data(0, QLightDM::UsersModel::NameRole).toString();
132+ QCOMPARE(name, QStringLiteral("*guest"));
133+ }
134+
135+ void testCustomEntriesAreLast()
136+ {
137+ QLightDM::MockController::instance()->setHasGuestAccountHint(true);
138+ QLightDM::MockController::instance()->setShowManualLoginHint(true);
139+
140+ QVERIFY(model->count() > 2);
141+
142+ int manualIndex = findName(model, QStringLiteral("*other"));
143+ QCOMPARE(manualIndex, model->count() - 2);
144+
145+ int guestIndex = findName(model, QStringLiteral("*guest"));
146+ QCOMPARE(guestIndex, model->count() - 1);
147+ }
148+
77149private:
78− UsersModel *model;
79− QLightDM::UsersModel *sourceModel;
150+ UsersModel *model = nullptr;
80151};
81152
82153QTEST_MAIN(GreeterUsersModelTest)
tests/plugins/Utils/CMakeLists.txt+4−1
@@ -6,11 +6,14 @@ include_directories(
66
77add_definitions(-DCONFIG_TESTING)
88
9+execute_process(COMMAND mktemp -d OUTPUT_VARIABLE TMPDIR OUTPUT_STRIP_TRAILING_WHITESPACE)
10+
911foreach(util_test
1012 QLimitProxyModel
1113 UnitySortFilterProxyModel
1214 WindowInputMonitor
1315 DeviceConfigParser
16+ WindowStateStorage
1417)
1518 add_executable(${util_test}TestExec ${util_test}Test.cpp ModelTest.cpp)
1619 qt5_use_modules(${util_test}TestExec Test Core Qml)
@@ -19,7 +22,7 @@ foreach(util_test
1922 DESTINATION "${SHELL_PRIVATE_LIBDIR}/tests/plugins/Utils"
2023 )
2124 add_unity8_unittest(${util_test} ${util_test}TestExec ADD_TEST
22− ENVIRONMENT LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/plugins/Utils
25+ ENVIRONMENT LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/plugins/Utils HOME=${TMPDIR}
2326 )
2427
2528endforeach()
tests/plugins/Utils/WindowStateStorageTest.cppnew file+64
@@ -0,0 +1,64 @@
1+/*
2+ * Copyright (C) 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 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 "windowstatestorage.h"
18+
19+#include <QTest>
20+#include <QStandardPaths>
21+
22+class WindowStateStorageTest : public QObject
23+{
24+ Q_OBJECT
25+
26+private Q_SLOTS:
27+ void initTestCase() {
28+ QStandardPaths::setTestModeEnabled(true);
29+ }
30+
31+ void init()
32+ {
33+ storage = new WindowStateStorage(this);
34+ }
35+
36+ void cleanup()
37+ {
38+ delete storage;
39+ }
40+
41+ void testSaveRestoreState() {
42+ const WindowStateStorage::WindowState state{WindowStateStorage::WindowState::WindowStateMaximizedTopLeft};
43+ storage->saveState(QTest::currentTestFunction(), state);
44+ QTRY_COMPARE(storage->getState(QTest::currentTestFunction(), WindowStateStorage::WindowState::WindowStateNormal), state);
45+ }
46+
47+ void testSaveRestoreGeometry() {
48+ const QRect geometry{10, 20, 30, 40};
49+ storage->saveGeometry(QTest::currentTestFunction(), geometry);
50+ QTRY_COMPARE(storage->getGeometry(QTest::currentTestFunction(), QRect()), geometry);
51+ }
52+
53+ void testSaveRestoreStage() {
54+ const int stage{1};
55+ storage->saveStage(QTest::currentTestFunction(), stage);
56+ QTRY_COMPARE(storage->getStage(QTest::currentTestFunction(), 0), stage);
57+ }
58+
59+private:
60+ WindowStateStorage * storage{nullptr};
61+};
62+
63+QTEST_GUILESS_MAIN(WindowStateStorageTest)
64+#include "WindowStateStorageTest.moc"
tests/qmltests/ApplicationMenuDataLoader.qml+13−5
@@ -10,6 +10,7 @@ Object {
1010
1111 Connections {
1212 id: sMgrHandler
13+ target: null
1314 onSurfaceCreated: {
1415 var fakeMenuPath = "/" + surface.persistentId.replace(/\W+/g, "");
1516
@@ -25,21 +26,28 @@ Object {
2526 var data = [];
2627 if (root === undefined) root = true;
2728
28− if (prefix === undefined) prefix = "menu"
29−
3029 for (var i = 0; i < length; i++) {
3130
31+ var menuName = prefix;
32+ if (menuName === undefined) {
33+ var chars = Math.random() * 20;
34+ menuName = "";
35+ for (var x = 0; x < chars; x++) {
36+ menuName += String.fromCharCode((Math.random() * 26) + 65);
37+ }
38+ }
39+
3240 var menuCode = String.fromCharCode(i+65);
3341
3442 var isSeparator = !root && separatorInterval > 0 && ((i+1) % separatorInterval == 0);
3543 var row = {
3644 "rowData": { // 1
37− "label": prefix + "&" + menuCode,
45+ "label": menuName + "&" + menuCode,
3846 "sensitive": true,
3947 "isSeparator": isSeparator,
4048 "icon": "",
4149 "ext": {},
42− "action": prefix + menuCode,
50+ "action": menuName + menuCode,
4351 "actionState": {},
4452 "isCheck": false,
4553 "isRadio": false,
@@ -49,7 +57,7 @@ Object {
4957 }
5058 var isSubmenu = root === undefined || root === true || (submenuInterval > 0 && ((i+1) % submenuInterval == 0));
5159 if (isSubmenu && !isSeparator && depth > 1) {
52− row["submenu"] = generateTestData(length, depth-1, submenuInterval, separatorInterval, prefix + menuCode + ".", false);
60+ row["submenu"] = generateTestData(length, depth-1, submenuInterval, separatorInterval,prefix, false);
5361 }
5462 data[i] = row;
5563 }
tests/qmltests/ApplicationMenus/tst_MenuBar.qml+18−5
@@ -65,7 +65,7 @@ Item {
6565
6666 unityMenuModel: UnityMenuModel {
6767 id: menuBackend
68− modelData: appMenuData.generateTestData(7,5,2,3,"menu")
68+ modelData: appMenuData.generateTestData(17,5,2,3)
6969 }
7070 }
7171 }
@@ -83,12 +83,12 @@ Item {
8383
8484 function init() {
8585 menuBar.dismiss();
86− menuBackend.modelData = appMenuData.generateTestData(5,5,2,3,"menu")
86+ menuBackend.modelData = appMenuData.generateTestData(5,5,2,3)
8787 activatedSpy.clear();
8888 }
8989
9090 function test_mouseNavigation() {
91− menuBackend.modelData = appMenuData.generateTestData(3,3,0,0,"menu");
91+ menuBackend.modelData = appMenuData.generateTestData(3,3,0,0);
9292 wait(50) // wait for row to build
9393 var priv = findInvisibleChild(menuBar, "d");
9494
@@ -114,7 +114,7 @@ Item {
114114 }
115115
116116 function test_keyboardNavigation_RightKeySelectsNextMenuItem(data) {
117− menuBackend.modelData = appMenuData.generateTestData(3,3,0,0,"menu");
117+ menuBackend.modelData = appMenuData.generateTestData(3,3,0,0);
118118 var priv = findInvisibleChild(menuBar, "d");
119119
120120 var menuItem0 = findChild(menuBar, "menuBar-item0"); verify(menuItem0);
@@ -139,7 +139,7 @@ Item {
139139 }
140140
141141 function test_keyboardNavigation_LeftKeySelectsPreviousMenuItem(data) {
142− menuBackend.modelData = appMenuData.generateTestData(3,3,0,0,"menu");
142+ menuBackend.modelData = appMenuData.generateTestData(3,3,0,0);
143143 var priv = findInvisibleChild(menuBar, "d");
144144
145145 var menuItem0 = findChild(menuBar, "menuBar-item0"); verify(menuItem0);
@@ -179,5 +179,18 @@ Item {
179179 keyPress(data.tag, Qt.AltModifier, 100);
180180 tryCompare(priv, "currentItem", menuItem);
181181 }
182+
183+ function test_openAppMenuShortcut() {
184+ var priv = findInvisibleChild(menuBar, "d");
185+
186+ var menuItem0 = findChild(menuBar, "menuBar-item0"); verify(menuItem0);
187+ menuItem0.enabled = false;
188+
189+ var menuItem1 = findChild(menuBar, "menuBar-item1"); verify(menuItem1);
190+ verify(priv.currentItem === null);
191+
192+ keyClick(Qt.Key_F10, Qt.AltModifier);
193+ compare(priv.currentItem, menuItem1, "First enabled item should be opened");
194+ }
182195 }
183196}
tests/qmltests/ApplicationMenus/tst_MenuPopup.qml+148−37
@@ -1,4 +1,4 @@
1−/*
1+/*
22 * Copyright (C) 2016 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
@@ -28,8 +28,8 @@ import ".."
2828
2929Item {
3030 id: root
31− width: Math.max(units.gu(100), page.width + units.gu(6))
32− height: Math.max(units.gu(50), page.height + units.gu(6))
31+ width: Math.max(units.gu(100), loader.width + units.gu(6))
32+ height: Math.max(units.gu(50), loader.height + units.gu(6))
3333
3434 Binding {
3535 target: MouseTouchAdaptor
@@ -39,25 +39,37 @@ Item {
3939
4040 ApplicationMenuDataLoader { id: appMenuData }
4141
42− MenuPopup {
43− id: page
42+ Loader {
43+ id: loader
44+ sourceComponent: MenuPopup {
45+ anchors {
46+ left: parent ? parent.left : undefined
47+ top: parent ? parent.top : undefined
48+ leftMargin: units.gu(3)
49+ topMargin: units.gu(3)
50+ }
4451
45− anchors {
46− left: parent.left
47− top: parent.top
48− leftMargin: units.gu(3)
49− topMargin: units.gu(3)
50− }
52+ unityMenuModel: UnityMenuModel {
53+ modelData: [{
54+ "rowData": {
55+ "label": "Short",
56+ }}, {
57+ "rowData": {
58+ "label": "This is a long menu item which tests width",
59+ }}
60+ ]
61+ }
5162
52− unityMenuModel: UnityMenuModel {
53− id: menuBackend
54− modelData: appMenuData.generateTestData(7,5,2,3,"menu")
63+ Binding {
64+ target: activatedSpy
65+ property: "target"
66+ value: unityMenuModel
67+ }
5568 }
5669 }
5770
5871 SignalSpy {
5972 id: activatedSpy
60− target: menuBackend
6173 signalName: "activated"
6274 }
6375
@@ -66,14 +78,20 @@ Item {
6678 name: "MenuPopup"
6779 when: windowShown
6880
81+ property var menu: loader.status === Loader.Ready ? loader.item : null
82+
6983 function init() {
70− page.show();
84+ loader.active = true;
85+ menu.show();
7186 }
7287
7388 function cleanup() {
74− page.reset();
89+ menu.reset();
7590 wait(100); // let the page dismiss
7691 activatedSpy.clear();
92+
93+ loader.active = false;
94+ tryCompare(loader, "item", null);
7795 }
7896
7997 // visit and verify that all the backend menus have been created
@@ -90,6 +108,7 @@ Item {
90108 // recurse into submenu
91109 var submenu = rows[i]["submenu"];
92110 if (submenu) {
111+ waitForRendering(menuItem);
93112 mouseClick(menuItem, menuItem.width/2, menuItem.height/2);
94113 tryCompare(menuPriv, "currentItem", menuItem);
95114
@@ -113,15 +132,15 @@ Item {
113132 }
114133
115134 function test_mouseNavigation(data) {
116− menuBackend.modelData = data.testData;
135+ menu.unityMenuModel.modelData = data.testData;
117136
118− recurseMenuConstruction(data.testData, page);
137+ recurseMenuConstruction(data.testData, menu);
119138 }
120139
121140 function test_checkableMenuTogglesOnClick() {
122− menuBackend.modelData = appMenuData.singleCheckable;
141+ menu.unityMenuModel.modelData = appMenuData.singleCheckable;
123142
124− var menuItem = findChild(page, "menu-item0-actionItem");
143+ var menuItem = findChild(menu, "menu-item0-actionItem");
125144 verify(menuItem);
126145 compare(menuItem.action.checkable, true, "Menu item should be checkable");
127146 compare(menuItem.action.checked, false, "Menu item should not be checked");
@@ -132,13 +151,13 @@ Item {
132151 }
133152
134153 function test_keyboardNavigation_DownKeySelectsAndOpensNextMenuItemAndRotates() {
135− menuBackend.modelData = appMenuData.generateTestData(3,3,0,0,"menu",false);
154+ menu.unityMenuModel.modelData = appMenuData.generateTestData(3,3,0,0,"menu",false);
136155
137− var item0 = findChild(page, "menu-item0"); verify(item0);
138− var item1 = findChild(page, "menu-item1"); verify(item1);
139− var item2 = findChild(page, "menu-item2"); verify(item2);
156+ var item0 = findChild(menu, "menu-item0"); verify(item0);
157+ var item1 = findChild(menu, "menu-item1"); verify(item1);
158+ var item2 = findChild(menu, "menu-item2"); verify(item2);
140159
141− var priv = findInvisibleChild(page, "d");
160+ var priv = findInvisibleChild(menu, "d");
142161
143162 keyClick(Qt.Key_Down, Qt.NoModifier);
144163 compare(priv.currentItem, item0, "CurrentItem should have moved to item 0");
@@ -154,13 +173,13 @@ Item {
154173 }
155174
156175 function test_keyboardNavigation_UpKeySelectsAndOpensPreviousMenuItemAndRotates() {
157− menuBackend.modelData = appMenuData.generateTestData(3,3,0,0,"menu",false);
176+ menu.unityMenuModel.modelData = appMenuData.generateTestData(3,3,0,0,"menu",false);
158177
159− var item0 = findChild(page, "menu-item0"); verify(item0);
160− var item1 = findChild(page, "menu-item1"); verify(item1);
161− var item2 = findChild(page, "menu-item2"); verify(item2);
178+ var item0 = findChild(menu, "menu-item0"); verify(item0);
179+ var item1 = findChild(menu, "menu-item1"); verify(item1);
180+ var item2 = findChild(menu, "menu-item2"); verify(item2);
162181
163− var priv = findInvisibleChild(page, "d");
182+ var priv = findInvisibleChild(menu, "d");
164183
165184 keyClick(Qt.Key_Down, Qt.NoModifier);
166185 compare(priv.currentItem, item0, "CurrentItem should have moved to item 2");
@@ -176,17 +195,17 @@ Item {
176195 }
177196
178197 function test_keyboardNavigation_RightKeyEntersSubMenu() {
179− menuBackend.modelData = appMenuData.generateTestData(3,3,1,0,"menu",false);
198+ menu.unityMenuModel.modelData = appMenuData.generateTestData(3,3,1,0,"menu",false);
180199
181− var menuItem = findChild(page, "menu-item0");
200+ var menuItem = findChild(menu, "menu-item0");
182201
183− var priv = findInvisibleChild(page, "d");
202+ var priv = findInvisibleChild(menu, "d");
184203 priv.currentItem = menuItem;
185204
186205 keyClick(Qt.Key_Right, Qt.NoModifier);
187206 tryCompareFunction(function() { return menuItem.popup !== null && menuItem.popup.visible }, true);
188207
189− var submenu0 = findChild(page, "menu-item0-menu"); verify(submenu0);
208+ var submenu0 = findChild(menu, "menu-item0-menu"); verify(submenu0);
190209 var submenu0item0 = findChild(submenu0, "menu-item0-menu-item0"); verify(submenu0item0);
191210
192211 var submenu0Priv = findInvisibleChild(submenu0, "d"); verify(submenu0Priv);
@@ -194,14 +213,106 @@ Item {
194213 }
195214
196215 function test_keyboardNavigation_LeftKeyClosesSubMenu() {
197− menuBackend.modelData = appMenuData.generateTestData(3,3,1,0,"menu",false);
216+ menu.unityMenuModel.modelData = appMenuData.generateTestData(3,3,1,0,"menu",false);
198217
199− var menuItem = findChild(page, "menu-item0"); verify(menuItem);
218+ var menuItem = findChild(menu, "menu-item0"); verify(menuItem);
200219 mouseClick(menuItem, menuItem.width/2, menuItem.height/2);
201220 tryCompareFunction(function() { return menuItem.popup !== null && menuItem.popup.visible }, true);
202221
203222 keyClick(Qt.Key_Left, Qt.NoModifier);
204223 tryCompareFunction(function() { return menuItem.popup !== null && menuItem.popup.visible }, false);
205224 }
225+
226+ function test_differentSizes() {
227+ var differentSizesMenu = [{
228+ "rowData": { "label": "Short" }}, {
229+ "rowData": { "label": "This is a long menu item which tests width" }
230+ }];
231+
232+ menu.unityMenuModel.modelData = differentSizesMenu;
233+
234+ waitForRendering(menu);
235+ var longWidth = menu.width;
236+ differentSizesMenu.pop();
237+ menu.unityMenuModel.modelData = differentSizesMenu;
238+
239+ waitForRendering(menu);
240+ verify(menu.width < longWidth);
241+ }
242+
243+ function test_minimumWidth() {
244+ var shortMenu = [{
245+ "rowData": { "label": "Short" }
246+ }];
247+ menu.unityMenuModel.modelData = shortMenu;
248+
249+ var priv = findInvisibleChild(menu, "d");
250+ priv.__minimumWidth = 0;
251+ priv.__maximumWidth = 1000;
252+ tryCompareFunction(function() { return menu.width > priv.__minimumWidth; }, true);
253+
254+ priv.__minimumWidth = 300;
255+ tryCompare(menu, "width", priv.__minimumWidth);
256+ }
257+
258+ function test_maximumWidth() {
259+ var longMenu = [{
260+ "rowData": { "label": "This is a long menu item which tests width" }
261+ }];
262+
263+ var priv = findInvisibleChild(menu, "d");
264+ priv.__minimumWidth = 0;
265+ priv.__maximumWidth = 100;
266+
267+ menu.unityMenuModel.modelData = longMenu;
268+ tryCompare(menu, "width", priv.__maximumWidth);
269+
270+ priv.__maximumWidth = 200;
271+ tryCompare(menu, "width", priv.__maximumWidth);
272+
273+ priv.__maximumWidth = 1200;
274+ tryCompareFunction(function() { return menu.width < priv.__maximumWidth; }, true);
275+ }
276+
277+ function test_minimumHeight() {
278+ var shortMenu = [{
279+ "rowData": { "label": "menu1" }
280+ }];
281+ menu.unityMenuModel.modelData = shortMenu;
282+
283+ var priv = findInvisibleChild(menu, "d");
284+ priv.__minimumHeight = 0;
285+ priv.__maximumHeight = 1000;
286+ tryCompareFunction(function() { return menu.height > priv.__minimumHeight; }, true);
287+
288+ priv.__minimumHeight = 300;
289+ tryCompare(menu, "height", priv.__minimumHeight);
290+ }
291+
292+ function test_maximumHeight() {
293+ var shortMenu = [{
294+ "rowData": { "label": "menu1" }}, {
295+ "rowData": { "label": "menu2" }}, {
296+ "rowData": { "label": "menu3" }}, {
297+ "rowData": { "label": "menu4" }}, {
298+ "rowData": { "label": "menu5" }}, {
299+ "rowData": { "label": "menu6" }}, {
300+ "rowData": { "label": "menu7" }}, {
301+ "rowData": { "label": "menu8" }}, {
302+ "rowData": { "label": "menu9" }
303+ }];
304+ menu.unityMenuModel.modelData = shortMenu;
305+
306+ var priv = findInvisibleChild(menu, "d");
307+ priv.__minimumHeight = 0;
308+ priv.__maximumHeight = 100;
309+ tryCompare(menu, "height", priv.__maximumHeight);
310+
311+ priv.__maximumHeight = 200;
312+ tryCompare(menu, "height", priv.__maximumHeight);
313+
314+ priv.__maximumHeight = 1200;
315+ tryCompareFunction(function() { return menu.height < priv.__maximumHeight; }, true);
316+ }
206317 }
207318}
tests/qmltests/Dash/tst_DashShell.qml+8−40
@@ -20,12 +20,20 @@ import "../../../qml/Dash"
2020import "../../../qml/"
2121import Ubuntu.Components 1.3
2222import Unity.Test 0.1 as UT
23+import LightDMController 0.1
24+import LightDM.FullLightDM 0.1 as LightDM
2325
2426Item {
2527 id: root
2628 width: units.gu(80)
2729 height: units.gu(80)
2830
31+ Binding {
32+ target: LightDMController
33+ property: "userMode"
34+ value: "single"
35+ }
36+
2937 Shell {
3038 id: shell
3139 width: parent.width / 2
@@ -72,45 +80,5 @@ Item {
7280 true, 15000);
7381 waitForRendering(findChild(dash, "scopeLoader0").item);
7482 }
75−
76− function test_setShellHome() {
77− var dashContentList = findChild(dash, "dashContentList");
78− var startX = dash.width - units.gu(1);
79− var startY = dash.height / 2;
80− var stopX = units.gu(1)
81− var stopY = startY;
82− waitForRendering(dashContentList);
83− touchFlick(dash, startX, startY, stopX, stopY);
84− touchFlick(dash, startX, startY, stopX, stopY);
85− compare(dashContentList.currentIndex, 2, "Could not flick to scope id 2");
86−
87− // Flick the greeter away
88− var startX = shell.width - units.gu(1);
89− var startY = shell.height / 2;
90− var stopX = units.gu(1)
91− var stopY = startY;
92− touchFlick(shell, startX, startY, stopX, stopY);
93−
94− var greeter = findChild(shell, "greeter");
95− tryCompare(greeter, "shown", false);
96−
97− // bring in the launcher
98− var startX = units.gu(1);
99− var startY = shell.height / 2;
100− var stopX = shell.width / 3;
101− var stopY = startY;
102− touchFlick(shell, startX, startY, stopX, stopY);
103−
104− var launcher = findChild(shell, "launcher");
105− var buttonShowDashHome = findChild(launcher, "buttonShowDashHome");
106− tryCompare(buttonShowDashHome, "enabled", true);
107− tryCompare(launcher, "visibleWidth", launcher.panelWidth);
108−
109− // Make sure that opening the launcher doesn't change the scope
110− compare(dashContentList.currentIndex, 2, "Opening the launcher changed the current scope");
111−
112− mouseClick(buttonShowDashHome);
113− tryCompare(dashContentList, "currentIndex", 0);
114− }
11583 }
11684}
tests/qmltests/Greeter/TestView.qml+7−16
@@ -32,6 +32,7 @@ Item {
3232 property bool alphanumeric
3333 property var userModel
3434 property var infographicModel
35+ property bool waiting
3536 readonly property bool fullyShown: _fullyShown
3637 readonly property bool required: _required
3738 readonly property bool animating: _animating
@@ -45,24 +46,14 @@ Item {
4546 signal tease()
4647 signal emergencyCall()
4748
48− signal _showMessageCalled(string html)
49− signal _showPromptCalled(string text, bool isSecret, bool isDefaultPrompt)
5049 signal _showLastChanceCalled()
5150 signal _hideCalled()
52− signal _notifyAuthenticationSucceededCalled(bool showFakePassword)
51+ signal _showFakePasswordCalled()
5352 signal _notifyAuthenticationFailedCalled()
5453 signal _showErrorMessageCalled(string msg)
55− signal _resetCalled(bool forceShow)
54+ signal _forceShowCalled()
5655 signal _tryToUnlockCalled(bool toTheRight)
5756
58− function showMessage(html) {
59− _showMessageCalled(html);
60− }
61−
62− function showPrompt(text, isSecret, isDefaultPrompt) {
63− _showPromptCalled(text, isSecret, isDefaultPrompt);
64− }
65−
6657 function showLastChance() {
6758 _showLastChanceCalled();
6859 }
@@ -73,8 +64,8 @@ Item {
7364 _fullyShown = false;
7465 }
7566
76− function notifyAuthenticationSucceeded(showFakePassword) {
77− _notifyAuthenticationSucceededCalled(showFakePassword);
67+ function showFakePassword() {
68+ _showFakePasswordCalled();
7869 }
7970
8071 function notifyAuthenticationFailed() {
@@ -85,8 +76,8 @@ Item {
8576 _showErrorMessageCalled(msg);
8677 }
8778
88− function reset(forceShow) {
89− _resetCalled(forceShow);
79+ function forceShow() {
80+ _forceShowCalled();
9081 }
9182
9283 function tryToUnlock(toTheRight) {
tests/qmltests/Greeter/tst_Greeter.qml+62−110
@@ -22,7 +22,8 @@ import Ubuntu.Components 1.3
2222import AccountsService 0.1
2323import Biometryd 0.0
2424import GSettings 1.0
25−import LightDM.IntegratedLightDM 0.1 as LightDM // This is the mock
25+import LightDMController 0.1
26+import LightDM.FullLightDM 0.1 as LightDM
2627import Unity.Test 0.1 as UT
2728
2829Item {
@@ -32,9 +33,6 @@ Item {
3233 property url defaultBackground: "/usr/share/backgrounds/warty-final-ubuntu.png"
3334
3435 Component.onCompleted: {
35− // set the mock mode before loading
36− LightDM.Greeter.mockMode = "full";
37− LightDM.Users.mockMode = "full";
3836 loader.active = true;
3937 }
4038
@@ -77,6 +75,12 @@ Item {
7775 signalName: "activeChanged"
7876 }
7977
78+ SignalSpy {
79+ id: authStartedSpy
80+ target: LightDM.Greeter
81+ signalName: "authenticationStarted"
82+ }
83+
8084 GSettings {
8185 id: greeterSettings
8286 schema.id: "com.canonical.Unity8.Greeter"
@@ -90,18 +94,6 @@ Item {
9094 property Item greeter: loader.status === Loader.Ready ? loader.item : null
9195 property Item view
9296
93− SignalSpy {
94− id: viewShowMessageSpy
95− target: testCase.view
96− signalName: "_showMessageCalled"
97− }
98−
99− SignalSpy {
100− id: viewShowPromptSpy
101− target: testCase.view
102− signalName: "_showPromptCalled"
103− }
104−
10597 SignalSpy {
10698 id: viewShowLastChanceSpy
10799 target: testCase.view
@@ -115,9 +107,9 @@ Item {
115107 }
116108
117109 SignalSpy {
118− id: viewAuthenticationSucceededSpy
110+ id: viewShowFakePasswordSpy
119111 target: testCase.view
120− signalName: "_notifyAuthenticationSucceededCalled"
112+ signalName: "_showFakePasswordCalled"
121113 }
122114
123115 SignalSpy {
@@ -133,9 +125,9 @@ Item {
133125 }
134126
135127 SignalSpy {
136− id: viewResetSpy
128+ id: viewForceShowSpy
137129 target: testCase.view
138− signalName: "_resetCalled"
130+ signalName: "_forceShowCalled"
139131 }
140132
141133 SignalSpy {
@@ -146,7 +138,7 @@ Item {
146138
147139 function init() {
148140 greeterSettings.lockedOutTime = 0;
149− LightDM.Greeter.selectUser = "";
141+ LightDMController.reset();
150142 greeter.failedLoginsDelayAttempts = 7;
151143 greeter.failedLoginsDelayMinutes = 5;
152144 teaseSpy.clear();
@@ -155,16 +147,15 @@ Item {
155147 Biometryd.available = true;
156148 AccountsService.enableFingerprintIdentification = true;
157149 AccountsService.failedFingerprintLogins = 0;
158− viewShowMessageSpy.clear();
159− viewShowPromptSpy.clear();
160150 viewShowLastChanceSpy.clear();
161151 viewHideSpy.clear();
162− viewAuthenticationSucceededSpy.clear();
152+ viewShowFakePasswordSpy.clear();
163153 viewAuthenticationFailedSpy.clear();
164154 viewShowErrorMessageSpy.clear();
165− viewResetSpy.clear();
155+ viewForceShowSpy.clear();
166156 viewTryToUnlockSpy.clear();
167157 resetLoader();
158+ authStartedSpy.clear();
168159 }
169160
170161 function resetLoader() {
@@ -203,13 +194,15 @@ Item {
203194 var i = getIndexOf(name);
204195 compare(view.currentIndex, i);
205196 compare(AccountsService.user, name);
206− compare(LightDM.Greeter.authenticationUser, name);
197+ if (name[0] === "*") // custom rows have no authenticationUser set
198+ compare(LightDM.Greeter.authenticationUser, "");
199+ else
200+ compare(LightDM.Greeter.authenticationUser, name);
207201 return i;
208202 }
209203
210204 function verifyLoggedIn() {
211205 tryCompare(sessionStartedSpy, "count", 1);
212− verify(viewAuthenticationSucceededSpy.count > 0);
213206 compare(LightDM.Greeter.authenticated, true);
214207 compare(greeter.shown, false);
215208 }
@@ -224,107 +217,52 @@ Item {
224217
225218 function test_unlockPass() {
226219 selectUser("has-password");
227− tryCompare(viewShowPromptSpy, "count", 1);
228− compare(viewShowPromptSpy.signalArguments[0][0], "Password");
229− compare(viewShowPromptSpy.signalArguments[0][1], true);
230− compare(viewShowPromptSpy.signalArguments[0][2], true);
231−
232220 view.responded("password");
233221 verifyLoggedIn();
234222 }
235223
236224 function test_unlockFail() {
237225 selectUser("has-password");
238− tryCompare(viewShowPromptSpy, "count", 1);
239− compare(viewShowPromptSpy.signalArguments[0][0], "Password");
240− compare(viewShowPromptSpy.signalArguments[0][1], true);
241− compare(viewShowPromptSpy.signalArguments[0][2], true);
226+ tryCompare(authStartedSpy, "count", 1);
242227
243228 view.responded("wr0ng p4ssw0rd");
244229 tryCompare(viewAuthenticationFailedSpy, "count", 1);
245230
246− tryCompare(viewShowPromptSpy, "count", 2);
247− compare(viewShowPromptSpy.signalArguments[0][0], "Password");
248− compare(viewShowPromptSpy.signalArguments[0][1], true);
249− compare(viewShowPromptSpy.signalArguments[0][2], true);
231+ tryCompare(authStartedSpy, "count", 2);
250232 }
251233
252234 function test_promptless() {
253235 selectUser("no-password");
254236 tryCompare(view, "locked", false);
255− compare(viewShowPromptSpy.count, 0);
256237 compare(viewHideSpy.count, 0);
257238 }
258239
259240 function test_twoFactorPass() {
260241 selectUser("two-factor");
261− tryCompare(viewShowPromptSpy, "count", 1);
262− compare(viewShowPromptSpy.signalArguments[0][0], "Password");
263− compare(viewShowPromptSpy.signalArguments[0][1], true);
264− compare(viewShowPromptSpy.signalArguments[0][2], true);
265−
266242 view.responded("password");
267− tryCompare(viewShowPromptSpy, "count", 2);
268− compare(viewShowPromptSpy.signalArguments[1][0], "otp");
269− compare(viewShowPromptSpy.signalArguments[1][1], false);
270− compare(viewShowPromptSpy.signalArguments[1][2], false);
271−
272243 view.responded("otp");
273244 verifyLoggedIn();
274245 }
275246
276247 function test_twoFactorFailOnFirst() {
277248 selectUser("two-factor");
278− tryCompare(viewShowPromptSpy, "count", 1);
279− compare(viewShowPromptSpy.signalArguments[0][0], "Password");
280− compare(viewShowPromptSpy.signalArguments[0][1], true);
281− compare(viewShowPromptSpy.signalArguments[0][2], true);
249+ tryCompare(authStartedSpy, "count", 1);
282250
283251 view.responded("wr0ng p4ssw0rd");
284252 tryCompare(viewAuthenticationFailedSpy, "count", 1);
285253
286− tryCompare(viewShowPromptSpy, "count", 2);
287− compare(viewShowPromptSpy.signalArguments[0][0], "Password");
288− compare(viewShowPromptSpy.signalArguments[0][1], true);
289− compare(viewShowPromptSpy.signalArguments[0][2], true);
254+ tryCompare(authStartedSpy, "count", 2);
290255 }
291256
292257 function test_twoFactorFailOnSecond() {
293258 selectUser("two-factor");
294− tryCompare(viewShowPromptSpy, "count", 1);
295− compare(viewShowPromptSpy.signalArguments[0][0], "Password");
296− compare(viewShowPromptSpy.signalArguments[0][1], true);
297− compare(viewShowPromptSpy.signalArguments[0][2], true);
259+ tryCompare(authStartedSpy, "count", 1);
298260
299261 view.responded("password");
300− tryCompare(viewShowPromptSpy, "count", 2);
301− compare(viewShowPromptSpy.signalArguments[1][0], "otp");
302− compare(viewShowPromptSpy.signalArguments[1][1], false);
303− compare(viewShowPromptSpy.signalArguments[1][2], false);
304−
305262 view.responded("wr0ng p4ssw0rd");
306263 tryCompare(viewAuthenticationFailedSpy, "count", 1);
307264
308− tryCompare(viewShowPromptSpy, "count", 3);
309− compare(viewShowPromptSpy.signalArguments[0][0], "Password");
310− compare(viewShowPromptSpy.signalArguments[0][1], true);
311− compare(viewShowPromptSpy.signalArguments[0][2], true);
312− }
313−
314− function test_htmlInfoPrompt() {
315− selectUser("html-info-prompt");
316− tryCompare(viewShowPromptSpy, "count", 1);
317− compare(viewShowMessageSpy.count, 1);
318− compare(viewShowMessageSpy.signalArguments[0][0], "&lt;b&gt;&amp;&lt;/b&gt;");
319− }
320−
321− function test_multiInfoPrompt() {
322− selectUser("multi-info-prompt");
323− tryCompare(viewShowPromptSpy, "count", 1);
324− compare(viewShowMessageSpy.count, 3);
325− compare(viewShowMessageSpy.signalArguments[0][0], "Welcome to Unity Greeter");
326− compare(viewShowMessageSpy.signalArguments[1][0], "<font color=\"#df382c\">This is an error</font>");
327− compare(viewShowMessageSpy.signalArguments[2][0], "You should have seen three messages");
265+ tryCompare(authStartedSpy, "count", 2);
328266 }
329267
330268 function test_locked() {
@@ -434,18 +372,9 @@ Item {
434372 compare(view.backgroundTopMargin, -5);
435373 }
436374
437− function test_differentPrompt() {
438− selectUser("different-prompt");
439− tryCompare(viewShowPromptSpy, "count", 1);
440− compare(viewShowPromptSpy.signalArguments[0][0], "Secret word");
441− compare(viewShowPromptSpy.signalArguments[0][1], true);
442− compare(viewShowPromptSpy.signalArguments[0][2], false);
443− }
444−
445375 function test_authError() {
446376 selectUser("auth-error");
447377 tryCompare(viewAuthenticationFailedSpy, "count", 1);
448− compare(viewShowPromptSpy.count, 0);
449378 compare(view.locked, true);
450379 }
451380
@@ -463,7 +392,6 @@ Item {
463392 greeter.failedLoginsDelayAttempts = 1;
464393 greeter.failedLoginsDelayMinutes = 1;
465394 selectUser("has-password");
466− tryCompare(viewShowPromptSpy, "count", 1);
467395 compare(greeterSettings.lockedOutTime, 0);
468396 view.responded("wr0ng p4ssw0rd");
469397
@@ -497,7 +425,6 @@ Item {
497425 greeter.failedLoginsDelayMinutes = 0.001; // make delay very short
498426
499427 selectUser("has-password");
500− tryCompare(viewShowPromptSpy, "count", 1);
501428
502429 compare(view.delayMinutes, 0);
503430 view.responded("wr0ng p4ssw0rd");
@@ -557,37 +484,62 @@ Item {
557484
558485 function test_dbusShowGreeterFromShownState() {
559486 selectUser("has-password");
560− compare(viewResetSpy.count, 1);
561− tryCompare(viewShowPromptSpy, "count", 1);
562−
563− viewResetSpy.clear();
564− viewShowPromptSpy.clear();
565487
488+ compare(viewForceShowSpy.count, 0);
566489 LightDM.Greeter.showGreeter();
567− compare(viewResetSpy.count, 1);
568− compare(viewResetSpy.signalArguments[0][0], true);
569− tryCompare(viewShowPromptSpy, "count", 1);
490+ compare(viewForceShowSpy.count, 1);
570491 }
571492
572493 function test_selectUserHint() {
573− LightDM.Greeter.selectUser = "info-prompt";
494+ LightDMController.selectUserHint = "info-prompt";
574495 resetLoader();
575496 var i = verifySelected("info-prompt");
576497 verify(i != 0); // sanity-check that info-prompt isn't default 0 answer
577498 }
578499
579500 function test_selectUserHintUnset() {
580− LightDM.Greeter.selectUser = "";
501+ LightDMController.selectUserHint = "";
581502 resetLoader();
582503 verifySelected(LightDM.Users.data(0, LightDM.UserRoles.NameRole));
583504 }
584505
585506 function test_selectUserHintInvalid() {
586− LightDM.Greeter.selectUser = "not-a-real-user";
507+ LightDMController.selectUserHint = "not-a-real-user";
587508 resetLoader();
588509 verifySelected(LightDM.Users.data(0, LightDM.UserRoles.NameRole));
589510 }
590511
512+ function test_selectUserHintGuest() {
513+ LightDMController.hasGuestAccountHint = true;
514+ LightDMController.selectGuestHint = true;
515+ LightDMController.selectUserHint = "info-prompt";
516+ resetLoader();
517+ verifySelected("*guest");
518+ }
519+
520+ function test_selectUserHintGuestWithNoGuest() {
521+ LightDMController.selectGuestHint = true;
522+ LightDMController.selectUserHint = "info-prompt";
523+ resetLoader();
524+ verifySelected("info-prompt");
525+ }
526+
527+ function test_hasGuestAccountHint() {
528+ LightDMController.hasGuestAccountHint = true;
529+ resetLoader();
530+ var i = selectUser("*guest");
531+ compare(i, LightDM.Users.count - 1); // guest should be last
532+ verify(!view.locked);
533+ }
534+
535+ function test_showManualLoginHint() {
536+ LightDMController.showManualLoginHint = true;
537+ resetLoader();
538+ var i = selectUser("*other");
539+ compare(i, LightDM.Users.count - 1); // manual should be last
540+ verify(view.locked);
541+ }
542+
591543 function test_fingerprintSuccess() {
592544 var index = selectUser("has-password");
593545 unlockAndShowGreeter(); // turn on lockscreen mode
tests/qmltests/Greeter/tst_Infographics.qml+1−11
@@ -20,19 +20,9 @@ import ".."
2020import "../../../qml/Greeter"
2121import Ubuntu.Components 1.3
2222import Unity.Test 0.1 as UT
23−import LightDM.IntegratedLightDM 0.1 as LightDM
23+import LightDM.FullLightDM 0.1 as LightDM
2424
2525Item {
26− Binding {
27− target: LightDM.Greeter
28− property: "mockMode"
29− value: "full"
30− }
31− Binding {
32− target: LightDM.Users
33− property: "mockMode"
34− value: "full"
35− }
3626 property variant infographicModel: LightDM.Infographic
3727
3828 width: units.gu(120)
tests/qmltests/Greeter/tst_NarrowView.qml+20−43
@@ -18,7 +18,8 @@ import QtQuick 2.4
1818import QtTest 1.0
1919import ".."
2020import "../../../qml/Greeter"
21−import LightDM.IntegratedLightDM 0.1 as LightDM
21+import LightDMController 0.1
22+import LightDM.FullLightDM 0.1 as LightDM
2223import Ubuntu.Components 1.3
2324import Ubuntu.Telephony 0.1 as Telephony
2425import Unity.Test 0.1 as UT
@@ -30,6 +31,12 @@ Item {
3031
3132 Component.onCompleted: theme.name = "Ubuntu.Components.Themes.SuruDark" // use the same theme as the real shell
3233
34+ Binding {
35+ target: LightDMController
36+ property: "userMode"
37+ value: "single"
38+ }
39+
3340 Row {
3441 anchors.fill: parent
3542 Loader {
@@ -72,28 +79,16 @@ Item {
7279 anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
7380 spacing: units.gu(1)
7481
75− Row {
76− Button {
77− text: "Show Last Chance"
78− onClicked: loader.item.showLastChance()
79− }
80− }
8182 Row {
8283 Button {
8384 text: "Hide"
8485 onClicked: loader.item.hide()
8586 }
8687 }
87− Row {
88− Button {
89− text: "Reset"
90− onClicked: loader.item.reset()
91− }
92− }
9388 Row {
9489 Button {
9590 text: "Show Message"
96− onClicked: loader.item.showMessage(messageField.text)
91+ onClicked: LightDMService.prompts.append(messageField.text, LightDMService.prompts.Message)
9792 }
9893 TextField {
9994 id: messageField
@@ -104,7 +99,7 @@ Item {
10499 Row {
105100 Button {
106101 text: "Show Prompt"
107− onClicked: loader.item.showPrompt(promptField.text, isSecretCheckBox.checked, isDefaultPromptCheckBox.checked)
102+ onClicked: LightDMService.prompts.append(promptField.text, isSecretCheckBox.checked ? LightDMService.prompts.Secret : LightDMService.prompts.Question)
108103 }
109104 TextField {
110105 id: promptField
@@ -117,36 +112,14 @@ Item {
117112 Label {
118113 text: "secret"
119114 }
120− CheckBox {
121− id: isDefaultPromptCheckBox
122− }
123− Label {
124− text: "default"
125− }
126115 }
127116 Row {
128117 Button {
129− text: "Authenticated"
118+ text: "Notify Auth Failure"
130119 onClicked: {
131− if (successCheckBox.checked) {
132− loader.item.notifyAuthenticationSucceeded(fakePasswordCheckBox.checked);
133− } else {
134− loader.item.notifyAuthenticationFailed();
135− }
120+ loader.item.notifyAuthenticationFailed();
136121 }
137122 }
138− CheckBox {
139− id: successCheckBox
140− }
141− Label {
142− text: "success"
143− }
144− CheckBox {
145− id: fakePasswordCheckBox
146− }
147− Label {
148− text: "fake password"
149− }
150123 }
151124 Row {
152125 Button {
@@ -211,6 +184,7 @@ Item {
211184 Row {
212185 CheckBox {
213186 id: alphanumericCheckBox
187+ checked: true
214188 }
215189 Label {
216190 text: "alphanumeric"
@@ -252,7 +226,7 @@ Item {
252226 Binding {
253227 target: LightDM.Infographic
254228 property: "username"
255− value: "single"
229+ value: "has-password"
256230 }
257231
258232 SignalSpy {
@@ -294,6 +268,9 @@ Item {
294268 function init() {
295269 view.currentIndex = 0; // break binding with text field
296270
271+ LightDM.Greeter.authenticate("no-password");
272+ tryCompare(LightDMService.prompts, "count", 1);
273+
297274 telepathyHelper.ready = true;
298275 telepathyHelper.emergencyCallsAvailable = true;
299276 selectedSpy.clear();
@@ -358,8 +335,9 @@ Item {
358335 }
359336
360337 function test_respondedWithPin() {
338+ LightDM.Greeter.authenticate("has-pin");
361339 view.locked = true;
362− view.showPrompt("", true, true);
340+ view.alphanumeric = false;
363341 swipeAwayCover();
364342 typeString("1234");
365343 compare(respondedSpy.count, 1);
@@ -367,9 +345,8 @@ Item {
367345 }
368346
369347 function test_respondedWithPassphrase() {
348+ LightDM.Greeter.authenticate("has-password");
370349 view.locked = true;
371− view.alphanumeric = true;
372− view.showPrompt("", true, true);
373350 swipeAwayCover();
374351 typeString("test");
375352 keyClick(Qt.Key_Enter);
tests/qmltests/Greeter/tst_WideView.qml+125−121
@@ -18,7 +18,8 @@ import QtQuick 2.4
1818import QtTest 1.0
1919import ".."
2020import "../../../qml/Greeter"
21−import LightDM.IntegratedLightDM 0.1 as LightDM
21+import LightDMController 0.1
22+import LightDM.FullLightDM 0.1 as LightDM
2223import Ubuntu.Components 1.3
2324import Unity.Test 0.1 as UT
2425
@@ -31,12 +32,6 @@ StyledItem {
3132
3233 theme.name: "Ubuntu.Components.Themes.SuruDark"
3334
34− Binding {
35− target: LightDM.Users
36− property: "mockMode"
37− value: "full"
38− }
39−
4035 Row {
4136 anchors.fill: parent
4237 Loader {
@@ -115,28 +110,16 @@ StyledItem {
115110 Column {
116111 anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
117112 spacing: units.gu(1)
118− Row {
119− Button {
120− text: "Show Last Chance"
121− onClicked: loader.item.showLastChance()
122− }
123− }
124113 Row {
125114 Button {
126115 text: "Hide"
127116 onClicked: loader.item.hide()
128117 }
129118 }
130− Row {
131− Button {
132− text: "Reset"
133− onClicked: loader.item.reset()
134− }
135− }
136119 Row {
137120 Button {
138121 text: "Show Message"
139− onClicked: loader.item.showMessage(messageField.text)
122+ onClicked: LightDMService.prompts.append(messageField.text, LightDMService.prompts.Message)
140123 }
141124 TextField {
142125 id: messageField
@@ -147,7 +130,7 @@ StyledItem {
147130 Row {
148131 Button {
149132 text: "Show Prompt"
150− onClicked: loader.item.showPrompt(promptField.text, isSecretCheckBox.checked, isDefaultPromptCheckBox.checked)
133+ onClicked: LightDMService.prompts.append(promptField.text, isSecretCheckBox.checked ? LightDMService.prompts.Secret : LightDMService.prompts.Question)
151134 }
152135 TextField {
153136 id: promptField
@@ -160,30 +143,14 @@ StyledItem {
160143 Label {
161144 text: "secret"
162145 }
163− CheckBox {
164− id: isDefaultPromptCheckBox
165− }
166− Label {
167− text: "default"
168− }
169146 }
170147 Row {
171148 Button {
172− text: "Authenticated"
149+ text: "Notify Auth Failure"
173150 onClicked: {
174− if (successCheckBox.checked) {
175− loader.item.notifyAuthenticationSucceeded(false);
176− } else {
177− loader.item.notifyAuthenticationFailed();
178− }
151+ loader.item.notifyAuthenticationFailed();
179152 }
180153 }
181− CheckBox {
182− id: successCheckBox
183− }
184− Label {
185− text: "success"
186− }
187154 }
188155 Row {
189156 Button {
@@ -271,15 +238,15 @@ StyledItem {
271238 id: multipleSessionsCheckbox
272239 onClicked: {
273240 if (checked) {
274− LightDM.Sessions.testScenario = "multipleSessions"
241+ LightDMController.sessionMode = "full";
275242 } else {
276− LightDM.Sessions.testScenario = "singleSession"
243+ LightDMController.sessionMode = "single";
277244 }
278245 }
279246 Connections {
280− target: LightDM.Sessions
281− onTestScenarioChanged: {
282− if (LightDM.Sessions.testScenario === "multipleSessions") {
247+ target: LightDMController
248+ onSessionModeChanged: {
249+ if (LightDMController.sessionMode === "full") {
283250 multipleSessionsCheckbox.checked = true;
284251 } else {
285252 multipleSessionsCheckbox.checked = false;
@@ -297,11 +264,11 @@ StyledItem {
297264
298265 width: units.gu(10)
299266 minimumValue: 0
300− maximumValue: LightDM.Sessions.numAvailableSessions
301− value: LightDM.Sessions.numSessions
302− visible: LightDM.Sessions.testScenario === "multipleSessions"
267+ maximumValue: LightDMController.numAvailableSessions
268+ value: LightDMController.numSessions
269+ visible: LightDMController.sessionMode === "full"
303270 Binding {
304− target: LightDM.Sessions
271+ target: LightDMController
305272 property: "numSessions"
306273 value: numSessionsSlider.value
307274 }
@@ -364,11 +331,17 @@ StyledItem {
364331
365332 function init() {
366333 selectIndex(0); // break binding with text field
334+ tryCompare(LightDMService.prompts, "count", 1);
367335 selectedSpy.clear();
368336 respondedSpy.clear();
369337 teaseSpy.clear();
370338 emergencySpy.clear();
371− LightDM.Sessions.testScenario = "multipleSessions"
339+ LightDMController.reset();
340+ LightDM.Sessions.iconSearchDirectories = [testIconDirectory];
341+
342+ waitForRendering(view);
343+ var userList = findChild(view, "userList");
344+ tryCompare(userList, "movingInternally", false);
372345 }
373346
374347 function cleanup() {
@@ -395,8 +368,14 @@ StyledItem {
395368 }
396369
397370 function selectIndex(i) {
398− view.currentIndex = i;
371+ var user = LightDM.Users.data(i, LightDM.UserRoles.NameRole);
372+ LightDM.Greeter.authenticate(user);
373+
399374 var userList = findChild(view, "userList");
375+ var promptList = findChild(view, "promptList");
376+
377+ view.currentIndex = i;
378+ tryCompare(promptList, "opacity", 1);
400379 tryCompare(userList, "movingInternally", false);
401380 }
402381
@@ -415,6 +394,19 @@ StyledItem {
415394 waitForRendering(view);
416395 }
417396
397+ function test_changingSessionSticksToUser() {
398+ var loginList = findChild(view, "loginList");
399+
400+ selectUser("invalid-session");
401+ tryCompare(loginList, "currentSession", "invalid");
402+
403+ selectUser("has-password");
404+ tryCompare(loginList, "currentSession", "ubuntu");
405+
406+ selectUser("invalid-session")
407+ tryCompare(loginList, "currentSession", "invalid");
408+ }
409+
418410 function test_tease_data() {
419411 return [
420412 {tag: "locked", x: 0, offset: 0, count: 0, locked: true},
@@ -426,10 +418,6 @@ StyledItem {
426418 }
427419
428420 function test_sessionIconsAreValid() {
429− LightDM.Sessions.testScenario = "multipleSessions"
430− var originalDirectories = LightDM.Sessions.iconSearchDirectories
431− LightDM.Sessions.iconSearchDirectories = [testIconDirectory]
432−
433421 selectUser("has-password");
434422
435423 // Test the login list icon is valid
@@ -449,12 +437,10 @@ StyledItem {
449437
450438 function test_choosingNewSessionChangesLoginListIcon() {
451439 // Ensure the default session is selected (Ubuntu)
452− loader.active = false;
453− loader.active = true;
440+ cleanup();
454441
455442 selectUser("has-password");
456443
457− LightDM.Sessions.testScenario = "multipleSessions";
458444 var sessionChooserButton = findChild(view, "sessionChooserButton");
459445 var icon = String(sessionChooserButton.icon);
460446 compare(icon.indexOf("ubuntu") > -1, true);
@@ -465,25 +451,25 @@ StyledItem {
465451 var currentDelegate = findChild(view, delegateName);
466452 var sessionKey = LightDM.Sessions.data(i,LightDM.SessionRoles.KeyRole);
467453 if (sessionKey === "gnome-classic") {
454+ waitForRendering(currentDelegate);
468455 tap(currentDelegate);
469− var sessionChooserButton = findChild(view, "sessionChooserButton");
470456 waitForRendering(sessionChooserButton);
471− var icon = String(sessionChooserButton.icon);
472457 break;
473458 }
474459 }
475460
461+ icon = String(sessionChooserButton.icon);
476462 compare(icon.indexOf("gnome") > -1, true,
477463 "Expected icon to contain gnome but it was " + icon);
478464 }
479465
480466 function test_noSessionsDoesntBreakView() {
481− LightDM.Sessions.testScenario = "noSessions"
467+ LightDMController.sessionMode = "none";
482468 compare(LightDM.Sessions.count, 0)
483469 }
484470
485471 function test_sessionIconNotShownWithOneSession() {
486− LightDM.Sessions.testScenario = "singleSession"
472+ LightDMController.sessionMode = "single";
487473 compare(LightDM.Sessions.count, 1);
488474
489475 var sessionChooserButton = findChild(view, "sessionChooserButton");
@@ -491,9 +477,6 @@ StyledItem {
491477 }
492478
493479 function test_sessionIconNotShownWithActiveUser() {
494− LightDM.Sessions.testScenario = "multipleSessions";
495− compare(LightDM.Sessions.count > 1, true);
496−
497480 selectUser("active");
498481
499482 var sessionChooserButton = findChild(view, "sessionChooserButton");
@@ -501,9 +484,6 @@ StyledItem {
501484 }
502485
503486 function test_sessionIconShownWithMultipleSessions() {
504− LightDM.Sessions.testScenario = "multipleSessions"
505− compare(LightDM.Sessions.count > 1, true);
506−
507487 selectUser("has-password");
508488
509489 var sessionChooserButton = findChild(view, "sessionChooserButton");
@@ -537,12 +517,9 @@ StyledItem {
537517 }
538518
539519 function test_respondedWithPassword() {
540− view.locked = true;
541− view.showPrompt("Prompt", true, false);
542− var passwordInput = findChild(view, "passwordInput");
543− compare(passwordInput.text, "Prompt");
544− verify(passwordInput.isSecret);
545− tap(passwordInput);
520+ selectUser("has-password");
521+ var greeterPrompt = findChild(view, "greeterPrompt0");
522+ verify(greeterPrompt.isSecret);
546523 typeString("password");
547524 keyClick(Qt.Key_Enter);
548525 compare(respondedSpy.count, 1);
@@ -550,12 +527,9 @@ StyledItem {
550527 }
551528
552529 function test_respondedWithNonSecret() {
553− view.locked = true;
554− view.showPrompt("otp", false, false);
555− var passwordInput = findChild(view, "passwordInput");
556− compare(passwordInput.text, "otp");
557− verify(!passwordInput.isSecret);
558− tap(passwordInput);
530+ selectUser("question-prompt");
531+ var greeterPrompt = findChild(view, "greeterPrompt0");
532+ verify(!greeterPrompt.isSecret);
559533 typeString("foo");
560534 keyClick(Qt.Key_Enter);
561535 compare(respondedSpy.count, 1);
@@ -580,24 +554,47 @@ StyledItem {
580554 tryCompare(view, "required", false);
581555 }
582556
583− function test_showMessage() {
584− view.showMessage("Welcome to Unity Greeter");
585− view.showMessage("<font color=\"#df382c\">This is an error</font>");
586− view.showMessage("You should have seen three messages and this is a really long message too. wow so long much length");
587− var infoLabel = findChild(view, "infoLabel");
588− compare(infoLabel.text, "Welcome to Unity Greeter<br><font color=\"#df382c\">This is an error</font><br>You should have seen three messages and this is a really long message too. wow so long much length");
589− compare(infoLabel.textFormat, Text.StyledText);
557+ function test_infoPrompt() {
558+ selectUser("info-prompt");
559+
560+ var infoLabel = findChild(view, "infoLabel0");
561+ compare(infoLabel.text, "Welcome to Unity Greeter");
562+ compare(infoLabel.textFormat, Text.PlainText);
563+
564+ verify(findChild(view, "greeterPrompt1") != null);
565+ }
566+
567+ function test_longInfoPrompt() {
568+ selectUser("long-info-prompt");
569+
570+ var infoLabel = findChild(view, "infoLabel0");
571+ compare(infoLabel.text, "Welcome to Unity Greeter\n\nWe like to annoy you with super ridiculously long messages.\nLike this one\n\nThis is the last line of a multiple line message.");
590572 verify(infoLabel.contentWidth > infoLabel.width);
591− verify(infoLabel.opacity < 1);
592− tryCompare(infoLabel, "opacity", 1);
573+
574+ verify(findChild(view, "greeterPrompt1") != null);
575+ }
576+
577+ function test_multiInfoPrompt() {
578+ selectUser("multi-info-prompt");
579+
580+ var infoLabel0 = findChild(view, "infoLabel0");
581+ compare(infoLabel0.text, "Welcome to Unity Greeter");
582+
583+ var infoLabel1 = findChild(view, "infoLabel1");
584+ compare(infoLabel1.text, "This is an error");
585+ compare(infoLabel1.color, theme.palette.normal.negative);
586+
587+ var infoLabel2 = findChild(view, "infoLabel2");
588+ compare(infoLabel2.text, "You should have seen three messages");
589+
590+ verify(findChild(view, "greeterPrompt3") != null);
593591 }
594592
595593 // Escape is used to reset the authentication, especially if PAM is unresponsive
596594 function test_escape() {
597− selectIndex(1);
595+ var index = selectUser("has-password");
598596 selectedSpy.clear();
599597 view.locked = true;
600− view.showPrompt("Prompt", true, true);
601598 var promptField = findChild(view, "promptField");
602599 tap(promptField);
603600 verify(promptField.activeFocus);
@@ -611,9 +608,10 @@ StyledItem {
611608 compare(selectedSpy.count, 0);
612609 keyClick(Qt.Key_Escape);
613610 compare(selectedSpy.count, 1);
614− compare(selectedSpy.signalArguments[0][0], 1);
611+ compare(selectedSpy.signalArguments[0][0], index);
615612
616− view.reset();
613+ selectIndex(index);
614+ var promptField = findChild(view, "promptField");
617615 verify(promptField.activeFocus);
618616 compare(promptField.opacity, 1);
619617 }
@@ -624,21 +622,22 @@ StyledItem {
624622 tryCompare(label, "text", "가나다라마");
625623 }
626624
627− function test_promptless() {
628− var passwordInput = findChild(view, "passwordInput");
629−
625+ function test_authError() {
626+ var index = selectUser("auth-error");
627+ var greeterPrompt = findChild(view, "greeterPrompt1"); // after error message
630628 view.locked = true;
631− compare(passwordInput.text, "Retry");
632− tap(passwordInput);
629+ compare(greeterPrompt.text, "Retry");
630+ tap(greeterPrompt);
633631 compare(respondedSpy.count, 0);
634632 compare(selectedSpy.count, 1);
635− compare(selectedSpy.signalArguments[0][0], 0);
636− selectedSpy.clear();
633+ compare(selectedSpy.signalArguments[0][0], index);
634+ }
637635
638− view.reset();
639− view.locked = false;
640− compare(passwordInput.text, "Log In");
641− tap(passwordInput);
636+ function test_noPassword() {
637+ selectUser("no-password");
638+ var greeterPrompt = findChild(view, "greeterPrompt0");
639+ compare(greeterPrompt.text, "Log In");
640+ tap(greeterPrompt);
642641 compare(selectedSpy.count, 0);
643642 compare(respondedSpy.count, 1);
644643 compare(respondedSpy.signalArguments[0][0], "");
@@ -670,8 +669,8 @@ StyledItem {
670669 }
671670
672671 function test_passphrase() {
672+ var index = selectUser("has-password");
673673 var promptField = findChild(view, "promptField");
674− view.showPrompt("", true, true);
675674
676675 verify(view.alphanumeric);
677676 compare(promptField.inputMethodHints & Qt.ImhDigitsOnly, 0);
@@ -681,8 +680,8 @@ StyledItem {
681680 }
682681
683682 function test_passcode() {
683+ var index = selectUser("has-pin");
684684 var promptField = findChild(view, "promptField");
685− view.showPrompt("", true, true);
686685
687686 view.alphanumeric = false;
688687 compare(promptField.inputMethodHints & Qt.ImhDigitsOnly, Qt.ImhDigitsOnly);
@@ -704,54 +703,59 @@ StyledItem {
704703
705704 function test_loginListMovement_data() {
706705 return [
707− {tag: "up", key: Qt.Key_Up, result: -1},
708− {tag: "down", key: Qt.Key_Down, result: 1},
706+ {tag: "up", key: Qt.Key_Up, result: 1},
707+ {tag: "down", key: Qt.Key_Down, result: 3},
709708 ]
710709 }
711710
712711 function test_loginListMovement(data) {
712+ selectIndex(2);
713+ selectedSpy.clear();
714+
713715 keyClick(data.key);
714716 compare(selectedSpy.count, 1);
715717 compare(selectedSpy.signalArguments[0][0], data.result);
716718 }
717719
718720 function test_focusStaysActive() {
719− var promptField = findChild(view, "promptField");
720− var promptButton = findChild(view, "promptButton");
721−
722− verify(promptButton.activeFocus);
721+ selectUser("no-password");
722+ var greeterPrompt = findChild(view, "greeterPrompt0");
723+ verify(greeterPrompt.activeFocus);
723724 keyClick(Qt.Key_Enter);
724725 compare(selectedSpy.count, 0);
725726 compare(respondedSpy.count, 1);
726727 compare(respondedSpy.signalArguments[0][0], "");
727− verify(promptButton.activeFocus);
728+ verify(greeterPrompt.activeFocus);
728729 keyClick(Qt.Key_Enter);
729730 compare(respondedSpy.count, 1);
730731
731− view.showPrompt("", true, true);
732− verify(promptField.activeFocus);
732+ selectUser("has-password");
733+ var greeterPrompt = findChild(view, "greeterPrompt0");
734+ verify(greeterPrompt.activeFocus);
733735 keyClick(Qt.Key_D);
734736 keyClick(Qt.Key_Enter);
735737 compare(selectedSpy.count, 0);
736738 compare(respondedSpy.count, 2);
737739 compare(respondedSpy.signalArguments[1][0], "d");
738− verify(promptField.activeFocus);
740+ verify(greeterPrompt.activeFocus);
739741 keyClick(Qt.Key_Enter);
740742 compare(respondedSpy.count, 2);
741743
742− view.reset();
744+ var index = selectUser("no-password");
745+ var greeterPrompt = findChild(view, "greeterPrompt0");
743746 view.locked = true;
744− verify(promptButton.activeFocus);
747+ verify(greeterPrompt.activeFocus);
745748 keyClick(Qt.Key_Enter);
746749 compare(respondedSpy.count, 2);
747750 compare(selectedSpy.count, 1);
748− compare(selectedSpy.signalArguments[0][0], 0);
749− verify(promptButton.activeFocus);
751+ compare(selectedSpy.signalArguments[0][0], index);
752+ verify(greeterPrompt.activeFocus);
750753 keyClick(Qt.Key_Enter);
751754 compare(selectedSpy.count, 1);
752755
753− view.showPrompt("", true, true);
754− verify(promptField.activeFocus);
756+ selectUser("no-password");
757+ var greeterPrompt = findChild(view, "greeterPrompt0");
758+ verify(greeterPrompt.activeFocus);
755759 }
756760 }
757761}
tests/qmltests/Launcher/tst_Drawer.qml+58−7
@@ -26,6 +26,7 @@ import Unity.Test 0.1
2626StyledItem {
2727 id: root
2828 theme.name: "Ubuntu.Components.Themes.SuruDark"
29+ focus: true
2930
3031 width: units.gu(140)
3132 height: units.gu(70)
@@ -108,6 +109,7 @@ StyledItem {
108109 tryCompareFunction( function(){ return drawer.x === 0; }, true );
109110 tryCompare(launcher, "state", "drawer");
110111 tryCompare(launcher, "drawerShown", true);
112+ return drawer;
111113 }
112114
113115 function revealByEdgePush() {
@@ -134,6 +136,8 @@ StyledItem {
134136 launcher.hide();
135137 var drawer = findChild(launcher, "drawer");
136138 tryCompare(drawer, "x", -drawer.width);
139+ var searchField = findChild(drawer, "searchField");
140+ searchField.text = "";
137141 }
138142
139143 function test_revealByEdgeDrag() {
@@ -168,12 +172,10 @@ StyledItem {
168172 function test_hideByDraggingDrawer(data) {
169173 launcher.lockedVisible = !data.autohide;
170174
171− dragDrawerIntoView();
175+ var drawer = dragDrawerIntoView();
172176 waitForRendering(launcher);
173177
174− var drawer = findChild(launcher, "drawer");
175−
176− mouseFlick(root, drawer.width - units.gu(1), drawer.height / 2, units.gu(2), drawer.height / 2, true, true);
178+ mouseFlick(root, drawer.width - units.gu(1), drawer.height / 2, units.gu(10), drawer.height / 2, true, true);
177179
178180 tryCompare(drawer.anchors, "rightMargin", 0);
179181 tryCompare(launcher, "state", data.endState);
@@ -181,9 +183,7 @@ StyledItem {
181183 }
182184
183185 function test_hideByClickingOutside() {
184− dragDrawerIntoView();
185−
186− var drawer = findChild(launcher, "drawer");
186+ var drawer = dragDrawerIntoView();
187187
188188 mouseClick(root, drawer.width + units.gu(1), root.height / 2);
189189
@@ -240,6 +240,8 @@ StyledItem {
240240 keyClick(Qt.Key_Enter);
241241
242242 compare(launcher.lastSelectedApplication, "camera-app");
243+ waitForRendering(launcher);
244+ tryCompare(launcher, "drawerShown", false);
243245 }
244246
245247 function test_dragDirectionOnLeftEdgeDrag_data() {
@@ -260,5 +262,54 @@ StyledItem {
260262
261263 tryCompare(launcher, "state", data.endState)
262264 }
265+
266+ function test_searchDirectly() {
267+ var drawer = dragDrawerIntoView();
268+ waitForRendering(launcher);
269+ waitUntilTransitionsEnd(launcher);
270+ tryCompare(drawer, "focus", true);
271+
272+ var searchField = findChild(drawer, "searchField");
273+ tryCompareFunction(function() { return !!searchField }, true);
274+ typeString("cam");
275+ tryCompareFunction(function() { return searchField.displayText }, "cam");
276+ }
277+
278+ function test_kbdNavigation() {
279+ launcher.openDrawer(true);
280+ waitForRendering(launcher);
281+ waitUntilTransitionsEnd(launcher);
282+ compare(launcher.lastSelectedApplication, "");
283+
284+ var drawer = findChild(launcher, "drawer");
285+ var searchField = findChild(drawer, "searchField");
286+ var sections = findChild(drawer, "drawerSections");
287+ var header = findChild(drawer, "headerFocusScope");
288+ var listLoader = findChild(drawer, "drawerListLoader");
289+
290+ tryCompare(searchField, "activeFocus", true);
291+ // for some reason, even when the searchField has activeFocus already,
292+ // it won't react on key down events if they're coming in too early...
293+ // let's repeat the press for a bit before giving up
294+ tryCompareFunction(function() {
295+ wait(10)
296+ keyClick(Qt.Key_Down);
297+ return sections.activeFocus;
298+ }, true)
299+
300+ keyClick(Qt.Key_Down);
301+ tryCompare(header, "activeFocus", true);
302+
303+ keyClick(Qt.Key_Down);
304+ tryCompare(listLoader, "activeFocus", true);
305+
306+ keyClick(Qt.Key_Down);
307+ keyClick(Qt.Key_Left);
308+ tryCompare(listLoader, "activeFocus", true);
309+
310+ keyClick(Qt.Key_Enter);
311+
312+ compare(launcher.lastSelectedApplication, "calendar-app");
313+ }
263314 }
264315}
tests/qmltests/Launcher/tst_Launcher.qml+15−6
@@ -295,6 +295,8 @@ Rectangle {
295295 compare(listView.snapMode, ListView.SnapToItem, "Snapping is not enabled");
296296
297297 removeTimeConstraintsFromSwipeAreas(root);
298+
299+ waitUntilLauncherDisappears();
298300 }
299301
300302 function dragLauncher() {
@@ -369,18 +371,17 @@ Rectangle {
369371 function assertFocusOnIndex(index) {
370372 var launcherPanel = findChild(launcher, "launcherPanel");
371373 var launcherListView = findChild(launcher, "launcherListView");
372− var bfbFocusHighlight = findChild(launcher, "bfbFocusHighlight");
374+ var bfb = findChild(launcher, "buttonShowDashHome");
373375
374376 waitForRendering(launcher);
375377 tryCompare(launcherPanel, "highlightIndex", index);
376− compare(bfbFocusHighlight.visible, index === -1);
378+ compare(bfb.highlighted, index === -1);
377379 for (var i = 0; i < launcherListView.count; i++) {
378380 var item = findChild(launcher, "launcherDelegate" + i);
379381 // Delegates might be destroyed when not visible. We can't check if they paint a focus highlight.
380382 // Make sure the requested index does have focus. for the others, try best effort to check if they don't
381383 if (index === i || item) {
382− var focusRing = findChild(item, "focusRing")
383− tryCompare(focusRing, "visible", index === i);
384+ tryCompare(item, "highlighted", index === i);
384385 }
385386 }
386387 }
@@ -1188,13 +1189,13 @@ Rectangle {
11881189 }
11891190
11901191 function test_keyboardNavigation(data) {
1191− var bfbFocusHighlight = findChild(launcher, "bfbFocusHighlight");
1192+ var bfb = findChild(launcher, "buttonShowDashHome");
11921193 var quickList = findChild(launcher, "quickList");
11931194 var launcherPanel = findChild(launcher, "launcherPanel");
11941195 var launcherListView = findChild(launcher, "launcherListView");
11951196 var last = launcherListView.count - 1;
11961197
1197− compare(bfbFocusHighlight.visible, false);
1198+ compare(bfb.highlighted, false);
11981199 launcher.openForKeyboardNavigation();
11991200 tryCompare(launcherPanel, "x", 0);
12001201 waitForRendering(launcher);
@@ -1478,5 +1479,13 @@ Rectangle {
14781479
14791480 launcher.available = true;
14801481 }
1482+
1483+ function test_hintOnSizeChange() {
1484+ var oldSize = launcher.panelWidth;
1485+ launcher.maxPanelX = -launcher.panelWidth;
1486+ launcher.panelWidth = oldSize + units.gu(2);
1487+ tryCompare(launcher, "maxPanelX", 0);
1488+ launcher.panelWidth = oldSize;
1489+ }
14811490 }
14821491}
tests/qmltests/Panel/tst_Panel.qml+36
@@ -338,6 +338,8 @@ PanelTest {
338338 // expose more of the panel, binding it to the selected indicator and opening it's menu.
339339 // Tested from first Y pixel to check for swipe from offscreen.
340340 function test_drag_indicator_item_down_shows_menu(data) {
341+ skip("Unstable test; panel expansion refactor may be required");
342+
341343 panel.fullscreenMode = data.fullscreen;
342344 callManager.foregroundCall = data.call;
343345
@@ -799,5 +801,39 @@ PanelTest {
799801 tryCompare(appTitle, "visible", false, undefined, "App title should still be visible on mouse hover when panel decorations are visible");
800802 tryCompare(appMenuBar, "visible", true, undefined, "App menu bar should be visible on mouse hover when panel decorations not visible");
801803 }
804+
805+ function test_keyboardNavigation_data() {
806+ return [
807+ {tag: "tab to start", doTab: false},
808+ {tag: "no tab to start", doTab: true}
809+ ]
810+ }
811+
812+ function test_keyboardNavigation(data) {
813+ var indicatorsBar = findChild(panel.indicators, "indicatorsBar");
814+
815+ pullDownIndicatorsMenu();
816+
817+ indicatorsBar.setCurrentItemIndex(0);
818+
819+ if (data.doTab) {
820+ keyClick(Qt.Key_Tab);
821+ }
822+
823+ keyClick(Qt.Key_Right);
824+ tryCompare(indicatorsBar, "currentItemIndex", 1);
825+
826+ keyClick(Qt.Key_Right);
827+ tryCompare(indicatorsBar, "currentItemIndex", 2);
828+
829+ keyClick(Qt.Key_Left);
830+ tryCompare(indicatorsBar, "currentItemIndex", 1);
831+
832+ keyClick(Qt.Key_Left);
833+ tryCompare(indicatorsBar, "currentItemIndex", 0);
834+
835+ keyClick(Qt.Key_Escape);
836+ tryCompare(panel.indicators, "fullyClosed", true);
837+ }
802838 }
803839}
tests/qmltests/Tutorial/tst_Tutorial.qml+3−3
@@ -17,7 +17,8 @@
1717import QtQuick 2.4
1818import QtTest 1.0
1919import AccountsService 0.1
20−import LightDM.IntegratedLightDM 0.1 as LightDM
20+import LightDMController 0.1
21+import LightDM.FullLightDM 0.1 as LightDM
2122import Ubuntu.Components 1.3
2223import Ubuntu.Components.ListItems 1.3
2324import Ubuntu.Telephony 0.1 as Telephony
@@ -85,8 +86,7 @@ Rectangle {
8586
8687 Component.onCompleted: {
8788 // must set the mock mode before loading the Shell
88− LightDM.Greeter.mockMode = "single-pin";
89− LightDM.Users.mockMode = "single-pin";
89+ LightDMController.userMode = "single-pin";
9090 shellLoader.active = true;
9191 }
9292
tests/qmltests/tst_OrientedShell.qml+90−8
@@ -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
@@ -22,7 +22,8 @@ import Ubuntu.Components 1.3
2222import Ubuntu.Components.ListItems 1.3 as ListItem
2323import Unity.Application 0.1
2424import Unity.Test 0.1
25−import LightDM.IntegratedLightDM 0.1 as LightDM
25+import LightDMController 0.1
26+import LightDM.FullLightDM 0.1 as LightDM
2627import Powerd 0.1
2728import Unity.InputInfo 0.1
2829import Utils 0.1
@@ -38,6 +39,12 @@ Rectangle {
3839 width: units.gu(160) + controls.width
3940 height: units.gu(100)
4041
42+ Binding {
43+ target: LightDMController
44+ property: "userMode"
45+ value: "single"
46+ }
47+
4148 QtObject {
4249 id: applicationArguments
4350 property string deviceName: "mako"
@@ -1365,12 +1372,7 @@ Rectangle {
13651372 }
13661373
13671374 function findAppWindowForSurfaceId(surfaceId) {
1368− // for PhoneStage and TabletStage
1369− var delegate = findChild(shell, "spreadDelegate_" + surfaceId);
1370− if (!delegate) {
1371− // for DesktopStage
1372− delegate = findChild(shell, "appDelegate_" + surfaceId);
1373− }
1375+ var delegate = findChild(shell, "appDelegate_" + surfaceId);
13741376 verify(delegate);
13751377 var appWindow = findChild(delegate, "appWindow");
13761378 return appWindow;
@@ -1558,5 +1560,85 @@ Rectangle {
15581560 verify(surface);
15591561 return surface.activeFocus;
15601562 }
1563+
1564+ function test_tabCyclyingInShutdownDialog_data() {
1565+ return [
1566+ {tag: "TAB", key: Qt.Key_Tab},
1567+ {tag: "DOWN", key: Qt.Key_Down}
1568+ ];
1569+ }
1570+
1571+ function test_tabCyclyingInShutdownDialog(data) {
1572+ loadShell("mako");
1573+
1574+ testCase.showPowerDialog();
1575+
1576+ var dialogs = findChild(orientedShell, "dialogs");
1577+ var buttons = findChildsByType(dialogs, "Button");
1578+
1579+ tryCompare(buttons[0], "activeFocus", true);
1580+
1581+ keyClick(data.key);
1582+ tryCompare(buttons[1], "activeFocus", true);
1583+
1584+ keyClick(data.key);
1585+ tryCompare(buttons[2], "activeFocus", true);
1586+
1587+ keyClick(data.key);
1588+ tryCompare(buttons[0], "activeFocus", true);
1589+
1590+ keyClick(Qt.Key_Escape);
1591+
1592+ var dialogLoader = findChild(orientedShell, "dialogLoader");
1593+ tryCompare(dialogLoader, "item", null);
1594+ }
1595+
1596+ function test_escClosesShutdownDialog() {
1597+ loadShell("mako");
1598+
1599+ testCase.showPowerDialog();
1600+
1601+ var dialogLoader = findChild(orientedShell, "dialogLoader");
1602+ tryCompareFunction(function() { return dialogLoader.item !== null }, true);
1603+
1604+ keyClick(Qt.Key_Escape);
1605+
1606+ tryCompare(dialogLoader, "item", null);
1607+ }
1608+
1609+ function test_focusOnShutdownDialogClose() {
1610+ loadShell("manta");
1611+ usageModeSelector.selectWindowed();
1612+
1613+ var surfaceId = topLevelSurfaceList.nextId;
1614+ var app = ApplicationManager.startApplication("twitter-webapp");
1615+ waitUntilAppWindowIsFullyLoaded(surfaceId);
1616+
1617+ var primaryAppWindow = findAppWindowForSurfaceId(surfaceId);
1618+ var surface = app.surfaceList.get(0);
1619+ var surfaceItem = findSurfaceItem(primaryAppWindow, surface);
1620+
1621+ compare(window.activeFocusItem, surfaceItem);
1622+
1623+ testCase.showPowerDialog();
1624+
1625+ var dialogLoader = findChild(orientedShell, "dialogLoader");
1626+ tryCompareFunction(function() { return dialogLoader.item !== null }, true);
1627+
1628+ keyClick(Qt.Key_Escape);
1629+
1630+ tryCompare(dialogLoader, "item", null);
1631+ compare(window.activeFocusItem, surfaceItem);
1632+
1633+ // Do it twice, our previous solution failed the second time ^_^
1634+ testCase.showPowerDialog();
1635+
1636+ tryCompareFunction(function() { return dialogLoader.item !== null }, true);
1637+
1638+ keyClick(Qt.Key_Escape);
1639+
1640+ tryCompare(dialogLoader, "item", null);
1641+ compare(window.activeFocusItem, surfaceItem);
1642+ }
15611643 }
15621644}
tests/qmltests/tst_Shell.qml+86−80
@@ -18,7 +18,8 @@ import QtQuick 2.4
1818import QtTest 1.0
1919import AccountsService 0.1
2020import GSettings 1.0
21−import LightDM.IntegratedLightDM 0.1 as LightDM
21+import LightDMController 0.1
22+import LightDM.FullLightDM 0.1 as LightDM
2223import SessionBroadcast 0.1
2324import Ubuntu.Components 1.3
2425import Ubuntu.Components.ListItems 1.3 as ListItem
@@ -48,8 +49,7 @@ Rectangle {
4849
4950 Component.onCompleted: {
5051 // must set the mock mode before loading the Shell
51− LightDM.Greeter.mockMode = "single";
52− LightDM.Users.mockMode = "single";
52+ LightDMController.userMode = "single";
5353 shellLoader.active = true;
5454 }
5555
@@ -248,8 +248,7 @@ Rectangle {
248248 model: ["single", "single-passphrase", "single-pin", "full"]
249249 onSelectedIndexChanged: {
250250 testCase.tearDown();
251− LightDM.Greeter.mockMode = model[selectedIndex];
252− LightDM.Users.mockMode = model[selectedIndex];
251+ LightDMController.userMode = model[selectedIndex];
253252 shellLoader.active = true;
254253 }
255254 }
@@ -568,7 +567,6 @@ Rectangle {
568567 // is loaded by default
569568 tearDown();
570569 }
571−
572570 }
573571
574572 function cleanup() {
@@ -656,7 +654,8 @@ Rectangle {
656654 // Shell instance gets destroyed.
657655 tryCompare(shellLoader, "itemDestroyed", true);
658656
659− setLightDMMockMode("single"); // back to the default value
657+ LightDMController.reset();
658+ setLightDMMockMode("single"); // these tests default to "single"
660659
661660 AccountsService.demoEdges = false;
662661 AccountsService.demoEdgesCompleted = [];
@@ -747,22 +746,6 @@ Rectangle {
747746 mockNotificationsModel.append(n)
748747 }
749748
750− function test_ClickUbuntuIconInLauncherTakesToAppsAndResetSearchString() {
751− loadShell("phone");
752− swipeAwayGreeter();
753− dragLauncherIntoView();
754− dashCommunicatorSpy.clear();
755−
756− var launcher = findChild(shell, "launcher");
757− var dashIcon = findChild(launcher, "dashItem");
758− verify(dashIcon != undefined);
759− mouseClick(dashIcon);
760−
761− tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash");
762−
763− compare(dashCommunicatorSpy.count, 1);
764− }
765−
766749 function test_suspend() {
767750 loadShell("phone");
768751 swipeAwayGreeter();
@@ -834,13 +817,15 @@ Rectangle {
834817 keyClick(Qt.Key_Escape) // Reset state if we're not moving
835818 while (userlist.currentIndex != i) {
836819 var next = userlist.currentIndex + 1
820+ var key = Qt.Key_Down;
837821 if (userlist.currentIndex > i) {
838822 next = userlist.currentIndex - 1
823+ key = Qt.Key_Up;
839824 }
840− tap(findChild(greeter, "username"+next));
825+ keyPress(key);
841826 tryCompare(userlist, "currentIndex", next)
842− tryCompare(userlist, "movingInternally", false)
843827 }
828+ tryCompare(userlist, "movingInternally", false);
844829 tryCompare(shell, "waitingOnGreeter", false); // wait for PAM to settle
845830 }
846831
@@ -863,15 +848,13 @@ Rectangle {
863848 var greeter = findChild(shell, "greeter")
864849 tryCompare(greeter, "fullyShown", true);
865850
866− var passwordInput = findChild(shell, "passwordInput");
867−
868− var promptButton = findChild(passwordInput, "promptButton");
851+ var promptButton = findChild(greeter, "promptButton");
869852 tryCompare(promptButton, "visible", isButton);
870853
871− var promptField = findChild(passwordInput, "promptField");
854+ var promptField = findChild(greeter, "promptField");
872855 tryCompare(promptField, "visible", !isButton);
873856
874− mouseClick(passwordInput)
857+ mouseClick(promptButton);
875858 }
876859
877860 function confirmLoggedIn(loggedIn) {
@@ -881,8 +864,7 @@ Rectangle {
881864 }
882865
883866 function setLightDMMockMode(mode) {
884− LightDM.Greeter.mockMode = mode;
885− LightDM.Users.mockMode = mode;
867+ LightDMController.userMode = mode;
886868 }
887869
888870 function test_showInputMethod() {
@@ -1380,55 +1362,25 @@ Rectangle {
13801362 tryCompare(stage, "state", "spread");
13811363 }
13821364
1383− function test_tapUbuntuIconInLauncherOverAppSpread() {
1384− launcherShowDashHomeSpy.clear();
1385−
1365+ function test_physicalHomeKeyPressDoesNothingWithActiveGreeter() {
13861366 loadShell("phone");
1387− swipeAwayGreeter();
1388−
1389− waitUntilFocusedApplicationIsShowingItsSurface();
13901367
1391− swipeFromRightEdgeToShowAppSpread();
1392−
1393− var launcher = findChild(shell, "launcher");
1394−
1395− dragLauncherIntoView();
1396−
1397− // Emulate a tap with a finger, where the touch position drifts during the tap.
1398− // This is to test the touch ownership changes. The tap is happening on the button
1399− // area but then drifting into the left edge drag area. This test makes sure
1400− // the touch ownership stays with the button and doesn't move over to the
1401− // left edge drag area.
1402− {
1403− var buttonShowDashHome = findChild(launcher, "buttonShowDashHome");
1404− touchFlick(buttonShowDashHome,
1405− buttonShowDashHome.width * 0.2, /* startPos.x */
1406− buttonShowDashHome.height * 0.8, /* startPos.y */
1407− buttonShowDashHome.width * 0.8, /* endPos.x */
1408− buttonShowDashHome.height * 0.2 /* endPos.y */);
1409− }
1410−
1411− compare(launcherShowDashHomeSpy.count, 1);
1412−
1413− // check that the stage has left spread mode.
1414− {
1415− var stage = findChild(shell, "stage");
1416− tryCompare(stage, "state", "staged");
1417− }
1368+ var windowInputMonitor = findInvisibleChild(shell, "windowInputMonitor");
1369+ var coverPage = findChild(shell, "coverPage");
14181370
1419− // check that the launcher got dismissed
1420− var launcherPanel = findChild(shell, "launcherPanel");
1421− tryCompare(launcherPanel, "x", -launcherPanel.width);
1371+ windowInputMonitor.homeKeyActivated();
1372+ verify(coverPage.shown);
14221373 }
14231374
1424− function test_physicalHomeKeyPressDoesNothingWithActiveGreeter() {
1375+ function test_physicalHomeKeyPressFocusesDash() {
14251376 loadShell("phone");
14261377
1427− var windowInputMonitor = findInvisibleChild(shell, "windowInputMonitor");
1428− var coverPage = findChild(shell, "coverPage");
1378+ var galleryApp = ApplicationManager.startApplication("gallery-app");
1379+ tryCompare(ApplicationManager, "focusedApplicationId", "gallery-app");
14291380
1381+ var windowInputMonitor = findInvisibleChild(shell, "windowInputMonitor");
14301382 windowInputMonitor.homeKeyActivated();
1431− verify(coverPage.shown);
1383+ tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash");
14321384 }
14331385
14341386 function test_tabletLogin_data() {
@@ -1475,11 +1427,39 @@ Rectangle {
14751427 confirmLoggedIn(data.loggedIn)
14761428
14771429 if (data.passwordFocus) {
1478− var passwordInput = findChild(greeter, "passwordInput")
1430+ var passwordInput = findChild(greeter, "promptField");
14791431 tryCompare(passwordInput, "focus", true)
14801432 }
14811433 }
14821434
1435+ function test_manualLoginFlow() {
1436+ LightDMController.showManualLoginHint = true;
1437+ setLightDMMockMode("full");
1438+ loadShell("desktop");
1439+
1440+ var i = selectUser("*other");
1441+ var greeter = findChild(shell, "greeter");
1442+ var username = findChild(greeter, "username" + i);
1443+ var promptField = findChild(greeter, "promptField");
1444+ var promptHint = findChild(greeter, "promptHint");
1445+
1446+ compare(username.text, "Login");
1447+ compare(promptHint.text, "Username");
1448+
1449+ tryCompare(promptField, "activeFocus", true);
1450+ typeString("has-password");
1451+ keyClick(Qt.Key_Enter);
1452+
1453+ promptHint = findChild(greeter, "promptHint");
1454+ tryCompare(username, "text", "has-password");
1455+ tryCompare(promptHint, "text", "Passphrase");
1456+
1457+ typeString("password");
1458+ keyClick(Qt.Key_Enter);
1459+
1460+ confirmLoggedIn(true);
1461+ }
1462+
14831463 function test_launcherInverted_data() {
14841464 return [
14851465 {tag: "phone", formFactor: "phone", usageScenario: "phone", launcherInverted: true},
@@ -1847,7 +1827,7 @@ Rectangle {
18471827 loadDesktopShellWithApps()
18481828 var launcher = findChild(shell, "launcher");
18491829 var stage = findChild(shell, "stage");
1850− var bfb = findChild(launcher, "buttonShowDashHome");
1830+ var app1 = findChild(launcher, "launcherDelegate0");
18511831
18521832 GSettingsController.setAutohideLauncher(!data.launcherLocked);
18531833 waitForRendering(shell);
@@ -1860,17 +1840,17 @@ Rectangle {
18601840 if (!data.launcherLocked) {
18611841 revealLauncherByEdgePushWithMouse();
18621842 tryCompare(launcher, "x", 0);
1863− mouseMove(bfb, bfb.width / 2, bfb.height / 2)
1843+ mouseMove(app1, app1.width / 2, app1.height / 2)
18641844 waitForRendering(shell)
18651845 }
18661846
1867− mouseClick(bfb, bfb.width / 2, bfb.height / 2)
1847+ mouseClick(app1, app1.width / 2, app1.height / 2)
18681848 if (!data.launcherLocked) {
18691849 tryCompare(launcher, "state", "")
18701850 }
18711851 tryCompare(stage, "state", "windowed")
18721852
1873− tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash")
1853+ tryCompare(ApplicationManager, "focusedApplicationId", "dialer-app")
18741854
18751855 keyRelease(Qt.Key_Alt);
18761856 }
@@ -2194,10 +2174,8 @@ Rectangle {
21942174 tryCompare(launcher, "state", "visible");
21952175 tryCompare(launcherPanel, "highlightIndex", -1);
21962176 keyRelease(Qt.Key_Super_L, Qt.MetaModifier);
2197− tryCompare(launcher, "state", data.launcherLocked ? "visible" : "");
2177+ tryCompare(launcher, "state", "drawer");
21982178 tryCompare(launcherPanel, "highlightIndex", -2);
2199− tryCompare(ApplicationManager, "focusedApplicationId", "unity8-dash");
2200− tryCompare(stage, "focus", true)
22012179 }
22022180
22032181 function test_longpressSuperOpensLauncherAndShortcutsOverlay() {
@@ -2773,5 +2751,33 @@ Rectangle {
27732751
27742752 tryCompare(panelTitle, "visible", true, undefined, "Panel title should be visible when greeter not shown");
27752753 }
2754+
2755+ function test_fourFingerTapOpensDrawer_data() {
2756+ return [
2757+ { tag: "1 finger", touchIds: [0], shown: false },
2758+ { tag: "2 finger", touchIds: [0, 1], shown: false },
2759+ { tag: "3 finger", touchIds: [0, 1, 2], shown: false },
2760+ { tag: "4 finger", touchIds: [0, 1, 2, 3], shown: true }
2761+ ];
2762+ }
2763+
2764+ function test_fourFingerTapOpensDrawer(data) {
2765+ loadShell("desktop");
2766+ shell.usageScenario = "desktop";
2767+ waitForRendering(shell);
2768+ swipeAwayGreeter();
2769+
2770+ var stage = findChild(shell, "stage");
2771+ var drawer = findChild(shell, "drawer")
2772+ verify(stage && drawer);
2773+
2774+ multiTouchTap(data.touchIds, stage);
2775+
2776+ if (data.shown) { // if shown, try to also hide it by clicking outside
2777+ tryCompareFunction(function() { return drawer.visible; }, true);
2778+ mouseClick(stage, stage.width-10, stage.height/2, undefined, undefined, 100);
2779+ }
2780+ tryCompareFunction(function() { return drawer.visible; }, false);
2781+ }
27762782 }
27772783}
tests/qmltests/tst_ShellWithPin.qml+22−13
@@ -19,7 +19,8 @@ import Ubuntu.Components 1.3
1919import QtTest 1.0
2020import AccountsService 0.1
2121import GSettings 1.0
22−import LightDM.IntegratedLightDM 0.1 as LightDM
22+import LightDMController 0.1
23+import LightDM.FullLightDM 0.1 as LightDM
2324import Ubuntu.SystemImage 0.1
2425import Ubuntu.Telephony 0.1 as Telephony
2526import Unity.Application 0.1
@@ -35,8 +36,7 @@ Item {
3536
3637 Component.onCompleted: {
3738 // must set the mock mode before loading the Shell
38− LightDM.Greeter.mockMode = "single-pin";
39− LightDM.Users.mockMode = "single-pin";
39+ LightDMController.userMode = "single-pin";
4040 shellLoader.active = true;
4141 }
4242
@@ -117,12 +117,6 @@ Item {
117117 signalName: "resettingDevice"
118118 }
119119
120− SignalSpy {
121− id: promptSpy
122− target: LightDM.Greeter
123− signalName: "showPrompt"
124− }
125−
126120 Telephony.CallEntry {
127121 id: phoneCall
128122 phoneNumber: "+447812221111"
@@ -539,24 +533,39 @@ Item {
539533 shellLoader.itemDestroyed = false;
540534 shellLoader.active = false;
541535 tryCompare(shellLoader, "itemDestroyed", true);
542− LightDM.Greeter.authenticate(""); // reset greeter
543536
544537 // Create new shell
545− promptSpy.clear();
546538 shellLoader.active = true;
547539 tryCompareFunction(function() {return shell !== null}, true);
548540
549541 // Confirm that we start disabled
550− compare(promptSpy.count, 0);
542+ compare(LightDM.Prompts.count, 0);
551543 verify(shell.waitingOnGreeter);
552544 var coverPageDragHandle = findChild(shell, "coverPageDragHandle");
553545 verify(!coverPageDragHandle.enabled);
554546
555547 // And that we only become enabled once the lockscreen is up
556548 tryCompare(shell, "waitingOnGreeter", false);
557− verify(promptSpy.count > 0);
549+ verify(LightDM.Prompts.count > 0);
558550 var lockscreen = findChild(shell, "lockscreen");
559551 verify(lockscreen.shown);
560552 }
553+
554+ function test_bfbOnLockedDevice() {
555+ var launcher = findChild(shell, "launcher");
556+ touchFlick(shell, units.gu(.5), shell.height / 2, units.gu(10), shell.height / 2);
557+
558+ tryCompare(launcher, "x", 0);
559+ tryCompare(launcher, "state", "visible");
560+
561+ waitForRendering(shell)
562+
563+ var bfb = findChild(launcher, "buttonShowDashHome");
564+ mouseClick(bfb, bfb.width / 2, bfb.height / 2);
565+
566+ enterPin("1234")
567+
568+ tryCompare(launcher, "state", "drawer");
569+ }
561570 }
562571}