mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
feat(settings): add session panel settings to the Panels tab
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user