support loading gui2 themes from add-ons

Requires a `gui-theme.cfg` file in Add-on root with a `[gui]` tag.
(Add-ons without all buttons in title_screen may get flagged as defunct.)
This commit is contained in:
Subhraman Sarkar 2024-08-25 09:36:54 +05:30 committed by Charles Dang
parent 37a0828428
commit 59911072aa
10 changed files with 166 additions and 70 deletions

View file

@ -0,0 +1,5 @@
### User interface
GUI2 themes can be loaded from add-ons. Requires a `gui-theme.cfg` file in add-on root with a `[gui]` tag that acts as the entry point for the theme.
### Lua API
Added new function gui.switch_theme() to allow switching to another gui2 theme from inside a scenario.

View file

@ -867,6 +867,8 @@
min="0"
max="infinite"
super="$generic/widget_definition"
{REQUIRED_KEY "id" string}
{REQUIRED_KEY "description" string}
[tag]
name="resolution"
min="0"

View file

@ -28,6 +28,7 @@
#include "game_initialization/multiplayer.hpp"
#include "generators/map_generator.hpp"
#include "gettext.hpp"
#include "gui/gui.hpp"
#include "gui/dialogs/message.hpp"
#include "gui/dialogs/outro.hpp"
#include "gui/widgets/retval.hpp"
@ -204,7 +205,6 @@ level_result::type campaign_controller::play_game()
gui2::dialogs::outro::display(state_.classification());
}
}
return res;
} else if(res == level_result::type::observer_end && mp_info_ && !mp_info_->is_host) {
const int dlg_res = gui2::show_message(_("Game Over"),
@ -291,3 +291,10 @@ level_result::type campaign_controller::play_game()
return level_result::type::victory;
}
campaign_controller::~campaign_controller()
{
// If the scenario changed the current gui2 theme,
// change it back to the value stored in preferences
gui2::switch_theme(prefs::get().gui2_theme());
}

View file

@ -56,6 +56,8 @@ public:
{
}
~campaign_controller();
level_result::type play_game();
level_result::type play_replay()
{

View file

@ -86,18 +86,19 @@ title_screen::~title_screen()
{
}
using btn_callback = std::function<void()>;
static void register_button(window& win, const std::string& id, hotkey::HOTKEY_COMMAND hk, btn_callback callback)
void title_screen::register_button(const std::string& id, hotkey::HOTKEY_COMMAND hk, std::function<void()> callback)
{
if(hk != hotkey::HOTKEY_NULL) {
win.register_hotkey(hk, std::bind(callback));
register_hotkey(hk, std::bind(callback));
}
auto b = find_widget<button>(&win, id, false, false);
if(b != nullptr)
{
connect_signal_mouse_left_click(*b, std::bind(callback));
try {
button& btn = find_widget<button>(this, id, false);
connect_signal_mouse_left_click(btn, std::bind(callback));
} catch(const wml_exception& e) {
ERR_GUI_P << e.user_message;
prefs::get().set_gui2_theme("default");
set_retval(RELOAD_UI);
}
}
@ -235,16 +236,16 @@ void title_screen::init_callbacks()
update_tip(true);
}
register_button(*this, "next_tip", hotkey::TITLE_SCREEN__NEXT_TIP,
register_button("next_tip", hotkey::TITLE_SCREEN__NEXT_TIP,
std::bind(&title_screen::update_tip, this, true));
register_button(*this, "previous_tip", hotkey::TITLE_SCREEN__PREVIOUS_TIP,
register_button("previous_tip", hotkey::TITLE_SCREEN__PREVIOUS_TIP,
std::bind(&title_screen::update_tip, this, false));
//
// Help
//
register_button(*this, "help", hotkey::HOTKEY_HELP, []() {
register_button("help", hotkey::HOTKEY_HELP, []() {
if(gui2::new_widgets) {
gui2::dialogs::help_browser::display();
}
@ -255,12 +256,12 @@ void title_screen::init_callbacks()
//
// About
//
register_button(*this, "about", hotkey::HOTKEY_NULL, std::bind(&game_version::display<>));
register_button("about", hotkey::HOTKEY_NULL, std::bind(&game_version::display<>));
//
// Campaign
//
register_button(*this, "campaign", hotkey::TITLE_SCREEN__CAMPAIGN, [this]() {
register_button("campaign", hotkey::TITLE_SCREEN__CAMPAIGN, [this]() {
try{
if(game_.new_campaign()) {
// Suspend drawing of the title screen,
@ -276,13 +277,13 @@ void title_screen::init_callbacks()
//
// Multiplayer
//
register_button(*this, "multiplayer", hotkey::TITLE_SCREEN__MULTIPLAYER,
register_button("multiplayer", hotkey::TITLE_SCREEN__MULTIPLAYER,
std::bind(&title_screen::button_callback_multiplayer, this));
//
// Load game
//
register_button(*this, "load", hotkey::HOTKEY_LOAD_GAME, [this]() {
register_button("load", hotkey::HOTKEY_LOAD_GAME, [this]() {
if(game_.load_game()) {
// Suspend drawing of the title screen,
// so it doesn't flicker in between loading screens.
@ -294,7 +295,7 @@ void title_screen::init_callbacks()
//
// Addons
//
register_button(*this, "addons", hotkey::TITLE_SCREEN__ADDONS, [this]() {
register_button("addons", hotkey::TITLE_SCREEN__ADDONS, [this]() {
if(manage_addons()) {
set_retval(RELOAD_GAME_DATA);
}
@ -303,7 +304,7 @@ void title_screen::init_callbacks()
//
// Editor
//
register_button(*this, "editor", hotkey::TITLE_SCREEN__EDITOR, [this]() { set_retval(MAP_EDITOR); });
register_button("editor", hotkey::TITLE_SCREEN__EDITOR, [this]() { set_retval(MAP_EDITOR); });
//
// Cores
@ -314,7 +315,7 @@ void title_screen::init_callbacks()
//
// Language
//
register_button(*this, "language", hotkey::HOTKEY_LANGUAGE, [this]() {
register_button("language", hotkey::HOTKEY_LANGUAGE, [this]() {
try {
if(game_.change_language()) {
on_resize();
@ -328,53 +329,32 @@ void title_screen::init_callbacks()
//
// Preferences
//
register_button(*this, "preferences", hotkey::HOTKEY_PREFERENCES, [this]() {
gui2::dialogs::preferences_dialog pref_dlg;
pref_dlg.show();
if (pref_dlg.get_retval() == RELOAD_UI) {
set_retval(RELOAD_UI);
}
// Currently blurred windows don't capture well if there is something
// on top of them at the time of blur. Resizing the game window in
// preferences will cause the title screen tip and menu panels to
// capture the prefs dialog in their blur. This workaround simply
// forces them to re-capture the blur after the dialog closes.
panel* tip_panel = find_widget<panel>(this, "tip_panel", false, false);
if(tip_panel != nullptr) {
tip_panel->get_canvas(tip_panel->get_state()).queue_reblur();
tip_panel->queue_redraw();
}
panel* menu_panel = find_widget<panel>(this, "menu_panel", false, false);
if(menu_panel != nullptr) {
menu_panel->get_canvas(menu_panel->get_state()).queue_reblur();
menu_panel->queue_redraw();
}
});
register_button("preferences", hotkey::HOTKEY_PREFERENCES,
std::bind(&title_screen::show_preferences, this));
//
// Achievements
//
register_button(*this, "achievements", hotkey::HOTKEY_ACHIEVEMENTS,
register_button("achievements", hotkey::HOTKEY_ACHIEVEMENTS,
std::bind(&title_screen::show_achievements, this));
//
// Community
//
register_button(*this, "community", hotkey::HOTKEY_NULL,
register_button("community", hotkey::HOTKEY_NULL,
std::bind(&title_screen::show_community, this));
//
// Quit
//
register_button(*this, "quit", hotkey::HOTKEY_QUIT_TO_DESKTOP, [this]() { set_retval(QUIT_GAME); });
register_button("quit", hotkey::HOTKEY_QUIT_TO_DESKTOP, [this]() { set_retval(QUIT_GAME); });
// A sanity check, exit immediately if the .cfg file didn't have a "quit" button.
find_widget<button>(this, "quit", false, true);
//
// Debug clock
//
register_button(*this, "clock", hotkey::HOTKEY_NULL,
register_button("clock", hotkey::HOTKEY_NULL,
std::bind(&title_screen::show_debug_clock_window, this));
auto clock = find_widget<button>(this, "clock", false, false);
@ -385,7 +365,7 @@ void title_screen::init_callbacks()
//
// GUI Test and Debug Window
//
register_button(*this, "test_dialog", hotkey::HOTKEY_NULL,
register_button("test_dialog", hotkey::HOTKEY_NULL,
std::bind(&title_screen::show_gui_test_dialog, this));
auto test_dialog = find_widget<button>(this, "test_dialog", false, false);
@ -484,11 +464,6 @@ void title_screen::show_debug_clock_window()
}
}
void title_screen::show_gui_test_dialog()
{
gui2::dialogs::gui_test_dialog::execute();
}
void title_screen::hotkey_callback_select_tests()
{
game_config_manager::get()->load_game_config_for_create(false, true);
@ -525,6 +500,36 @@ void title_screen::show_community()
dlg.display(4);
}
void title_screen::show_gui_test_dialog()
{
gui2::dialogs::gui_test_dialog::execute();
}
void title_screen::show_preferences()
{
gui2::dialogs::preferences_dialog pref_dlg;
pref_dlg.show();
if (pref_dlg.get_retval() == RELOAD_UI) {
set_retval(RELOAD_UI);
}
// Currently blurred windows don't capture well if there is something
// on top of them at the time of blur. Resizing the game window in
// preferences will cause the title screen tip and menu panels to
// capture the prefs dialog in their blur. This workaround simply
// forces them to re-capture the blur after the dialog closes.
panel* tip_panel = find_widget<panel>(this, "tip_panel", false, false);
if(tip_panel != nullptr) {
tip_panel->get_canvas(tip_panel->get_state()).queue_reblur();
tip_panel->queue_redraw();
}
panel* menu_panel = find_widget<panel>(this, "menu_panel", false, false);
if(menu_panel != nullptr) {
menu_panel->get_canvas(menu_panel->get_state()).queue_reblur();
menu_panel->queue_redraw();
}
}
void title_screen::button_callback_multiplayer()
{
while(true) {

View file

@ -71,6 +71,8 @@ private:
void init_callbacks();
void register_button(const std::string& id, hotkey::HOTKEY_COMMAND hk, std::function<void()> callback);
/***** ***** ***** ***** Callbacks ***** ***** ****** *****/
void on_resize();
@ -91,6 +93,9 @@ private:
/** Shows the gui test window. */
void show_gui_test_dialog();
/** Shows the preferences dialog. */
void show_preferences();
void hotkey_callback_select_tests();
void show_achievements();

View file

@ -38,34 +38,94 @@ void init()
// Save current screen size.
settings::update_screen_size_variables();
//
// Read and validate the WML files.
//
config guis_cfg;
try {
schema_validation::schema_validator validator(filesystem::get_wml_location("schema/gui.cfg").value());
config guis_cfg, addons_cfg;
preproc_map preproc(game_config::config_cache::instance().get_preproc_map());
preproc_map preproc(game_config::config_cache::instance().get_preproc_map());
filesystem::scoped_istream stream = preprocess_file(filesystem::get_wml_location("gui/_main.cfg").value(), &preproc);
//
// Read and validate theme WML files from mainline
//
std::string current_file;
const std::string schema_file = "schema/gui.cfg";
try {
schema_validation::schema_validator validator(filesystem::get_wml_location(schema_file).value());
// Core theme files
current_file = "gui/_main.cfg";
filesystem::scoped_istream stream = preprocess_file(filesystem::get_wml_location(current_file).value(), &preproc);
read(guis_cfg, *stream, &validator);
} catch(const config::error& e) {
ERR_GUI_P << e.what();
ERR_GUI_P << "Setting: could not read file 'data/gui/_main.cfg'.";
ERR_GUI_P << "Setting: could not read gui file: " << current_file;
} catch(const abstract_validator::error& e) {
ERR_GUI_P << "Setting: could not read file 'data/schema/gui.cfg'.";
ERR_GUI_P << "Setting: could not read schema file: " << schema_file;
ERR_GUI_P << e.message;
}
//
// Parse GUI definitions.
// Read and validate theme WML files from addons
//
// Add the $user_campaign_dir/*/gui.cfg files to the addon gui config.
std::vector<std::string> user_dirs;
{
const std::string user_campaign_dir = filesystem::get_addons_dir();
std::vector<std::string> user_files;
filesystem::get_files_in_dir(
user_campaign_dir, &user_files, &user_dirs, filesystem::name_mode::ENTIRE_FILE_PATH);
}
for(const std::string& umc : user_dirs) {
try {
const std::string gui_file = umc + "/gui-theme.cfg";
current_file = filesystem::get_short_wml_path(gui_file);
if(filesystem::file_exists(gui_file)) {
config addon_cfg;
schema_validation::schema_validator validator(filesystem::get_wml_location(schema_file).value());
read(addon_cfg, *preprocess_file(gui_file, &preproc), &validator);
addons_cfg.append(addon_cfg);
}
} catch(const config::error& e) {
ERR_GUI_P << e.what();
ERR_GUI_P << "Setting: could not read gui file: " << current_file;
} catch(const abstract_validator::error& e) {
ERR_GUI_P << "Setting: could not read schema file: " << schema_file;
ERR_GUI_P << e.message;
}
}
//
// Parse GUI definitions from mainline
//
for(const config& g : guis_cfg.child_range("gui")) {
const std::string id = g["id"];
auto iter = guis.emplace(id, gui_definition(g)).first;
auto [iter, is_unique] = guis.try_emplace(id, g);
if(id == "default") {
default_gui = iter;
if (!is_unique) {
ERR_GUI_P << "GUI Theme ID '" << id << "' already exists.";
} else {
if(id == "default") {
default_gui = iter;
}
}
}
//
// Parse GUI definitions from addons
//
for(const config& g : addons_cfg.child_range("gui")) {
const std::string id = g["id"];
try {
auto [iter, is_unique] = guis.try_emplace(id, g);
if (!is_unique) {
ERR_GUI_P << "GUI Theme ID '" << id << "' already exists.";
}
} catch (const wml_exception& e) {
ERR_GUI_P << "Non-functional theme: " << id;
ERR_GUI_P << e.user_message;
}
}

View file

@ -16,6 +16,7 @@
#include "hotkey/command_executor.hpp"
#include "hotkey/hotkey_item.hpp"
#include "gui/gui.hpp"
#include "gui/dialogs/achievements_dialog.hpp"
#include "gui/dialogs/lua_interpreter.hpp"
#include "gui/dialogs/message.hpp"
@ -361,6 +362,7 @@ bool command_executor::do_execute_command(const hotkey::ui_command& cmd, bool pr
quit_confirmation::quit_to_desktop();
break;
case HOTKEY_QUIT_GAME:
gui2::switch_theme(prefs::get().gui2_theme());
quit_confirmation::quit_to_title();
break;
case HOTKEY_SURRENDER:

View file

@ -418,6 +418,10 @@ void schema_validator::close_tag()
void schema_validator::print_cache()
{
if (cache_.empty()) {
return;
}
for(auto& m : cache_.top()) {
for(auto& list : m.second) {
print(list);
@ -432,10 +436,12 @@ void schema_validator::validate(const config& cfg, const std::string& name, int
// close previous errors and print them to output.
print_cache();
// clear cache
auto cache_it = cache_.top().find(&cfg);
if(cache_it != cache_.top().end()) {
cache_it->second.clear();
if (!cache_.empty()) {
// clear cache
auto cache_it = cache_.top().find(&cfg);
if(cache_it != cache_.top().end()) {
cache_it->second.clear();
}
}
// Please note that validating unknown tag keys the result will be false

View file

@ -882,6 +882,8 @@ static int do_gameloop(commandline_options& cmdline_opts)
case gui2::dialogs::title_screen::RELOAD_GAME_DATA:
gui2::dialogs::loading_screen::display([&config_manager]() {
config_manager.reload_changed_game_config();
gui2::init();
gui2::switch_theme(prefs::get().gui2_theme());
});
break;
case gui2::dialogs::title_screen::MAP_EDITOR: