feat(settings): add session panel settings to the Panels tab

This commit is contained in:
Ly-sec
2026-05-10 15:34:14 +02:00
parent 32c6be928a
commit 614208e034
16 changed files with 1314 additions and 93 deletions
+38 -3
View File
@@ -6,7 +6,8 @@
},
"actions": {
"cancel": "Cancel",
"apply": "Apply"
"apply": "Apply",
"remove": "Remove"
}
},
"clipboard": {
@@ -404,7 +405,8 @@
"lock": "Lock",
"logout": "Log Out",
"reboot": "Reboot",
"shutdown": "Shut Down"
"shutdown": "Shut Down",
"custom": "Custom"
},
"errors": {
"logout-title": "Logout unavailable",
@@ -587,6 +589,32 @@
"add-entry-placeholder": "Add entry…"
}
},
"session-actions": {
"entry-heading": "Menu item {n}",
"icon-label": "Icon",
"kind-section-label": "Behavior",
"glyph-picker-title": "Session menu icon",
"clear-glyph": "Clear",
"enabled": "Shown",
"show-in-menu": "Show",
"command-label": "Shell command",
"command-placeholder": "Optional bash command, leave empty for built-in behavior",
"label-field": "Label",
"label-placeholder": "Optional button text",
"glyph-field": "Glyph",
"glyph-placeholder": "Optional icon id",
"destructive-label": "Warn style",
"move-up": "Up",
"move-down": "Down",
"add": "Add action",
"kind": {
"lock": "Lock",
"logout": "Log out",
"reboot": "Reboot",
"shutdown": "Shut down",
"command": "Custom command"
}
},
"dialogs": {
"color-picker": {
"title": "Pick a Color"
@@ -764,7 +792,8 @@
"weather": "Weather",
"wallpaper": "Wallpaper",
"widget-list": "Widget List",
"widgets": "Widgets"
"widgets": "Widgets",
"session-panel": "Session Menu"
}
},
"options": {
@@ -1388,6 +1417,12 @@
"attach-wallpaper": {
"label": "Attach to Bar",
"description": "Attach the Wallpaper panel to the bar when a suitable bar is available"
},
"session-actions": {
"label": "Entries",
"description": "Power menu entries, order, custom commands, and icons.",
"configure": "Configure…",
"entry-settings": "Details…"
}
},
"backdrop": {
+1
View File
@@ -480,6 +480,7 @@ _noctalia_sources = files(
'src/shell/settings/widget_settings_registry.cpp',
'src/shell/settings/widget_add_popup.cpp',
'src/shell/settings/search_picker_popup.cpp',
'src/shell/settings/session_actions_editor_popup.cpp',
'src/shell/settings/settings_window.cpp',
'src/shell/test/test_panel.cpp',
'src/shell/tray/tray_drawer_panel.cpp',
+27 -1
View File
@@ -298,7 +298,8 @@ namespace {
a.panel.attachLauncher == b.panel.attachLauncher && a.panel.attachClipboard == b.panel.attachClipboard &&
a.panel.attachControlCenter == b.panel.attachControlCenter &&
a.panel.attachWallpaper == b.panel.attachWallpaper && a.screenCorners.enabled == b.screenCorners.enabled &&
a.screenCorners.size == b.screenCorners.size && a.mpris.blacklist == b.mpris.blacklist;
a.screenCorners.size == b.screenCorners.size && a.mpris.blacklist == b.mpris.blacklist &&
a.session.actions == b.session.actions;
}
bool notificationConfigEqual(const NotificationConfig& a, const NotificationConfig& b) {
@@ -385,6 +386,30 @@ namespace {
array.push_back(std::move(shortcut));
}
table.insert_or_assign(key, std::move(array));
} else if constexpr (std::is_same_v<T, std::vector<SessionPanelActionConfig>>) {
toml::array array;
for (const auto& item : concrete) {
if (item.action.empty()) {
continue;
}
toml::table row;
row.insert_or_assign("action", item.action);
row.insert_or_assign("enabled", item.enabled);
if (item.command.has_value() && !item.command->empty()) {
row.insert_or_assign("command", *item.command);
}
if (item.label.has_value() && !item.label->empty()) {
row.insert_or_assign("label", *item.label);
}
if (item.glyph.has_value() && !item.glyph->empty()) {
row.insert_or_assign("glyph", *item.glyph);
}
if (item.destructive) {
row.insert_or_assign("destructive", true);
}
array.push_back(std::move(row));
}
table.insert_or_assign(key, std::move(array));
} else {
table.insert_or_assign(key, concrete);
}
@@ -613,6 +638,7 @@ std::optional<Config> ConfigService::configForOverrides(const toml::table& overr
});
parsed.bars.push_back(BarConfig{});
parsed.controlCenter.shortcuts = defaultControlCenterShortcuts();
parsed.shell.session.actions = defaultSessionPanelActions();
return parsed;
}
+54
View File
@@ -885,6 +885,7 @@ void ConfigService::loadAll() {
});
m_config.bars.push_back(BarConfig{});
m_config.controlCenter.shortcuts = defaultControlCenterShortcuts();
m_config.shell.session.actions = defaultSessionPanelActions();
setConfigParseError(m_overridesParseError);
return;
}
@@ -1273,6 +1274,59 @@ void ConfigService::parseTableInto(const toml::table& tbl, Config& config, bool
if (auto v = (*shellTbl)["clipboard_image_action_command"].value<std::string>()) {
shell.clipboardImageActionCommand = *v;
}
bool sessionActionsKeyPresent = false;
if (const auto* sessionTbl = (*shellTbl)["session"].as_table()) {
if (sessionTbl->contains("actions")) {
sessionActionsKeyPresent = true;
shell.session.actions.clear();
if (const auto* actionsArr = (*sessionTbl)["actions"].as_array()) {
for (const auto& entry : *actionsArr) {
auto* entryTbl = entry.as_table();
if (entryTbl == nullptr) {
continue;
}
SessionPanelActionConfig row{};
if (auto v = (*entryTbl)["action"].value<std::string>()) {
row.action = StringUtils::toLower(StringUtils::trim(*v));
}
if (row.action.empty()) {
continue;
}
if (auto v = (*entryTbl)["enabled"].value<bool>()) {
row.enabled = *v;
}
if (const auto* cmdNode = entryTbl->get("command")) {
if (auto s = cmdNode->value<std::string>()) {
row.command = StringUtils::trim(*s);
if (row.command->empty()) {
row.command = std::nullopt;
}
}
}
if (auto v = (*entryTbl)["label"].value<std::string>()) {
row.label = StringUtils::trim(*v);
if (row.label->empty()) {
row.label = std::nullopt;
}
}
if (auto v = (*entryTbl)["glyph"].value<std::string>()) {
row.glyph = StringUtils::trim(*v);
if (row.glyph->empty()) {
row.glyph = std::nullopt;
}
}
if (auto v = (*entryTbl)["destructive"].value<bool>()) {
row.destructive = *v;
}
shell.session.actions.push_back(std::move(row));
}
}
}
}
if (!sessionActionsKeyPresent && shell.session.actions.empty()) {
shell.session.actions = defaultSessionPanelActions();
}
}
// Parse [theme]
+9
View File
@@ -38,6 +38,15 @@ std::vector<ShortcutConfig> defaultControlCenterShortcuts() {
};
}
std::vector<SessionPanelActionConfig> defaultSessionPanelActions() {
return {
SessionPanelActionConfig{"lock", true, std::nullopt, std::nullopt, std::nullopt, false},
SessionPanelActionConfig{"logout", true, std::nullopt, std::nullopt, std::nullopt, false},
SessionPanelActionConfig{"reboot", true, std::nullopt, std::nullopt, std::nullopt, false},
SessionPanelActionConfig{"shutdown", true, std::nullopt, std::nullopt, std::nullopt, true},
};
}
std::string WidgetConfig::getString(const std::string& key, const std::string& fallback) const {
auto it = settings.find(key);
if (it == settings.end()) {
+23 -2
View File
@@ -103,11 +103,31 @@ struct ShortcutConfig {
bool operator==(const ShortcutConfig&) const = default;
};
struct SessionPanelActionConfig {
// "lock" | "logout" | "reboot" | "shutdown" | "command"
std::string action;
bool enabled = true;
// When set, runs via `process::runAsync` (shell string) instead of the built-in handler.
std::optional<std::string> command;
std::optional<std::string> label;
std::optional<std::string> glyph;
bool destructive = false;
bool operator==(const SessionPanelActionConfig&) const = default;
};
struct ShellSessionConfig {
std::vector<SessionPanelActionConfig> actions;
bool operator==(const ShellSessionConfig&) const = default;
};
[[nodiscard]] std::vector<ShortcutConfig> defaultControlCenterShortcuts();
[[nodiscard]] std::vector<SessionPanelActionConfig> defaultSessionPanelActions();
using WidgetSettingValue = std::variant<bool, std::int64_t, double, std::string, std::vector<std::string>>;
using ConfigOverrideValue =
std::variant<bool, std::int64_t, double, std::string, std::vector<std::string>, std::vector<ShortcutConfig>>;
using ConfigOverrideValue = std::variant<bool, std::int64_t, double, std::string, std::vector<std::string>,
std::vector<ShortcutConfig>, std::vector<SessionPanelActionConfig>>;
// Optional rounded “capsule” behind a bar widget (see `[widget.*] capsule_*` in CONFIG.md).
// Corner shape (pill), border width, and edge softness are fixed in the shell code; padding is configurable.
@@ -387,6 +407,7 @@ struct ShellConfig {
PanelConfig panel;
ScreenCornersConfig screenCorners;
MprisConfig mpris;
ShellSessionConfig session;
};
struct WeatherConfig {
+242 -65
View File
@@ -12,7 +12,9 @@
#include "shell/panel/panel_manager.h"
#include "ui/controls/button.h"
#include "ui/controls/flex.h"
#include "ui/controls/grid_view.h"
#include "ui/style.h"
#include "util/string_utils.h"
#include <algorithm>
#include <cstdlib>
@@ -27,20 +29,6 @@ namespace {
constexpr Logger kLog("session");
struct ActionSpec {
SessionPanel::ActionId id;
const char* labelKey;
const char* glyph;
ButtonVariant variant;
};
constexpr std::array<ActionSpec, 4> kActionSpecs{{
{SessionPanel::ActionId::Lock, "session.actions.lock", "lock", ButtonVariant::Default},
{SessionPanel::ActionId::Logout, "session.actions.logout", "logout", ButtonVariant::Default},
{SessionPanel::ActionId::Reboot, "session.actions.reboot", "reboot", ButtonVariant::Default},
{SessionPanel::ActionId::Shutdown, "session.actions.shutdown", "shutdown", ButtonVariant::Destructive},
}};
[[nodiscard]] const char* valueOrUnset(const char* value) {
return value != nullptr && value[0] != '\0' ? value : "<unset>";
}
@@ -130,16 +118,157 @@ namespace {
}).detach();
}
void runShellCommand(std::function<bool()> hook, std::string command, std::string_view actionName) {
std::thread([hook = std::move(hook), command = std::move(command), actionName = std::string(actionName)]() mutable {
if (hook && !hook()) {
kLog.warn("{} cancelled because a configured hook failed", actionName);
return;
}
if (!process::runAsync(command)) {
kLog.warn("{}: command failed", actionName);
}
}).detach();
}
[[nodiscard]] bool isKnownAction(std::string_view action) {
return action == "lock" || action == "logout" || action == "reboot" || action == "shutdown" || action == "command";
}
[[nodiscard]] const char* labelKeyForAction(std::string_view action) {
if (action == "lock") {
return "session.actions.lock";
}
if (action == "logout") {
return "session.actions.logout";
}
if (action == "reboot") {
return "session.actions.reboot";
}
if (action == "shutdown") {
return "session.actions.shutdown";
}
return "session.actions.custom";
}
[[nodiscard]] const char* defaultGlyphForAction(std::string_view action) {
if (action == "lock") {
return "lock";
}
if (action == "logout") {
return "logout";
}
if (action == "reboot") {
return "reboot";
}
if (action == "shutdown") {
return "shutdown";
}
return "terminal";
}
[[nodiscard]] ButtonVariant variantFor(const SessionPanelActionConfig& cfg) {
if (cfg.destructive) {
return ButtonVariant::Destructive;
}
if (cfg.action == "shutdown" && !cfg.command.has_value()) {
return ButtonVariant::Destructive;
}
return ButtonVariant::Default;
}
} // namespace
std::vector<SessionPanelActionConfig> SessionPanel::effectiveActions() const {
std::vector<SessionPanelActionConfig> src;
if (m_config != nullptr) {
src = m_config->config().shell.session.actions;
}
if (src.empty()) {
src = defaultSessionPanelActions();
}
std::vector<SessionPanelActionConfig> out;
out.reserve(src.size());
for (const auto& row : src) {
if (!row.enabled) {
continue;
}
if (!isKnownAction(row.action)) {
kLog.warn("session panel: skipping unknown action \"{}\"", row.action);
continue;
}
if (row.action == "command" && (!row.command.has_value() || StringUtils::trim(*row.command).empty())) {
kLog.warn("session panel: skipping \"command\" entry with no command");
continue;
}
out.push_back(row);
}
return out;
}
std::function<bool()> SessionPanel::hookFor(const std::string& action) const {
if (action == "logout") {
return m_actionHooks.onLogout;
}
if (action == "reboot") {
return m_actionHooks.onReboot;
}
if (action == "shutdown") {
return m_actionHooks.onShutdown;
}
return {};
}
float SessionPanel::preferredWidth() const {
const std::size_t n = visibleColumnCount();
const float gap = Style::spaceSm;
const float w = kButtonMinWidth * static_cast<float>(n) + gap * static_cast<float>(n > 1 ? n - 1 : 0) +
Style::panelPadding * 2.0f;
return scaled(std::max(kPanelMinWidth, w));
}
float SessionPanel::preferredHeight() const {
const std::size_t rows = visibleRowCount();
const float gap = Style::spaceSm;
const float h = kActionButtonMinHeight * static_cast<float>(rows) +
gap * static_cast<float>(rows > 1 ? rows - 1 : 0) + Style::panelPadding * 2.0f;
return std::ceil(scaled(h));
}
std::size_t SessionPanel::entryCountForLayout() const {
if (!m_visibleEntries.empty()) {
return m_visibleEntries.size();
}
return effectiveActions().size();
}
std::size_t SessionPanel::visibleColumnCount() const {
const std::size_t n = std::max<std::size_t>(1, entryCountForLayout());
if (n <= kMaxColumns) {
return n;
}
return std::min<std::size_t>(kMaxColumns, (n + 1) / 2);
}
std::size_t SessionPanel::visibleRowCount() const {
const std::size_t n = std::max<std::size_t>(1, entryCountForLayout());
const std::size_t columns = visibleColumnCount();
return (n + columns - 1) / columns;
}
void SessionPanel::create() {
const float scale = contentScale();
m_visibleEntries = effectiveActions();
const std::size_t columns = visibleColumnCount();
auto rootLayout = std::make_unique<Flex>();
rootLayout->setDirection(FlexDirection::Horizontal);
rootLayout->setAlign(FlexAlign::Stretch);
rootLayout->setGap(Style::spaceSm * scale);
rootLayout->setJustify(FlexJustify::Start);
auto rootLayout = std::make_unique<GridView>();
rootLayout->setColumns(columns);
rootLayout->setColumnGap(Style::spaceSm * scale);
rootLayout->setRowGap(Style::spaceSm * scale);
rootLayout->setStretchItems(true);
rootLayout->setUniformCellSize(true);
rootLayout->setMinCellWidth(kButtonMinWidth * scale);
rootLayout->setMinCellHeight(kActionButtonMinHeight * scale);
m_rootLayout = rootLayout.get();
auto focusArea = std::make_unique<InputArea>();
@@ -152,13 +281,15 @@ void SessionPanel::create() {
});
m_focusArea = static_cast<InputArea*>(rootLayout->addChild(std::move(focusArea)));
for (const auto& spec : kActionSpecs) {
const std::size_t visualIndex = rootLayout->children().size() - (m_focusArea != nullptr ? 1U : 0U);
if (auto* button = createActionButton(spec.id, scale); button != nullptr) {
m_actionOrder[visualIndex] = spec.id;
rootLayout->addChild(std::unique_ptr<Button>(button));
m_visibleButtons.clear();
m_visibleButtons.reserve(m_visibleEntries.size());
for (const auto& cfg : m_visibleEntries) {
if (Button* b = createActionButton(cfg, scale); b != nullptr) {
m_visibleButtons.push_back(b);
rootLayout->addChild(std::unique_ptr<Button>(b));
}
}
setRoot(std::move(rootLayout));
if (m_animations != nullptr) {
@@ -168,17 +299,13 @@ void SessionPanel::create() {
updateSelectionVisuals();
}
Button* SessionPanel::createActionButton(ActionId id, float scale) {
const auto* spec = std::find_if(kActionSpecs.begin(), kActionSpecs.end(),
[id](const ActionSpec& candidate) { return candidate.id == id; });
if (spec == kActionSpecs.end()) {
return nullptr;
}
Button* SessionPanel::createActionButton(const SessionPanelActionConfig& cfg, float scale) {
auto button = std::make_unique<Button>();
button->setText(i18n::tr(spec->labelKey));
button->setGlyph(spec->glyph);
button->setVariant(spec->variant);
const std::string labelText =
cfg.label.has_value() && !cfg.label->empty() ? *cfg.label : i18n::tr(labelKeyForAction(cfg.action));
button->setText(labelText);
button->setGlyph(cfg.glyph.has_value() && !cfg.glyph->empty() ? *cfg.glyph : defaultGlyphForAction(cfg.action));
button->setVariant(variantFor(cfg));
button->setDirection(FlexDirection::Vertical);
button->setAlign(FlexAlign::Center);
button->setJustify(FlexJustify::Center);
@@ -188,19 +315,18 @@ Button* SessionPanel::createActionButton(ActionId id, float scale) {
button->setGlyphSize(28.0f * scale);
button->setPadding(Style::spaceMd * scale, Style::spaceLg * scale);
button->setRadius(Style::radiusLg * scale);
button->setMinWidth(152.0f * scale);
button->setMinWidth(kButtonMinWidth * scale);
button->setMinHeight(kActionButtonMinHeight * scale);
button->setFlexGrow(1.0f);
button->setOnClick([this, id]() {
SessionPanelActionConfig cfgCopy = cfg;
button->setOnClick([this, cfgCopy]() {
PanelManager::instance().close();
invokeAction(id);
invokeEntry(cfgCopy);
});
button->setOnMotion([this]() { activateMouse(); });
button->setHoverSuppressed(!m_mouseActive);
auto* raw = button.get();
m_actionButtons[static_cast<std::size_t>(id)] = raw;
return button.release();
}
@@ -217,7 +343,7 @@ void SessionPanel::activateMouse() {
return;
}
m_mouseActive = true;
for (Button* button : m_actionButtons) {
for (Button* button : m_visibleButtons) {
if (button != nullptr) {
button->setHoverSuppressed(false);
}
@@ -226,41 +352,63 @@ void SessionPanel::activateMouse() {
}
void SessionPanel::activateSelected() {
if (!m_selectedIndex.has_value() || *m_selectedIndex >= static_cast<std::size_t>(ActionId::Count)) {
if (!m_selectedIndex.has_value() || m_visibleButtons.empty()) {
return;
}
const ActionId selectedAction = m_actionOrder[*m_selectedIndex];
if (Button* button = m_actionButtons[static_cast<std::size_t>(selectedAction)];
button != nullptr && button->enabled()) {
const std::size_t i = *m_selectedIndex;
if (i >= m_visibleButtons.size() || i >= m_visibleEntries.size()) {
return;
}
Button* button = m_visibleButtons[i];
if (button != nullptr && button->enabled()) {
PanelManager::instance().close();
invokeAction(selectedAction);
invokeEntry(m_visibleEntries[i]);
}
}
void SessionPanel::invokeAction(ActionId id) {
switch (id) {
case ActionId::Logout:
void SessionPanel::invokeEntry(const SessionPanelActionConfig& cfg) {
if (cfg.command.has_value()) {
const std::string cmd = StringUtils::trim(*cfg.command);
if (!cmd.empty()) {
std::function<bool()> hook;
if (cfg.action == "logout" || cfg.action == "reboot" || cfg.action == "shutdown") {
hook = hookFor(cfg.action);
}
runShellCommand(std::move(hook), cmd, cfg.action);
return;
}
}
if (cfg.action == "command") {
kLog.warn("session panel: custom action missing command");
return;
}
if (cfg.action == "logout") {
runPowerAction(m_actionHooks.onLogout, doLogout, "logout");
break;
case ActionId::Reboot:
return;
}
if (cfg.action == "reboot") {
runPowerAction(m_actionHooks.onReboot, doReboot, "reboot");
break;
case ActionId::Shutdown:
return;
}
if (cfg.action == "shutdown") {
runPowerAction(m_actionHooks.onShutdown, doShutdown, "shutdown");
break;
case ActionId::Lock:
return;
}
if (cfg.action == "lock") {
if (!doLock()) {
notify::error("Noctalia", i18n::tr("session.errors.lock-title"), i18n::tr("session.errors.lock-body"));
}
break;
case ActionId::Count:
break;
return;
}
}
bool SessionPanel::handleKeyEvent(std::uint32_t sym, std::uint32_t modifiers) {
const std::size_t lastIndex = static_cast<std::size_t>(ActionId::Count) - 1;
if (m_visibleButtons.empty()) {
return false;
}
const std::size_t lastIndex = m_visibleButtons.size() - 1;
if (m_config != nullptr && m_config->matchesKeybind(KeybindAction::Left, sym, modifiers)) {
if (!m_selectedIndex.has_value()) {
@@ -300,6 +448,36 @@ bool SessionPanel::handleKeyEvent(std::uint32_t sym, std::uint32_t modifiers) {
return true;
}
if (m_config != nullptr && m_config->matchesKeybind(KeybindAction::Up, sym, modifiers)) {
const std::size_t columns = visibleColumnCount();
if (!m_selectedIndex.has_value()) {
m_selectedIndex = lastIndex;
} else if (*m_selectedIndex >= columns) {
*m_selectedIndex -= columns;
}
updateSelectionVisuals();
if (root() != nullptr) {
root()->markPaintDirty();
}
PanelManager::instance().refresh();
return true;
}
if (m_config != nullptr && m_config->matchesKeybind(KeybindAction::Down, sym, modifiers)) {
const std::size_t columns = visibleColumnCount();
if (!m_selectedIndex.has_value()) {
m_selectedIndex = 0;
} else if (*m_selectedIndex + columns <= lastIndex) {
*m_selectedIndex += columns;
}
updateSelectionVisuals();
if (root() != nullptr) {
root()->markPaintDirty();
}
PanelManager::instance().refresh();
return true;
}
if ((m_config != nullptr && m_config->matchesKeybind(KeybindAction::Validate, sym, modifiers)) ||
sym == XKB_KEY_space) {
activateSelected();
@@ -310,9 +488,8 @@ bool SessionPanel::handleKeyEvent(std::uint32_t sym, std::uint32_t modifiers) {
}
void SessionPanel::updateSelectionVisuals() {
for (std::size_t i = 0; i < m_actionOrder.size(); ++i) {
const ActionId actionId = m_actionOrder[i];
Button* button = m_actionButtons[static_cast<std::size_t>(actionId)];
for (std::size_t i = 0; i < m_visibleButtons.size(); ++i) {
Button* button = m_visibleButtons[i];
if (button == nullptr) {
continue;
}
@@ -328,7 +505,7 @@ void SessionPanel::doLayout(Renderer& renderer, float width, float height) {
m_rootLayout->setSize(width, height);
m_rootLayout->layout(renderer);
for (Button* button : m_actionButtons) {
for (Button* button : m_visibleButtons) {
if (button != nullptr) {
button->updateInputArea();
}
@@ -340,7 +517,7 @@ void SessionPanel::doUpdate(Renderer& /*renderer*/) {}
void SessionPanel::onClose() {
m_rootLayout = nullptr;
m_focusArea = nullptr;
m_actionOrder.fill(ActionId::Logout);
m_actionButtons.fill(nullptr);
m_visibleEntries.clear();
m_visibleButtons.clear();
clearReleasedRoot();
}
+19 -18
View File
@@ -1,15 +1,18 @@
#pragma once
#include "config/config_types.h"
#include "shell/panel/panel.h"
#include "ui/style.h"
#include <array>
#include <cmath>
#include <functional>
#include <optional>
#include <string>
#include <vector>
class Button;
class Flex;
class GridView;
class InputArea;
class Renderer;
class ConfigService;
@@ -22,14 +25,6 @@ struct SessionActionHooks {
class SessionPanel : public Panel {
public:
enum class ActionId : std::size_t {
Logout = 0,
Reboot = 1,
Shutdown = 2,
Lock = 3,
Count = 4,
};
explicit SessionPanel(ConfigService* config, SessionActionHooks actionHooks = {})
: m_config(config), m_actionHooks(std::move(actionHooks)) {}
@@ -37,10 +32,8 @@ public:
void onOpen(std::string_view context) override;
void onClose() override;
[[nodiscard]] float preferredWidth() const override { return scaled(680.0f); }
[[nodiscard]] float preferredHeight() const override {
return std::ceil(scaled(kActionButtonMinHeight + Style::panelPadding * 2.0f));
}
[[nodiscard]] float preferredWidth() const override;
[[nodiscard]] float preferredHeight() const override;
[[nodiscard]] bool centeredHorizontally() const override { return true; }
[[nodiscard]] bool centeredVertically() const override { return true; }
[[nodiscard]] bool hasDecoration() const override { return true; }
@@ -50,6 +43,9 @@ public:
private:
static constexpr float kActionButtonMinHeight = 112.0f;
static constexpr float kButtonMinWidth = 152.0f;
static constexpr float kPanelMinWidth = 680.0f;
static constexpr std::size_t kMaxColumns = 5;
void doLayout(Renderer& renderer, float width, float height) override;
void doUpdate(Renderer& renderer) override;
@@ -57,13 +53,18 @@ private:
bool handleKeyEvent(std::uint32_t sym, std::uint32_t modifiers);
void updateSelectionVisuals();
void activateMouse();
void invokeAction(ActionId id);
[[nodiscard]] Button* createActionButton(ActionId id, float scale);
void invokeEntry(const SessionPanelActionConfig& cfg);
[[nodiscard]] std::vector<SessionPanelActionConfig> effectiveActions() const;
[[nodiscard]] std::function<bool()> hookFor(const std::string& action) const;
[[nodiscard]] Button* createActionButton(const SessionPanelActionConfig& cfg, float scale);
[[nodiscard]] std::size_t entryCountForLayout() const;
[[nodiscard]] std::size_t visibleColumnCount() const;
[[nodiscard]] std::size_t visibleRowCount() const;
Flex* m_rootLayout = nullptr;
GridView* m_rootLayout = nullptr;
InputArea* m_focusArea = nullptr;
std::array<ActionId, static_cast<std::size_t>(ActionId::Count)> m_actionOrder{};
std::array<Button*, static_cast<std::size_t>(ActionId::Count)> m_actionButtons{};
std::vector<SessionPanelActionConfig> m_visibleEntries;
std::vector<Button*> m_visibleButtons;
std::optional<std::size_t> m_selectedIndex;
bool m_mouseActive = false;
ConfigService* m_config = nullptr;
@@ -0,0 +1,192 @@
#include "shell/settings/session_actions_editor_popup.h"
#include "config/config_service.h"
#include "core/deferred_call.h"
#include "i18n/i18n.h"
#include "render/render_context.h"
#include "render/scene/node.h"
#include "ui/controls/button.h"
#include "ui/controls/flex.h"
#include "ui/controls/label.h"
#include "ui/palette.h"
#include "ui/style.h"
#include "wayland/popup_surface.h"
#include "wayland/wayland_connection.h"
#include "xdg-shell-client-protocol.h"
#include <algorithm>
#include <cstdint>
namespace settings {
namespace {
PopupSurfaceConfig centeredPopupConfig(std::uint32_t parentWidth, std::uint32_t parentHeight, std::uint32_t width,
std::uint32_t height, std::uint32_t serial) {
return PopupSurfaceConfig{
.anchorX = static_cast<std::int32_t>(parentWidth / 2),
.anchorY = static_cast<std::int32_t>(parentHeight / 2),
.anchorWidth = 1,
.anchorHeight = 1,
.width = std::max<std::uint32_t>(1, width),
.height = std::max<std::uint32_t>(1, height),
.anchor = XDG_POSITIONER_ANCHOR_NONE,
.gravity = XDG_POSITIONER_GRAVITY_NONE,
.constraintAdjustment =
XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y,
.offsetX = 0,
.offsetY = 0,
.serial = serial,
.grab = true,
};
}
} // namespace
SessionActionsEditorPopup::~SessionActionsEditorPopup() { destroyPopup(); }
void SessionActionsEditorPopup::initialize(WaylandConnection& wayland, ConfigService& config,
RenderContext& renderContext) {
initializeBase(wayland, config, renderContext);
}
void SessionActionsEditorPopup::open(xdg_surface* parentXdgSurface, wl_output* output, std::uint32_t serial,
wl_surface* parentWlSurface, std::uint32_t parentWidth,
std::uint32_t parentHeight, float scale, std::string sheetTitle,
std::function<void()> removeAction,
std::function<void(Flex& sheetBody)> populateSheetBody) {
if (parentXdgSurface == nullptr || parentWlSurface == nullptr) {
return;
}
if (isOpen()) {
close();
}
m_scale = std::max(0.1f, scale);
m_sheetTitle = std::move(sheetTitle);
m_removeAction = std::move(removeAction);
m_populateSheetBody = std::move(populateSheetBody);
m_root = nullptr;
const float panelWidth = 640.0f * m_scale;
const float panelHeight = 420.0f * m_scale;
const auto cfg =
centeredPopupConfig(parentWidth, parentHeight, static_cast<std::uint32_t>(std::max(1.0f, panelWidth)),
static_cast<std::uint32_t>(std::max(1.0f, panelHeight)), serial);
if (!openPopupAsChild(cfg, parentXdgSurface, parentWlSurface, output)) {
close();
}
}
void SessionActionsEditorPopup::close() { destroyPopup(); }
bool SessionActionsEditorPopup::isOpen() const noexcept { return DialogPopupHost::isOpen(); }
bool SessionActionsEditorPopup::onPointerEvent(const PointerEvent& event) {
return DialogPopupHost::onPointerEvent(event);
}
void SessionActionsEditorPopup::onKeyboardEvent(const KeyboardEvent& event) {
DialogPopupHost::onKeyboardEvent(event);
}
wl_surface* SessionActionsEditorPopup::wlSurface() const noexcept { return DialogPopupHost::wlSurface(); }
void SessionActionsEditorPopup::populateContent(Node* contentParent, std::uint32_t /*width*/,
std::uint32_t /*height*/) {
const float panelPadding = Style::spaceSm * m_scale;
const float panelGap = Style::spaceSm * m_scale;
auto root = std::make_unique<Flex>();
root->setDirection(FlexDirection::Vertical);
root->setAlign(FlexAlign::Stretch);
root->setGap(panelGap);
root->setPadding(panelPadding);
m_root = root.get();
auto header = std::make_unique<Flex>();
header->setDirection(FlexDirection::Horizontal);
header->setAlign(FlexAlign::Center);
header->setGap(Style::spaceSm * m_scale);
auto titleLabel = std::make_unique<Label>();
titleLabel->setText(m_sheetTitle);
titleLabel->setFontSize(Style::fontSizeBody * m_scale);
titleLabel->setColor(colorSpecFromRole(ColorRole::OnSurface));
titleLabel->setBold(true);
header->addChild(std::move(titleLabel));
auto spacer = std::make_unique<Flex>();
spacer->setFlexGrow(1.0f);
header->addChild(std::move(spacer));
if (m_removeAction) {
auto removeBtn = std::make_unique<Button>();
removeBtn->setGlyph("trash");
removeBtn->setVariant(ButtonVariant::Destructive);
removeBtn->setGlyphSize(Style::fontSizeBody * m_scale);
removeBtn->setMinWidth(Style::controlHeightSm * m_scale);
removeBtn->setMinHeight(Style::controlHeightSm * m_scale);
removeBtn->setPadding(Style::spaceXs * m_scale);
removeBtn->setRadius(Style::radiusMd * m_scale);
removeBtn->setOnClick([removeAction = m_removeAction]() {
if (removeAction) {
DeferredCall::callLater(removeAction);
}
});
header->addChild(std::move(removeBtn));
}
auto closeBtn = std::make_unique<Button>();
closeBtn->setGlyph("close");
closeBtn->setVariant(ButtonVariant::Default);
closeBtn->setGlyphSize(Style::fontSizeBody * m_scale);
closeBtn->setMinWidth(Style::controlHeightSm * m_scale);
closeBtn->setMinHeight(Style::controlHeightSm * m_scale);
closeBtn->setPadding(Style::spaceXs * m_scale);
closeBtn->setRadius(Style::radiusMd * m_scale);
closeBtn->setOnClick([this]() { DeferredCall::callLater([this]() { close(); }); });
header->addChild(std::move(closeBtn));
root->addChild(std::move(header));
auto body = std::make_unique<Flex>();
body->setDirection(FlexDirection::Vertical);
body->setAlign(FlexAlign::Stretch);
body->setGap(Style::spaceMd * m_scale);
if (m_populateSheetBody) {
m_populateSheetBody(*body);
}
root->addChild(std::move(body));
contentParent->addChild(std::move(root));
}
void SessionActionsEditorPopup::layoutSheet(float contentWidth, float contentHeight) {
if (m_root == nullptr || renderContext() == nullptr || m_surface == nullptr) {
return;
}
Renderer& renderer = *renderContext();
const LayoutSize pref = m_root->measure(renderer, LayoutConstraints::available(contentWidth, 1.0e6f));
const float sheetH = std::max(1.0f, std::min(pref.height, contentHeight));
m_root->setSize(contentWidth, sheetH);
m_root->layout(renderer);
}
void SessionActionsEditorPopup::cancelToFacade() {}
InputArea* SessionActionsEditorPopup::initialFocusArea() { return nullptr; }
void SessionActionsEditorPopup::onSheetClose() {
m_sheetTitle.clear();
m_removeAction = nullptr;
m_populateSheetBody = nullptr;
m_root = nullptr;
}
} // namespace settings
@@ -0,0 +1,53 @@
#pragma once
#include "shell/settings/settings_content.h"
#include "ui/dialogs/dialog_popup_host.h"
#include <functional>
#include <string>
class Flex;
class RenderContext;
class WaylandConnection;
struct KeyboardEvent;
struct PointerEvent;
struct wl_output;
struct wl_surface;
struct xdg_surface;
namespace settings {
class SessionActionsEditorPopup final : public DialogPopupHost {
public:
SessionActionsEditorPopup() = default;
~SessionActionsEditorPopup();
void initialize(WaylandConnection& wayland, ConfigService& config, RenderContext& renderContext);
void open(xdg_surface* parentXdgSurface, wl_output* output, std::uint32_t serial, wl_surface* parentWlSurface,
std::uint32_t parentWidth, std::uint32_t parentHeight, float scale, std::string sheetTitle,
std::function<void()> removeAction, std::function<void(Flex& sheetBody)> populateSheetBody);
void close();
[[nodiscard]] bool isOpen() const noexcept;
[[nodiscard]] bool onPointerEvent(const PointerEvent& event);
void onKeyboardEvent(const KeyboardEvent& event);
[[nodiscard]] wl_surface* wlSurface() const noexcept;
protected:
void populateContent(Node* contentParent, std::uint32_t width, std::uint32_t height) override;
void layoutSheet(float contentWidth, float contentHeight) override;
void cancelToFacade() override;
[[nodiscard]] InputArea* initialFocusArea() override;
void onSheetClose() override;
private:
float m_scale = 1.0f;
std::string m_sheetTitle;
std::function<void()> m_removeAction;
std::function<void(Flex&)> m_populateSheetBody;
Flex* m_root = nullptr;
};
} // namespace settings
+427 -1
View File
@@ -1,5 +1,6 @@
#include "shell/settings/settings_content.h"
#include "config/config_types.h"
#include "i18n/i18n.h"
#include "render/core/color.h"
#include "shell/settings/bar_widget_editor.h"
@@ -17,6 +18,7 @@
#include "ui/controls/slider.h"
#include "ui/controls/toggle.h"
#include "ui/dialogs/color_picker_dialog.h"
#include "ui/dialogs/glyph_picker_dialog.h"
#include "ui/palette.h"
#include "ui/style.h"
#include "util/string_utils.h"
@@ -79,6 +81,22 @@ namespace settings {
return labels;
}
const char* sessionActionDefaultGlyphName(std::string_view action) {
if (action == "lock") {
return "lock";
}
if (action == "logout") {
return "logout";
}
if (action == "reboot") {
return "reboot";
}
if (action == "shutdown") {
return "shutdown";
}
return "terminal";
}
bool isBlankInput(std::string_view text) { return StringUtils::trim(text).empty(); }
const std::string& localeDecimalSeparator() {
@@ -326,6 +344,237 @@ namespace settings {
return overrides;
}
std::string sessionActionRowSummary(const std::vector<SelectOption>& kindOptions,
const SessionPanelActionConfig& row) {
if (row.label.has_value() && !row.label->empty()) {
return *row.label;
}
return optionLabel(kindOptions, row.action);
}
void buildSessionActionEntryDetailContentImpl(Flex& section, SettingsContentContext& ctx,
SessionPanelActionConfig& row, const std::function<void()>& persist,
const std::function<void()>& closeHostedEditor) {
const float scale = ctx.scale;
const std::vector<SelectOption> kindOptions = {
{"lock", i18n::tr("settings.session-actions.kind.lock"), {}},
{"logout", i18n::tr("settings.session-actions.kind.logout"), {}},
{"reboot", i18n::tr("settings.session-actions.kind.reboot"), {}},
{"shutdown", i18n::tr("settings.session-actions.kind.shutdown"), {}},
{"command", i18n::tr("settings.session-actions.kind.command"), {}},
};
const float iconSq = Style::controlHeight * scale;
const float iconGlyphSize = Style::fontSizeBody * scale;
auto body = std::make_unique<Flex>();
body->setDirection(FlexDirection::Horizontal);
body->setAlign(FlexAlign::Start);
body->setGap(Style::spaceMd * scale);
body->setFillWidth(true);
auto iconCol = std::make_unique<Flex>();
iconCol->setDirection(FlexDirection::Vertical);
iconCol->setAlign(FlexAlign::Stretch);
iconCol->setGap(Style::spaceSm * scale);
iconCol->addChild(makeLabel(i18n::tr("settings.session-actions.icon-label"), Style::fontSizeCaption * scale,
colorSpecFromRole(ColorRole::OnSurfaceVariant), false));
auto glyphBtnRow = std::make_unique<Flex>();
glyphBtnRow->setDirection(FlexDirection::Horizontal);
glyphBtnRow->setAlign(FlexAlign::Center);
glyphBtnRow->setGap(Style::spaceXs * scale);
const std::string previewGlyph = [&] {
if (row.glyph.has_value() && !row.glyph->empty()) {
return *row.glyph;
}
return std::string(sessionActionDefaultGlyphName(row.action));
}();
auto glyphPickBtn = std::make_unique<Button>();
glyphPickBtn->setVariant(ButtonVariant::Outline);
glyphPickBtn->setText("");
glyphPickBtn->setGlyph(previewGlyph);
glyphPickBtn->setGlyphSize(iconGlyphSize);
glyphPickBtn->setMinWidth(iconSq);
glyphPickBtn->setMaxWidth(iconSq);
glyphPickBtn->setMinHeight(iconSq);
glyphPickBtn->setMaxHeight(iconSq);
glyphPickBtn->setPadding(0.0f, 0.0f);
glyphPickBtn->setRadius(Style::radiusMd * scale);
glyphPickBtn->setOnClick([&row, persist]() {
GlyphPickerDialogOptions options;
options.title = i18n::tr("settings.session-actions.glyph-picker-title");
if (row.glyph.has_value() && !row.glyph->empty()) {
options.initialGlyph = *row.glyph;
}
(void)GlyphPickerDialog::open(std::move(options), [&row, persist](std::optional<GlyphPickerResult> result) {
if (!result.has_value()) {
return;
}
row.glyph = result->name;
persist();
});
});
glyphBtnRow->addChild(std::move(glyphPickBtn));
if (row.glyph.has_value() && !row.glyph->empty()) {
auto clearG = std::make_unique<Button>();
clearG->setVariant(ButtonVariant::Ghost);
clearG->setText(i18n::tr("settings.session-actions.clear-glyph"));
clearG->setFontSize(Style::fontSizeCaption * scale);
clearG->setMinHeight(iconSq);
clearG->setPadding(Style::spaceXs * scale, Style::spaceSm * scale);
clearG->setRadius(Style::radiusSm * scale);
clearG->setOnClick([&row, persist]() {
row.glyph = std::nullopt;
persist();
});
glyphBtnRow->addChild(std::move(clearG));
}
iconCol->addChild(std::move(glyphBtnRow));
body->addChild(std::move(iconCol));
auto fields = std::make_unique<Flex>();
fields->setDirection(FlexDirection::Vertical);
fields->setAlign(FlexAlign::Stretch);
fields->setGap(Style::spaceSm * scale);
fields->setFlexGrow(1.0f);
fields->addChild(makeLabel(i18n::tr("settings.session-actions.kind-section-label"),
Style::fontSizeCaption * scale, colorSpecFromRole(ColorRole::OnSurfaceVariant),
false));
auto kindSelect = std::make_unique<Select>();
kindSelect->setOptions(optionLabels(kindOptions));
if (const auto ki = optionIndex(kindOptions, row.action)) {
kindSelect->setSelectedIndex(*ki);
} else {
kindSelect->clearSelection();
}
kindSelect->setFontSize(Style::fontSizeBody * scale);
kindSelect->setControlHeight(Style::controlHeight * scale);
kindSelect->setGlyphSize(Style::fontSizeBody * scale);
kindSelect->setFillWidth(true);
kindSelect->setOnSelectionChanged([&row, kindOptions, persist](std::size_t index, std::string_view /*label*/) {
if (index < kindOptions.size()) {
row.action = kindOptions[index].value;
persist();
}
});
fields->addChild(std::move(kindSelect));
auto labelBlock = std::make_unique<Flex>();
labelBlock->setDirection(FlexDirection::Vertical);
labelBlock->setAlign(FlexAlign::Stretch);
labelBlock->setGap(Style::spaceXs * scale);
labelBlock->setFlexGrow(1.0f);
labelBlock->addChild(makeLabel(i18n::tr("settings.session-actions.label-field"), Style::fontSizeCaption * scale,
colorSpecFromRole(ColorRole::OnSurfaceVariant), false));
auto labelIn = std::make_unique<Input>();
labelIn->setValue(row.label.value_or(""));
labelIn->setPlaceholder(i18n::tr("settings.session-actions.label-placeholder"));
labelIn->setFontSize(Style::fontSizeBody * scale);
labelIn->setControlHeight(Style::controlHeight * scale);
labelIn->setHorizontalPadding(Style::spaceSm * scale);
labelIn->setMinLayoutWidth(200.0f * scale);
auto* labelPtr = labelIn.get();
const auto commitLabel = [&row, persist, labelPtr]() {
const std::string t = StringUtils::trim(labelPtr->value());
if (t.empty()) {
row.label = std::nullopt;
} else {
row.label = t;
}
labelPtr->setInvalid(false);
persist();
};
labelIn->setOnChange([labelPtr](const std::string& /*t*/) { labelPtr->setInvalid(false); });
labelIn->setOnSubmit([commitLabel](const std::string& /*text*/) { commitLabel(); });
labelIn->setOnFocusLoss(commitLabel);
labelBlock->addChild(std::move(labelIn));
fields->addChild(std::move(labelBlock));
auto cmdBlock = std::make_unique<Flex>();
cmdBlock->setDirection(FlexDirection::Vertical);
cmdBlock->setAlign(FlexAlign::Stretch);
cmdBlock->setGap(Style::spaceXs * scale);
cmdBlock->setFlexGrow(1.0f);
cmdBlock->addChild(makeLabel(i18n::tr("settings.session-actions.command-label"), Style::fontSizeCaption * scale,
colorSpecFromRole(ColorRole::OnSurfaceVariant), false));
auto cmdIn = std::make_unique<Input>();
cmdIn->setValue(row.command.value_or(""));
cmdIn->setPlaceholder(i18n::tr("settings.session-actions.command-placeholder"));
cmdIn->setFontSize(Style::fontSizeBody * scale);
cmdIn->setControlHeight(Style::controlHeight * scale);
cmdIn->setHorizontalPadding(Style::spaceSm * scale);
cmdIn->setMinLayoutWidth(280.0f * scale);
auto* cmdPtr = cmdIn.get();
const auto commitCommand = [&row, persist, cmdPtr]() {
const std::string t = StringUtils::trim(cmdPtr->value());
if (t.empty()) {
row.command = std::nullopt;
} else {
row.command = t;
}
cmdPtr->setInvalid(false);
persist();
};
cmdIn->setOnChange([cmdPtr](const std::string& /*t*/) { cmdPtr->setInvalid(false); });
cmdIn->setOnSubmit([commitCommand](const std::string& /*text*/) { commitCommand(); });
cmdIn->setOnFocusLoss(commitCommand);
cmdBlock->addChild(std::move(cmdIn));
fields->addChild(std::move(cmdBlock));
auto destGrp = std::make_unique<Flex>();
destGrp->setDirection(FlexDirection::Horizontal);
destGrp->setAlign(FlexAlign::Center);
destGrp->setGap(Style::spaceXs * scale);
destGrp->addChild(makeLabel(i18n::tr("settings.session-actions.destructive-label"),
Style::fontSizeCaption * scale, colorSpecFromRole(ColorRole::OnSurfaceVariant),
false));
auto destToggle = std::make_unique<Toggle>();
destToggle->setScale(scale);
destToggle->setChecked(row.destructive);
destToggle->setOnChange([&row, persist](bool v) {
row.destructive = v;
persist();
});
destGrp->addChild(std::move(destToggle));
fields->addChild(std::move(destGrp));
body->addChild(std::move(fields));
section.addChild(std::move(body));
auto actions = std::make_unique<Flex>();
actions->setDirection(FlexDirection::Horizontal);
actions->setAlign(FlexAlign::Center);
actions->setGap(Style::spaceSm * scale);
actions->setFillWidth(true);
auto applyBtn = std::make_unique<Button>();
applyBtn->setGlyph("check");
applyBtn->setText(i18n::tr("common.actions.apply"));
applyBtn->setVariant(ButtonVariant::Default);
applyBtn->setFontSize(Style::fontSizeBody * scale);
applyBtn->setGlyphSize(Style::fontSizeBody * scale);
applyBtn->setMinHeight(Style::controlHeight * scale);
applyBtn->setPadding(Style::spaceSm * scale, Style::spaceMd * scale);
applyBtn->setRadius(Style::radiusMd * scale);
applyBtn->setFlexGrow(1.0f);
applyBtn->setOnClick([commitLabel, commitCommand, closeHostedEditor]() {
commitLabel();
commitCommand();
if (closeHostedEditor) {
closeHostedEditor();
}
});
actions->addChild(std::move(applyBtn));
section.addChild(std::move(actions));
}
} // namespace
std::size_t addSettingsContentSections(Flex& content, const std::vector<SettingEntry>& registry,
@@ -337,7 +586,7 @@ namespace settings {
return i18n::tr("settings.navigation.sections." + std::string(section));
};
const auto groupLabel = [](std::string_view group) {
const auto groupLabel = [](std::string_view group) -> std::string {
return i18n::tr("settings.navigation.groups." + std::string(group));
};
@@ -1028,6 +1277,174 @@ namespace settings {
section.addChild(std::move(block));
};
const auto makeSessionActionsInlineBlock = [&](Flex& section, const SettingEntry& entry,
const SessionPanelActionsSetting& sa) {
const bool overridden = (ctx.configService != nullptr && ctx.configService->hasEffectiveOverride(entry.path));
auto block = std::make_unique<Flex>();
block->setDirection(FlexDirection::Vertical);
block->setAlign(FlexAlign::Stretch);
block->setGap(Style::spaceSm * scale);
block->setPadding(2.0f * scale, 0.0f);
auto titleRow = std::make_unique<Flex>();
titleRow->setDirection(FlexDirection::Horizontal);
titleRow->setAlign(FlexAlign::Center);
titleRow->setGap(Style::spaceSm * scale);
titleRow->addChild(
makeLabel(entry.title, Style::fontSizeBody * scale, colorSpecFromRole(ColorRole::OnSurface), true));
if (overridden) {
auto badge = std::make_unique<Flex>();
badge->setAlign(FlexAlign::Center);
badge->setPadding(1.0f * scale, Style::spaceXs * scale);
badge->setRadius(Style::radiusSm * scale);
badge->setFill(colorSpecFromRole(ColorRole::Primary, 0.15f));
badge->addChild(makeLabel(i18n::tr("settings.badges.override"), Style::fontSizeCaption * scale,
colorSpecFromRole(ColorRole::Primary), true));
titleRow->addChild(std::move(badge));
}
if (overridden) {
titleRow->addChild(makeResetButton(entry.path));
}
block->addChild(std::move(titleRow));
if (!entry.subtitle.empty()) {
block->addChild(makeLabel(entry.subtitle, Style::fontSizeCaption * scale,
colorSpecFromRole(ColorRole::OnSurfaceVariant), false));
}
const std::vector<SelectOption> kindOptions = {
{"lock", i18n::tr("settings.session-actions.kind.lock"), {}},
{"logout", i18n::tr("settings.session-actions.kind.logout"), {}},
{"reboot", i18n::tr("settings.session-actions.kind.reboot"), {}},
{"shutdown", i18n::tr("settings.session-actions.kind.shutdown"), {}},
{"command", i18n::tr("settings.session-actions.kind.command"), {}},
};
auto state = std::make_shared<std::vector<SessionPanelActionConfig>>(sa.items);
const auto commit = [setOverride = ctx.setOverride, path = entry.path, state, req = ctx.requestContentRebuild]() {
setOverride(path, *state);
req();
};
const float iconBtnH = Style::controlHeight * scale;
for (std::size_t idx = 0; idx < state->size(); ++idx) {
if (idx > 0) {
auto sep = std::make_unique<Separator>();
sep->setColor(colorSpecFromRole(ColorRole::Outline, 0.35f));
block->addChild(std::move(sep));
}
auto row = std::make_unique<Flex>();
row->setDirection(FlexDirection::Horizontal);
row->setAlign(FlexAlign::Center);
row->setJustify(FlexJustify::SpaceBetween);
row->setGap(Style::spaceSm * scale);
row->setMinHeight(Style::controlHeight * scale);
auto summary = std::make_unique<Label>();
summary->setText(sessionActionRowSummary(kindOptions, (*state)[idx]));
summary->setFontSize(Style::fontSizeBody * scale);
summary->setColor(colorSpecFromRole(ColorRole::OnSurface));
summary->setFlexGrow(1.0f);
row->addChild(std::move(summary));
auto reorder = std::make_unique<Flex>();
reorder->setDirection(FlexDirection::Horizontal);
reorder->setAlign(FlexAlign::Center);
reorder->setGap(Style::spaceXs * scale);
auto upBtn = std::make_unique<Button>();
upBtn->setGlyph("chevron-up");
upBtn->setVariant(ButtonVariant::Ghost);
upBtn->setGlyphSize(Style::fontSizeBody * scale);
upBtn->setMinWidth(Style::controlHeightSm * scale);
upBtn->setMinHeight(iconBtnH);
upBtn->setPadding(Style::spaceXs * scale);
upBtn->setRadius(Style::radiusMd * scale);
upBtn->setEnabled(idx > 0);
upBtn->setOnClick([state, rowIndex = idx, commit]() {
if (rowIndex == 0 || rowIndex >= state->size()) {
return;
}
std::swap((*state)[rowIndex - 1], (*state)[rowIndex]);
commit();
});
reorder->addChild(std::move(upBtn));
auto downBtn = std::make_unique<Button>();
downBtn->setGlyph("chevron-down");
downBtn->setVariant(ButtonVariant::Ghost);
downBtn->setGlyphSize(Style::fontSizeBody * scale);
downBtn->setMinWidth(Style::controlHeightSm * scale);
downBtn->setMinHeight(iconBtnH);
downBtn->setPadding(Style::spaceXs * scale);
downBtn->setRadius(Style::radiusMd * scale);
downBtn->setEnabled(idx + 1 < state->size());
downBtn->setOnClick([state, rowIndex = idx, commit]() {
if (rowIndex + 1 >= state->size()) {
return;
}
std::swap((*state)[rowIndex + 1], (*state)[rowIndex]);
commit();
});
reorder->addChild(std::move(downBtn));
row->addChild(std::move(reorder));
auto showGrp = std::make_unique<Flex>();
showGrp->setDirection(FlexDirection::Horizontal);
showGrp->setAlign(FlexAlign::Center);
showGrp->setGap(Style::spaceXs * scale);
showGrp->addChild(makeLabel(i18n::tr("settings.session-actions.show-in-menu"), Style::fontSizeCaption * scale,
colorSpecFromRole(ColorRole::OnSurfaceVariant), false));
auto showToggle = std::make_unique<Toggle>();
showToggle->setScale(scale);
showToggle->setChecked((*state)[idx].enabled);
showToggle->setOnChange([state, rowIndex = idx, commit](bool v) {
(*state)[rowIndex].enabled = v;
commit();
});
showGrp->addChild(std::move(showToggle));
row->addChild(std::move(showGrp));
auto entrySettings = std::make_unique<Button>();
entrySettings->setGlyph("settings");
entrySettings->setVariant(ButtonVariant::Ghost);
entrySettings->setGlyphSize(Style::fontSizeCaption * scale);
entrySettings->setMinWidth(Style::controlHeightSm * scale);
entrySettings->setMinHeight(Style::controlHeightSm * scale);
entrySettings->setPadding(Style::spaceXs * scale);
entrySettings->setRadius(Style::radiusSm * scale);
entrySettings->setOnClick([openEntry = ctx.openSessionActionEntryEditor, rowIndex = idx]() {
if (openEntry) {
openEntry(rowIndex);
}
});
row->addChild(std::move(entrySettings));
block->addChild(std::move(row));
}
auto addBtn = std::make_unique<Button>();
addBtn->setGlyph("add");
addBtn->setText(i18n::tr("settings.session-actions.add"));
addBtn->setVariant(ButtonVariant::Default);
addBtn->setFontSize(Style::fontSizeBody * scale);
addBtn->setGlyphSize(Style::fontSizeBody * scale);
addBtn->setMinHeight(Style::controlHeight * scale);
addBtn->setPadding(Style::spaceSm * scale, Style::spaceMd * scale);
addBtn->setRadius(Style::radiusMd * scale);
addBtn->setOnClick([state, commit]() {
state->push_back(SessionPanelActionConfig{"command", true, "notify-send 'Noctalia' 'Custom session entry'",
std::nullopt, std::nullopt, false});
commit();
});
block->addChild(std::move(addBtn));
section.addChild(std::move(block));
};
const auto makeControl = [&](const SettingEntry& entry) -> std::unique_ptr<Node> {
return std::visit(
[&](const auto& control) -> std::unique_ptr<Node> {
@@ -1053,6 +1470,8 @@ namespace settings {
return nullptr;
} else if constexpr (std::is_same_v<T, ShortcutListSetting>) {
return nullptr;
} else if constexpr (std::is_same_v<T, SessionPanelActionsSetting>) {
return nullptr;
} else if constexpr (std::is_same_v<T, ButtonSetting>) {
auto button = std::make_unique<Button>();
button->setVariant(ButtonVariant::Outline);
@@ -1163,6 +1582,8 @@ namespace settings {
}
} else if (const auto* shortcuts = std::get_if<ShortcutListSetting>(&entry.control)) {
makeShortcutListBlock(*activeSection, entry, *shortcuts);
} else if (const auto* sessionActs = std::get_if<SessionPanelActionsSetting>(&entry.control)) {
makeSessionActionsInlineBlock(*activeSection, entry, *sessionActs);
} else if (const auto* picker = std::get_if<SearchPickerSetting>(&entry.control)) {
makeRow(*activeSection, entry, makeSearchPickerButton(entry, *picker));
} else if (const auto* multi = std::get_if<MultiSelectSetting>(&entry.control)) {
@@ -1201,4 +1622,9 @@ namespace settings {
return visibleEntries;
}
void buildSessionActionEntryDetailContent(Flex& parent, SettingsContentContext& ctx, SessionPanelActionConfig& row,
const std::function<void()>& persist) {
buildSessionActionEntryDetailContentImpl(parent, ctx, row, persist, ctx.closeHostedEditor);
}
} // namespace settings
+9
View File
@@ -51,9 +51,18 @@ namespace settings {
std::function<void(std::vector<std::string>)> clearOverride;
std::function<void(std::string, std::string, std::vector<std::pair<std::vector<std::string>, ConfigOverrideValue>>)>
renameWidgetInstance;
std::function<void(std::size_t)> openSessionActionEntryEditor;
// When set (session action entry popup), called after commits instead of requestRebuild.
std::function<void()> afterSessionActionsCommit;
std::function<void()> closeHostedEditor;
};
std::size_t addSettingsContentSections(Flex& content, const std::vector<SettingEntry>& registry,
SettingsContentContext ctx);
void buildSessionActionEntryDetailContent(Flex& parent, SettingsContentContext& ctx, SessionPanelActionConfig& row,
const std::function<void()>& persist);
} // namespace settings
+5
View File
@@ -546,6 +546,11 @@ namespace settings {
tr("settings.schema.panels.attach-wallpaper.description"),
{"shell", "panel", "attach_wallpaper"}, ToggleSetting{cfg.shell.panel.attachWallpaper},
"attach bar panel"));
entries.push_back(makeEntry("panels", "session-panel", tr("settings.schema.panels.session-actions.label"),
tr("settings.schema.panels.session-actions.description"),
{"shell", "session", "actions"},
SessionPanelActionsSetting{.items = cfg.shell.session.actions},
"session panel power menu logout reboot shutdown lock command actions order"));
// Desktop
entries.push_back(makeEntry("desktop", "widgets", tr("settings.schema.desktop.widgets.label"),
+6 -2
View File
@@ -82,6 +82,10 @@ namespace settings {
std::size_t maxItems = 0;
};
struct SessionPanelActionsSetting {
std::vector<SessionPanelActionConfig> items;
};
struct ColorSetting {
std::string hex; // current resolved value as #RRGGBB; empty when unset
bool unset = true;
@@ -105,8 +109,8 @@ namespace settings {
};
using SettingControl = std::variant<ToggleSetting, SelectSetting, SliderSetting, TextSetting, OptionalNumberSetting,
ListSetting, ShortcutListSetting, ColorSetting, MultiSelectSetting, ButtonSetting,
ColorRolePickerSetting, SearchPickerSetting>;
ListSetting, ShortcutListSetting, SessionPanelActionsSetting, ColorSetting,
MultiSelectSetting, ButtonSetting, ColorRolePickerSetting, SearchPickerSetting>;
struct SettingEntry {
std::string section;
+206 -1
View File
@@ -81,6 +81,28 @@ namespace {
return sections;
}
std::string sessionActionTitle(const SessionPanelActionConfig& row) {
if (row.label.has_value() && !StringUtils::trim(*row.label).empty()) {
return *row.label;
}
if (row.action == "lock") {
return i18n::tr("settings.session-actions.kind.lock");
}
if (row.action == "logout") {
return i18n::tr("settings.session-actions.kind.logout");
}
if (row.action == "reboot") {
return i18n::tr("settings.session-actions.kind.reboot");
}
if (row.action == "shutdown") {
return i18n::tr("settings.session-actions.kind.shutdown");
}
if (row.action == "command") {
return i18n::tr("settings.session-actions.kind.command");
}
return row.action;
}
bool containsPath(const std::vector<std::vector<std::string>>& paths, const std::vector<std::string>& path) {
return std::find(paths.begin(), paths.end(), path) != paths.end();
}
@@ -235,7 +257,10 @@ bool SettingsWindow::ownsKeyboardSurface(wl_surface* surface) const noexcept {
if (m_widgetAddPopup != nullptr && m_widgetAddPopup->wlSurface() == surface) {
return true;
}
return m_searchPickerPopup != nullptr && m_searchPickerPopup->wlSurface() == surface;
if (m_searchPickerPopup != nullptr && m_searchPickerPopup->wlSurface() == surface) {
return true;
}
return m_sessionActionsEditorPopup != nullptr && m_sessionActionsEditorPopup->wlSurface() == surface;
}
void SettingsWindow::open() {
@@ -353,6 +378,10 @@ void SettingsWindow::destroyWindow() {
m_searchPickerPopup->close();
m_searchPickerPopup.reset();
}
if (m_sessionActionsEditorPopup != nullptr) {
m_sessionActionsEditorPopup->close();
m_sessionActionsEditorPopup.reset();
}
m_sceneRoot.reset();
m_surface.reset();
m_pointerInside = false;
@@ -603,6 +632,13 @@ void SettingsWindow::openBarWidgetAddPopup(const std::vector<std::string>& laneP
return;
}
if (m_searchPickerPopup != nullptr && m_searchPickerPopup->isOpen()) {
m_searchPickerPopup->close();
}
if (m_sessionActionsEditorPopup != nullptr && m_sessionActionsEditorPopup->isOpen()) {
m_sessionActionsEditorPopup->close();
}
if (m_widgetAddPopup == nullptr) {
m_widgetAddPopup = std::make_unique<settings::WidgetAddPopup>();
m_widgetAddPopup->initialize(*m_wayland, *m_config, *m_renderContext);
@@ -657,6 +693,13 @@ void SettingsWindow::openSearchPickerPopup(const std::string& title, const std::
m_searchPickerPopup->initialize(*m_wayland, *m_config, *m_renderContext);
}
if (m_widgetAddPopup != nullptr && m_widgetAddPopup->isOpen()) {
m_widgetAddPopup->close();
}
if (m_sessionActionsEditorPopup != nullptr && m_sessionActionsEditorPopup->isOpen()) {
m_sessionActionsEditorPopup->close();
}
m_searchPickerPopup->setOnSelect([this, settingPath, selectedValue](const std::string& value) {
if (value != selectedValue) {
setSettingOverride(settingPath, value);
@@ -680,6 +723,142 @@ void SettingsWindow::openSearchPickerPopup(const std::string& title, const std::
emptyText, uiScale());
}
void SettingsWindow::openSessionActionEntryEditor(std::size_t index) {
if (m_wayland == nullptr || m_renderContext == nullptr || m_surface == nullptr ||
m_surface->xdgSurface() == nullptr || m_config == nullptr) {
return;
}
const Config& cfg = m_config->config();
if (index >= cfg.shell.session.actions.size()) {
return;
}
if (m_widgetAddPopup != nullptr && m_widgetAddPopup->isOpen()) {
m_widgetAddPopup->close();
}
if (m_searchPickerPopup != nullptr && m_searchPickerPopup->isOpen()) {
m_searchPickerPopup->close();
}
if (m_sessionActionsEditorPopup == nullptr) {
m_sessionActionsEditorPopup = std::make_unique<settings::SessionActionsEditorPopup>();
m_sessionActionsEditorPopup->initialize(*m_wayland, *m_config, *m_renderContext);
}
const float scale = uiScale();
const BarConfig* selectedBar = settings::findBar(cfg, m_selectedBarName);
const BarMonitorOverride* selectedMonitorOverride = nullptr;
if (selectedBar != nullptr && !m_selectedMonitorOverride.empty()) {
selectedMonitorOverride = settings::findMonitorOverride(*selectedBar, m_selectedMonitorOverride);
}
const auto requestRebuild = [this]() { requestSceneRebuild(); };
const auto requestContent = [this]() { requestContentRebuild(); };
const auto setOverride = [this](std::vector<std::string> path, ConfigOverrideValue value) {
setSettingOverride(std::move(path), std::move(value));
};
const auto setOverrides = [this](std::vector<std::pair<std::vector<std::string>, ConfigOverrideValue>> overrides) {
setSettingOverrides(std::move(overrides));
};
const auto clearOverride = [this](std::vector<std::string> path) { clearSettingOverride(std::move(path)); };
const auto renameWidget =
[this](std::string oldName, std::string newName,
std::vector<std::pair<std::vector<std::string>, ConfigOverrideValue>> referenceOverrides) {
renameWidgetInstance(std::move(oldName), std::move(newName), std::move(referenceOverrides));
};
auto rowState = std::make_shared<SessionPanelActionConfig>(cfg.shell.session.actions[index]);
const auto persist = [this, rowState, index]() {
if (m_config == nullptr) {
return;
}
auto next = m_config->config().shell.session.actions;
if (index >= next.size()) {
return;
}
next[index] = *rowState;
setSettingOverride({"shell", "session", "actions"}, next);
requestContentRebuild();
if (m_sessionActionsEditorPopup != nullptr && m_sessionActionsEditorPopup->isOpen()) {
m_sessionActionsEditorPopup->requestLayout();
}
};
const auto removeRow = [this, index]() {
if (m_config == nullptr) {
return;
}
auto next = m_config->config().shell.session.actions;
if (index >= next.size()) {
return;
}
next.erase(next.begin() + static_cast<std::ptrdiff_t>(index));
setSettingOverride({"shell", "session", "actions"}, next);
if (m_sessionActionsEditorPopup != nullptr) {
m_sessionActionsEditorPopup->close();
}
requestContentRebuild();
};
settings::SettingsContentContext ctx{
.config = cfg,
.configService = m_config,
.scale = scale,
.searchQuery = m_searchQuery,
.selectedSection = m_selectedSection,
.selectedBar = selectedBar,
.selectedMonitorOverride = selectedMonitorOverride,
.showAdvanced = m_showAdvanced,
.showOverriddenOnly = m_showOverriddenOnly,
.batteryDeviceOptions = upowerBatteryDeviceOptions(m_upower),
.openWidgetPickerPath = m_openWidgetPickerPath,
.editingWidgetName = m_editingWidgetName,
.pendingDeleteWidgetName = m_pendingDeleteWidgetName,
.pendingDeleteWidgetSettingPath = m_pendingDeleteWidgetSettingPath,
.renamingWidgetName = m_renamingWidgetName,
.creatingWidgetType = m_creatingWidgetType,
.requestRebuild = requestRebuild,
.requestContentRebuild = requestContent,
.resetContentScroll = [this]() { m_contentScrollState.offset = 0.0f; },
.setScrollTarget = [this](Node* target) { m_pendingContentScrollTarget = target; },
.focusArea = [this](InputArea* area) { m_inputDispatcher.setFocus(area); },
.openBarWidgetAddPopup = [this](const std::vector<std::string>& lanePath) { openBarWidgetAddPopup(lanePath); },
.openSearchPickerPopup =
[this](const std::string& title, const std::vector<settings::SelectOption>& options,
const std::string& selectedValue, const std::string& placeholder, const std::string& emptyText,
const std::vector<std::string>& settingPath) {
openSearchPickerPopup(title, options, selectedValue, placeholder, emptyText, settingPath);
},
.setOverride = setOverride,
.setOverrides = setOverrides,
.clearOverride = clearOverride,
.renameWidgetInstance = renameWidget,
.openSessionActionEntryEditor = {},
.afterSessionActionsCommit = {},
.closeHostedEditor =
[this]() {
if (m_sessionActionsEditorPopup != nullptr) {
m_sessionActionsEditorPopup->close();
}
},
};
const std::string sheetTitle = sessionActionTitle(*rowState);
wl_output* output = m_wayland->lastPointerOutput();
if (output == nullptr) {
output = m_output;
}
m_sessionActionsEditorPopup->open(m_surface->xdgSurface(), output, m_wayland->lastInputSerial(),
m_surface->wlSurface(), m_surface->width(), m_surface->height(), scale, sheetTitle,
removeRow, [ctx, rowState, persist](Flex& body) mutable {
settings::buildSessionActionEntryDetailContent(body, ctx, *rowState, persist);
});
}
void SettingsWindow::saveSupportReport() {
if (m_config == nullptr) {
return;
@@ -1215,6 +1394,9 @@ void SettingsWindow::rebuildSettingsContent() {
.setOverrides = setOverrides,
.clearOverride = clearOverride,
.renameWidgetInstance = renameWidget,
.openSessionActionEntryEditor = [this](std::size_t entryIndex) { openSessionActionEntryEditor(entryIndex); },
.afterSessionActionsCommit = {},
.closeHostedEditor = {},
});
}
@@ -1628,6 +1810,14 @@ bool SettingsWindow::onPointerEvent(const PointerEvent& event) {
m_searchPickerPopup->close();
return true;
}
if (m_sessionActionsEditorPopup != nullptr && m_sessionActionsEditorPopup->onPointerEvent(event)) {
return true;
}
if (m_sessionActionsEditorPopup != nullptr && m_sessionActionsEditorPopup->isOpen() &&
event.type == PointerEvent::Type::Button && event.state == 1) {
m_sessionActionsEditorPopup->close();
return true;
}
if (m_actionsMenuPopup != nullptr && m_actionsMenuPopup->onPointerEvent(event)) {
return true;
@@ -1723,6 +1913,15 @@ void SettingsWindow::onKeyboardEvent(const KeyboardEvent& event) {
return;
}
if (m_sessionActionsEditorPopup != nullptr && m_sessionActionsEditorPopup->isOpen()) {
if (event.pressed && m_config->matchesKeybind(KeybindAction::Cancel, event.sym, event.modifiers)) {
m_sessionActionsEditorPopup->close();
return;
}
m_sessionActionsEditorPopup->onKeyboardEvent(event);
return;
}
const auto requestRebuild = [this]() {
if (m_surface != nullptr) {
m_rebuildRequested = true;
@@ -1779,6 +1978,9 @@ void SettingsWindow::onThemeChanged() {
if (m_widgetAddPopup != nullptr && m_widgetAddPopup->isOpen()) {
m_widgetAddPopup->requestRedraw();
}
if (m_sessionActionsEditorPopup != nullptr && m_sessionActionsEditorPopup->isOpen()) {
m_sessionActionsEditorPopup->requestRedraw();
}
m_surface->requestRedraw();
}
}
@@ -1788,6 +1990,9 @@ void SettingsWindow::onFontChanged() {
if (m_widgetAddPopup != nullptr && m_widgetAddPopup->isOpen()) {
m_widgetAddPopup->requestLayout();
}
if (m_sessionActionsEditorPopup != nullptr && m_sessionActionsEditorPopup->isOpen()) {
m_sessionActionsEditorPopup->requestLayout();
}
m_surface->requestLayout();
}
}
+3
View File
@@ -4,6 +4,7 @@
#include "render/scene/input_dispatcher.h"
#include "render/scene/node.h"
#include "shell/settings/search_picker_popup.h"
#include "shell/settings/session_actions_editor_popup.h"
#include "shell/settings/settings_registry.h"
#include "shell/settings/widget_add_popup.h"
#include "ui/controls/context_menu_popup.h"
@@ -68,6 +69,7 @@ private:
void openSearchPickerPopup(const std::string& title, const std::vector<settings::SelectOption>& options,
const std::string& selectedValue, const std::string& placeholder,
const std::string& emptyText, const std::vector<std::string>& settingPath);
void openSessionActionEntryEditor(std::size_t index);
void saveSupportReport();
void saveFlattenedConfig();
void setSettingOverride(std::vector<std::string> path, ConfigOverrideValue value);
@@ -101,6 +103,7 @@ private:
std::unique_ptr<ContextMenuPopup> m_actionsMenuPopup;
std::unique_ptr<settings::WidgetAddPopup> m_widgetAddPopup;
std::unique_ptr<settings::SearchPickerPopup> m_searchPickerPopup;
std::unique_ptr<settings::SessionActionsEditorPopup> m_sessionActionsEditorPopup;
InputDispatcher m_inputDispatcher;
AnimationManager m_animations;
bool m_pointerInside = false;