mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
1346 lines
57 KiB
C++
1346 lines
57 KiB
C++
#include "shell/settings/settings_content.h"
|
|
|
|
#include "i18n/i18n.h"
|
|
#include "render/core/color.h"
|
|
#include "shell/settings/bar_widget_editor.h"
|
|
#include "ui/controls/box.h"
|
|
#include "ui/controls/button.h"
|
|
#include "ui/controls/checkbox.h"
|
|
#include "ui/controls/flex.h"
|
|
#include "ui/controls/glyph.h"
|
|
#include "ui/controls/input.h"
|
|
#include "ui/controls/label.h"
|
|
#include "ui/controls/list_editor.h"
|
|
#include "ui/controls/search_picker.h"
|
|
#include "ui/controls/segmented.h"
|
|
#include "ui/controls/select.h"
|
|
#include "ui/controls/separator.h"
|
|
#include "ui/controls/slider.h"
|
|
#include "ui/controls/toggle.h"
|
|
#include "ui/dialogs/color_picker_dialog.h"
|
|
#include "ui/palette.h"
|
|
#include "ui/style.h"
|
|
#include "util/string_utils.h"
|
|
|
|
#include <algorithm>
|
|
#include <charconv>
|
|
#include <cmath>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <format>
|
|
#include <functional>
|
|
#include <limits>
|
|
#include <locale>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <type_traits>
|
|
#include <unordered_set>
|
|
#include <utility>
|
|
#include <variant>
|
|
#include <vector>
|
|
|
|
namespace settings {
|
|
namespace {
|
|
|
|
std::unique_ptr<Label> makeLabel(std::string_view text, float fontSize, const ColorSpec& color, bool bold = false) {
|
|
auto label = std::make_unique<Label>();
|
|
label->setText(text);
|
|
label->setFontSize(fontSize);
|
|
label->setColor(color);
|
|
label->setBold(bold);
|
|
return label;
|
|
}
|
|
|
|
std::optional<std::size_t> optionIndex(const std::vector<SelectOption>& options, std::string_view value) {
|
|
for (std::size_t i = 0; i < options.size(); ++i) {
|
|
if (options[i].value == value) {
|
|
return i;
|
|
}
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::string pathKey(const std::vector<std::string>& path) {
|
|
std::string out;
|
|
for (const auto& part : path) {
|
|
if (!out.empty()) {
|
|
out.push_back('.');
|
|
}
|
|
out += part;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
std::string optionLabel(const std::vector<SelectOption>& options, std::string_view value) {
|
|
for (const auto& opt : options) {
|
|
if (opt.value == value) {
|
|
return opt.label;
|
|
}
|
|
}
|
|
return std::string(value);
|
|
}
|
|
|
|
std::vector<std::string> optionLabels(const std::vector<SelectOption>& options) {
|
|
std::vector<std::string> labels;
|
|
labels.reserve(options.size());
|
|
for (const auto& opt : options) {
|
|
labels.push_back(opt.label);
|
|
}
|
|
return labels;
|
|
}
|
|
|
|
std::vector<SearchPickerOption> searchPickerOptions(const std::vector<SelectOption>& options) {
|
|
std::vector<SearchPickerOption> out;
|
|
out.reserve(options.size());
|
|
for (const auto& opt : options) {
|
|
out.push_back(SearchPickerOption{.value = opt.value,
|
|
.label = opt.label,
|
|
.description = opt.description,
|
|
.category = opt.category,
|
|
.enabled = true});
|
|
}
|
|
return out;
|
|
}
|
|
|
|
bool isBlankInput(std::string_view text) { return StringUtils::trim(text).empty(); }
|
|
|
|
const std::string& localeDecimalSeparator() {
|
|
static const std::string separator = [] {
|
|
try {
|
|
const std::locale userLocale("");
|
|
const char decimalPoint = std::use_facet<std::numpunct<char>>(userLocale).decimal_point();
|
|
return std::string(1, decimalPoint);
|
|
} catch (...) {
|
|
return std::string(".");
|
|
}
|
|
}();
|
|
return separator;
|
|
}
|
|
|
|
std::string formatSliderValue(float value, bool integerValue, char decimalSeparator = '\0') {
|
|
if (integerValue) {
|
|
return std::format("{}", static_cast<int>(std::lround(value)));
|
|
}
|
|
std::string formatted = std::format("{:.2f}", value);
|
|
const std::string decimalSep =
|
|
decimalSeparator == '\0' ? localeDecimalSeparator() : std::string(1, decimalSeparator);
|
|
if (decimalSep != ".") {
|
|
std::size_t dotPos = formatted.find('.');
|
|
if (dotPos != std::string::npos) {
|
|
formatted.replace(dotPos, 1, decimalSep);
|
|
}
|
|
}
|
|
return formatted;
|
|
}
|
|
|
|
template <typename T> std::optional<T> parseDecimalInput(std::string_view text) {
|
|
const std::string trimmed = StringUtils::trim(text);
|
|
if (trimmed.empty()) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::string normalized = trimmed;
|
|
std::replace(normalized.begin(), normalized.end(), ',', '.');
|
|
|
|
T value{};
|
|
const char* begin = normalized.data();
|
|
const char* end = begin + normalized.size();
|
|
const auto [ptr, ec] = std::from_chars(begin, end, value, std::chars_format::general);
|
|
if (ec != std::errc{} || ptr != end || !std::isfinite(value)) {
|
|
return std::nullopt;
|
|
}
|
|
if constexpr (std::is_same_v<T, float>) {
|
|
if (value > std::numeric_limits<float>::max() || value < -std::numeric_limits<float>::max()) {
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
return value;
|
|
}
|
|
|
|
std::optional<float> parseFloatInput(std::string_view text) {
|
|
const auto parsed = parseDecimalInput<double>(text);
|
|
if (!parsed.has_value()) {
|
|
return std::nullopt;
|
|
}
|
|
return static_cast<float>(*parsed);
|
|
}
|
|
|
|
std::optional<double> parseDoubleInput(std::string_view text) { return parseDecimalInput<double>(text); }
|
|
|
|
bool isMonitorOverrideSettingPath(const std::vector<std::string>& path) {
|
|
return path.size() >= 5 && path[0] == "bar" && path[2] == "monitor";
|
|
}
|
|
|
|
bool monitorOverrideHasExplicitValue(const Config& cfg, const std::vector<std::string>& path) {
|
|
if (!isMonitorOverrideSettingPath(path)) {
|
|
return false;
|
|
}
|
|
|
|
const auto* bar = findBar(cfg, path[1]);
|
|
if (bar == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
const auto* override = findMonitorOverride(*bar, path[3]);
|
|
if (override == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
const std::string_view key = path.back();
|
|
if (key == "enabled") {
|
|
return override->enabled.has_value();
|
|
}
|
|
if (key == "auto_hide") {
|
|
return override->autoHide.has_value();
|
|
}
|
|
if (key == "reserve_space") {
|
|
return override->reserveSpace.has_value();
|
|
}
|
|
if (key == "thickness") {
|
|
return override->thickness.has_value();
|
|
}
|
|
if (key == "scale") {
|
|
return override->scale.has_value();
|
|
}
|
|
if (key == "margin_ends") {
|
|
return override->marginEnds.has_value();
|
|
}
|
|
if (key == "margin_edge") {
|
|
return override->marginEdge.has_value();
|
|
}
|
|
if (key == "padding") {
|
|
return override->padding.has_value();
|
|
}
|
|
if (key == "radius") {
|
|
return override->radius.has_value();
|
|
}
|
|
if (key == "radius_top_left") {
|
|
return override->radiusTopLeft.has_value();
|
|
}
|
|
if (key == "radius_top_right") {
|
|
return override->radiusTopRight.has_value();
|
|
}
|
|
if (key == "radius_bottom_left") {
|
|
return override->radiusBottomLeft.has_value();
|
|
}
|
|
if (key == "radius_bottom_right") {
|
|
return override->radiusBottomRight.has_value();
|
|
}
|
|
if (key == "background_opacity") {
|
|
return override->backgroundOpacity.has_value();
|
|
}
|
|
if (key == "shadow") {
|
|
return override->shadow.has_value();
|
|
}
|
|
if (key == "widget_spacing") {
|
|
return override->widgetSpacing.has_value();
|
|
}
|
|
if (key == "capsule") {
|
|
return override->widgetCapsuleDefault.has_value();
|
|
}
|
|
if (key == "capsule_fill") {
|
|
return override->widgetCapsuleFill.has_value();
|
|
}
|
|
if (key == "capsule_border") {
|
|
return override->widgetCapsuleBorderSpecified;
|
|
}
|
|
if (key == "capsule_foreground") {
|
|
return override->widgetCapsuleForeground.has_value();
|
|
}
|
|
if (key == "color") {
|
|
return override->widgetColor.has_value();
|
|
}
|
|
if (key == "capsule_groups") {
|
|
return override->widgetCapsuleGroups.has_value();
|
|
}
|
|
if (key == "capsule_padding") {
|
|
return override->widgetCapsulePadding.has_value();
|
|
}
|
|
if (key == "capsule_opacity") {
|
|
return override->widgetCapsuleOpacity.has_value();
|
|
}
|
|
if (key == "start") {
|
|
return override->startWidgets.has_value();
|
|
}
|
|
if (key == "center") {
|
|
return override->centerWidgets.has_value();
|
|
}
|
|
if (key == "end") {
|
|
return override->endWidgets.has_value();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool isBarCapsuleGroupsPath(const std::vector<std::string>& path) {
|
|
return path.size() == 3 && path[0] == "bar" && path[2] == "capsule_groups";
|
|
}
|
|
|
|
bool isMonitorCapsuleGroupsPath(const std::vector<std::string>& path) {
|
|
return path.size() == 5 && path[0] == "bar" && path[2] == "monitor" && path[4] == "capsule_groups";
|
|
}
|
|
|
|
bool isCapsuleGroupsPath(const std::vector<std::string>& path) {
|
|
return isBarCapsuleGroupsPath(path) || isMonitorCapsuleGroupsPath(path);
|
|
}
|
|
|
|
void collectWidgetNames(std::unordered_set<std::string>& widgetNames, const std::vector<std::string>& widgets) {
|
|
for (const auto& widget : widgets) {
|
|
widgetNames.insert(widget);
|
|
}
|
|
}
|
|
|
|
std::unordered_set<std::string> scopedBarWidgetNames(const Config& cfg, const std::vector<std::string>& path) {
|
|
std::unordered_set<std::string> widgetNames;
|
|
|
|
const auto* bar = path.size() >= 2 ? findBar(cfg, path[1]) : nullptr;
|
|
if (bar == nullptr) {
|
|
return widgetNames;
|
|
}
|
|
|
|
if (isBarCapsuleGroupsPath(path)) {
|
|
collectWidgetNames(widgetNames, bar->startWidgets);
|
|
collectWidgetNames(widgetNames, bar->centerWidgets);
|
|
collectWidgetNames(widgetNames, bar->endWidgets);
|
|
for (const auto& ovr : bar->monitorOverrides) {
|
|
collectWidgetNames(widgetNames, ovr.startWidgets.value_or(bar->startWidgets));
|
|
collectWidgetNames(widgetNames, ovr.centerWidgets.value_or(bar->centerWidgets));
|
|
collectWidgetNames(widgetNames, ovr.endWidgets.value_or(bar->endWidgets));
|
|
}
|
|
return widgetNames;
|
|
}
|
|
|
|
const auto* ovr = path.size() >= 4 ? findMonitorOverride(*bar, path[3]) : nullptr;
|
|
if (ovr == nullptr) {
|
|
return widgetNames;
|
|
}
|
|
|
|
collectWidgetNames(widgetNames, ovr->startWidgets.value_or(bar->startWidgets));
|
|
collectWidgetNames(widgetNames, ovr->centerWidgets.value_or(bar->centerWidgets));
|
|
collectWidgetNames(widgetNames, ovr->endWidgets.value_or(bar->endWidgets));
|
|
return widgetNames;
|
|
}
|
|
|
|
std::vector<std::pair<std::vector<std::string>, ConfigOverrideValue>>
|
|
capsuleGroupRemovalOverrides(const Config& cfg, const std::vector<std::string>& path, std::string_view removedGroup,
|
|
std::vector<std::string> updatedGroups) {
|
|
std::vector<std::pair<std::vector<std::string>, ConfigOverrideValue>> overrides;
|
|
overrides.push_back({path, std::move(updatedGroups)});
|
|
|
|
if (!isCapsuleGroupsPath(path)) {
|
|
return overrides;
|
|
}
|
|
|
|
const std::string trimmedRemoved = StringUtils::trim(removedGroup);
|
|
if (trimmedRemoved.empty()) {
|
|
return overrides;
|
|
}
|
|
|
|
for (const auto& widgetName : scopedBarWidgetNames(cfg, path)) {
|
|
const auto widgetIt = cfg.widgets.find(widgetName);
|
|
if (widgetIt == cfg.widgets.end() || !widgetIt->second.hasSetting("capsule_group")) {
|
|
continue;
|
|
}
|
|
if (StringUtils::trim(widgetIt->second.getString("capsule_group", "")) != trimmedRemoved) {
|
|
continue;
|
|
}
|
|
overrides.push_back({{"widget", widgetName, "capsule_group"}, std::string()});
|
|
}
|
|
|
|
return overrides;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
std::size_t addSettingsContentSections(Flex& content, const std::vector<SettingEntry>& registry,
|
|
SettingsContentContext ctx) {
|
|
const Config& cfg = ctx.config;
|
|
const float scale = ctx.scale;
|
|
|
|
const auto sectionLabel = [](std::string_view section) {
|
|
return i18n::tr("settings.navigation.sections." + std::string(section));
|
|
};
|
|
|
|
const auto groupLabel = [](std::string_view group) {
|
|
return i18n::tr("settings.navigation.groups." + std::string(group));
|
|
};
|
|
|
|
const auto makeSection = [&](std::string_view title, std::string_view sectionKey) -> Flex* {
|
|
auto section = std::make_unique<Flex>();
|
|
section->setDirection(FlexDirection::Vertical);
|
|
section->setAlign(FlexAlign::Stretch);
|
|
section->setGap(Style::spaceSm * scale);
|
|
section->setPadding(Style::spaceLg * scale);
|
|
section->setFill(clearColorSpec());
|
|
|
|
auto titleRow = std::make_unique<Flex>();
|
|
titleRow->setDirection(FlexDirection::Horizontal);
|
|
titleRow->setAlign(FlexAlign::Center);
|
|
titleRow->setGap(Style::spaceSm * scale);
|
|
|
|
auto titleGlyph = std::make_unique<Glyph>();
|
|
titleGlyph->setGlyph(sectionGlyph(sectionKey));
|
|
titleGlyph->setGlyphSize(Style::fontSizeHeader * scale);
|
|
titleGlyph->setColor(colorSpecFromRole(ColorRole::Primary));
|
|
titleRow->addChild(std::move(titleGlyph));
|
|
|
|
titleRow->addChild(makeLabel(title, Style::fontSizeHeader * scale, colorSpecFromRole(ColorRole::Primary), true));
|
|
|
|
section->addChild(std::move(titleRow));
|
|
auto* raw = section.get();
|
|
content.addChild(std::move(section));
|
|
return raw;
|
|
};
|
|
|
|
const auto addGroupLabel = [&](Flex& section, std::string_view title, bool isFirst) {
|
|
if (title.empty()) {
|
|
return;
|
|
}
|
|
if (!isFirst) {
|
|
auto groupHeader = std::make_unique<Flex>();
|
|
groupHeader->setDirection(FlexDirection::Vertical);
|
|
groupHeader->setAlign(FlexAlign::Stretch);
|
|
groupHeader->setGap(Style::spaceSm * scale);
|
|
groupHeader->setPadding(Style::spaceSm * scale, 0.0f, 0.0f, 0.0f);
|
|
groupHeader->addChild(std::make_unique<Separator>());
|
|
groupHeader->addChild(
|
|
makeLabel(title, Style::fontSizeBody * scale, colorSpecFromRole(ColorRole::OnSurfaceVariant), true));
|
|
section.addChild(std::move(groupHeader));
|
|
} else {
|
|
section.addChild(
|
|
makeLabel(title, Style::fontSizeBody * scale, colorSpecFromRole(ColorRole::OnSurfaceVariant), true));
|
|
}
|
|
};
|
|
|
|
const auto makeResetButton = [&](const std::vector<std::string>& path) {
|
|
auto reset = std::make_unique<Button>();
|
|
reset->setText(i18n::tr("settings.actions.reset"));
|
|
reset->setVariant(ButtonVariant::Ghost);
|
|
reset->setFontSize(Style::fontSizeCaption * scale);
|
|
reset->setMinHeight(Style::controlHeightSm * scale);
|
|
reset->setPadding(Style::spaceXs * scale, Style::spaceSm * scale);
|
|
reset->setRadius(Style::radiusMd * scale);
|
|
reset->setOnClick([clearOverride = ctx.clearOverride, path]() { clearOverride(path); });
|
|
return reset;
|
|
};
|
|
|
|
const auto makeRow = [&](Flex& section, const SettingEntry& entry, std::unique_ptr<Node> control) {
|
|
const bool overridden = (ctx.configService != nullptr && ctx.configService->hasEffectiveOverride(entry.path));
|
|
const bool redundantGuiOverride =
|
|
ctx.configService != nullptr && ctx.configService->hasOverride(entry.path) && !overridden;
|
|
const bool monitorSetting = isMonitorOverrideSettingPath(entry.path);
|
|
const bool monitorExplicit = monitorOverrideHasExplicitValue(cfg, entry.path) && !redundantGuiOverride;
|
|
const bool monitorInherited = monitorSetting && !monitorExplicit;
|
|
|
|
auto row = std::make_unique<Flex>();
|
|
row->setDirection(FlexDirection::Horizontal);
|
|
row->setAlign(FlexAlign::Center);
|
|
row->setJustify(FlexJustify::SpaceBetween);
|
|
row->setGap(Style::spaceXs * scale);
|
|
row->setPadding(2.0f * scale, 0.0f);
|
|
row->setMinHeight(Style::controlHeight * scale);
|
|
|
|
auto copy = std::make_unique<Flex>();
|
|
copy->setDirection(FlexDirection::Vertical);
|
|
copy->setAlign(FlexAlign::Start);
|
|
copy->setGap(Style::spaceXs * scale);
|
|
copy->setFlexGrow(1.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));
|
|
|
|
const auto makeBadge = [&](std::string_view label, const ColorSpec& fill, const ColorSpec& color) {
|
|
auto badge = std::make_unique<Flex>();
|
|
badge->setAlign(FlexAlign::Center);
|
|
badge->setPadding(0, Style::spaceXs * scale);
|
|
badge->setRadius(Style::radiusSm * scale);
|
|
badge->setFill(fill);
|
|
badge->addChild(makeLabel(label, Style::fontSizeCaption * scale, color, true));
|
|
return badge;
|
|
};
|
|
|
|
if (monitorExplicit) {
|
|
titleRow->addChild(makeBadge(i18n::tr("settings.badges.monitor"),
|
|
colorSpecFromRole(ColorRole::Secondary, 0.15f),
|
|
colorSpecFromRole(ColorRole::Secondary)));
|
|
} else if (monitorInherited) {
|
|
titleRow->addChild(makeBadge(i18n::tr("settings.badges.inherited"),
|
|
colorSpecFromRole(ColorRole::OnSurfaceVariant, 0.12f),
|
|
colorSpecFromRole(ColorRole::OnSurfaceVariant)));
|
|
}
|
|
if (overridden) {
|
|
titleRow->addChild(makeBadge(i18n::tr("settings.badges.override"), colorSpecFromRole(ColorRole::Primary, 0.15f),
|
|
colorSpecFromRole(ColorRole::Primary)));
|
|
}
|
|
if (entry.advanced) {
|
|
titleRow->addChild(makeBadge(i18n::tr("settings.badges.advanced"),
|
|
colorSpecFromRole(ColorRole::OnSurfaceVariant, 0.12f),
|
|
colorSpecFromRole(ColorRole::OnSurfaceVariant)));
|
|
}
|
|
copy->addChild(std::move(titleRow));
|
|
|
|
if (!entry.subtitle.empty()) {
|
|
auto detail = makeLabel(entry.subtitle, Style::fontSizeCaption * scale,
|
|
colorSpecFromRole(ColorRole::OnSurfaceVariant), false);
|
|
copy->addChild(std::move(detail));
|
|
}
|
|
|
|
row->addChild(std::move(copy));
|
|
|
|
auto actions = std::make_unique<Flex>();
|
|
actions->setDirection(FlexDirection::Horizontal);
|
|
actions->setAlign(FlexAlign::Center);
|
|
actions->setGap(Style::spaceSm * scale);
|
|
if (overridden) {
|
|
actions->addChild(makeResetButton(entry.path));
|
|
}
|
|
actions->addChild(std::move(control));
|
|
row->addChild(std::move(actions));
|
|
|
|
section.addChild(std::move(row));
|
|
};
|
|
|
|
const auto makeToggle = [&](bool checked, bool enabled, std::vector<std::string> path) {
|
|
auto toggle = std::make_unique<Toggle>();
|
|
toggle->setScale(scale);
|
|
toggle->setChecked(checked);
|
|
toggle->setEnabled(enabled);
|
|
if (enabled) {
|
|
toggle->setOnChange([setOverride = ctx.setOverride, path](bool value) { setOverride(path, value); });
|
|
}
|
|
return toggle;
|
|
};
|
|
|
|
const auto makeSelect = [&](const SelectSetting& setting, std::vector<std::string> path) -> std::unique_ptr<Node> {
|
|
if (setting.segmented) {
|
|
auto segmented = std::make_unique<Segmented>();
|
|
segmented->setScale(scale);
|
|
for (const auto& opt : setting.options) {
|
|
segmented->addOption(opt.label);
|
|
}
|
|
if (const auto index = optionIndex(setting.options, setting.selectedValue)) {
|
|
segmented->setSelectedIndex(*index);
|
|
}
|
|
auto options = setting.options;
|
|
segmented->setOnChange([setOverride = ctx.setOverride, path, options](std::size_t index) {
|
|
if (index < options.size()) {
|
|
setOverride(path, options[index].value);
|
|
}
|
|
});
|
|
return segmented;
|
|
}
|
|
|
|
auto select = std::make_unique<Select>();
|
|
select->setOptions(optionLabels(setting.options));
|
|
if (const auto index = optionIndex(setting.options, setting.selectedValue)) {
|
|
select->setSelectedIndex(*index);
|
|
} else if (!setting.selectedValue.empty()) {
|
|
select->clearSelection();
|
|
select->setPlaceholder(i18n::tr("settings.controls.select.unknown-value", "value", setting.selectedValue));
|
|
}
|
|
select->setFontSize(Style::fontSizeBody * scale);
|
|
select->setControlHeight(Style::controlHeight * scale);
|
|
select->setGlyphSize(Style::fontSizeBody * scale);
|
|
select->setSize(190.0f * scale, Style::controlHeight * scale);
|
|
auto options = setting.options;
|
|
const bool clearOnEmpty = setting.clearOnEmpty;
|
|
select->setOnSelectionChanged([configService = ctx.configService, clearOverride = ctx.clearOverride,
|
|
setOverride = ctx.setOverride, requestRebuild = ctx.requestRebuild, path, options,
|
|
clearOnEmpty](std::size_t index, std::string_view /*label*/) {
|
|
if (index < options.size()) {
|
|
if (clearOnEmpty && options[index].value.empty()) {
|
|
if (configService != nullptr && configService->hasOverride(path)) {
|
|
clearOverride(path);
|
|
} else {
|
|
requestRebuild();
|
|
}
|
|
return;
|
|
}
|
|
setOverride(path, options[index].value);
|
|
}
|
|
});
|
|
return select;
|
|
};
|
|
|
|
const auto makeSlider =
|
|
[&](float value, float minValue, float maxValue, float step, std::vector<std::string> path,
|
|
bool integerValue = false,
|
|
std::function<std::vector<std::pair<std::vector<std::string>, ConfigOverrideValue>>(double)> linkedCommit =
|
|
{}) {
|
|
auto wrap = std::make_unique<Flex>();
|
|
wrap->setDirection(FlexDirection::Horizontal);
|
|
wrap->setAlign(FlexAlign::Center);
|
|
wrap->setGap(Style::spaceSm * scale);
|
|
|
|
auto valueInput = std::make_unique<Input>();
|
|
valueInput->setValue(formatSliderValue(value, integerValue));
|
|
valueInput->setFontSize(Style::fontSizeCaption * scale);
|
|
valueInput->setControlHeight(Style::controlHeightSm * scale);
|
|
valueInput->setHorizontalPadding(Style::spaceXs * scale);
|
|
valueInput->setSize(50.0f * scale, Style::controlHeightSm * scale);
|
|
auto* valueInputPtr = valueInput.get();
|
|
|
|
auto slider = std::make_unique<Slider>();
|
|
slider->setRange(minValue, maxValue);
|
|
slider->setStep(step);
|
|
slider->setSize(Style::sliderDefaultWidth * scale, Style::controlHeight * scale);
|
|
slider->setControlHeight(Style::controlHeight * scale);
|
|
slider->setThumbSize(Style::sliderThumbSize * scale);
|
|
slider->setTrackHeight(Style::sliderTrackHeight * scale);
|
|
slider->setValue(value);
|
|
auto* sliderPtr = slider.get();
|
|
slider->setOnValueChanged([valueInputPtr, integerValue](float next) {
|
|
valueInputPtr->setInvalid(false);
|
|
valueInputPtr->setValue(formatSliderValue(next, integerValue));
|
|
});
|
|
|
|
// Helper: commit either via single setOverride or as an atomic batch when linkedCommit
|
|
// returns extra overrides (cross-field constraints).
|
|
const auto commit = [setOverride = ctx.setOverride, setOverrides = ctx.setOverrides, path, integerValue,
|
|
linkedCommit](double v) {
|
|
ConfigOverrideValue primary =
|
|
integerValue ? ConfigOverrideValue{static_cast<std::int64_t>(std::lround(v))} : ConfigOverrideValue{v};
|
|
if (linkedCommit) {
|
|
auto extras = linkedCommit(v);
|
|
if (!extras.empty()) {
|
|
std::vector<std::pair<std::vector<std::string>, ConfigOverrideValue>> all;
|
|
all.reserve(extras.size() + 1);
|
|
all.emplace_back(path, std::move(primary));
|
|
for (auto& e : extras) {
|
|
all.push_back(std::move(e));
|
|
}
|
|
setOverrides(std::move(all));
|
|
return;
|
|
}
|
|
}
|
|
setOverride(path, std::move(primary));
|
|
};
|
|
|
|
slider->setOnDragEnd([commit, sliderPtr]() { commit(static_cast<double>(sliderPtr->value())); });
|
|
|
|
valueInput->setOnChange([valueInputPtr](const std::string& /*text*/) { valueInputPtr->setInvalid(false); });
|
|
valueInput->setOnSubmit(
|
|
[commit, sliderPtr, valueInputPtr, minValue, maxValue, integerValue](const std::string& text) {
|
|
const auto parsed = parseFloatInput(text);
|
|
if (!parsed.has_value() || *parsed < minValue || *parsed > maxValue) {
|
|
valueInputPtr->setInvalid(true);
|
|
return;
|
|
}
|
|
const float v = *parsed;
|
|
valueInputPtr->setInvalid(false);
|
|
sliderPtr->setValue(v);
|
|
if (!integerValue) {
|
|
const std::string trimmed = StringUtils::trim(text);
|
|
const char preferredSeparator = trimmed.find(',') != std::string::npos ? ',' : '.';
|
|
valueInputPtr->setValue(formatSliderValue(sliderPtr->value(), false, preferredSeparator));
|
|
}
|
|
commit(static_cast<double>(v));
|
|
});
|
|
|
|
// Slider first, numeric value field on the right (reset from makeRow stays left of this cluster).
|
|
wrap->addChild(std::move(slider));
|
|
wrap->addChild(std::move(valueInput));
|
|
return wrap;
|
|
};
|
|
|
|
const auto makeText = [&](const std::string& value, const std::string& placeholder, std::vector<std::string> path) {
|
|
auto input = std::make_unique<Input>();
|
|
input->setValue(value);
|
|
input->setPlaceholder(placeholder.empty() ? i18n::tr("settings.controls.list.add-entry-placeholder")
|
|
: placeholder);
|
|
input->setFontSize(Style::fontSizeBody * scale);
|
|
input->setControlHeight(Style::controlHeight * scale);
|
|
input->setHorizontalPadding(Style::spaceSm * scale);
|
|
input->setSize(190.0f * scale, Style::controlHeight * scale);
|
|
input->setOnSubmit([setOverride = ctx.setOverride, path](const std::string& v) { setOverride(path, v); });
|
|
return input;
|
|
};
|
|
|
|
const auto makeOptionalNumber = [&](const OptionalNumberSetting& setting, std::vector<std::string> path) {
|
|
auto input = std::make_unique<Input>();
|
|
input->setValue(setting.value.has_value() ? std::format("{}", *setting.value) : "");
|
|
input->setPlaceholder(setting.placeholder);
|
|
input->setFontSize(Style::fontSizeBody * scale);
|
|
input->setControlHeight(Style::controlHeight * scale);
|
|
input->setHorizontalPadding(Style::spaceSm * scale);
|
|
input->setSize(190.0f * scale, Style::controlHeight * scale);
|
|
auto* inputPtr = input.get();
|
|
input->setOnChange([inputPtr](const std::string& /*text*/) { inputPtr->setInvalid(false); });
|
|
input->setOnSubmit([configService = ctx.configService, clearOverride = ctx.clearOverride,
|
|
setOverride = ctx.setOverride, path, inputPtr, minValue = setting.minValue,
|
|
maxValue = setting.maxValue](const std::string& text) {
|
|
if (isBlankInput(text)) {
|
|
inputPtr->setInvalid(false);
|
|
if (configService != nullptr && configService->hasOverride(path)) {
|
|
clearOverride(path);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const auto parsed = parseDoubleInput(text);
|
|
if (!parsed.has_value() || *parsed < minValue || *parsed > maxValue) {
|
|
inputPtr->setInvalid(true);
|
|
return;
|
|
}
|
|
|
|
inputPtr->setInvalid(false);
|
|
setOverride(path, *parsed);
|
|
});
|
|
return input;
|
|
};
|
|
|
|
const auto makeColor = [&](const ColorSetting& setting, std::vector<std::string> path) {
|
|
auto wrap = std::make_unique<Flex>();
|
|
wrap->setDirection(FlexDirection::Horizontal);
|
|
wrap->setAlign(FlexAlign::Center);
|
|
wrap->setGap(Style::spaceSm * scale);
|
|
|
|
const float swatchSize = Style::controlHeight * scale;
|
|
auto swatch = std::make_unique<Box>();
|
|
swatch->setSize(swatchSize, swatchSize);
|
|
swatch->setRadius(Style::radiusSm * scale);
|
|
swatch->setBorder(colorSpecFromRole(ColorRole::Outline), 1.0f);
|
|
Color initialColor;
|
|
const bool hasColor = !setting.unset && tryParseHexColor(setting.hex, initialColor);
|
|
if (hasColor) {
|
|
swatch->setFill(initialColor);
|
|
} else {
|
|
swatch->setFill(colorSpecFromRole(ColorRole::SurfaceVariant));
|
|
}
|
|
|
|
auto button = std::make_unique<Button>();
|
|
button->setVariant(ButtonVariant::Outline);
|
|
button->setText(setting.unset ? i18n::tr("settings.options.theme-role.default") : setting.hex);
|
|
button->setFontSize(Style::fontSizeBody * scale);
|
|
button->setMinHeight(Style::controlHeight * scale);
|
|
button->setPadding(Style::spaceSm * scale, Style::spaceMd * scale);
|
|
button->setRadius(Style::radiusMd * scale);
|
|
const std::optional<Color> initialOpt = hasColor ? std::optional<Color>{initialColor} : std::nullopt;
|
|
const std::string title = i18n::tr("settings.dialogs.color-picker.title");
|
|
button->setOnClick([setOverride = ctx.setOverride, path, initialOpt, title]() {
|
|
ColorPickerDialogOptions options;
|
|
options.title = title;
|
|
if (initialOpt.has_value()) {
|
|
options.initialColor = *initialOpt;
|
|
} else if (const auto last = ColorPickerDialog::lastResult()) {
|
|
options.initialColor = *last;
|
|
}
|
|
(void)ColorPickerDialog::open(std::move(options), [setOverride, path](std::optional<Color> result) {
|
|
if (!result.has_value()) {
|
|
return;
|
|
}
|
|
Color rgb = *result;
|
|
rgb.a = 1.0f;
|
|
setOverride(path, formatRgbHex(rgb));
|
|
});
|
|
});
|
|
|
|
wrap->addChild(std::move(swatch));
|
|
wrap->addChild(std::move(button));
|
|
return wrap;
|
|
};
|
|
|
|
const auto makeColorRolePicker = [&](const ColorRolePickerSetting& setting,
|
|
std::vector<std::string> path) -> std::unique_ptr<Node> {
|
|
std::vector<SelectOption> opts;
|
|
opts.reserve(setting.roles.size() + (setting.allowNone ? 1 : 0));
|
|
std::vector<ColorSpec> indicators;
|
|
indicators.reserve(setting.roles.size() + (setting.allowNone ? 1 : 0));
|
|
|
|
if (setting.allowNone) {
|
|
opts.push_back(SelectOption{"", i18n::tr("settings.options.theme-role.default")});
|
|
indicators.push_back(clearColorSpec());
|
|
}
|
|
for (const auto role : setting.roles) {
|
|
opts.push_back(SelectOption{std::string(colorRoleToken(role)), std::string(colorRoleToken(role))});
|
|
indicators.push_back(colorSpecFromRole(role));
|
|
}
|
|
|
|
SelectSetting selectSetting{std::move(opts), setting.selectedValue, setting.allowNone};
|
|
auto select = makeSelect(selectSetting, std::move(path));
|
|
|
|
if (auto* sel = dynamic_cast<Select*>(select.get())) {
|
|
sel->setOptionIndicators(std::move(indicators));
|
|
}
|
|
|
|
return select;
|
|
};
|
|
|
|
const auto makeSearchPickerButton = [&](const SettingEntry& entry,
|
|
const SearchPickerSetting& setting) -> std::unique_ptr<Node> {
|
|
auto button = std::make_unique<Button>();
|
|
button->setVariant(ButtonVariant::Outline);
|
|
button->setGlyph("search");
|
|
button->setText(optionLabel(setting.options, setting.selectedValue));
|
|
button->setContentAlign(ButtonContentAlign::Start);
|
|
button->setFontSize(Style::fontSizeBody * scale);
|
|
button->setGlyphSize(Style::fontSizeBody * scale);
|
|
button->setMinWidth(190.0f * scale);
|
|
button->setMinHeight(Style::controlHeight * scale);
|
|
button->setPadding(Style::spaceSm * scale, Style::spaceMd * scale);
|
|
button->setRadius(Style::radiusMd * scale);
|
|
button->setOnClick([&openPath = ctx.openSearchPickerPath, requestContentRebuild = ctx.requestContentRebuild,
|
|
path = entry.path]() {
|
|
openPath = pathKey(path);
|
|
requestContentRebuild();
|
|
});
|
|
return button;
|
|
};
|
|
|
|
const auto makeSearchPickerBlock = [&](Flex& section, const SettingEntry& entry,
|
|
const SearchPickerSetting& setting) {
|
|
const bool overridden = (ctx.configService != nullptr && ctx.configService->hasEffectiveOverride(entry.path));
|
|
const std::string pickerPath = pathKey(entry.path);
|
|
|
|
auto block = std::make_unique<Flex>();
|
|
block->setDirection(FlexDirection::Vertical);
|
|
block->setAlign(FlexAlign::Stretch);
|
|
block->setGap(Style::spaceXs * scale);
|
|
|
|
const auto makeBadge = [&](std::string_view label, const ColorSpec& fill, const ColorSpec& color) {
|
|
auto badge = std::make_unique<Flex>();
|
|
badge->setAlign(FlexAlign::Center);
|
|
badge->setPadding(0, Style::spaceXs * scale);
|
|
badge->setRadius(Style::radiusSm * scale);
|
|
badge->setFill(fill);
|
|
badge->addChild(makeLabel(label, Style::fontSizeCaption * scale, color, true));
|
|
return badge;
|
|
};
|
|
|
|
auto headerRow = std::make_unique<Flex>();
|
|
headerRow->setDirection(FlexDirection::Horizontal);
|
|
headerRow->setAlign(FlexAlign::Center);
|
|
headerRow->setJustify(FlexJustify::SpaceBetween);
|
|
headerRow->setGap(Style::spaceXs * scale);
|
|
headerRow->setPadding(2.0f * scale, 0.0f);
|
|
headerRow->setMinHeight(Style::controlHeight * scale);
|
|
|
|
auto copy = std::make_unique<Flex>();
|
|
copy->setDirection(FlexDirection::Vertical);
|
|
copy->setAlign(FlexAlign::Start);
|
|
copy->setGap(Style::spaceXs * scale);
|
|
copy->setFlexGrow(1.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) {
|
|
titleRow->addChild(makeBadge(i18n::tr("settings.badges.override"), colorSpecFromRole(ColorRole::Primary, 0.15f),
|
|
colorSpecFromRole(ColorRole::Primary)));
|
|
}
|
|
if (entry.advanced) {
|
|
titleRow->addChild(makeBadge(i18n::tr("settings.badges.advanced"),
|
|
colorSpecFromRole(ColorRole::OnSurfaceVariant, 0.12f),
|
|
colorSpecFromRole(ColorRole::OnSurfaceVariant)));
|
|
}
|
|
copy->addChild(std::move(titleRow));
|
|
|
|
if (!entry.subtitle.empty()) {
|
|
auto detail = makeLabel(entry.subtitle, Style::fontSizeCaption * scale,
|
|
colorSpecFromRole(ColorRole::OnSurfaceVariant), false);
|
|
copy->addChild(std::move(detail));
|
|
}
|
|
|
|
headerRow->addChild(std::move(copy));
|
|
|
|
auto actions = std::make_unique<Flex>();
|
|
actions->setDirection(FlexDirection::Horizontal);
|
|
actions->setAlign(FlexAlign::Center);
|
|
actions->setGap(Style::spaceSm * scale);
|
|
if (overridden) {
|
|
actions->addChild(makeResetButton(entry.path));
|
|
}
|
|
|
|
auto closeBtn = std::make_unique<Button>();
|
|
closeBtn->setVariant(ButtonVariant::Ghost);
|
|
closeBtn->setGlyph("close");
|
|
closeBtn->setGlyphSize(Style::fontSizeCaption * scale);
|
|
closeBtn->setMinWidth(Style::controlHeightSm * scale);
|
|
closeBtn->setMinHeight(Style::controlHeightSm * scale);
|
|
closeBtn->setPadding(Style::spaceXs * scale);
|
|
closeBtn->setRadius(Style::radiusSm * scale);
|
|
closeBtn->setOnClick([&openPath = ctx.openSearchPickerPath, requestContentRebuild = ctx.requestContentRebuild]() {
|
|
openPath.clear();
|
|
requestContentRebuild();
|
|
});
|
|
actions->addChild(std::move(closeBtn));
|
|
headerRow->addChild(std::move(actions));
|
|
block->addChild(std::move(headerRow));
|
|
|
|
auto picker = std::make_unique<SearchPicker>();
|
|
if (!setting.placeholder.empty()) {
|
|
picker->setPlaceholder(setting.placeholder);
|
|
}
|
|
if (!setting.emptyText.empty()) {
|
|
picker->setEmptyText(setting.emptyText);
|
|
}
|
|
picker->setOptions(searchPickerOptions(setting.options));
|
|
picker->setSelectedValue(setting.selectedValue);
|
|
picker->setSize(0.0f, setting.preferredHeight * scale);
|
|
picker->setFillWidth(true);
|
|
auto* pickerPtr = picker.get();
|
|
picker->setOnActivated([&openPath = ctx.openSearchPickerPath, requestContentRebuild = ctx.requestContentRebuild,
|
|
setOverride = ctx.setOverride, path = entry.path,
|
|
selectedValue = setting.selectedValue](const SearchPickerOption& option) {
|
|
if (option.value.empty()) {
|
|
return;
|
|
}
|
|
openPath.clear();
|
|
if (option.value == selectedValue) {
|
|
requestContentRebuild();
|
|
return;
|
|
}
|
|
setOverride(path, option.value);
|
|
});
|
|
picker->setOnCancel([&openPath = ctx.openSearchPickerPath, requestContentRebuild = ctx.requestContentRebuild]() {
|
|
openPath.clear();
|
|
requestContentRebuild();
|
|
});
|
|
block->addChild(std::move(picker));
|
|
|
|
section.addChild(std::move(block));
|
|
if (ctx.openSearchPickerPath == pickerPath && ctx.focusArea) {
|
|
ctx.focusArea(pickerPtr->filterInputArea());
|
|
}
|
|
};
|
|
|
|
const auto makeMultiSelectBlock = [&](Flex& section, const SettingEntry& entry, const MultiSelectSetting& setting) {
|
|
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));
|
|
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));
|
|
}
|
|
|
|
auto checkRow = std::make_unique<Flex>();
|
|
checkRow->setDirection(FlexDirection::Horizontal);
|
|
checkRow->setAlign(FlexAlign::Center);
|
|
checkRow->setGap(Style::spaceMd * scale);
|
|
checkRow->setPadding(Style::spaceXs * scale, 0.0f);
|
|
|
|
auto options = setting.options;
|
|
auto selected = setting.selectedValues;
|
|
const bool requireAtLeastOne = setting.requireAtLeastOne;
|
|
auto path = entry.path;
|
|
|
|
for (const auto& option : options) {
|
|
auto item = std::make_unique<Flex>();
|
|
item->setDirection(FlexDirection::Horizontal);
|
|
item->setAlign(FlexAlign::Center);
|
|
item->setGap(Style::spaceXs * scale);
|
|
|
|
auto checkbox = std::make_unique<Checkbox>();
|
|
checkbox->setScale(scale);
|
|
const bool isSelected = std::find(selected.begin(), selected.end(), option.value) != selected.end();
|
|
checkbox->setChecked(isSelected);
|
|
const std::string optionValue = option.value;
|
|
checkbox->setOnChange([setOverride = ctx.setOverride, requestRebuild = ctx.requestRebuild, path, options,
|
|
selected, optionValue, requireAtLeastOne](bool checked) mutable {
|
|
auto it = std::find(selected.begin(), selected.end(), optionValue);
|
|
if (checked) {
|
|
if (it == selected.end()) {
|
|
selected.push_back(optionValue);
|
|
}
|
|
} else {
|
|
if (it != selected.end()) {
|
|
if (requireAtLeastOne && selected.size() <= 1) {
|
|
requestRebuild();
|
|
return;
|
|
}
|
|
selected.erase(it);
|
|
}
|
|
}
|
|
// Preserve the option order so the override file is stable.
|
|
std::vector<std::string> ordered;
|
|
ordered.reserve(selected.size());
|
|
for (const auto& opt : options) {
|
|
if (std::find(selected.begin(), selected.end(), opt.value) != selected.end()) {
|
|
ordered.push_back(opt.value);
|
|
}
|
|
}
|
|
setOverride(path, ordered);
|
|
});
|
|
item->addChild(std::move(checkbox));
|
|
item->addChild(
|
|
makeLabel(option.label, Style::fontSizeBody * scale, colorSpecFromRole(ColorRole::OnSurface), false));
|
|
|
|
checkRow->addChild(std::move(item));
|
|
}
|
|
|
|
block->addChild(std::move(checkRow));
|
|
section.addChild(std::move(block));
|
|
};
|
|
|
|
const auto makeListBlock = [&](Flex& section, const SettingEntry& entry, const ListSetting& list) {
|
|
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));
|
|
}
|
|
|
|
auto listEditor = std::make_unique<ListEditor>();
|
|
listEditor->setScale(scale);
|
|
listEditor->setAddPlaceholder(i18n::tr("settings.controls.list.add-entry-placeholder"));
|
|
std::vector<ListEditorOption> suggestedOptions;
|
|
suggestedOptions.reserve(list.suggestedOptions.size());
|
|
for (const auto& opt : list.suggestedOptions) {
|
|
suggestedOptions.push_back(ListEditorOption{.value = opt.value, .label = opt.label});
|
|
}
|
|
listEditor->setSuggestedOptions(std::move(suggestedOptions));
|
|
listEditor->setItems(list.items);
|
|
listEditor->setOnAddRequested(
|
|
[setOverride = ctx.setOverride, items = list.items, path = entry.path](std::string value) mutable {
|
|
if (value.empty()) {
|
|
return;
|
|
}
|
|
items.push_back(std::move(value));
|
|
setOverride(path, items);
|
|
});
|
|
listEditor->setOnRemoveRequested([setOverride = ctx.setOverride, setOverrides = ctx.setOverrides,
|
|
config = std::cref(cfg), items = list.items,
|
|
path = entry.path](std::size_t index) mutable {
|
|
if (index >= items.size()) {
|
|
return;
|
|
}
|
|
const std::string removedItem = items[index];
|
|
items.erase(items.begin() + static_cast<std::ptrdiff_t>(index));
|
|
const auto overrides = capsuleGroupRemovalOverrides(config.get(), path, removedItem, items);
|
|
if (overrides.size() == 1) {
|
|
setOverride(path, items);
|
|
return;
|
|
}
|
|
setOverrides(overrides);
|
|
});
|
|
listEditor->setOnMoveRequested([setOverride = ctx.setOverride, items = list.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 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> {
|
|
using T = std::decay_t<decltype(control)>;
|
|
if constexpr (std::is_same_v<T, ToggleSetting>) {
|
|
return makeToggle(control.checked, control.enabled, entry.path);
|
|
} else if constexpr (std::is_same_v<T, SelectSetting>) {
|
|
return makeSelect(control, entry.path);
|
|
} else if constexpr (std::is_same_v<T, SliderSetting>) {
|
|
return makeSlider(control.value, control.minValue, control.maxValue, control.step, entry.path,
|
|
control.integerValue, control.linkedCommit);
|
|
} else if constexpr (std::is_same_v<T, TextSetting>) {
|
|
return makeText(control.value, control.placeholder, entry.path);
|
|
} else if constexpr (std::is_same_v<T, OptionalNumberSetting>) {
|
|
return makeOptionalNumber(control, entry.path);
|
|
} else if constexpr (std::is_same_v<T, ColorSetting>) {
|
|
return makeColor(control, entry.path);
|
|
} else if constexpr (std::is_same_v<T, SearchPickerSetting>) {
|
|
return nullptr;
|
|
} else if constexpr (std::is_same_v<T, MultiSelectSetting>) {
|
|
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);
|
|
button->setText(control.label);
|
|
button->setFontSize(Style::fontSizeBody * scale);
|
|
button->setMinHeight(Style::controlHeight * scale);
|
|
button->setPadding(Style::spaceSm * scale, Style::spaceMd * scale);
|
|
button->setRadius(Style::radiusMd * scale);
|
|
button->setOnClick(control.action);
|
|
return button;
|
|
} else if constexpr (std::is_same_v<T, ColorRolePickerSetting>) {
|
|
return makeColorRolePicker(control, entry.path);
|
|
}
|
|
},
|
|
entry.control);
|
|
};
|
|
|
|
std::string activeSectionKey;
|
|
std::string activeGroupKey;
|
|
Flex* activeSection = nullptr;
|
|
std::size_t visibleEntries = 0;
|
|
const std::string normalizedSearchQuery = normalizedSettingQuery(ctx.searchQuery);
|
|
|
|
BarWidgetEditorContext barWidgetEditorCtx{
|
|
.config = cfg,
|
|
.configService = ctx.configService,
|
|
.scale = scale,
|
|
.showAdvanced = ctx.showAdvanced,
|
|
.showOverriddenOnly = ctx.showOverriddenOnly,
|
|
.openWidgetPickerPath = ctx.openWidgetPickerPath,
|
|
.editingWidgetName = ctx.editingWidgetName,
|
|
.pendingDeleteWidgetName = ctx.pendingDeleteWidgetName,
|
|
.pendingDeleteWidgetSettingPath = ctx.pendingDeleteWidgetSettingPath,
|
|
.renamingWidgetName = ctx.renamingWidgetName,
|
|
.creatingWidgetType = ctx.creatingWidgetType,
|
|
.requestRebuild = ctx.requestRebuild,
|
|
.resetContentScroll = ctx.resetContentScroll,
|
|
.focusArea = ctx.focusArea,
|
|
.setOverride = ctx.setOverride,
|
|
.setOverrides = ctx.setOverrides,
|
|
.clearOverride = ctx.clearOverride,
|
|
.renameWidgetInstance = ctx.renameWidgetInstance,
|
|
.makeResetButton = makeResetButton,
|
|
.makeRow = makeRow,
|
|
.makeToggle = [&](bool checked, std::vector<std::string> path) -> std::unique_ptr<Node> {
|
|
return makeToggle(checked, true, std::move(path));
|
|
},
|
|
.makeSelect = [&](const SelectSetting& setting, std::vector<std::string> path) -> std::unique_ptr<Node> {
|
|
return makeSelect(setting, std::move(path));
|
|
},
|
|
.makeSlider = [&](float value, float minValue, float maxValue, float step, std::vector<std::string> path,
|
|
bool integerValue) -> std::unique_ptr<Node> {
|
|
return makeSlider(value, minValue, maxValue, step, std::move(path), integerValue);
|
|
},
|
|
.makeText = [&](const std::string& value, const std::string& placeholder, std::vector<std::string> path)
|
|
-> std::unique_ptr<Node> { return makeText(value, placeholder, std::move(path)); },
|
|
.makeColorRolePicker = [&](const ColorRolePickerSetting& setting, std::vector<std::string> path)
|
|
-> std::unique_ptr<Node> { return makeColorRolePicker(setting, std::move(path)); },
|
|
.makeListBlock = [&](Flex& section, const SettingEntry& entry,
|
|
const ListSetting& list) { makeListBlock(section, entry, list); },
|
|
};
|
|
|
|
for (const auto& entry : registry) {
|
|
if (ctx.searchQuery.empty() && !ctx.selectedSection.empty() && entry.section != ctx.selectedSection) {
|
|
continue;
|
|
}
|
|
if (!ctx.showAdvanced && entry.advanced) {
|
|
continue;
|
|
}
|
|
if (ctx.showOverriddenOnly && ctx.configService != nullptr &&
|
|
!ctx.configService->hasEffectiveOverride(entry.path)) {
|
|
continue;
|
|
}
|
|
if (!matchesNormalizedSettingQuery(entry, normalizedSearchQuery)) {
|
|
continue;
|
|
}
|
|
|
|
if (entry.section != activeSectionKey) {
|
|
activeSectionKey = entry.section;
|
|
activeGroupKey.clear();
|
|
std::string displayTitle;
|
|
if (entry.section == "bar" && ctx.selectedBar != nullptr) {
|
|
displayTitle = i18n::tr("settings.entities.bar.label", "name", ctx.selectedBar->name);
|
|
if (ctx.selectedMonitorOverride != nullptr) {
|
|
displayTitle += " / " + ctx.selectedMonitorOverride->match;
|
|
}
|
|
} else {
|
|
displayTitle = sectionLabel(entry.section);
|
|
}
|
|
activeSection = makeSection(displayTitle, entry.section);
|
|
}
|
|
if (activeSection != nullptr) {
|
|
if (entry.group != activeGroupKey) {
|
|
const bool isFirstGroup = activeGroupKey.empty();
|
|
activeGroupKey = entry.group;
|
|
addGroupLabel(*activeSection, groupLabel(entry.group), isFirstGroup);
|
|
}
|
|
if (const auto* list = std::get_if<ListSetting>(&entry.control)) {
|
|
if (isFirstBarWidgetListPath(entry.path)) {
|
|
addBarWidgetLaneEditor(*activeSection, entry, barWidgetEditorCtx);
|
|
} 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);
|
|
} else {
|
|
makeRow(*activeSection, entry, makeSearchPickerButton(entry, *picker));
|
|
}
|
|
} else if (const auto* multi = std::get_if<MultiSelectSetting>(&entry.control)) {
|
|
makeMultiSelectBlock(*activeSection, entry, *multi);
|
|
} else {
|
|
makeRow(*activeSection, entry, makeControl(entry));
|
|
}
|
|
++visibleEntries;
|
|
}
|
|
}
|
|
|
|
if (visibleEntries == 0) {
|
|
auto emptyState = std::make_unique<Flex>();
|
|
emptyState->setDirection(FlexDirection::Vertical);
|
|
emptyState->setAlign(FlexAlign::Center);
|
|
emptyState->setJustify(FlexJustify::Center);
|
|
emptyState->setGap(Style::spaceXs * scale);
|
|
emptyState->setPadding((Style::spaceLg + Style::spaceMd) * scale);
|
|
emptyState->setFill(colorSpecFromRole(ColorRole::SurfaceVariant, 0.24f));
|
|
emptyState->setBorder(colorSpecFromRole(ColorRole::Outline, 0.28f), Style::borderWidth);
|
|
emptyState->setRadius(Style::radiusMd * scale);
|
|
emptyState->addChild(makeLabel(i18n::tr("settings.window.no-results"), Style::fontSizeBody * scale,
|
|
colorSpecFromRole(ColorRole::OnSurface), true));
|
|
emptyState->addChild(makeLabel(i18n::tr("settings.window.no-results-hint"), Style::fontSizeCaption * scale,
|
|
colorSpecFromRole(ColorRole::OnSurfaceVariant), false));
|
|
|
|
auto emptyRow = std::make_unique<Flex>();
|
|
emptyRow->setDirection(FlexDirection::Horizontal);
|
|
emptyRow->setAlign(FlexAlign::Center);
|
|
emptyRow->setJustify(FlexJustify::Center);
|
|
emptyRow->setFillWidth(true);
|
|
emptyRow->addChild(std::move(emptyState));
|
|
content.addChild(std::move(emptyRow));
|
|
}
|
|
|
|
return visibleEntries;
|
|
}
|
|
|
|
} // namespace settings
|