diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f851f2827..224b935e8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -13,6 +13,9 @@ include(${CMAKE_SOURCE_DIR}/cmake/TranslationUtils.cmake) find_package(Dtk6 REQUIRED COMPONENTS Core Declarative SystemSettings Tools) if (NOT DISABLE_DDM) find_package(DDM 0.2.0 REQUIRED COMPONENTS Common) + if (DDM_VERSION VERSION_LESS 0.3.8) + add_compile_definitions(DDM_VERSION_LT_0_3_8) + endif() endif() find_package(QT NAMES Qt6 COMPONENTS Core REQUIRED) find_package(Qt6 CONFIG REQUIRED ShaderTools Concurrent RemoteObjects Network) diff --git a/src/core/lockscreen.cpp b/src/core/lockscreen.cpp index 8ba86b52b..fd58d4627 100644 --- a/src/core/lockscreen.cpp +++ b/src/core/lockscreen.cpp @@ -36,10 +36,9 @@ LockScreen::LockScreen(ILockScreen *impl, SurfaceContainer *parent, GreeterProxy void LockScreen::lock() { - if (isVisible()) { - return; - } - + // Do not early-return on isVisible(): the greeter may start in the + // undecided state (surface visible, not yet locked), so lock() must + // still flip the lock state afterwards. setVisible(true); if (!m_greeterProxy->isLocked()) diff --git a/src/greeter/greeterproxy.cpp b/src/greeter/greeterproxy.cpp index 1a10e3823..a085594fe 100644 --- a/src/greeter/greeterproxy.cpp +++ b/src/greeter/greeterproxy.cpp @@ -37,6 +37,8 @@ #include #include +#include + using namespace DDM; ///////////////////// @@ -120,6 +122,19 @@ GreeterProxy::GreeterProxy(QObject *parent) connect(m_socket, &QLocalSocket::readyRead, this, &GreeterProxy::readyRead); connect(m_socket, &QLocalSocket::errorOccurred, this, &GreeterProxy::error); + // Fallback: if DDM never decides (ShowGreeter / UserActivateMessage), show + // the greeter after a grace period instead of staying on the wallpaper. + m_undecidedTimer = new QTimer(this); + m_undecidedTimer->setSingleShot(true); + m_undecidedTimer->setInterval(5000); + connect(m_undecidedTimer, &QTimer::timeout, this, [this] { + if (m_undecided) { + qCWarning(lcTlGreeter) << "DDM did not decide in time, showing greeter by fallback"; + lock(); + } + }); + m_undecidedTimer->start(); + conn.connect(Logind::serviceName(), Logind::managerPath(), Logind::managerIfaceName(), @@ -159,6 +174,11 @@ void GreeterProxy::setLock(bool isLocked) { if (isLocked && !m_isLocked) { m_isLocked = true; + if (m_undecided) { + m_undecided = false; + m_undecidedTimer->stop(); + Q_EMIT undecidedChanged(false); + } if (m_lockScreen && !m_lockScreen->isVisible()) m_lockScreen->lock(); Q_EMIT lockChanged(true); @@ -290,7 +310,7 @@ void GreeterProxy::onSessionNew(const QString &id, [[maybe_unused]] const QDBusO return; } - if (strcmp(service, "ddm") == 0) { + if (strncmp(service, "ddm", 3) == 0) { QString user = QString::fromLocal8Bit(username); qCInfo(lcTlGreeter) << "New session added: id=" << id << ", user=" << user; userModel()->updateUserLoginState(user, true); @@ -496,6 +516,14 @@ void GreeterProxy::readyRead() qCInfo(lcTlGreeter) << "switch to greeter"; lock(); } break; +#ifndef DDM_VERSION_LT_0_3_8 + case DaemonMessages::ShowGreeter: { + qCInfo(lcTlGreeter) << "show greeter"; + // Full lock screen transition (LockScreen mode, hidden workspace), + // matching the previous startup behavior of showLockScreen(). + Helper::instance()->showLockScreen(false); + } break; +#endif case DaemonMessages::UserActivateMessage: { QString user; int sessionId; @@ -512,6 +540,23 @@ void GreeterProxy::readyRead() break; qCInfo(lcTlGreeter) << "activate successfully: " << user << ", XDG_SESSION_ID: " << sessionId; + + // DDM drives the direct login (autologin / unlock), so the greeter + // must hide unconditionally. onSessionNew's setLock(false) may never + // fire (currentUserName mismatch / session service not "ddm"). When + // still in the undecided state, the lock screen surface is visible + // (wallpaper) but not locked, so setLock(false) alone would no-op: + // restore the desktop mode and hide the surface explicitly. + if (m_undecided) { + m_undecided = false; + m_undecidedTimer->stop(); + Q_EMIT undecidedChanged(false); + } + if (m_lockScreen && m_lockScreen->isVisible()) { + Q_EMIT m_lockScreen->unlock(); + m_lockScreen->setVisible(false); + } + setLock(false); } break; case DaemonMessages::UserLoggedIn: { QString user; diff --git a/src/greeter/greeterproxy.h b/src/greeter/greeterproxy.h index e628e0a51..b64bb19c6 100644 --- a/src/greeter/greeterproxy.h +++ b/src/greeter/greeterproxy.h @@ -9,6 +9,7 @@ #include class QLocalSocket; +class QTimer; class LockScreen; @@ -34,6 +35,7 @@ class GreeterProxy Q_PROPERTY(bool canHybridSleep READ canHybridSleep NOTIFY canHybridSleepChanged) Q_PROPERTY(bool isLocked READ isLocked NOTIFY lockChanged) + Q_PROPERTY(bool undecided READ undecided NOTIFY undecidedChanged) Q_PROPERTY(int failedAttempts READ failedAttempts NOTIFY failedAttemptsChanged) Q_PROPERTY(bool showShutdownView READ showShutdownView WRITE setShowShutdownView NOTIFY showShutdownViewChanged) Q_PROPERTY(bool showAnimation READ showAnimation NOTIFY showAnimationChanged) @@ -91,6 +93,16 @@ class GreeterProxy */ inline bool isLocked() const { return m_isLocked; }; + /** + * @brief Get whether DDM has not decided yet whether to show the greeter + * or to log in directly. The greeter surface (wallpaper) is shown but the + * login UI stays hidden until DDM sends ShowGreeter / UserActivateMessage, + * or the fallback timer fires. + * + * @return true if DDM has not decided yet + */ + inline bool undecided() const { return m_undecided; }; + /** * @brief Get the number of failed login attempts (password incorrect) * The value is reset to 0 when unlocked successfully @@ -275,6 +287,9 @@ private Q_SLOTS: /** @brief Emitted when lock state changes. See isLocked() */ void lockChanged (bool isLocked); + /** @brief Emitted when undecided state changes. See undecided() */ + void undecidedChanged (bool undecided); + /** @brief Emitted when failed attempts changes. See failedAttempts() */ void failedAttemptsChanged (int failedAttempts); @@ -324,6 +339,8 @@ private Q_SLOTS: bool m_canHybridSleep { false }; bool m_isLocked { false }; + bool m_undecided { true }; + QTimer *m_undecidedTimer{ nullptr }; int m_failedAttempts { 0 }; bool m_showShutdownView { false }; bool m_showAnimation { true }; diff --git a/src/greeter/usermodel.cpp b/src/greeter/usermodel.cpp index 97b65b767..219e6b283 100644 --- a/src/greeter/usermodel.cpp +++ b/src/greeter/usermodel.cpp @@ -138,6 +138,15 @@ int UserModel::rowCount(const QModelIndex &parent) const void UserModel::updateUserLoginState(const QString &username, bool loggedIn) { + // TODO: May remove once UserModel is guaranteed to resolve every loggable + // user (consider whether users not manually added can still log in directly). + if (loggedIn && !getUser(username)) { + if (!tryAddNssUser(username)) { + qCWarning(lcTlGreeter) << "User" << username << "not found when updating login state"; + return; + } + } + auto user = std::find_if(d->users.begin(), d->users.end(), [&username](const UserPtr &user) { return user->userName() == username; }); diff --git a/src/plugins/lockscreen/qml/Greeter.qml b/src/plugins/lockscreen/qml/Greeter.qml index 60257bec5..0db1b34c4 100644 --- a/src/plugins/lockscreen/qml/Greeter.qml +++ b/src/plugins/lockscreen/qml/Greeter.qml @@ -39,7 +39,7 @@ FocusScope { color: 'black' opacity: 0.0 transformOrigin: Item.Center - state: (GreeterProxy.isLocked || GreeterProxy.showShutdownView) ? "Show" : "Hide" + state: (GreeterProxy.isLocked || GreeterProxy.undecided || GreeterProxy.showShutdownView) ? "Show" : "Hide" states: [ State { name: "Show" diff --git a/src/plugins/lockscreen/qml/UserInput.qml b/src/plugins/lockscreen/qml/UserInput.qml index 6ab066bcf..2285cc982 100644 --- a/src/plugins/lockscreen/qml/UserInput.qml +++ b/src/plugins/lockscreen/qml/UserInput.qml @@ -15,6 +15,7 @@ Item { property string normalHint: qsTr("Please enter password") property bool enteringOtherUser: false property bool showUserNotFoundError: false + property bool noPasswdLogin: false /**************/ /* Components */ @@ -101,132 +102,159 @@ Item { anchors.horizontalCenter: parent.horizontalCenter } - TextField { - id: passwordField + Item { + id: passwordSlot + width: loginGroup.width + height: 30 + anchors.horizontalCenter: parent.horizontalCenter - property bool capsIndicatorVisible: false + TextField { + id: passwordField - cursorDelegate: Rectangle { - id: cursor + property bool capsIndicatorVisible: false - width: 1 - height: 18 - color: palette.windowText + visible: !loginGroup.noPasswdLogin - visible: parent.activeFocus && !parent.readOnly && parent.selectionStart === parent.selectionEnd + anchors.fill: parent + + cursorDelegate: Rectangle { + id: cursor + + width: 1 + height: 18 + color: palette.windowText + + visible: parent.activeFocus && !parent.readOnly && parent.selectionStart === parent.selectionEnd + + Connections { + target: cursor.parent + function onCursorPositionChanged() { + // keep a moving cursor visible + cursor.opacity = 1 + cursorTimer.restart() + } + } - Connections { - target: cursor.parent - function onCursorPositionChanged() { - // keep a moving cursor visible - cursor.opacity = 1 - cursorTimer.restart() + Timer { + id: cursorTimer + running: cursor.parent.activeFocus && !cursor.parent.readOnly && interval != 0 + repeat: true + // TODO: Application.styleHints.cursorFlashTime / 2, waylib is not supports + // Application.styleHints now. + interval: 600 + onTriggered: cursor.opacity = !cursor.opacity ? 1 : 0 + // force the cursor visible when gaining focus + onRunningChanged: cursor.opacity = 1 } } - Timer { - id: cursorTimer - running: cursor.parent.activeFocus && !cursor.parent.readOnly && interval != 0 - repeat: true - // TODO: Application.styleHints.cursorFlashTime / 2, waylib is not supports - // Application.styleHints now. - interval: 600 - onTriggered: cursor.opacity = !cursor.opacity ? 1 : 0 - // force the cursor visible when gaining focus - onRunningChanged: cursor.opacity = 1 + horizontalAlignment: TextInput.AlignHCenter + echoMode: loginGroup.enteringOtherUser ? TextInput.Normal + : (showPasswordBtn.hiddenPWD ? TextInput.Password : TextInput.Normal) + rightPadding: 22 + leftPadding: { + var remaining = loginGroup.width - contentWidth - rightPadding + if (capsIndicator.visible) + return rightPadding + return Math.max(8, remaining > rightPadding ? rightPadding : remaining) + } + maximumLength: 510 + placeholderText: loginGroup.enteringOtherUser ? qsTr("Username") : qsTr("Password") + placeholderTextColor: Qt.rgba(1.0, 1.0, 1.0, 0.6) + color: palette.windowText + font: D.DTK.fontManager.t8 + Keys.onPressed: function (event) { + if (event.key === Qt.Key_CapsLock) { + capsIndicatorVisible = !capsIndicatorVisible + event.accepted = true + } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + if (loginGroup.enteringOtherUser) + confirmOtherUser() + else + userLogin() + event.accepted = true + } } - } - width: loginGroup.width - height: 30 - anchors.horizontalCenter: parent.horizontalCenter - horizontalAlignment: TextInput.AlignHCenter - echoMode: loginGroup.enteringOtherUser ? TextInput.Normal - : (showPasswordBtn.hiddenPWD ? TextInput.Password : TextInput.Normal) - rightPadding: 22 - leftPadding: { - var remaining = loginGroup.width - contentWidth - rightPadding - if (capsIndicator.visible) - return rightPadding - return Math.max(8, remaining > rightPadding ? rightPadding : remaining) - } - maximumLength: 510 - placeholderText: loginGroup.enteringOtherUser ? qsTr("Username") : qsTr("Password") - placeholderTextColor: Qt.rgba(1.0, 1.0, 1.0, 0.6) - color: palette.windowText - font: D.DTK.fontManager.t8 - Keys.onPressed: function (event) { - if (event.key === Qt.Key_CapsLock) { - capsIndicatorVisible = !capsIndicatorVisible - event.accepted = true - } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { - if (loginGroup.enteringOtherUser) - confirmOtherUser() - else - userLogin() - event.accepted = true + D.ActionButton { + id: capsIndicator + height: parent.height + anchors { + left: parent.left + leftMargin: 3 + verticalCenter: parent.verticalCenter + } + visible: passwordField.capsIndicatorVisible && !loginGroup.enteringOtherUser + palette.windowText: undefined + icon { + name: "login_capslock" + height: 10 + width: 10 + } + Layout.alignment: Qt.AlignHCenter + implicitWidth: 16 + implicitHeight: 16 } - } - D.ActionButton { - id: capsIndicator - height: parent.height - anchors { - left: parent.left - leftMargin: 3 - verticalCenter: parent.verticalCenter + D.ActionButton { + id: showPasswordBtn + anchors { + right: parent.right + rightMargin: 3 + verticalCenter: parent.verticalCenter + } + property bool hiddenPWD: true + visible: !loginGroup.enteringOtherUser + icon { + name: hiddenPWD ? "login_display_password" : "login_hidden_password" + height: 10 + width: 10 + } + Layout.alignment: Qt.AlignHCenter + implicitWidth: 16 + implicitHeight: 16 + hoverEnabled: true + + background: Rectangle { + anchors.fill: parent + radius: 4 + color: showPasswordBtn.hovered ? Qt.rgba( + 0, 0, 0, + 0.1) : "transparent" + } + + onClicked: hiddenPWD = !hiddenPWD } - visible: passwordField.capsIndicatorVisible && !loginGroup.enteringOtherUser - palette.windowText: undefined - icon { - name: "login_capslock" - height: 10 - width: 10 + + background: RoundBlur { + color: Qt.rgba(1, 1, 1, 0.4) + radius: 6 } - Layout.alignment: Qt.AlignHCenter - implicitWidth: 16 - implicitHeight: 16 } D.ActionButton { - id: showPasswordBtn - anchors { - right: parent.right - rightMargin: 3 - verticalCenter: parent.verticalCenter - } - property bool hiddenPWD: true - visible: !loginGroup.enteringOtherUser + id: noPwdLoginBtn + visible: loginGroup.noPasswdLogin + anchors.fill: parent icon { - name: hiddenPWD ? "login_display_password" : "login_hidden_password" - height: 10 - width: 10 + name: "login_open" + width: 16 + height: 16 } - Layout.alignment: Qt.AlignHCenter - implicitWidth: 16 - implicitHeight: 16 - hoverEnabled: true - - background: Rectangle { + background: RoundBlur { anchors.fill: parent - radius: 4 - color: showPasswordBtn.hovered ? Qt.rgba( - 0, 0, 0, - 0.1) : "transparent" + color: Qt.rgba(1.0, 1.0, 1.0, 0.4) + radius: parent.height / 2 } - onClicked: hiddenPWD = !hiddenPWD - } - - background: RoundBlur { - color: Qt.rgba(1, 1, 1, 0.4) - radius: 6 + onClicked: userLogin() } } } D.ActionButton { id: loginBtn + visible: !loginGroup.noPasswdLogin icon { name: "login_open" width: 16 @@ -325,6 +353,10 @@ Item { /* Functions and Connections */ /*****************************/ + function resetHint() { + hintText.text = loginGroup.noPasswdLogin ? "" : normalHint + } + function updateUser() { loginGroup.enteringOtherUser = false loginGroup.showUserNotFoundError = false @@ -332,11 +364,15 @@ Item { username.text = currentUser.realName.length === 0 ? currentUser.name : currentUser.realName passwordField.text = '' avatar.fallbackSource = currentUser.icon - hintText.text = normalHint + loginGroup.noPasswdLogin = currentUser.noPassword === true + resetHint() + if (loginGroup.noPasswdLogin) + noPwdLoginBtn.forceActiveFocus() } function startOtherUserMode() { loginGroup.enteringOtherUser = true + loginGroup.noPasswdLogin = false loginGroup.showUserNotFoundError = false username.text = qsTr("Enter username") passwordField.text = "" @@ -359,10 +395,11 @@ Item { function userLogin() { let user = UserModel.get(UserModel.currentUserName) + let pwd = loginGroup.noPasswdLogin ? "" : passwordField.text if (user.loggedIn) - GreeterProxy.unlock(user.name, passwordField.text) + GreeterProxy.unlock(user.name, pwd) else - GreeterProxy.login(user.name, passwordField.text, SessionModel.currentIndex) + GreeterProxy.login(user.name, pwd, SessionModel.currentIndex) } Connections { @@ -376,7 +413,7 @@ Item { hintText.text = qsTr("Password is incorrect.") } else { passwordField.text = "" - hintText.text = normalHint + resetHint() } } } diff --git a/src/seat/helper.cpp b/src/seat/helper.cpp index 10f85a55d..c2bd875de 100644 --- a/src/seat/helper.cpp +++ b/src/seat/helper.cpp @@ -3396,7 +3396,10 @@ void Helper::setLockScreenImpl(ILockScreen *impl) return; } if (CmdLine::ref().useLockScreen()) { - showLockScreen(false); + // Start in the undecided state: make the lock screen surface (wallpaper) + // visible but keep the login UI hidden until DDM decides (ShowGreeter / + // UserActivateMessage) or the fallback timeout in GreeterProxy fires. + m_lockScreen->setVisible(true); } #else Q_UNUSED(impl) @@ -3434,7 +3437,10 @@ void Helper::showLockScreen(bool switchToGreeter) if (!isLockScreenAvailable()) { return; } - if (m_lockScreen->isLocked()) { + // LockScreen::isLocked() is isVisible(), which is also true in the + // undecided state (surface shown, not yet locked), so check the real + // lock state instead. + if (m_greeterProxy->isLocked()) { return; }