mirror of
https://github.com/aydencharles/onionHEN.git
synced 2026-10-22 11:53:07 -05:00
384 lines
14 KiB
C++
384 lines
14 KiB
C++
/* Copyright (C) 2025 OnionHEN / LightningMods
|
|
*
|
|
* Fluent builder for PS5 Debug Settings (system_settings) XML.
|
|
* Business code describes widgets; build() returns the full document string.
|
|
*/
|
|
#pragma once
|
|
|
|
#include <functional>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace ps5ui {
|
|
|
|
enum class Style { None, Center, Left };
|
|
|
|
/**
|
|
* Escape for filesystem path attrs (icon): & < > " and / → //.
|
|
* ShellUI resolves icon paths only when slashes are doubled.
|
|
*/
|
|
std::string escape(std::string_view text);
|
|
|
|
/**
|
|
* Escape for display attrs (title/second_title/description): & < > " only.
|
|
* Do not double '/' — UI shows // literally in path text.
|
|
*/
|
|
std::string escape_xml(std::string_view text);
|
|
|
|
struct Attrs {
|
|
std::string id;
|
|
std::string title;
|
|
std::optional<std::string> second_title;
|
|
std::optional<std::string> description;
|
|
std::optional<std::string> icon;
|
|
std::optional<std::string> file;
|
|
std::optional<std::string> value;
|
|
std::optional<std::string> confirm;
|
|
std::optional<std::string> confirm_phrase;
|
|
std::optional<std::string> keyboard_type;
|
|
std::optional<std::string> min_length;
|
|
std::optional<std::string> max_length;
|
|
std::optional<std::string> key;
|
|
std::optional<std::string> initial_focus_to;
|
|
std::optional<std::string> list_size;
|
|
std::optional<std::string> list_highlight;
|
|
std::optional<std::string> raw_title;
|
|
std::optional<bool> restorable;
|
|
Style style = Style::None;
|
|
bool operator==(const Attrs &) const = default;
|
|
};
|
|
|
|
struct Node {
|
|
enum class Kind {
|
|
SettingList,
|
|
Toggle,
|
|
Button,
|
|
Label,
|
|
Link,
|
|
List,
|
|
ListItem,
|
|
TextField,
|
|
UserCustom,
|
|
Option,
|
|
OptionItem,
|
|
} kind = Kind::SettingList;
|
|
|
|
Attrs attrs;
|
|
std::vector<Node> children;
|
|
bool operator==(const Node &) const = default;
|
|
};
|
|
|
|
const char *node_tag(Node::Kind kind);
|
|
std::vector<std::pair<std::string, std::string>> node_attributes(const Node &node);
|
|
std::string build_document(const Node &root,
|
|
std::string_view plugin = "debug_settings_plugin");
|
|
|
|
class ListBuilder {
|
|
public:
|
|
ListBuilder& item(std::string id, std::string title, std::string value,
|
|
std::optional<std::string> icon = std::nullopt);
|
|
|
|
private:
|
|
template <typename>
|
|
friend class GroupT;
|
|
explicit ListBuilder(Node& list_node) : list_node_(list_node) {}
|
|
Node& list_node_;
|
|
};
|
|
|
|
class Group; // nested scopes always use Group
|
|
|
|
/**
|
|
* CRTP base so Page::toggle(...).build() type-checks (returns Derived&).
|
|
*/
|
|
template <typename Derived>
|
|
class GroupT {
|
|
public:
|
|
Derived& label(std::string id, std::string title, Style style = Style::None,
|
|
std::optional<std::string> icon = std::nullopt);
|
|
|
|
Derived& button(std::string id, std::string title,
|
|
std::optional<std::string> second_title = std::nullopt,
|
|
std::optional<std::string> description = std::nullopt,
|
|
std::optional<std::string> icon = std::nullopt,
|
|
Style style = Style::None,
|
|
std::optional<std::string> confirm = std::nullopt,
|
|
std::optional<std::string> confirm_phrase = std::nullopt);
|
|
|
|
Derived& toggle(std::string id, std::string title, bool on,
|
|
std::optional<std::string> second_title = std::nullopt,
|
|
std::optional<std::string> description = std::nullopt,
|
|
std::optional<std::string> icon = std::nullopt,
|
|
std::optional<std::string> confirm = std::nullopt);
|
|
|
|
Derived& link(std::string id, std::string title, std::string file,
|
|
std::optional<std::string> second_title = std::nullopt,
|
|
std::optional<std::string> icon = std::nullopt,
|
|
std::optional<std::string> confirm = std::nullopt,
|
|
std::optional<std::string> confirm_phrase = std::nullopt);
|
|
|
|
Derived& list(std::string id, std::string title,
|
|
const std::function<void(ListBuilder&)>& body,
|
|
std::optional<std::string> second_title = std::nullopt,
|
|
std::optional<std::string> value = std::nullopt,
|
|
std::optional<std::string> confirm = std::nullopt,
|
|
std::optional<std::string> confirm_phrase = std::nullopt,
|
|
std::optional<std::string> icon = std::nullopt);
|
|
|
|
Derived& text_field(std::string id, std::string title,
|
|
std::optional<std::string> second_title = std::nullopt,
|
|
std::optional<std::string> keyboard_type = std::nullopt,
|
|
std::optional<std::string> min_length = std::nullopt,
|
|
std::optional<std::string> max_length = std::nullopt,
|
|
std::optional<std::string> key = std::nullopt,
|
|
std::optional<std::string> confirm = std::nullopt,
|
|
std::optional<std::string> confirm_phrase = std::nullopt,
|
|
std::optional<std::string> value = std::nullopt,
|
|
std::optional<std::string> icon = std::nullopt);
|
|
|
|
/** Legacy Settings placeholder whose real Panel is appended during Reset. */
|
|
Derived& user_custom(std::string id);
|
|
|
|
/** Options-button context menu container (page level). */
|
|
Derived& option(std::string id,
|
|
const std::function<void(Group&)>& body,
|
|
std::optional<std::string> list_highlight = std::nullopt);
|
|
|
|
/** Options-button context menu entry. */
|
|
Derived& option_item(std::string id, std::string title,
|
|
std::optional<std::string> raw_title = std::nullopt);
|
|
|
|
Derived& group(std::string id, std::string title,
|
|
const std::function<void(Group&)>& body,
|
|
std::optional<std::string> second_title = std::nullopt,
|
|
std::optional<std::string> icon = std::nullopt,
|
|
std::optional<std::string> initial_focus_to = std::nullopt,
|
|
Style style = Style::None);
|
|
|
|
Derived& add(Node node);
|
|
|
|
protected:
|
|
GroupT() = default;
|
|
void bind(Node* node) { node_ = node; }
|
|
Node* node_ = nullptr;
|
|
|
|
Derived& self() { return static_cast<Derived&>(*this); }
|
|
};
|
|
|
|
/** Nested setting_list scope (and the type accepted by group() callbacks). */
|
|
class Group : public GroupT<Group> {
|
|
public:
|
|
explicit Group(Node* node) { bind(node); }
|
|
};
|
|
|
|
/**
|
|
* Root document: one root setting_list under system_settings.
|
|
* Call build() to get the full XML string.
|
|
*/
|
|
class Page : public GroupT<Page> {
|
|
public:
|
|
Page(std::string root_list_id, std::string root_title,
|
|
std::string plugin = "debug_settings_plugin");
|
|
|
|
Page& root_style(Style style);
|
|
Page& root_focus(std::string id);
|
|
Page& root_icon(std::string icon);
|
|
Page& root_list_size(std::string size);
|
|
Page& root_restorable(bool restorable);
|
|
|
|
/** Serialize to a complete system_settings document. */
|
|
[[nodiscard]] std::string build() const;
|
|
const Node &root() const { return root_; }
|
|
|
|
private:
|
|
std::string plugin_;
|
|
Node root_;
|
|
};
|
|
|
|
// ---- template method definitions ----
|
|
|
|
template <typename Derived>
|
|
Derived& GroupT<Derived>::label(std::string id, std::string title, Style style,
|
|
std::optional<std::string> icon) {
|
|
Node n;
|
|
n.kind = Node::Kind::Label;
|
|
n.attrs.id = std::move(id);
|
|
n.attrs.title = std::move(title);
|
|
n.attrs.style = style;
|
|
n.attrs.icon = std::move(icon);
|
|
return add(std::move(n));
|
|
}
|
|
|
|
template <typename Derived>
|
|
Derived& GroupT<Derived>::button(std::string id, std::string title,
|
|
std::optional<std::string> second_title,
|
|
std::optional<std::string> description,
|
|
std::optional<std::string> icon, Style style,
|
|
std::optional<std::string> confirm,
|
|
std::optional<std::string> confirm_phrase) {
|
|
Node n;
|
|
n.kind = Node::Kind::Button;
|
|
n.attrs.id = std::move(id);
|
|
n.attrs.title = std::move(title);
|
|
n.attrs.second_title = std::move(second_title);
|
|
n.attrs.description = std::move(description);
|
|
n.attrs.icon = std::move(icon);
|
|
n.attrs.style = style;
|
|
n.attrs.confirm = std::move(confirm);
|
|
n.attrs.confirm_phrase = std::move(confirm_phrase);
|
|
return add(std::move(n));
|
|
}
|
|
|
|
template <typename Derived>
|
|
Derived& GroupT<Derived>::toggle(std::string id, std::string title, bool on,
|
|
std::optional<std::string> second_title,
|
|
std::optional<std::string> description,
|
|
std::optional<std::string> icon,
|
|
std::optional<std::string> confirm) {
|
|
Node n;
|
|
n.kind = Node::Kind::Toggle;
|
|
n.attrs.id = std::move(id);
|
|
n.attrs.title = std::move(title);
|
|
n.attrs.second_title = std::move(second_title);
|
|
n.attrs.description = std::move(description);
|
|
n.attrs.icon = std::move(icon);
|
|
n.attrs.confirm = std::move(confirm);
|
|
n.attrs.value = on ? "1" : "0";
|
|
return add(std::move(n));
|
|
}
|
|
|
|
template <typename Derived>
|
|
Derived& GroupT<Derived>::link(std::string id, std::string title, std::string file,
|
|
std::optional<std::string> second_title,
|
|
std::optional<std::string> icon,
|
|
std::optional<std::string> confirm,
|
|
std::optional<std::string> confirm_phrase) {
|
|
Node n;
|
|
n.kind = Node::Kind::Link;
|
|
n.attrs.id = std::move(id);
|
|
n.attrs.title = std::move(title);
|
|
n.attrs.file = std::move(file);
|
|
n.attrs.second_title = std::move(second_title);
|
|
n.attrs.icon = std::move(icon);
|
|
n.attrs.confirm = std::move(confirm);
|
|
n.attrs.confirm_phrase = std::move(confirm_phrase);
|
|
return add(std::move(n));
|
|
}
|
|
|
|
template <typename Derived>
|
|
Derived& GroupT<Derived>::list(std::string id, std::string title,
|
|
const std::function<void(ListBuilder&)>& body,
|
|
std::optional<std::string> second_title,
|
|
std::optional<std::string> value,
|
|
std::optional<std::string> confirm,
|
|
std::optional<std::string> confirm_phrase,
|
|
std::optional<std::string> icon) {
|
|
Node n;
|
|
n.kind = Node::Kind::List;
|
|
n.attrs.id = std::move(id);
|
|
n.attrs.title = std::move(title);
|
|
n.attrs.second_title = std::move(second_title);
|
|
n.attrs.value = std::move(value);
|
|
n.attrs.confirm = std::move(confirm);
|
|
n.attrs.confirm_phrase = std::move(confirm_phrase);
|
|
n.attrs.icon = std::move(icon);
|
|
node_->children.push_back(std::move(n));
|
|
ListBuilder lb(node_->children.back());
|
|
if (body)
|
|
body(lb);
|
|
return self();
|
|
}
|
|
|
|
template <typename Derived>
|
|
Derived& GroupT<Derived>::text_field(std::string id, std::string title,
|
|
std::optional<std::string> second_title,
|
|
std::optional<std::string> keyboard_type,
|
|
std::optional<std::string> min_length,
|
|
std::optional<std::string> max_length,
|
|
std::optional<std::string> key,
|
|
std::optional<std::string> confirm,
|
|
std::optional<std::string> confirm_phrase,
|
|
std::optional<std::string> value,
|
|
std::optional<std::string> icon) {
|
|
Node n;
|
|
n.kind = Node::Kind::TextField;
|
|
n.attrs.id = std::move(id);
|
|
n.attrs.title = std::move(title);
|
|
n.attrs.second_title = std::move(second_title);
|
|
n.attrs.keyboard_type = std::move(keyboard_type);
|
|
n.attrs.min_length = std::move(min_length);
|
|
n.attrs.max_length = std::move(max_length);
|
|
n.attrs.key = std::move(key);
|
|
n.attrs.confirm = std::move(confirm);
|
|
n.attrs.confirm_phrase = std::move(confirm_phrase);
|
|
n.attrs.value = std::move(value);
|
|
n.attrs.icon = std::move(icon);
|
|
return add(std::move(n));
|
|
}
|
|
|
|
template <typename Derived>
|
|
Derived& GroupT<Derived>::user_custom(std::string id) {
|
|
Node n;
|
|
n.kind = Node::Kind::UserCustom;
|
|
n.attrs.id = std::move(id);
|
|
return add(std::move(n));
|
|
}
|
|
|
|
template <typename Derived>
|
|
Derived& GroupT<Derived>::option(std::string id,
|
|
const std::function<void(Group&)>& body,
|
|
std::optional<std::string> list_highlight) {
|
|
Node n;
|
|
n.kind = Node::Kind::Option;
|
|
n.attrs.id = std::move(id);
|
|
n.attrs.list_highlight = std::move(list_highlight);
|
|
node_->children.push_back(std::move(n));
|
|
Group sub(&node_->children.back());
|
|
if (body)
|
|
body(sub);
|
|
return self();
|
|
}
|
|
|
|
template <typename Derived>
|
|
Derived& GroupT<Derived>::option_item(std::string id, std::string title,
|
|
std::optional<std::string> raw_title) {
|
|
Node n;
|
|
n.kind = Node::Kind::OptionItem;
|
|
n.attrs.id = std::move(id);
|
|
n.attrs.title = std::move(title);
|
|
n.attrs.raw_title = std::move(raw_title);
|
|
return add(std::move(n));
|
|
}
|
|
|
|
template <typename Derived>
|
|
Derived& GroupT<Derived>::group(std::string id, std::string title,
|
|
const std::function<void(Group&)>& body,
|
|
std::optional<std::string> second_title,
|
|
std::optional<std::string> icon,
|
|
std::optional<std::string> initial_focus_to,
|
|
Style style) {
|
|
Node n;
|
|
n.kind = Node::Kind::SettingList;
|
|
n.attrs.id = std::move(id);
|
|
n.attrs.title = std::move(title);
|
|
n.attrs.second_title = std::move(second_title);
|
|
n.attrs.icon = std::move(icon);
|
|
n.attrs.initial_focus_to = std::move(initial_focus_to);
|
|
n.attrs.style = style;
|
|
node_->children.push_back(std::move(n));
|
|
Group sub(&node_->children.back());
|
|
if (body)
|
|
body(sub);
|
|
return self();
|
|
}
|
|
|
|
template <typename Derived>
|
|
Derived& GroupT<Derived>::add(Node node) {
|
|
node_->children.push_back(std::move(node));
|
|
return self();
|
|
}
|
|
|
|
} // namespace ps5ui
|