GUI2: rework init codepath for a second time

Resolves #9973. Reworks #9974, since I misunderstood how schema validators worked. They are indeed stateful and you need one instance per read call.

I also decided to do away with the global macro context because I do *not* want to have to deal with people claiming the core GUI2 macros constitute a public API (nor do we want macros from one addon affecting the themes in another in the unlikely event that multiple people make gui2 themes.)
This commit is contained in:
Charles Dang 2025-03-05 10:57:53 -05:00
parent ea52702bd0
commit 968e9b95e1

View File

@ -32,93 +32,78 @@ namespace gui2
{
namespace
{
class theme_parser
config read_and_validate(const std::string& path)
try {
preproc_map defines;
schema_validation::schema_validator validator{filesystem::get_wml_location("schema/gui.cfg").value()};
filesystem::scoped_istream stream = preprocess_file(path, &defines);
return ::read(*stream, &validator);
} catch(const utils::bad_optional_access&) {
FAIL("GUI2: schema/gui.cfg not found.");
} catch(const abstract_validator::error& e) {
FAIL("GUI2: could not read schema file: " + e.message);
} catch(const config::error& e) {
ERR_GUI_P << "Could not read gui file: " << path;
ERR_GUI_P << e.what();
return {};
}
/**
* Adds a theme definition object to the global registry.
*
* @param def A valid gui_definition config.
*
* @returns An optional iterator to the newly-constructed definition.
* If gui_definition throws wml_exception or a theme with the
* given ID already exists, returns nullopt.
*/
auto register_theme(const config& def) -> utils::optional<gui_theme_map_t::iterator>
try {
auto [iter, is_unique] = guis.try_emplace(def["id"], def);
if(is_unique) return iter;
ERR_GUI_P << "UI Theme '" << def["id"] << "' already exists.";
return utils::nullopt;
} catch(const wml_exception& e) {
ERR_GUI_P << "Invalid UI theme: " << def["id"];
ERR_GUI_P << e.user_message;
return utils::nullopt;
}
/**
* Parses any GUI2 theme definitions at the file path specified.
*
* @param full_path Path to file containing one or more [gui] tags.
* @param is_core If true, look for the default theme here.
*/
void parse(const std::string& full_path, bool is_core)
{
public:
theme_parser()
try
: validator_(filesystem::get_wml_location("schema/gui.cfg").value())
, defines_(game_config::config_cache::instance().get_preproc_map())
{
guis.clear(); // Reset in case we're re-initializing the UI subsystem
} catch(const utils::bad_optional_access&) {
FAIL("GUI2: schema/gui.cfg not found.");
} catch(const abstract_validator::error& e) {
FAIL("GUI2: could not read schema file: " + e.message);
}
/**
* Parses any GUI2 theme definitions at the file path specified.
*
* @param full_path Path to file containing one or more [gui] tags.
* @param is_core If true, look for the default theme here.
*/
void parse(const std::string& full_path, bool is_core)
{
#if __cpp_range_based_for >= 202211L // lifetime extension of temporaries
for(const config& def : read_and_validate(full_path).child_range("gui")) {
for(const config& def : read_and_validate(full_path).child_range("gui")) {
#else
config cfg = read_and_validate(full_path);
for(const config& def : cfg.child_range("gui")) {
config cfg = read_and_validate(full_path);
for(const config& def : cfg.child_range("gui")) {
#endif
const bool is_default = def["id"] == "default";
const bool is_default = def["id"] == "default";
if(is_default && !is_core) {
ERR_GUI_P << "UI theme id 'default' is reserved for core themes.";
continue;
}
if(is_default && !is_core) {
ERR_GUI_P << "UI theme id 'default' is reserved for core themes.";
continue;
}
const auto iter = register_theme(def);
if(!iter) continue;
const auto iter = register_theme(def);
if(!iter) continue;
if(is_default && is_core) {
default_gui = *iter;
current_gui = default_gui;
}
if(is_default && is_core) {
default_gui = *iter;
current_gui = default_gui;
}
}
private:
auto read_and_validate(const std::string& path) -> config
try {
filesystem::scoped_istream stream = preprocess_file(path, &defines_);
return ::read(*stream, &validator_);
} catch(const config::error& e) {
ERR_GUI_P << "Could not read gui file: " << path;
ERR_GUI_P << e.what();
return {};
};
/**
* Adds a theme definition object to the global registry.
*
* @param def A valid gui_definition config.
*
* @returns An optional iterator to the newly-constructed definition.
* If errors occurred while parsing the config or a theme with
* the given ID already exists, returns nullopt.
*/
auto register_theme(const config& def) const -> utils::optional<gui_theme_map_t::iterator>
try {
auto [iter, is_unique] = guis.try_emplace(def["id"], def);
if(is_unique) return iter;
ERR_GUI_P << "UI Theme '" << def["id"] << "' already exists.";
return utils::nullopt;
} catch(const wml_exception& e) {
ERR_GUI_P << "Invalid UI theme: " << def["id"];
ERR_GUI_P << e.user_message;
return utils::nullopt;
}
/** GUI2 schema validator. */
schema_validation::schema_validator validator_;
/** Common macro context shared by all themes (@todo: document this fact). */
preproc_map defines_;
};
}
} // namespace
@ -126,6 +111,9 @@ void init()
{
LOG_GUI_G << "Initializing UI subststem.";
// Reset the registry in case we're re-initializing
guis.clear();
// Save current screen size.
settings::update_screen_size_variables();
@ -133,10 +121,8 @@ void init()
// Parse GUI definitions from mainline
//
theme_parser parser;
try {
parser.parse(filesystem::get_wml_location("gui/_main.cfg").value(), true);
parse(filesystem::get_wml_location("gui/_main.cfg").value(), true);
} catch(const utils::bad_optional_access&) {
FAIL("GUI2: gui/_main.cfg not found.");
}
@ -157,7 +143,7 @@ void init()
const std::string gui_file = umc + "/gui-theme.cfg";
if(filesystem::file_exists(gui_file)) {
parser.parse(gui_file, false);
parse(gui_file, false);
}
}
}