From 45a0f687cd11607ccfab432badfd58545fe0e8de Mon Sep 17 00:00:00 2001 From: reionwong Date: Sun, 18 Oct 2026 02:50:16 +0800 Subject: [PATCH] refactor(settings): list the users as rows with their details in a dialog The Users page shows its accounts in a list card, as Wi-Fi does, the one logged in first. Each row keeps the picture, the names and whether it is logged in, and ends in an info button that opens the account's type, the way to a new password and deleting it. --- src/pages/accounts/accountspage.cpp | 107 ++--- src/pages/accounts/usercard.cpp | 477 ----------------------- src/pages/accounts/usercard.h | 88 ----- src/pages/accounts/userdetailsdialog.cpp | 85 ++++ src/pages/accounts/userdetailsdialog.h | 58 +++ src/pages/accounts/userrow.cpp | 265 +++++++++++++ src/pages/accounts/userrow.h | 59 +++ src/widgets/colors.cpp | 2 - src/widgets/colors.h | 1 - tests/ui/accounts_test.cpp | 163 ++++++-- 10 files changed, 650 insertions(+), 655 deletions(-) delete mode 100644 src/pages/accounts/usercard.cpp delete mode 100644 src/pages/accounts/usercard.h create mode 100644 src/pages/accounts/userdetailsdialog.cpp create mode 100644 src/pages/accounts/userdetailsdialog.h create mode 100644 src/pages/accounts/userrow.cpp create mode 100644 src/pages/accounts/userrow.h diff --git a/src/pages/accounts/accountspage.cpp b/src/pages/accounts/accountspage.cpp index a55b6fb..54bf96f 100644 --- a/src/pages/accounts/accountspage.cpp +++ b/src/pages/accounts/accountspage.cpp @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2026 Reion Wong // SPDX-License-Identifier: GPL-3.0-or-later +#include #include #include @@ -9,11 +10,13 @@ #include "base/paths.h" #include "pages/accounts/password.h" #include "pages/accounts/passwordchanger.h" -#include "pages/accounts/usercard.h" +#include "pages/accounts/userdetailsdialog.h" +#include "pages/accounts/userrow.h" #include "pages/accounts/userdialogs.h" #include "ui/dialog_window.h" #include "ui/file_chooser.h" #include "ui/cell_layout.h" +#include "widgets/card.h" #include "widgets/metrics.h" #include "widgets/standardbutton.h" #include "window/page.h" @@ -33,8 +36,7 @@ public: explicit AccountsPage(SettingsWindow& window) : Page(window, "Users") { - m_users = content()->addChild(); - m_users->setSpacing(metrics::sectionSpacing); + m_users = content()->addChild(Card::Style::List); m_users->setLayoutHints({.fillWidth = true}); StandardButton* add = content()->addChild("Add user"); add->onClicked = [this] { @@ -58,28 +60,33 @@ private: void updateUsers() { - // Cards that are still there keep their state, such as being unfolded. + // Rows that are still there are kept; the one logged in goes first. std::vector> previous; while (!m_users->children().empty()) previous.push_back(m_users->takeChild(m_users->children().front().get())); - for (const auto& account : m_manager.accounts()) { - std::unique_ptr card; + const auto accounts = loggedInFirst(m_manager.accounts(), int64_t(getuid())); + for (const auto& account : accounts) { + std::unique_ptr row; for (auto& view : previous) { - if (view && static_cast(view.get())->account() == account) { - card = std::move(view); + if (view && static_cast(view.get())->account() == account) { + row = std::move(view); break; } } - if (!card) - card = makeCard(account); - m_users->addChild(std::move(card)); + if (!row) + row = makeRow(account); + m_users->addChild(std::move(row)); } + m_users->setVisible(!accounts.empty()); + // The details of an account that is gone close with it. + if (m_details && std::find(accounts.begin(), accounts.end(), m_details->account()) == accounts.end()) + m_details->close(); } - std::unique_ptr makeCard(const accounts::AccountsManager::AccountPtr& account) + std::unique_ptr makeRow(const accounts::AccountsManager::AccountPtr& account) { - auto card = std::make_unique(account, int64_t(getuid())); - card->onPictureClicked = [this, account] { + auto row = std::make_unique(account, int64_t(getuid())); + row->onPictureClicked = [this, account] { m_chooser = std::make_unique(&settingsWindow().window()); m_chooser->setMode(FileChooser::Mode::OpenFile); // As QML's FileDialog, window-modal. @@ -95,46 +102,58 @@ private: account->setIconFileName(paths.front()); // Shown from the file picked while the service copies it. for (const auto& child : m_users->children()) { - auto* card = static_cast(child.get()); - if (card->account() == account) - card->showPicture(paths.front()); + auto* row = static_cast(child.get()); + if (row->account() == account) + row->showPicture(paths.front()); } }; m_chooser->open(); }; - card->onChangePassword = [this, account] { - // The user's own goes through PAM, which asks for the current - // password and updates the login keyring with it. - const bool own = account->userId() == int64_t(getuid()); - m_changePassword = std::make_unique( - settingsWindow(), account->userName(), own ? std::make_unique() : nullptr); - m_changePassword->onPasswordSubmitted = [account](const std::string& password) { - account->setPassword(cryptPassword(password)); - }; - m_changePassword->onClosed = [this] { dispose(m_changePassword); }; - m_changePassword->show(); + row->onInfo = [this, account] { + m_details = std::make_unique(settingsWindow(), account, int64_t(getuid())); + m_details->onChangePassword = [this, account] { changePassword(account); }; + m_details->onDelete = [this, account] { deleteUser(account); }; + m_details->onClosed = [this] { dispose(m_details); }; + m_details->show(); }; - card->onDelete = [this, account] { - const int64_t uid = account->userId(); - if (uid == int64_t(getuid())) - return; - m_confirm = std::make_unique( - &settingsWindow().window(), "Delete this user", - "Delete user “" + account->userName() - + "”? Their home directory and its contents will be permanently deleted. This " - "cannot be undone."); - m_confirm->setCancelText("Cancel"); - m_confirm->setConfirmText("Delete"); - m_confirm->setDestructive(true); - m_confirm->onAccepted = [this, uid] { m_manager.deleteUser(uid, true); }; - m_confirm->show(); + return row; + } + + void changePassword(const accounts::AccountsManager::AccountPtr& account) + { + // The user's own goes through PAM, which asks for the current + // password and updates the login keyring with it. + const bool own = account->userId() == int64_t(getuid()); + m_changePassword = std::make_unique( + settingsWindow(), account->userName(), own ? std::make_unique() : nullptr); + m_changePassword->onPasswordSubmitted = [account](const std::string& password) { + account->setPassword(cryptPassword(password)); }; - return card; + m_changePassword->onClosed = [this] { dispose(m_changePassword); }; + m_changePassword->show(); + } + + void deleteUser(const accounts::AccountsManager::AccountPtr& account) + { + const int64_t uid = account->userId(); + if (uid == int64_t(getuid())) + return; + m_confirm = std::make_unique( + &settingsWindow().window(), "Delete this user", + "Delete user “" + account->userName() + + "”? Their home directory and its contents will be permanently deleted. This " + "cannot be undone."); + m_confirm->setCancelText("Cancel"); + m_confirm->setConfirmText("Delete"); + m_confirm->setDestructive(true); + m_confirm->onAccepted = [this, uid] { m_manager.deleteUser(uid, true); }; + m_confirm->show(); } accounts::AccountsManager m_manager; base::Subscription m_accountsChanged; - ColumnLayout* m_users; + Card* m_users; + std::unique_ptr m_details; std::unique_ptr m_addUser; std::unique_ptr m_changePassword; std::unique_ptr m_confirm; diff --git a/src/pages/accounts/usercard.cpp b/src/pages/accounts/usercard.cpp deleted file mode 100644 index e35febd..0000000 --- a/src/pages/accounts/usercard.cpp +++ /dev/null @@ -1,477 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Reion Wong -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "pages/accounts/usercard.h" - -#include -#include -#include - -#include -#include - -#include "base/transform.h" -#include "ui/animation.h" -#include "ui/color_provider.h" -#include "ui/icon.h" -#include "ui/image_view.h" -#include "ui/label.h" -#include "ui/layout.h" -#include "ui/cell_layout.h" -#include "ui/painter.h" -#include "ui/path.h" -#include "ui/theme.h" -#include "ui/window.h" -#include "widgets/colors.h" -#include "widgets/metrics.h" -#include "widgets/standardbutton.h" - -using namespace cutefish; -using namespace cutefish::ui; - -namespace settings { - -namespace { - -// FishUI's RoundImageButton with a glyph: a round plate on hover and press, -// kept on the glyph's pixel parity so both share a centre. -class DisclosureButton : public View -{ -public: - DisclosureButton(double size, double iconMargins) - : m_size(size) - , m_iconSize(std::round(size - iconMargins * 2)) - { - m_icon = addChild(std::string(), m_iconSize); - } - - void setIconName(std::string name) { m_icon->setIconName(std::move(name)); } - const std::string& iconName() const { return m_icon->iconName(); } - double iconSize() const { return m_iconSize; } - - SizeF sizeHint() const override { return {m_size, m_size}; } - bool acceptsPointer() const override { return true; } - - std::function onClicked; - -protected: - void paint(Painter& painter) override - { - const Rect box = painter.deviceRect(localRect()); - const int icon = int(std::lround(m_iconSize * painter.scale())); - if (isEnabled() && (m_hovered || m_pressed)) { - const int margin = int(std::lround(std::round(m_size * 0.1) * painter.scale())); - int plate = std::min(box.width, box.height) - 2 * margin; - if ((plate - icon) % 2 != 0) - plate -= 1; - painter.fillDeviceRoundedRect({box.x + (box.width - plate) / 2, box.y + (box.height - plate) / 2, plate, - plate}, - plate / 2.0, - color(m_pressed ? ColorId::RoundButtonPressed : ColorId::RoundButtonHovered)); - } - } - - void layout() override - { - m_icon->setGeometry({std::round((m_size - m_iconSize) / 2), std::round((m_size - m_iconSize) / 2), m_iconSize, - m_iconSize}); - } - - void pointerEnter() override - { - m_hovered = true; - requestPaint(); - } - void pointerLeave() override - { - m_hovered = false; - requestPaint(); - } - void pointerPress(const PointerEvent& event) override - { - if (event.button != MouseButton::Left) - return; - m_pressed = true; - requestPaint(); - } - void pointerRelease(const PointerEvent& event) override - { - const bool clicked = m_pressed && localRect().contains(event.position); - m_pressed = false; - requestPaint(); - if (clicked && onClicked) - onClicked(); - } - -private: - double m_size; - double m_iconSize; - Icon* m_icon; - bool m_hovered = false; - bool m_pressed = false; -}; - -// The account's picture in a circle, or its initial, as on the lock screen, -// for an account with no picture: the icon theme has no avatar that size. -// Hovering dims it under a pencil, as the lock screen's avatar shows its -// swap arrows, since a click changes the picture. -class UserAvatar : public View -{ -public: - static constexpr double kSize = 48; - - UserAvatar() - { - m_initial = addChild