// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #include "settings_window.h" #include "../../app.h" #include "../../config/version.h" #include "../../config/settings.h" #include "../../util/i18n.h" #include "../../util/platform.h" #include "../../rpc/rpc_client.h" #include "../theme.h" #include "../schema/ui_schema.h" #include "../schema/skin_manager.h" #include "../notifications.h" #include "../effects/imgui_acrylic.h" #include "../material/draw_helpers.h" #include "../../embedded/IconsMaterialDesign.h" #include "imgui.h" #include #include // Icon text for settings UI #define ICON_CUSTOM_THEME ICON_MD_TUNE #define ICON_REFRESH_THEMES ICON_MD_REFRESH namespace dragonx { namespace ui { // Helper: build "TranslatedLabel##id" for ImGui widgets that use label as ID static std::string TrId(const char* tr_key, const char* id) { std::string s = TR(tr_key); s += "##"; s += id; return s; } // Settings state - these get loaded from Settings on window open static bool s_initialized = false; static int s_language_index = 0; static bool s_save_ztxs = true; static bool s_allow_custom_fees = false; static bool s_auto_shield = false; static bool s_fetch_prices = true; static bool s_use_tor = false; static char s_rpc_host[128] = DRAGONX_DEFAULT_RPC_HOST; static char s_rpc_port[16] = DRAGONX_DEFAULT_RPC_PORT; static char s_rpc_user[64] = ""; static char s_rpc_password[64] = ""; static char s_tx_explorer[256] = "https://explorer.dragonx.is/tx/"; static char s_addr_explorer[256] = "https://explorer.dragonx.is/address/"; // Acrylic settings static bool s_acrylic_enabled = true; static float s_blur_amount = 1.5f; // 0.0=Off, continuous blur multiplier static float s_noise_opacity = 0.5f; static bool s_reduced_transparency = false; // Accessibility option static bool s_gradient_background = false; // Gradient background mode // Saved skin ID for cancel/revert static std::string s_saved_skin_id; // Load current settings into UI state static void loadSettingsToUI(config::Settings* settings) { if (!settings) return; s_saved_skin_id = settings->getSkinId(); s_save_ztxs = settings->getSaveZtxs(); s_allow_custom_fees = settings->getAllowCustomFees(); s_auto_shield = settings->getAutoShield(); s_fetch_prices = settings->getFetchPrices(); s_use_tor = settings->getUseTor(); strncpy(s_tx_explorer, settings->getTxExplorerUrl().c_str(), sizeof(s_tx_explorer) - 1); strncpy(s_addr_explorer, settings->getAddressExplorerUrl().c_str(), sizeof(s_addr_explorer) - 1); // Set language index auto& i18n = util::I18n::instance(); const auto& languages = i18n.getAvailableLanguages(); std::string current_lang = settings->getLanguage(); if (current_lang.empty()) current_lang = "en"; s_language_index = 0; int idx = 0; for (const auto& lang : languages) { if (lang.first == current_lang) { s_language_index = idx; break; } idx++; } s_initialized = true; } // Save UI state to settings static void saveSettingsFromUI(config::Settings* settings) { if (!settings) return; settings->setTheme(settings->getSkinId()); // Theme now synced with skin settings->setSaveZtxs(s_save_ztxs); settings->setAllowCustomFees(s_allow_custom_fees); settings->setAutoShield(s_auto_shield); settings->setFetchPrices(s_fetch_prices); settings->setUseTor(s_use_tor); settings->setTxExplorerUrl(s_tx_explorer); settings->setAddressExplorerUrl(s_addr_explorer); // Save language auto& i18n = util::I18n::instance(); const auto& languages = i18n.getAvailableLanguages(); auto it = languages.begin(); std::advance(it, s_language_index); if (it != languages.end()) { settings->setLanguage(it->first); } // Save acrylic / visual effects settings settings->setAcrylicEnabled(s_acrylic_enabled); settings->setAcrylicQuality(s_blur_amount > 0.001f ? static_cast(effects::AcrylicQuality::Low) : static_cast(effects::AcrylicQuality::Off)); settings->setBlurMultiplier(s_blur_amount); settings->setReducedTransparency(s_reduced_transparency); settings->setNoiseOpacity(s_noise_opacity); settings->setGradientBackground(s_gradient_background); settings->save(); } void RenderSettingsWindow(App* app, bool* p_open) { // Load settings on first open if (!s_initialized && app->settings()) { loadSettingsToUI(app->settings()); // Initialize acrylic settings from current state s_acrylic_enabled = effects::ImGuiAcrylic::IsEnabled(); s_blur_amount = effects::ImGuiAcrylic::GetBlurMultiplier(); s_noise_opacity = effects::ImGuiAcrylic::GetNoiseOpacity(); s_reduced_transparency = effects::ImGuiAcrylic::GetReducedTransparency(); s_gradient_background = schema::SkinManager::instance().isGradientMode(); } auto& S = schema::UI(); auto win = S.window("dialogs.settings"); auto lbl = S.label("dialogs.settings", "label"); auto cmb = S.combo("dialogs.settings", "combo"); auto connLbl = S.label("dialogs.settings", "connection-label"); auto portInput = S.input("dialogs.settings", "port-input"); auto walletBtn = S.button("dialogs.settings", "wallet-button"); auto saveBtn = S.button("dialogs.settings", "save-button"); auto cancelBtn = S.button("dialogs.settings", "cancel-button"); if (!material::BeginOverlayDialog(TR("settings"), p_open, win.width, 0.94f)) { return; } if (ImGui::BeginTabBar("SettingsTabs")) { // General settings tab if (ImGui::BeginTabItem(TR("general"))) { ImGui::Spacing(); // Skin/theme selection ImGui::Text("%s", TR("theme")); ImGui::SameLine(lbl.position); // Active skin combo (populated from SkinManager) auto& skinMgr = schema::SkinManager::instance(); const auto& skins = skinMgr.available(); // Find active skin for preview text std::string active_preview = "DragonX"; bool active_is_custom = false; for (const auto& skin : skins) { if (skin.id == skinMgr.activeSkinId()) { active_preview = skin.name; active_is_custom = !skin.bundled; break; } } ImGui::SetNextItemWidth(cmb.width); if (ImGui::BeginCombo("##Theme", active_preview.c_str())) { // Bundled themes header ImGui::TextDisabled("%s", TR("settings_builtin")); ImGui::Separator(); for (size_t i = 0; i < skins.size(); i++) { const auto& skin = skins[i]; if (!skin.bundled) continue; bool is_selected = (skin.id == skinMgr.activeSkinId()); if (ImGui::Selectable(skin.name.c_str(), is_selected)) { skinMgr.setActiveSkin(skin.id); if (app->settings()) { app->settings()->setSkinId(skin.id); app->settings()->save(); } } if (is_selected) { ImGui::SetItemDefaultFocus(); } } // Custom themes (if any) bool has_custom = false; for (const auto& skin : skins) { if (!skin.bundled) { has_custom = true; break; } } if (has_custom) { ImGui::Spacing(); ImGui::TextDisabled("%s", TR("settings_custom")); ImGui::Separator(); for (size_t i = 0; i < skins.size(); i++) { const auto& skin = skins[i]; if (skin.bundled) continue; bool is_selected = (skin.id == skinMgr.activeSkinId()); if (!skin.valid) { ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f)); ImGui::BeginDisabled(true); std::string label = skin.name + " (invalid)"; ImGui::Selectable(label.c_str(), false); ImGui::EndDisabled(); ImGui::PopStyleColor(); if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) { ImGui::SetTooltip("%s", skin.validationError.c_str()); } } else { std::string label = skin.name; if (!skin.author.empty()) { label += " (" + skin.author + ")"; } if (ImGui::Selectable(label.c_str(), is_selected)) { skinMgr.setActiveSkin(skin.id); if (app->settings()) { app->settings()->setSkinId(skin.id); app->settings()->save(); } } if (is_selected) { ImGui::SetItemDefaultFocus(); } } } } ImGui::EndCombo(); } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_theme_hotkey")); // Show indicator if custom theme is active if (active_is_custom) { ImGui::SameLine(); ImGui::PushFont(material::Type().iconSmall()); ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), ICON_CUSTOM_THEME); ImGui::PopFont(); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("%s", TR("tt_custom_theme")); } } ImGui::SameLine(); ImGui::PushFont(material::Type().iconSmall()); if (material::StyledButton(ICON_REFRESH_THEMES, ImVec2(0, 0))) { skinMgr.refresh(); Notifications::instance().info("Theme list refreshed"); } ImGui::PopFont(); if (ImGui::IsItemHovered()) { ImGui::SetTooltip(TR("tt_scan_themes"), schema::SkinManager::getUserSkinsDirectory().c_str()); } ImGui::Spacing(); // Language selection ImGui::Text("%s", TR("language")); ImGui::SameLine(lbl.position); auto& i18n = util::I18n::instance(); const auto& languages = i18n.getAvailableLanguages(); // Build language display names array std::vector lang_names; lang_names.reserve(languages.size()); for (const auto& lang : languages) { lang_names.push_back(lang.second.c_str()); // Display name } ImGui::SetNextItemWidth(cmb.width); if (ImGui::Combo("##Language", &s_language_index, lang_names.data(), static_cast(lang_names.size()))) { // Get locale code from index auto it = languages.begin(); std::advance(it, s_language_index); i18n.loadLanguage(it->first); } ImGui::TextDisabled(" %s", TR("settings_language_note")); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); // Acrylic Effects settings ImGui::Text("%s", TR("settings_visual_effects")); ImGui::Spacing(); ImGui::Text("%s", TR("settings_acrylic_level")); ImGui::SameLine(lbl.position); ImGui::SetNextItemWidth(cmb.width); { char blur_fmt[16]; if (s_blur_amount < 0.01f) snprintf(blur_fmt, sizeof(blur_fmt), "Off"); else snprintf(blur_fmt, sizeof(blur_fmt), "%.0f%%%%", s_blur_amount * 25.0f); if (ImGui::SliderFloat("##AcrylicBlur", &s_blur_amount, 0.0f, 4.0f, blur_fmt, ImGuiSliderFlags_AlwaysClamp)) { if (s_blur_amount > 0.0f && s_blur_amount < 0.15f) s_blur_amount = 0.0f; s_acrylic_enabled = (s_blur_amount > 0.001f); effects::ImGuiAcrylic::ApplyBlurAmount(s_blur_amount); } } ImGui::TextDisabled(" %s", TR("tt_blur")); ImGui::Spacing(); ImGui::Text("%s", TR("settings_noise_opacity")); ImGui::SameLine(lbl.position); ImGui::SetNextItemWidth(cmb.width); { char noise_fmt[16]; if (s_noise_opacity < 0.01f) snprintf(noise_fmt, sizeof(noise_fmt), "Off"); else snprintf(noise_fmt, sizeof(noise_fmt), "%.0f%%%%", s_noise_opacity * 100.0f); if (ImGui::SliderFloat("##NoiseOpacity", &s_noise_opacity, 0.0f, 1.0f, noise_fmt, ImGuiSliderFlags_AlwaysClamp)) { effects::ImGuiAcrylic::SetNoiseOpacity(s_noise_opacity); } } ImGui::TextDisabled(" %s", TR("tt_noise")); ImGui::Spacing(); // Accessibility: Reduced transparency if (ImGui::Checkbox(TrId("settings_reduce_transparency", "reduce_trans").c_str(), &s_reduced_transparency)) { effects::ImGuiAcrylic::SetReducedTransparency(s_reduced_transparency); } ImGui::TextDisabled(" %s", TR("settings_solid_colors_desc")); ImGui::Spacing(); if (ImGui::Checkbox(TrId("simple_background", "simple_bg").c_str(), &s_gradient_background)) { schema::SkinManager::instance().setGradientMode(s_gradient_background); } ImGui::TextDisabled(" %s", TR("settings_gradient_desc")); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); // Privacy settings ImGui::Text("%s", TR("settings_privacy")); ImGui::Spacing(); ImGui::Checkbox(TrId("settings_save_shielded_local", "save_ztx_w").c_str(), &s_save_ztxs); ImGui::TextDisabled(" %s", TR("settings_save_shielded_desc")); ImGui::Spacing(); ImGui::Checkbox(TrId("settings_auto_shield_funds", "auto_shld_w").c_str(), &s_auto_shield); ImGui::TextDisabled(" %s", TR("settings_auto_shield_desc")); ImGui::Spacing(); ImGui::Checkbox(TrId("settings_use_tor_network", "tor_w").c_str(), &s_use_tor); ImGui::TextDisabled(" %s", TR("settings_tor_desc")); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); // Other settings ImGui::Text("%s", TR("settings_other")); ImGui::Spacing(); ImGui::Checkbox(TrId("custom_fees", "fees_w").c_str(), &s_allow_custom_fees); ImGui::Checkbox(TrId("fetch_prices", "prices_w").c_str(), &s_fetch_prices); ImGui::EndTabItem(); } // Connection settings tab if (ImGui::BeginTabItem(TR("settings_connection"))) { ImGui::Spacing(); ImGui::Text("%s", TR("settings_rpc_connection")); ImGui::TextDisabled("%s", TR("settings_configure_rpc")); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); ImGui::Text("%s", TR("rpc_host")); ImGui::SameLine(connLbl.position); ImGui::SetNextItemWidth(cmb.width); ImGui::InputText("##RPCHost", s_rpc_host, sizeof(s_rpc_host)); ImGui::Text("%s", TR("rpc_port")); ImGui::SameLine(connLbl.position); ImGui::SetNextItemWidth(portInput.width); ImGui::InputText("##RPCPort", s_rpc_port, sizeof(s_rpc_port)); ImGui::Spacing(); ImGui::Text("%s", TR("rpc_user")); ImGui::SameLine(connLbl.position); ImGui::SetNextItemWidth(cmb.width); ImGui::InputText("##RPCUser", s_rpc_user, sizeof(s_rpc_user)); ImGui::Text("%s", TR("rpc_pass")); ImGui::SameLine(connLbl.position); ImGui::SetNextItemWidth(cmb.width); ImGui::InputText("##RPCPassword", s_rpc_password, sizeof(s_rpc_password), ImGuiInputTextFlags_Password); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); ImGui::TextDisabled("%s", TR("settings_rpc_note")); ImGui::Spacing(); if (material::StyledButton(TR("test_connection"), ImVec2(0,0), S.resolveFont("button"))) { if (app->rpc()) { app->rpc()->getInfo([](const nlohmann::json& result, const std::string& error) { if (error.empty()) { std::string version = result.value("version", "unknown"); std::string msg = "Connection successful!\ndragonxd version: " + version; Notifications::instance().success(msg); } else { Notifications::instance().error("Connection failed: " + error); } }); } else { Notifications::instance().error("RPC client not initialized"); } } ImGui::EndTabItem(); } // Wallet tab if (ImGui::BeginTabItem(TR("wallet"))) { ImGui::Spacing(); ImGui::Text("%s", TR("settings_wallet_maintenance")); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); if (material::StyledButton(TR("rescan"), ImVec2(walletBtn.width, 0), S.resolveFont(walletBtn.font))) { if (app->rpc()) { // Start rescan from block 0 app->rpc()->rescanBlockchain(0, [](const nlohmann::json& result, const std::string& error) { if (error.empty()) { int start = result.value("start_height", 0); int end = result.value("stop_height", 0); std::string msg = "Rescan started from block " + std::to_string(start) + " to " + std::to_string(end); Notifications::instance().success(msg); } else { Notifications::instance().error("Rescan failed: " + error); } }); } else { Notifications::instance().error("RPC client not initialized"); } } ImGui::TextDisabled(" %s", TR("settings_rescan_desc")); ImGui::Spacing(); static bool s_confirm_clear_ztx = false; if (material::StyledButton(TR("settings_clear_ztx_long"), ImVec2(walletBtn.width, 0), S.resolveFont(walletBtn.font))) { s_confirm_clear_ztx = true; } ImGui::TextDisabled(" %s", TR("settings_clear_ztx_desc")); // Confirmation dialog if (s_confirm_clear_ztx) { if (material::BeginOverlayDialog(TR("confirm_clear_ztx_title"), &s_confirm_clear_ztx, 480.0f, 0.94f)) { ImGui::PushFont(material::Type().iconLarge()); ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), ICON_MD_WARNING); ImGui::PopFont(); ImGui::SameLine(); ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f), "%s", TR("warning")); ImGui::Spacing(); ImGui::TextWrapped("%s", TR("confirm_clear_ztx_warning1")); ImGui::Spacing(); ImGui::TextWrapped("%s", TR("confirm_clear_ztx_warning2")); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); float btnW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; if (ImGui::Button(TR("cancel"), ImVec2(btnW, 40))) { s_confirm_clear_ztx = false; } ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.8f, 0.2f, 0.2f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.9f, 0.3f, 0.3f, 1.0f)); if (ImGui::Button(TrId("clear_anyway", "clear_ztx_w").c_str(), ImVec2(btnW, 40))) { std::string ztx_file = util::Platform::getDragonXDataDir() + "ztx_history.json"; if (util::Platform::deleteFile(ztx_file)) { Notifications::instance().success("Z-transaction history cleared"); } else { Notifications::instance().info("No history file found"); } s_confirm_clear_ztx = false; } ImGui::PopStyleColor(2); material::EndOverlayDialog(); } } ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); ImGui::Text("%s", TR("settings_wallet_info")); ImGui::Spacing(); // Get actual wallet size std::string wallet_path = util::Platform::getDragonXDataDir() + "wallet.dat"; uint64_t wallet_size = util::Platform::getFileSize(wallet_path); if (wallet_size > 0) { std::string size_str = util::Platform::formatFileSize(wallet_size); ImGui::Text(TR("settings_wallet_file_size"), size_str.c_str()); } else { ImGui::TextDisabled("%s", TR("settings_wallet_not_found")); } ImGui::Text(TR("settings_wallet_location"), wallet_path.c_str()); ImGui::EndTabItem(); } // Explorer tab if (ImGui::BeginTabItem(TR("explorer"))) { ImGui::Spacing(); ImGui::Text("%s", TR("settings_block_explorer_urls")); ImGui::TextDisabled("%s", TR("settings_configure_explorer")); ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); ImGui::Text("%s", TR("transaction_url")); ImGui::SetNextItemWidth(-1); ImGui::InputText("##TxExplorer", s_tx_explorer, sizeof(s_tx_explorer)); ImGui::Text("%s", TR("address_url")); ImGui::SetNextItemWidth(-1); ImGui::InputText("##AddrExplorer", s_addr_explorer, sizeof(s_addr_explorer)); ImGui::Spacing(); ImGui::TextDisabled("%s", TR("settings_explorer_hint")); ImGui::EndTabItem(); } ImGui::EndTabBar(); } ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); // Save/Cancel buttons if (material::StyledButton(TR("save"), ImVec2(saveBtn.width, 0), S.resolveFont(saveBtn.font))) { saveSettingsFromUI(app->settings()); Notifications::instance().success("Settings saved"); *p_open = false; } ImGui::SameLine(); if (material::StyledButton(TR("cancel"), ImVec2(cancelBtn.width, 0), S.resolveFont(cancelBtn.font))) { // Reload settings to revert changes loadSettingsToUI(app->settings()); // Revert skin to what was active when settings opened if (!s_saved_skin_id.empty()) { schema::SkinManager::instance().setActiveSkin(s_saved_skin_id); if (app->settings()) { app->settings()->setSkinId(s_saved_skin_id); app->settings()->save(); } } *p_open = false; } material::EndOverlayDialog(); } } // namespace ui } // namespace dragonx