mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
settings: cc shortcuts editor
This commit is contained in:
@@ -704,6 +704,7 @@
|
||||
"appearance": "Appearance",
|
||||
"desktop": "Desktop",
|
||||
"dock": "Dock",
|
||||
"panels": "Panels",
|
||||
"notifications": "Notifications",
|
||||
"backdrop": "Backdrop",
|
||||
"services": "Services",
|
||||
@@ -720,6 +721,7 @@
|
||||
"built-in": "Built-in",
|
||||
"clipboard": "Clipboard",
|
||||
"community": "Community",
|
||||
"control-center": "Control Center",
|
||||
"directories": "Directories",
|
||||
"effects": "Effects",
|
||||
"focus-styling": "Focus Styling",
|
||||
@@ -733,6 +735,7 @@
|
||||
"network": "Network",
|
||||
"night-light": "Night Light",
|
||||
"osd": "OSD",
|
||||
"overview": "Overview",
|
||||
"pinned-apps": "Pinned Apps",
|
||||
"profile": "Profile",
|
||||
"screen-corners": "Screen Corners",
|
||||
@@ -1319,6 +1322,12 @@
|
||||
"description": "Reserve screen space for the dock"
|
||||
}
|
||||
},
|
||||
"panels": {
|
||||
"overview-shortcuts": {
|
||||
"label": "Overview Shortcuts",
|
||||
"description": "Choose up to six shortcut buttons for the overview tab"
|
||||
}
|
||||
},
|
||||
"backdrop": {
|
||||
"blur-intensity": {
|
||||
"label": "Blur Intensity",
|
||||
|
||||
@@ -369,6 +369,17 @@ namespace {
|
||||
array.push_back(item);
|
||||
}
|
||||
table.insert_or_assign(key, std::move(array));
|
||||
} else if constexpr (std::is_same_v<T, std::vector<ShortcutConfig>>) {
|
||||
toml::array array;
|
||||
for (const auto& item : concrete) {
|
||||
if (item.type.empty()) {
|
||||
continue;
|
||||
}
|
||||
toml::table shortcut;
|
||||
shortcut.insert_or_assign("type", item.type);
|
||||
array.push_back(std::move(shortcut));
|
||||
}
|
||||
table.insert_or_assign(key, std::move(array));
|
||||
} else {
|
||||
table.insert_or_assign(key, concrete);
|
||||
}
|
||||
@@ -596,6 +607,7 @@ std::optional<Config> ConfigService::configForOverrides(const toml::table& overr
|
||||
.resumeCommand = "",
|
||||
});
|
||||
parsed.bars.push_back(BarConfig{});
|
||||
parsed.controlCenter.shortcuts = defaultControlCenterShortcuts();
|
||||
return parsed;
|
||||
}
|
||||
|
||||
|
||||
@@ -835,6 +835,7 @@ void ConfigService::loadAll() {
|
||||
.resumeCommand = "",
|
||||
});
|
||||
m_config.bars.push_back(BarConfig{});
|
||||
m_config.controlCenter.shortcuts = defaultControlCenterShortcuts();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1644,8 +1645,10 @@ void ConfigService::parseTableInto(const toml::table& tbl, Config& config, bool
|
||||
}
|
||||
|
||||
// Parse [[control_center.shortcuts]]
|
||||
bool controlCenterShortcutsConfigured = false;
|
||||
if (auto* ccTbl = tbl["control_center"].as_table()) {
|
||||
if (auto* shortcutsArr = (*ccTbl)["shortcuts"].as_array()) {
|
||||
controlCenterShortcutsConfigured = true;
|
||||
config.controlCenter.shortcuts.clear();
|
||||
for (const auto& entry : *shortcutsArr) {
|
||||
auto* entryTbl = entry.as_table();
|
||||
@@ -1656,23 +1659,14 @@ void ConfigService::parseTableInto(const toml::table& tbl, Config& config, bool
|
||||
if (auto v = (*entryTbl)["type"].value<std::string>()) {
|
||||
sc.type = *v;
|
||||
}
|
||||
if (auto v = (*entryTbl)["label"].value<std::string>()) {
|
||||
sc.label = *v;
|
||||
}
|
||||
if (auto v = (*entryTbl)["icon"].value<std::string>()) {
|
||||
sc.icon = *v;
|
||||
}
|
||||
if (!sc.type.empty()) {
|
||||
config.controlCenter.shortcuts.push_back(std::move(sc));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (config.controlCenter.shortcuts.empty()) {
|
||||
config.controlCenter.shortcuts = {
|
||||
{"wifi", {}, {}}, {"bluetooth", {}, {}}, {"caffeine", {}, {}},
|
||||
{"nightlight", {}, {}}, {"notification", {}, {}}, {"power_profile", {}, {}},
|
||||
};
|
||||
if (!controlCenterShortcutsConfigured && config.controlCenter.shortcuts.empty()) {
|
||||
config.controlCenter.shortcuts = defaultControlCenterShortcuts();
|
||||
}
|
||||
|
||||
// Parse [idle.behavior.*]
|
||||
|
||||
@@ -32,6 +32,12 @@ namespace {
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<ShortcutConfig> defaultControlCenterShortcuts() {
|
||||
return {
|
||||
{"wifi"}, {"bluetooth"}, {"caffeine"}, {"nightlight"}, {"notification"}, {"power_profile"},
|
||||
};
|
||||
}
|
||||
|
||||
std::string WidgetConfig::getString(const std::string& key, const std::string& fallback) const {
|
||||
auto it = settings.find(key);
|
||||
if (it == settings.end()) {
|
||||
|
||||
@@ -98,8 +98,16 @@ struct BarConfig {
|
||||
bool operator==(const BarConfig&) const = default;
|
||||
};
|
||||
|
||||
struct ShortcutConfig {
|
||||
std::string type;
|
||||
bool operator==(const ShortcutConfig&) const = default;
|
||||
};
|
||||
|
||||
[[nodiscard]] std::vector<ShortcutConfig> defaultControlCenterShortcuts();
|
||||
|
||||
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>>;
|
||||
using ConfigOverrideValue =
|
||||
std::variant<bool, std::int64_t, double, std::string, std::vector<std::string>, std::vector<ShortcutConfig>>;
|
||||
|
||||
// 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.
|
||||
@@ -574,13 +582,6 @@ struct ThemeConfig {
|
||||
TemplatesConfig templates;
|
||||
};
|
||||
|
||||
struct ShortcutConfig {
|
||||
std::string type;
|
||||
std::optional<std::string> label;
|
||||
std::optional<std::string> icon;
|
||||
bool operator==(const ShortcutConfig&) const = default;
|
||||
};
|
||||
|
||||
struct ControlCenterConfig {
|
||||
std::vector<ShortcutConfig> shortcuts;
|
||||
bool operator==(const ControlCenterConfig&) const = default;
|
||||
|
||||
@@ -334,7 +334,7 @@ std::unique_ptr<Flex> OverviewTab::create() {
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::string label = sc.label.has_value() ? *sc.label : shortcut->displayLabel();
|
||||
const std::string label = shortcut->displayLabel();
|
||||
const bool enabled = shortcut->enabled();
|
||||
const bool isActive = shortcut->isToggle() && shortcut->active();
|
||||
|
||||
@@ -378,7 +378,6 @@ std::unique_ptr<Flex> OverviewTab::create() {
|
||||
pad.button = btnPtr;
|
||||
pad.glyph = btnPtr->glyph();
|
||||
pad.label = btnPtr->label();
|
||||
pad.labelOverride = sc.label;
|
||||
m_shortcutPads.push_back(std::move(pad));
|
||||
grid->addChild(std::move(btn));
|
||||
}
|
||||
@@ -944,7 +943,7 @@ void OverviewTab::syncShortcuts() {
|
||||
pad.glyph->setGlyph(sc.displayIcon());
|
||||
}
|
||||
if (pad.button != nullptr && pad.label != nullptr) {
|
||||
const std::string label = pad.labelOverride.has_value() ? *pad.labelOverride : sc.displayLabel();
|
||||
const std::string label = sc.displayLabel();
|
||||
if (pad.label->text() != label) {
|
||||
pad.button->setText(label);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -28,7 +27,6 @@ struct ShortcutPad {
|
||||
Button* button = nullptr;
|
||||
Glyph* glyph = nullptr;
|
||||
Label* label = nullptr;
|
||||
std::optional<std::string> labelOverride;
|
||||
};
|
||||
|
||||
class OverviewTab : public Tab {
|
||||
|
||||
@@ -20,12 +20,32 @@
|
||||
#include "wayland/wayland_connection.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <format>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::array<ShortcutRegistry::CatalogEntry, 16> kShortcutCatalog{{
|
||||
{"wifi", "control-center.shortcuts.wifi"},
|
||||
{"bluetooth", "control-center.shortcuts.bluetooth"},
|
||||
{"nightlight", "control-center.shortcuts.nightlight"},
|
||||
{"notification", "control-center.shortcuts.notification"},
|
||||
{"dark_mode", "control-center.shortcuts.dark-mode.dark"},
|
||||
{"caffeine", "control-center.shortcuts.caffeine"},
|
||||
{"audio", "control-center.shortcuts.audio"},
|
||||
{"mic_mute", "control-center.shortcuts.mic-mute"},
|
||||
{"power_profile", "control-center.shortcuts.power-profile"},
|
||||
{"media", "control-center.shortcuts.media"},
|
||||
{"weather", "control-center.shortcuts.weather"},
|
||||
{"sysmon", "control-center.shortcuts.sysmon"},
|
||||
{"keyboard_layout", "control-center.shortcuts.keyboard-layout"},
|
||||
{"wallpaper", "control-center.shortcuts.wallpaper"},
|
||||
{"session", "control-center.shortcuts.session"},
|
||||
{"clipboard", "control-center.shortcuts.clipboard"},
|
||||
}};
|
||||
|
||||
void openTab(std::string_view tab) {
|
||||
PanelManager::instance().togglePanel("control-center", PanelOpenRequest{.context = tab});
|
||||
}
|
||||
@@ -499,6 +519,8 @@ namespace {
|
||||
|
||||
} // namespace
|
||||
|
||||
std::span<const ShortcutRegistry::CatalogEntry> ShortcutRegistry::catalog() { return kShortcutCatalog; }
|
||||
|
||||
std::unique_ptr<Shortcut> ShortcutRegistry::create(std::string_view type, const ShortcutServices& s) {
|
||||
if (type == "wifi")
|
||||
return std::make_unique<WifiShortcut>(s.network);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
@@ -29,5 +30,11 @@ struct ShortcutServices;
|
||||
|
||||
class ShortcutRegistry {
|
||||
public:
|
||||
struct CatalogEntry {
|
||||
std::string_view type;
|
||||
std::string_view labelKey;
|
||||
};
|
||||
|
||||
[[nodiscard]] static std::span<const CatalogEntry> catalog();
|
||||
static std::unique_ptr<Shortcut> create(std::string_view type, const ShortcutServices& services);
|
||||
};
|
||||
|
||||
@@ -1086,6 +1086,90 @@ namespace settings {
|
||||
section.addChild(std::move(block));
|
||||
};
|
||||
|
||||
const auto makeShortcutListBlock = [&](Flex& section, const SettingEntry& entry,
|
||||
const ShortcutListSetting& shortcuts) {
|
||||
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::spaceXs * 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));
|
||||
}
|
||||
|
||||
std::vector<std::string> itemTypes;
|
||||
itemTypes.reserve(shortcuts.items.size());
|
||||
for (const auto& item : shortcuts.items) {
|
||||
itemTypes.push_back(item.type);
|
||||
}
|
||||
|
||||
std::vector<ListEditorOption> suggestedOptions;
|
||||
suggestedOptions.reserve(shortcuts.suggestedOptions.size());
|
||||
for (const auto& opt : shortcuts.suggestedOptions) {
|
||||
suggestedOptions.push_back(ListEditorOption{.value = opt.value, .label = opt.label});
|
||||
}
|
||||
|
||||
auto listEditor = std::make_unique<ListEditor>();
|
||||
listEditor->setScale(scale);
|
||||
listEditor->setMaxItems(shortcuts.maxItems);
|
||||
listEditor->setAddPlaceholder(i18n::tr("settings.controls.list.add-entry-placeholder"));
|
||||
listEditor->setSuggestedOptions(std::move(suggestedOptions));
|
||||
listEditor->setItems(std::move(itemTypes));
|
||||
listEditor->setOnAddRequested(
|
||||
[setOverride = ctx.setOverride, items = shortcuts.items, path = entry.path](std::string value) mutable {
|
||||
if (value.empty() || std::any_of(items.begin(), items.end(),
|
||||
[&value](const ShortcutConfig& item) { return item.type == value; })) {
|
||||
return;
|
||||
}
|
||||
items.push_back(ShortcutConfig{std::move(value)});
|
||||
setOverride(path, items);
|
||||
});
|
||||
listEditor->setOnRemoveRequested(
|
||||
[setOverride = ctx.setOverride, items = shortcuts.items, path = entry.path](std::size_t index) mutable {
|
||||
if (index >= items.size()) {
|
||||
return;
|
||||
}
|
||||
items.erase(items.begin() + static_cast<std::ptrdiff_t>(index));
|
||||
setOverride(path, items);
|
||||
});
|
||||
listEditor->setOnMoveRequested([setOverride = ctx.setOverride, items = shortcuts.items,
|
||||
path = entry.path](std::size_t from, std::size_t to) mutable {
|
||||
if (from >= items.size() || to >= items.size() || from == to) {
|
||||
return;
|
||||
}
|
||||
std::swap(items[from], items[to]);
|
||||
setOverride(path, items);
|
||||
});
|
||||
block->addChild(std::move(listEditor));
|
||||
|
||||
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> {
|
||||
@@ -1109,6 +1193,8 @@ namespace settings {
|
||||
return nullptr;
|
||||
} else if constexpr (std::is_same_v<T, ListSetting>) {
|
||||
return nullptr;
|
||||
} else if constexpr (std::is_same_v<T, ShortcutListSetting>) {
|
||||
return nullptr;
|
||||
} else if constexpr (std::is_same_v<T, ButtonSetting>) {
|
||||
auto button = std::make_unique<Button>();
|
||||
button->setVariant(ButtonVariant::Outline);
|
||||
@@ -1212,6 +1298,8 @@ namespace settings {
|
||||
} else if (!isBarWidgetListPath(entry.path)) {
|
||||
makeListBlock(*activeSection, entry, *list);
|
||||
}
|
||||
} else if (const auto* shortcuts = std::get_if<ShortcutListSetting>(&entry.control)) {
|
||||
makeShortcutListBlock(*activeSection, entry, *shortcuts);
|
||||
} else if (const auto* picker = std::get_if<SearchPickerSetting>(&entry.control)) {
|
||||
if (ctx.openSearchPickerPath == pathKey(entry.path)) {
|
||||
makeSearchPickerBlock(*activeSection, entry, *picker);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "i18n/i18n.h"
|
||||
#include "render/core/color.h"
|
||||
#include "shell/control_center/shortcut_registry.h"
|
||||
#include "theme/builtin_palettes.h"
|
||||
#include "theme/builtin_templates.h"
|
||||
|
||||
@@ -69,6 +70,15 @@ namespace settings {
|
||||
selected);
|
||||
}
|
||||
|
||||
std::vector<SelectOption> controlCenterShortcutOptions() {
|
||||
std::vector<SelectOption> opts;
|
||||
opts.reserve(ShortcutRegistry::catalog().size());
|
||||
for (const auto& shortcut : ShortcutRegistry::catalog()) {
|
||||
opts.push_back(SelectOption{std::string(shortcut.type), i18n::tr(shortcut.labelKey)});
|
||||
}
|
||||
return opts;
|
||||
}
|
||||
|
||||
SelectSetting languageSelect(std::string_view selected) {
|
||||
std::vector<SelectOption> opts;
|
||||
opts.reserve(i18n::kSupportedLanguages.size() + 1);
|
||||
@@ -202,6 +212,8 @@ namespace settings {
|
||||
return "app-window";
|
||||
if (section == "dock")
|
||||
return "layout-bottombar";
|
||||
if (section == "panels")
|
||||
return "layout-dashboard";
|
||||
if (section == "backdrop")
|
||||
return "niri";
|
||||
if (section == "wallpaper")
|
||||
@@ -518,6 +530,14 @@ namespace settings {
|
||||
tr("settings.schema.dock.pinned-apps.description"), {"dock", "pinned"},
|
||||
ListSetting{.items = cfg.dock.pinned}, "favorites"));
|
||||
|
||||
// Panels
|
||||
entries.push_back(makeEntry(
|
||||
"panels", "control-center", tr("settings.schema.panels.overview-shortcuts.label"),
|
||||
tr("settings.schema.panels.overview-shortcuts.description"), {"control_center", "shortcuts"},
|
||||
ShortcutListSetting{
|
||||
.items = cfg.controlCenter.shortcuts, .suggestedOptions = controlCenterShortcutOptions(), .maxItems = 6},
|
||||
"quick settings shortcuts toggles wifi bluetooth caffeine night light dnd power media weather clipboard"));
|
||||
|
||||
// Desktop
|
||||
entries.push_back(makeEntry("desktop", "widgets", tr("settings.schema.desktop.widgets.label"),
|
||||
tr("settings.schema.desktop.widgets.description"), {"desktop_widgets", "enabled"},
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "config/config_service.h"
|
||||
#include "ui/palette.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
@@ -75,6 +76,12 @@ namespace settings {
|
||||
std::vector<SelectOption> suggestedOptions = {};
|
||||
};
|
||||
|
||||
struct ShortcutListSetting {
|
||||
std::vector<ShortcutConfig> items;
|
||||
std::vector<SelectOption> suggestedOptions = {};
|
||||
std::size_t maxItems = 0;
|
||||
};
|
||||
|
||||
struct ColorSetting {
|
||||
std::string hex; // current resolved value as #RRGGBB; empty when unset
|
||||
bool unset = true;
|
||||
@@ -97,9 +104,9 @@ namespace settings {
|
||||
bool allowNone = false;
|
||||
};
|
||||
|
||||
using SettingControl =
|
||||
std::variant<ToggleSetting, SelectSetting, SliderSetting, TextSetting, OptionalNumberSetting, ListSetting,
|
||||
ColorSetting, MultiSelectSetting, ButtonSetting, ColorRolePickerSetting, SearchPickerSetting>;
|
||||
using SettingControl = std::variant<ToggleSetting, SelectSetting, SliderSetting, TextSetting, OptionalNumberSetting,
|
||||
ListSetting, ShortcutListSetting, ColorSetting, MultiSelectSetting, ButtonSetting,
|
||||
ColorRolePickerSetting, SearchPickerSetting>;
|
||||
|
||||
struct SettingEntry {
|
||||
std::string section;
|
||||
|
||||
@@ -56,6 +56,11 @@ void ListEditor::setScale(float scale) {
|
||||
rebuildRows();
|
||||
}
|
||||
|
||||
void ListEditor::setMaxItems(std::size_t maxItems) {
|
||||
m_maxItems = maxItems;
|
||||
rebuildRows();
|
||||
}
|
||||
|
||||
void ListEditor::setOnAddRequested(std::function<void(std::string)> callback) {
|
||||
m_onAddRequested = std::move(callback);
|
||||
}
|
||||
@@ -135,6 +140,11 @@ void ListEditor::rebuildRows() {
|
||||
addChild(std::move(itemRow));
|
||||
}
|
||||
|
||||
if (m_maxItems > 0 && m_items.size() >= m_maxItems) {
|
||||
markLayoutDirty();
|
||||
return;
|
||||
}
|
||||
|
||||
auto addRow = std::make_unique<Flex>();
|
||||
addRow->setDirection(FlexDirection::Horizontal);
|
||||
addRow->setAlign(FlexAlign::Center);
|
||||
|
||||
@@ -21,6 +21,7 @@ public:
|
||||
void setSuggestedOptions(std::vector<ListEditorOption> options);
|
||||
void setAddPlaceholder(std::string_view placeholder);
|
||||
void setScale(float scale);
|
||||
void setMaxItems(std::size_t maxItems);
|
||||
void setOnAddRequested(std::function<void(std::string)> callback);
|
||||
void setOnRemoveRequested(std::function<void(std::size_t)> callback);
|
||||
void setOnMoveRequested(std::function<void(std::size_t, std::size_t)> callback);
|
||||
@@ -37,6 +38,7 @@ private:
|
||||
std::vector<std::string> m_items;
|
||||
std::vector<ListEditorOption> m_suggestedOptions;
|
||||
std::string m_addPlaceholder;
|
||||
std::size_t m_maxItems = 0;
|
||||
std::function<void(std::string)> m_onAddRequested;
|
||||
std::function<void(std::size_t)> m_onRemoveRequested;
|
||||
std::function<void(std::size_t, std::size_t)> m_onMoveRequested;
|
||||
|
||||
Reference in New Issue
Block a user