From e384b73e8b7395d537054d8b2502ea1e023a5425 Mon Sep 17 00:00:00 2001 From: matey Date: Sun, 18 Jan 2026 00:35:11 +0200 Subject: [PATCH] feat: Add theme (and general config.json) import functionality ! Generated with a significant LLM assistance. ! Adds the ability to import color themes from JSON files within the preferences dialog. This commit introduces: - A `SetColor` method to `ColourButton` to allow programmatic color updates. - A `ThemeImportVisitor` to recursively parse JSON objects and update corresponding color options. - An "Import Theme..." button in the Interface Colors preference page that opens a file dialog to select a JSON theme file. - Error handling for theme import, displaying messages for parsing errors or unknown issues. - Integration of the theme import logic to update color preferences and refresh the UI. --- src/colour_button.h | 6 ++ src/preferences.cpp | 181 ++++++++++++++++++++++++++++++++++++++- src/preferences.h | 14 +++ src/preferences_base.cpp | 1 + 4 files changed, 200 insertions(+), 2 deletions(-) diff --git a/src/colour_button.h b/src/colour_button.h index b2e0f9335e..0f78f77d27 100644 --- a/src/colour_button.h +++ b/src/colour_button.h @@ -43,6 +43,12 @@ class ColourButton: public wxButton { /// Get the currently selected color agi::Color GetColor() { return colour; } + + /// Set the currently selected color + void SetColor(agi::Color color) { + colour = color; + UpdateBitmap(); + }; }; struct ColorValidator final : public wxValidator { diff --git a/src/preferences.cpp b/src/preferences.cpp index 9569bb1d67..3b60e53ca9 100644 --- a/src/preferences.cpp +++ b/src/preferences.cpp @@ -15,7 +15,6 @@ /// @file preferences.cpp /// @brief Preferences dialogue /// @ingroup configuration_ui - #include "preferences.h" #include "ass_style_storage.h" @@ -41,7 +40,10 @@ #include #endif +#include "colour_button.h" +#include #include +#include #include @@ -49,6 +51,7 @@ #include #include #include +#include #include #include #include @@ -241,6 +244,95 @@ void Interface(wxTreebook *book, Preferences *parent) { p->SetSizerAndFit(p->sizer); } +struct ThemeImportVisitor final : json::ConstVisitor { + std::string prefix; + Preferences* parent; + ThemeImportVisitor(std::string prefix, Preferences* parent) + : prefix(std::move(prefix)) + , parent(parent) + { + } + + void Visit(const json::Object& obj) override + { + for (auto const& pair : obj) { + std::string path = prefix; + if (!path.empty()) + path += "/"; + path += pair.first; + + ThemeImportVisitor sub(path, parent); + pair.second.Accept(sub); + } + } + + void Visit(const json::Array&) override { } + void Visit(int64_t val) override + { + try { + if (config::opt->Get(prefix)) { + parent->SetOption(std::make_unique(prefix, val)); + parent->RefreshControl(prefix); + } + } catch (...) { + } + } + void Visit(double val) override + { + try { + if (config::opt->Get(prefix)) { + parent->SetOption(std::make_unique(prefix, val)); + parent->RefreshControl(prefix); + } + } catch (...) { + } + } + void Visit(const json::String& val) override + { + try { + auto opt = config::opt->Get(prefix); + if (opt->GetType() == agi::OptionType::Color) { + parent->SetOption(std::make_unique(prefix, agi::Color(val))); + } else { + parent->SetOption(std::make_unique(prefix, val)); + } + parent->RefreshControl(prefix); + wxSafeYield(); // Update UI during loop + } catch (...) { + } + } + void Visit(bool val) override + { + try { + if (config::opt->Get(prefix)) { + parent->SetOption(std::make_unique(prefix, val)); + parent->RefreshControl(prefix); + } + } catch (...) { + } + } + void Visit(const json::Null&) override { } +}; + +void OnImportTheme(wxWindow* parent) { + wxFileDialog dlg(parent, _("Select Theme File"), "", "", "JSON files (*.json)|*.json", wxFD_OPEN | wxFD_FILE_MUST_EXIST); + if (dlg.ShowModal() == wxID_CANCEL) + return; + + try { + auto root = agi::json_util::file(agi::fs::path(dlg.GetPath().ToStdString()), "{}"); + ThemeImportVisitor tv("Colour", static_cast(parent)); + root.Accept(tv); + + parent->Refresh(); + parent->Update(); + } catch (agi::Exception const& e) { + wxMessageBox(to_wx(e.GetMessage()), _("Error importing theme"), wxOK | wxICON_ERROR); + } catch (...) { + wxMessageBox(_("Unknown error importing theme"), _("Error importing theme"), wxOK | wxICON_ERROR); + } +} + /// Interface Colours preferences subpage void Interface_Colours(wxTreebook *book, Preferences *parent) { auto p = new OptionPage(book, parent, _("Colors"), OptionPage::PAGE_SCROLL|OptionPage::PAGE_SUB); @@ -314,7 +406,19 @@ void Interface_Colours(wxTreebook *book, Preferences *parent) { p->sizer = main_sizer; - p->SetSizerAndFit(p->sizer); + auto btn_sizer = new wxBoxSizer(wxHORIZONTAL); + auto import_btn = new wxButton(p, -1, _("Import Theme...")); + import_btn->Bind(wxEVT_BUTTON, [=](wxCommandEvent&) { OnImportTheme(parent); }); + btn_sizer->Add(import_btn, 0, wxALL, 5); + auto reset_btn = new wxButton(p, -1, _("Reset Colors")); + reset_btn->Bind(wxEVT_BUTTON, [=](wxCommandEvent&) { parent->ResetColors(); }); + btn_sizer->Add(reset_btn, 0, wxALL, 5); + auto wrapper = new wxBoxSizer(wxVERTICAL); + wrapper->Add(main_sizer, 1, wxEXPAND); + wrapper->Add(btn_sizer, 0, wxALIGN_CENTER); + p->sizer = wrapper; + p->SetSizerAndFit(p->sizer); + } /// Backup preferences page @@ -722,6 +826,78 @@ void Preferences::OnResetDefault(wxCommandEvent&) { EndModal(-1); } +void Preferences::RefreshControl(std::string const& name) +{ + auto it = option_controls.find(name); + if (it == option_controls.end()) + return; + + agi::OptionValue* opt = nullptr; + auto pending_it = pending_changes.find(name); + if (pending_it != pending_changes.end()) + opt = pending_it->second.get(); + else { + try { + opt = config::opt->Get(name); + } catch (...) { + return; + } + } + + if (!opt) + return; + + wxControl* ctrl = it->second; + switch (opt->GetType()) { + case agi::OptionType::Bool: + if (auto cb = dynamic_cast(ctrl)) + cb->SetValue(opt->GetBool()); + break; + case agi::OptionType::Int: + if (auto sc = dynamic_cast(ctrl)) + sc->SetValue((int)opt->GetInt()); + else if (auto cb = dynamic_cast(ctrl)) + cb->SetSelection((int)opt->GetInt()); + break; + case agi::OptionType::Double: + if (auto scd = dynamic_cast(ctrl)) + scd->SetValue(opt->GetDouble()); + break; + case agi::OptionType::String: + if (auto tc = dynamic_cast(ctrl)) + tc->SetValue(to_wx(opt->GetString())); + else if (auto cb = dynamic_cast(ctrl)) + cb->SetStringSelection(to_wx(opt->GetString())); + break; + case agi::OptionType::Color: + if (auto cb = dynamic_cast(ctrl)) { + cb->SetColor(opt->GetColor()); + cb->Update(); + } + break; + default: + break; + } +} + +void Preferences::RegisterControl(std::string const& name, wxControl* control) { option_controls[name] = control; } + +void Preferences::ResetColors() +{ + if (wxYES != wxMessageBox(_("Are you sure you want to reset all colors to their default values?"), _("Reset colors?"), wxYES_NO | wxICON_QUESTION, this)) + return; + for (auto const& opt_name : option_names) { + if (opt_name.compare(0, 7, "Colour/") == 0) { + agi::OptionValue* opt = OPT_SET(opt_name); + if (!opt->IsDefault()) { + opt->Reset(); + RefreshControl(opt_name); + } + } + } + applyButton->Enable(true); + +} Preferences::Preferences(wxWindow *parent): wxDialog(parent, -1, _("Preferences"), wxDefaultPosition, wxSize(-1, -1), wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) { SetIcons(GETICONS(options_button)); @@ -773,4 +949,5 @@ Preferences::Preferences(wxWindow *parent): wxDialog(parent, -1, _("Preferences" void ShowPreferences(wxWindow *parent) { while (Preferences(parent).ShowModal() < 0); + } diff --git a/src/preferences.h b/src/preferences.h index e21998c9b3..94f0d33e11 100644 --- a/src/preferences.h +++ b/src/preferences.h @@ -25,6 +25,7 @@ #include class wxButton; +class wxControl; class wxTreebook; namespace agi { class OptionValue; } @@ -36,6 +37,7 @@ class Preferences final : public wxDialog { wxButton *applyButton; std::map> pending_changes; + std::map option_controls; std::vector pending_callbacks; std::vector option_names; @@ -62,4 +64,16 @@ class Preferences final : public wxDialog { /// simply revert to the default config file as a bunch of things other than /// user options are stored in it. Perhaps that should change in the future. void AddChangeableOption(std::string const& name); + + /// Refresh the UI control for an option + /// @param name Name of the option to refresh + void RefreshControl(std::string const& name); + + /// Add a control to the list of controls for an option + /// @param name Name of the option + /// @param control Control to add + void RegisterControl(std::string const& name, wxControl *control); + + /// Reset all color options to their default values + void ResetColors(); }; diff --git a/src/preferences_base.cpp b/src/preferences_base.cpp index 381244e44f..670f78fbba 100644 --- a/src/preferences_base.cpp +++ b/src/preferences_base.cpp @@ -163,6 +163,7 @@ wxControl *OptionPage::OptionAdd(PageSection section, const wxString &name, cons auto cb = new ColourButton(section.box, wxSize(40,10), false, opt->GetColor()); cb->Bind(EVT_COLOR, ColourUpdater(opt_name, parent)); Add(section, name, cb); + parent->RegisterControl(opt_name, cb); return cb; }