// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #include "settings_page.h" #include "../../app.h" #include "../../config/version.h" #include "../../config/settings.h" #include "../../util/logger.h" #include "../windows/balance_tab.h" #include "../windows/console_tab.h" #include "../../util/i18n.h" #include "../../util/platform.h" #include "../../rpc/rpc_client.h" #include "../../rpc/rpc_worker.h" #include "../theme.h" #include "../layout.h" #include "../schema/ui_schema.h" #include "../schema/skin_manager.h" #include "../notifications.h" #include "../effects/imgui_acrylic.h" #include "../effects/theme_effects.h" #include "../effects/low_spec.h" #include "../effects/scroll_fade_shader.h" #include "../material/draw_helpers.h" #include "../material/type.h" #include "../material/colors.h" #include "../windows/validate_address_dialog.h" #include "../windows/address_book_dialog.h" #include "../windows/shield_dialog.h" #include "../windows/request_payment_dialog.h" #include "../windows/block_info_dialog.h" #include "../windows/export_all_keys_dialog.h" #include "../windows/export_transactions_dialog.h" #include "../../embedded/IconsMaterialDesign.h" #include "imgui.h" #include #include #include #include #include #include namespace dragonx { namespace ui { using namespace material; // 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 — loaded from config::Settings on first render // ============================================================================ static bool sp_initialized = false; static int sp_language_index = 0; static bool sp_save_ztxs = true; static bool sp_allow_custom_fees = false; static bool sp_auto_shield = false; static bool sp_fetch_prices = true; static bool sp_use_tor = false; static char sp_rpc_host[128] = DRAGONX_DEFAULT_RPC_HOST; static char sp_rpc_port[16] = DRAGONX_DEFAULT_RPC_PORT; static char sp_rpc_user[64] = ""; static char sp_rpc_password[64] = ""; static char sp_tx_explorer[256] = "https://explorer.dragonx.is/tx/"; static char sp_addr_explorer[256] = "https://explorer.dragonx.is/address/"; // Acrylic settings static bool sp_acrylic_enabled = true; static float sp_blur_amount = 1.5f; // 0.0=Off, 0.01–4.0 continuous blur multiplier static float sp_noise_opacity = 0.5f; // 0.0–1.0 multiplier static float sp_ui_opacity = 1.0f; // 0.3–1.0 card/sidebar opacity static float sp_window_opacity = 1.0f; // 0.3–1.0 background alpha // Balance layout (string ID) static std::string sp_balance_layout = "classic"; // Console scanline static bool sp_scanline_enabled = true; // Theme effects static bool sp_theme_effects_enabled = true; // Gradient background mode static bool sp_gradient_background = false; // Low-spec mode static bool sp_low_spec_mode = false; // Font scale (user accessibility, 1.0–3.0) static float sp_font_scale = 1.0f; // Snapshot of effect settings saved when low-spec is toggled ON, // restored when toggled OFF so user state isn't lost. struct LowSpecSnapshot { bool valid = false; bool acrylic_enabled; float blur_amount; float ui_opacity; float window_opacity; bool theme_effects_enabled; bool scanline_enabled; }; static LowSpecSnapshot s_lowSpecSnap; // Daemon — keep running on close static bool sp_keep_daemon_running = false; static bool sp_stop_external_daemon = false; // Mining — mine when idle static bool sp_mine_when_idle = false; static int sp_mine_idle_delay = 120; static bool sp_idle_thread_scaling = false; static int sp_idle_threads_active = 0; static int sp_idle_threads_idle = 0; static bool sp_verbose_logging = false; // Debug logging categories static std::set sp_debug_categories; static bool sp_debug_cats_dirty = false; // true when changed but daemon not yet restarted static bool sp_debug_expanded = false; // collapsible card state static bool sp_effects_expanded = false; // "Advanced Effects..." toggle static bool sp_tools_expanded = false; // "Tools & Actions..." toggle static bool sp_confirm_clear_ztx = false; // confirmation dialog for clearing z-tx history static bool sp_confirm_delete_blockchain = false; // confirmation dialog for deleting blockchain data // (APPEARANCE card now uses ChannelsSplit like all other cards) // Shader-based scroll-edge fade (per-pixel alpha mask via custom fragment shader) static effects::ScrollFadeShader s_fadeShader; static void loadSettingsPageState(config::Settings* settings) { if (!settings) return; sp_save_ztxs = settings->getSaveZtxs(); sp_allow_custom_fees = settings->getAllowCustomFees(); sp_auto_shield = settings->getAutoShield(); sp_fetch_prices = settings->getFetchPrices(); sp_use_tor = settings->getUseTor(); strncpy(sp_tx_explorer, settings->getTxExplorerUrl().c_str(), sizeof(sp_tx_explorer) - 1); strncpy(sp_addr_explorer, settings->getAddressExplorerUrl().c_str(), sizeof(sp_addr_explorer) - 1); auto& i18n = util::I18n::instance(); const auto& languages = i18n.getAvailableLanguages(); std::string current_lang = settings->getLanguage(); if (current_lang.empty()) current_lang = "en"; sp_language_index = 0; int idx = 0; for (const auto& lang : languages) { if (lang.first == current_lang) { sp_language_index = idx; break; } idx++; } // Load blur amount directly from saved multiplier sp_blur_amount = settings->getBlurMultiplier(); sp_acrylic_enabled = (sp_blur_amount > 0.001f); sp_ui_opacity = settings->getUIOpacity(); sp_window_opacity = settings->getWindowOpacity(); sp_noise_opacity = settings->getNoiseOpacity(); sp_gradient_background = settings->getGradientBackground(); sp_balance_layout = settings->getBalanceLayout(); sp_scanline_enabled = settings->getScanlineEnabled(); ConsoleTab::s_scanline_enabled = sp_scanline_enabled; sp_theme_effects_enabled = settings->getThemeEffectsEnabled(); sp_low_spec_mode = settings->getLowSpecMode(); effects::setLowSpecMode(sp_low_spec_mode); sp_font_scale = settings->getFontScale(); Layout::setUserFontScale(sp_font_scale); // sync with Layout on load sp_keep_daemon_running = settings->getKeepDaemonRunning(); sp_stop_external_daemon = settings->getStopExternalDaemon(); sp_mine_when_idle = settings->getMineWhenIdle(); sp_mine_idle_delay = settings->getMineIdleDelay(); sp_idle_thread_scaling = settings->getIdleThreadScaling(); sp_idle_threads_active = settings->getIdleThreadsActive(); sp_idle_threads_idle = settings->getIdleThreadsIdle(); sp_verbose_logging = settings->getVerboseLogging(); sp_debug_categories = settings->getDebugCategories(); sp_debug_cats_dirty = false; // Apply loaded visual effects settings effects::ImGuiAcrylic::ApplyBlurAmount(sp_blur_amount); effects::ImGuiAcrylic::SetUIOpacity(sp_ui_opacity); effects::ImGuiAcrylic::SetNoiseOpacity(sp_noise_opacity); effects::ThemeEffects::instance().setEnabled(sp_theme_effects_enabled); sp_initialized = true; } static void saveSettingsPageState(config::Settings* settings) { if (!settings) return; settings->setTheme(settings->getSkinId()); settings->setSaveZtxs(sp_save_ztxs); settings->setAllowCustomFees(sp_allow_custom_fees); settings->setAutoShield(sp_auto_shield); settings->setFetchPrices(sp_fetch_prices); settings->setUseTor(sp_use_tor); settings->setTxExplorerUrl(sp_tx_explorer); settings->setAddressExplorerUrl(sp_addr_explorer); auto& i18n = util::I18n::instance(); const auto& languages = i18n.getAvailableLanguages(); auto it = languages.begin(); std::advance(it, sp_language_index); if (it != languages.end()) { settings->setLanguage(it->first); } // Visual effects settings settings->setAcrylicEnabled(sp_acrylic_enabled); settings->setAcrylicQuality(sp_blur_amount > 0.001f ? static_cast(effects::AcrylicQuality::Low) : static_cast(effects::AcrylicQuality::Off)); settings->setBlurMultiplier(sp_blur_amount); settings->setUIOpacity(sp_ui_opacity); settings->setWindowOpacity(sp_window_opacity); settings->setNoiseOpacity(sp_noise_opacity); settings->setGradientBackground(sp_gradient_background); settings->setScanlineEnabled(sp_scanline_enabled); settings->setThemeEffectsEnabled(sp_theme_effects_enabled); settings->setLowSpecMode(sp_low_spec_mode); settings->setFontScale(sp_font_scale); settings->setKeepDaemonRunning(sp_keep_daemon_running); settings->setStopExternalDaemon(sp_stop_external_daemon); settings->setMineWhenIdle(sp_mine_when_idle); settings->setMineIdleDelay(sp_mine_idle_delay); settings->setIdleThreadScaling(sp_idle_thread_scaling); settings->setIdleThreadsActive(sp_idle_threads_active); settings->setIdleThreadsIdle(sp_idle_threads_idle); settings->setVerboseLogging(sp_verbose_logging); settings->setDebugCategories(sp_debug_categories); settings->save(); } // ============================================================================ // Settings Page Renderer // ============================================================================ void RenderSettingsPage(App* app) { // Load settings state on first render if (!sp_initialized && app->settings()) { loadSettingsPageState(app->settings()); } // Sync low-spec / theme-effects state from runtime each frame // so that hotkey toggles are reflected in the checkboxes. { bool runtimeLowSpec = effects::isLowSpecMode(); if (sp_low_spec_mode != runtimeLowSpec) { if (runtimeLowSpec) { // Hotkey turned low-spec ON — save snapshot, override statics s_lowSpecSnap.valid = true; s_lowSpecSnap.acrylic_enabled = sp_acrylic_enabled; s_lowSpecSnap.blur_amount = sp_blur_amount; s_lowSpecSnap.ui_opacity = sp_ui_opacity; s_lowSpecSnap.window_opacity = sp_window_opacity; s_lowSpecSnap.theme_effects_enabled = sp_theme_effects_enabled; s_lowSpecSnap.scanline_enabled = sp_scanline_enabled; sp_acrylic_enabled = false; sp_blur_amount = 0.0f; sp_ui_opacity = 1.0f; sp_window_opacity = 1.0f; sp_theme_effects_enabled = false; sp_scanline_enabled = false; } else if (s_lowSpecSnap.valid) { // Hotkey turned low-spec OFF — restore snapshot sp_blur_amount = s_lowSpecSnap.blur_amount; sp_acrylic_enabled = (sp_blur_amount > 0.001f); sp_ui_opacity = s_lowSpecSnap.ui_opacity; sp_window_opacity = s_lowSpecSnap.window_opacity; sp_theme_effects_enabled = s_lowSpecSnap.theme_effects_enabled; sp_scanline_enabled = s_lowSpecSnap.scanline_enabled; s_lowSpecSnap.valid = false; } else if (app->settings()) { // No snapshot — read prefs from settings file sp_blur_amount = app->settings()->getBlurMultiplier(); sp_acrylic_enabled = (sp_blur_amount > 0.001f); sp_ui_opacity = app->settings()->getUIOpacity(); sp_window_opacity = app->settings()->getWindowOpacity(); sp_theme_effects_enabled = app->settings()->getThemeEffectsEnabled(); sp_scanline_enabled = app->settings()->getScanlineEnabled(); } sp_low_spec_mode = runtimeLowSpec; } bool runtimeThemeEffects = effects::ThemeEffects::instance().isEnabled(); if (sp_theme_effects_enabled != runtimeThemeEffects) { sp_theme_effects_enabled = runtimeThemeEffects; } } auto& S = schema::UI(); // Responsive layout — matches other tabs ImVec2 contentAvail = ImGui::GetContentRegionAvail(); float scrollbarMargin = ImGui::GetStyle().ScrollbarSize + Layout::spacingSm(); float availWidth = contentAvail.x - scrollbarMargin; float hs = Layout::hScale(availWidth); float vs = Layout::vScale(contentAvail.y); float pad = Layout::cardInnerPadding(); float bottomPad = std::max(0.0f, pad - ImGui::GetStyle().ItemSpacing.y); float gap = Layout::cardGap(); float glassRound = Layout::glassRounding(); char buf[256]; // Label column position — adaptive to width float labelW = std::max(S.drawElement("components.settings-page", "label-min-width").size, S.drawElement("components.settings-page", "label-width").size * hs); // Widen labelW if any translated label is wider (prevents overflow) { ImFont* mf = Type().body2(); const char* labelKeys[] = {"theme", "balance_layout", "language", "acrylic", "noise", "ui_opacity", "window_opacity", "font_scale", "rpc_host", "rpc_port", "rpc_user", "rpc_pass", "transaction_url", "address_url"}; for (const char* k : labelKeys) { float tw = mf->CalcTextSizeA(mf->LegacySize, FLT_MAX, 0, TR(k)).x + Layout::spacingMd(); if (tw > labelW) labelW = tw; } } // Input field width — fill remaining space in card float inputW = std::max(S.drawElement("components.settings-page", "input-min-width").size, availWidth - labelW - pad * 2); // Scrollable content area — NoBackground matches other tabs ImGui::BeginChild("##SettingsPageScroll", ImVec2(0, 0), false, ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollWithMouse); ApplySmoothScroll(); // Capture the ACTUAL clip boundaries from inside the child window ImVec2 childClipMin = ImGui::GetWindowPos(); ImVec2 childClipMax(childClipMin.x + ImGui::GetWindowSize().x, childClipMin.y + ImGui::GetWindowSize().y); const float dp = Layout::dpiScale(); const float fadeH = schema::UI().drawElement("components.settings-page", "edge-fade-zone").size * dp; const float fadeOffTop = schema::UI().drawElement("components.settings-page", "edge-fade-offset-top").size * dp; const float fadeOffBot = schema::UI().drawElement("components.settings-page", "edge-fade-offset-bottom").size * dp; // Get draw list AFTER BeginChild so we draw on the child window's list ImDrawList* dl = ImGui::GetWindowDrawList(); (void)dl; // used by cards below // Capture ForegroundDrawList vertex start — DrawGlassPanel draws // theme effects (rainbow border, shimmer, specular glare, edge trace) // on the ForegroundDrawList; those bypass the shader fade so we'll // apply a vertex-based alpha fade to them after rendering. ImDrawList* fgDL = ImGui::GetForegroundDrawList(); int fgVtxStart = fgDL->VtxBuffer.Size; // --- Shader-based scroll fade: bind custom fragment shader --- // The shader multiplies output alpha by a smoothstep gradient based // on screen Y, giving a true per-pixel alpha mask at scroll edges. float settingsScrollY_pre = ImGui::GetScrollY(); float settingsScrollMaxY_pre = ImGui::GetScrollMaxY(); float settingsFadeTopY = childClipMin.y + fadeOffTop; float settingsFadeBottomY = childClipMax.y - fadeOffBot; float settingsFadeZoneTop = (settingsScrollY_pre > 1.0f) ? fadeH : 0.0f; float settingsFadeZoneBot = (settingsScrollMaxY_pre > 0 && settingsScrollY_pre < settingsScrollMaxY_pre - 1.0f) ? fadeH : 0.0f; if (fadeH > 0.0f && !sp_low_spec_mode && s_fadeShader.init()) { s_fadeShader.fadeTopY = settingsFadeTopY; s_fadeShader.fadeBottomY = settingsFadeBottomY; s_fadeShader.fadeZoneTop = settingsFadeZoneTop; s_fadeShader.fadeZoneBottom = settingsFadeZoneBot; s_fadeShader.addBind(dl); } // Top margin from schema float topMargin = schema::UI().drawElement("components.settings-page", "top-margin").size; if (topMargin > 0.0f) ImGui::Dummy(ImVec2(0, topMargin)); GlassPanelSpec glassSpec; glassSpec.rounding = glassRound; ImFont* capFont = Type().caption(); ImFont* body2 = Type().body2(); ImFont* sub1 = Type().subtitle1(); // ==================================================================== // THEME & LANGUAGE — card (draw-first approach; avoids ChannelsSplit // which breaks BeginCombo popup rendering in some ImGui versions) // ==================================================================== { Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("theme_language")); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); ImVec2 cardMin = ImGui::GetCursorScreenPos(); dl->ChannelsSplit(2); dl->ChannelsSetCurrent(1); ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad)); ImGui::Indent(pad); float contentW = availWidth - pad * 2; float comboGap = S.drawElement("components.settings-page", "combo-row-gap").size; float compactBP = S.drawElement("components.settings-page", "compact-breakpoint").size; bool wideLayout = availWidth >= compactBP; float refreshBtnW = S.drawElement("components.settings-page", "refresh-btn-width").size; // --- Skin data --- auto& skinMgr = schema::SkinManager::instance(); const auto& skins = skinMgr.available(); 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; } } // --- Language data --- auto& i18n = util::I18n::instance(); const auto& languages = i18n.getAvailableLanguages(); std::vector lang_names; lang_names.reserve(languages.size()); for (const auto& lang : languages) { lang_names.push_back(lang.second.c_str()); } // --- Balance layout data --- const auto& layouts = GetBalanceLayouts(); std::string balPreview = sp_balance_layout; for (const auto& l : layouts) { if (l.id == sp_balance_layout) { balPreview = l.name; break; } } // --- Theme combo popup (shared between wide and narrow paths) --- auto renderThemeComboPopup = [&]() { 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(); } 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 lbl = skin.name + " (invalid)"; ImGui::Selectable(lbl.c_str(), false); ImGui::EndDisabled(); ImGui::PopStyleColor(); if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", skin.validationError.c_str()); } else { std::string lbl = skin.name; if (!skin.author.empty()) lbl += " (" + skin.author + ")"; if (ImGui::Selectable(lbl.c_str(), is_selected)) { skinMgr.setActiveSkin(skin.id); if (app->settings()) { app->settings()->setSkinId(skin.id); app->settings()->save(); } } if (is_selected) ImGui::SetItemDefaultFocus(); } } } }; if (wideLayout) { // ============================================================ // Wide: 3 combos on one row + compact 3-column effects grid // ============================================================ // --- Combo row: Theme | Layout | Language [Refresh] --- { ImGui::PushFont(body2); float lblGap = Layout::spacingXs(); float lblThemeW = ImGui::CalcTextSize(TR("theme")).x + lblGap; float lblLayoutW = ImGui::CalcTextSize(TR("balance_layout")).x + lblGap; float lblLangW = ImGui::CalcTextSize(TR("language")).x + lblGap; float totalFixed = lblThemeW + lblLayoutW + lblLangW + comboGap * 2 + Layout::spacingSm() + refreshBtnW; float comboW = std::max(80.0f, (contentW - totalFixed) / 3.0f); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("theme")); ImGui::SameLine(0, lblGap); ImGui::SetNextItemWidth(comboW); if (ImGui::BeginCombo("##Theme", active_preview.c_str())) { renderThemeComboPopup(); ImGui::EndCombo(); } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_theme_hotkey")); ImGui::SameLine(0, comboGap); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("balance_layout")); ImGui::SameLine(0, lblGap); ImGui::SetNextItemWidth(comboW); if (ImGui::BeginCombo("##BalanceLayout", balPreview.c_str())) { for (const auto& l : layouts) { if (!l.enabled) continue; bool selected = (l.id == sp_balance_layout); if (ImGui::Selectable(l.name.c_str(), selected)) { sp_balance_layout = l.id; if (app->settings()) { app->settings()->setBalanceLayout(sp_balance_layout); app->settings()->save(); } } if (selected) ImGui::SetItemDefaultFocus(); } ImGui::EndCombo(); } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_layout_hotkey")); ImGui::SameLine(0, comboGap); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("language")); ImGui::SameLine(0, lblGap); ImGui::SetNextItemWidth(comboW); if (ImGui::Combo("##Language", &sp_language_index, lang_names.data(), static_cast(lang_names.size()))) { auto it = languages.begin(); std::advance(it, sp_language_index); i18n.loadLanguage(it->first); if (app->settings()) { app->settings()->setLanguage(it->first); app->settings()->save(); } } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_language")); ImGui::SameLine(0, Layout::spacingSm()); if (TactileButton(TR("refresh"), ImVec2(refreshBtnW, 0), S.resolveFont("button"))) { schema::SkinManager::instance().refresh(); Notifications::instance().info("Theme list refreshed"); } if (ImGui::IsItemHovered()) { ImGui::SetTooltip(TR("tt_scan_themes"), schema::SkinManager::getUserSkinsDirectory().c_str()); } ImGui::PopFont(); } ImGui::Dummy(ImVec2(0, Layout::spacingSm())); // --- Font Scale slider (always visible) --- { ImGui::PushFont(body2); ImGui::TextUnformatted(TR("font_scale")); float fontSliderW = std::max(S.drawElement("components.settings-page", "effects-input-min-width").size, contentW); ImGui::SetNextItemWidth(fontSliderW); sp_font_scale = Layout::userFontScale(); float prev_font_scale = sp_font_scale; { char fs_fmt[16]; snprintf(fs_fmt, sizeof(fs_fmt), "%.2fx", sp_font_scale); ImGui::SliderFloat("##FontScale", &sp_font_scale, 1.0f, 1.5f, fs_fmt, ImGuiSliderFlags_AlwaysClamp); } sp_font_scale = std::max(1.0f, std::min(1.5f, std::round(sp_font_scale * 20.0f) / 20.0f)); if (sp_font_scale != prev_font_scale) Layout::setUserFontScaleVisual(sp_font_scale); if (ImGui::IsItemDeactivatedAfterEdit()) { Layout::setUserFontScale(sp_font_scale); saveSettingsPageState(app->settings()); } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_font_scale")); ImGui::PopFont(); } ImGui::Dummy(ImVec2(0, Layout::spacingSm())); // --- Collapsible: Advanced Effects... --- { const char* arrow = sp_effects_expanded ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE; ImGui::PushFont(body2); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0,0,0,0)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1,1,1,0.05f)); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1,1,1,0.08f)); { ImVec2 hdrPos = ImGui::GetCursorScreenPos(); if (ImGui::Button("##EffectsToggle", ImVec2(contentW, ImGui::GetFrameHeight()))) { sp_effects_expanded = !sp_effects_expanded; } float textY = hdrPos.y + (ImGui::GetFrameHeight() - body2->LegacySize) * 0.5f; dl->AddText(body2, body2->LegacySize, ImVec2(hdrPos.x, textY), OnSurfaceMedium(), TR("advanced_effects")); ImFont* iconFont = Type().iconSmall(); if (!iconFont) iconFont = body2; float arrowW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, arrow).x; dl->AddText(iconFont, iconFont->LegacySize, ImVec2(hdrPos.x + contentW - arrowW, textY), OnSurfaceMedium(), arrow); } ImGui::PopStyleColor(3); ImGui::PopFont(); } if (sp_effects_expanded) { ImGui::PushFont(body2); // Checkbox row: Low-spec | Console scanline | Theme effects | Gradient background if (ImGui::Checkbox(TrId("low_spec_mode", "low_spec").c_str(), &sp_low_spec_mode)) { effects::setLowSpecMode(sp_low_spec_mode); if (sp_low_spec_mode) { s_lowSpecSnap.valid = true; s_lowSpecSnap.acrylic_enabled = sp_acrylic_enabled; s_lowSpecSnap.blur_amount = sp_blur_amount; s_lowSpecSnap.ui_opacity = sp_ui_opacity; s_lowSpecSnap.window_opacity = sp_window_opacity; s_lowSpecSnap.theme_effects_enabled = sp_theme_effects_enabled; s_lowSpecSnap.scanline_enabled = sp_scanline_enabled; sp_acrylic_enabled = false; sp_blur_amount = 0.0f; sp_ui_opacity = 1.0f; sp_window_opacity = 1.0f; sp_theme_effects_enabled = false; sp_scanline_enabled = false; effects::ImGuiAcrylic::ApplyBlurAmount(0.0f); effects::ImGuiAcrylic::SetUIOpacity(1.0f); effects::ThemeEffects::instance().setEnabled(false); ConsoleTab::s_scanline_enabled = false; } else if (s_lowSpecSnap.valid) { sp_blur_amount = s_lowSpecSnap.blur_amount; sp_acrylic_enabled = (sp_blur_amount > 0.001f); sp_ui_opacity = s_lowSpecSnap.ui_opacity; sp_window_opacity = s_lowSpecSnap.window_opacity; sp_theme_effects_enabled = s_lowSpecSnap.theme_effects_enabled; sp_scanline_enabled = s_lowSpecSnap.scanline_enabled; effects::ImGuiAcrylic::ApplyBlurAmount(sp_blur_amount); effects::ImGuiAcrylic::SetUIOpacity(sp_ui_opacity); effects::ThemeEffects::instance().setEnabled(sp_theme_effects_enabled); ConsoleTab::s_scanline_enabled = sp_scanline_enabled; s_lowSpecSnap.valid = false; } saveSettingsPageState(app->settings()); } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_low_spec")); ImGui::SameLine(0, Layout::spacingLg()); if (ImGui::Checkbox(TrId("simple_background", "simple_bg").c_str(), &sp_gradient_background)) { schema::SkinManager::instance().setGradientMode(sp_gradient_background); saveSettingsPageState(app->settings()); } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_simple_bg")); ImGui::BeginDisabled(sp_low_spec_mode); ImGui::SameLine(0, Layout::spacingLg()); if (ImGui::Checkbox(TrId("console_scanline", "scanline").c_str(), &sp_scanline_enabled)) { ConsoleTab::s_scanline_enabled = sp_scanline_enabled; app->settings()->setScanlineEnabled(sp_scanline_enabled); app->settings()->save(); } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_scanline")); ImGui::SameLine(0, Layout::spacingLg()); if (ImGui::Checkbox(TrId("theme_effects", "effects").c_str(), &sp_theme_effects_enabled)) { effects::ThemeEffects::instance().setEnabled(sp_theme_effects_enabled); saveSettingsPageState(app->settings()); } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_theme_effects")); // Row 1: Acrylic preset slider + Noise slider (side by side, labels above) float effCtrlMinW = S.drawElement("components.settings-page", "effects-input-min-width").size; float halfW = (contentW - Layout::spacingLg()) * 0.5f; float ctrlW = std::max(effCtrlMinW, halfW); float baseX = ImGui::GetCursorScreenPos().x; float rightX = baseX + ctrlW + Layout::spacingLg(); ImGui::TextUnformatted(TR("acrylic")); float row1Y = ImGui::GetCursorScreenPos().y; ImGui::SetNextItemWidth(ctrlW); { char blur_fmt[16]; if (sp_blur_amount < 0.01f) snprintf(blur_fmt, sizeof(blur_fmt), "Off"); else snprintf(blur_fmt, sizeof(blur_fmt), "%.0f%%%%", sp_blur_amount * 25.0f); if (ImGui::SliderFloat("##AcrylicBlur", &sp_blur_amount, 0.0f, 4.0f, blur_fmt, ImGuiSliderFlags_AlwaysClamp)) { if (sp_blur_amount > 0.0f && sp_blur_amount < 0.15f) sp_blur_amount = 0.0f; sp_acrylic_enabled = (sp_blur_amount > 0.001f); effects::ImGuiAcrylic::ApplyBlurAmount(sp_blur_amount); } } if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings()); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_blur")); float afterRow1Y = ImGui::GetCursorScreenPos().y; float lblH = ImGui::GetTextLineHeight() + ImGui::GetStyle().ItemSpacing.y; ImGui::SetCursorScreenPos(ImVec2(rightX, row1Y - lblH)); ImGui::TextUnformatted(TR("noise")); ImGui::SetCursorScreenPos(ImVec2(rightX, row1Y)); ImGui::SetNextItemWidth(ctrlW); { char noise_fmt[16]; if (sp_noise_opacity < 0.01f) snprintf(noise_fmt, sizeof(noise_fmt), "Off"); else snprintf(noise_fmt, sizeof(noise_fmt), "%.0f%%%%", sp_noise_opacity * 100.0f); if (ImGui::SliderFloat("##NoiseOpacity", &sp_noise_opacity, 0.0f, 1.0f, noise_fmt, ImGuiSliderFlags_AlwaysClamp)) { effects::ImGuiAcrylic::SetNoiseOpacity(sp_noise_opacity); saveSettingsPageState(app->settings()); } } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_noise")); ImGui::SetCursorScreenPos(ImVec2(baseX, afterRow1Y)); ImGui::TextUnformatted(TR("ui_opacity")); float row2Y = ImGui::GetCursorScreenPos().y; ImGui::SetNextItemWidth(ctrlW); { char uiop_fmt[16]; snprintf(uiop_fmt, sizeof(uiop_fmt), "%.0f%%%%", sp_ui_opacity * 100.0f); if (ImGui::SliderFloat("##UIOpacity", &sp_ui_opacity, 0.3f, 1.0f, uiop_fmt, ImGuiSliderFlags_AlwaysClamp)) { effects::ImGuiAcrylic::SetUIOpacity(sp_ui_opacity); saveSettingsPageState(app->settings()); } } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_ui_opacity")); float afterRow2Y = ImGui::GetCursorScreenPos().y; ImGui::SetCursorScreenPos(ImVec2(rightX, row2Y - lblH)); ImGui::TextUnformatted(TR("window_opacity")); ImGui::SetCursorScreenPos(ImVec2(rightX, row2Y)); ImGui::SetNextItemWidth(ctrlW); { char winop_fmt[16]; snprintf(winop_fmt, sizeof(winop_fmt), "%.0f%%%%", sp_window_opacity * 100.0f); if (ImGui::SliderFloat("##WindowOpacity", &sp_window_opacity, 0.3f, 1.0f, winop_fmt, ImGuiSliderFlags_AlwaysClamp)) { saveSettingsPageState(app->settings()); } } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_window_opacity")); ImGui::SetCursorScreenPos(ImVec2(baseX, afterRow2Y)); ImGui::EndDisabled(); // low-spec ImGui::PopFont(); } // sp_effects_expanded } else { // ============================================================ // Narrow: stacked combos + 2-column effects (original layout) // ============================================================ // --- Theme row --- { ImGui::PushFont(body2); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("theme")); ImGui::SameLine(labelW); float themeComboW = std::max(S.drawElement("components.settings-page", "theme-combo-min-width").size, availWidth - pad * 2 - labelW - refreshBtnW - Layout::spacingSm()); ImGui::SetNextItemWidth(themeComboW); if (ImGui::BeginCombo("##Theme", active_preview.c_str())) { renderThemeComboPopup(); ImGui::EndCombo(); } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_theme_hotkey")); if (active_is_custom) { ImGui::SameLine(); ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "*"); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_custom_theme")); } ImGui::SameLine(); if (TactileButton(TR("refresh"), ImVec2(refreshBtnW, 0), S.resolveFont("button"))) { schema::SkinManager::instance().refresh(); Notifications::instance().info("Theme list refreshed"); } if (ImGui::IsItemHovered()) { ImGui::SetTooltip(TR("tt_scan_themes"), schema::SkinManager::getUserSkinsDirectory().c_str()); } ImGui::PopFont(); } ImGui::Dummy(ImVec2(0, Layout::spacingXs())); // --- Balance Layout row --- { ImGui::PushFont(body2); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("balance_layout")); ImGui::SameLine(labelW); ImGui::SetNextItemWidth(std::max(180.0f, inputW)); if (ImGui::BeginCombo("##BalanceLayout", balPreview.c_str())) { for (const auto& l : layouts) { if (!l.enabled) continue; bool selected = (l.id == sp_balance_layout); if (ImGui::Selectable(l.name.c_str(), selected)) { sp_balance_layout = l.id; if (app->settings()) { app->settings()->setBalanceLayout(sp_balance_layout); app->settings()->save(); } } if (selected) ImGui::SetItemDefaultFocus(); } ImGui::EndCombo(); } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_layout_hotkey")); ImGui::PopFont(); } ImGui::Dummy(ImVec2(0, Layout::spacingXs())); // --- Language row --- { ImGui::PushFont(body2); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("language")); ImGui::SameLine(labelW); ImGui::SetNextItemWidth(inputW); if (ImGui::Combo("##Language", &sp_language_index, lang_names.data(), static_cast(lang_names.size()))) { auto it = languages.begin(); std::advance(it, sp_language_index); i18n.loadLanguage(it->first); if (app->settings()) { app->settings()->setLanguage(it->first); app->settings()->save(); } } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_language")); ImGui::PopFont(); } ImGui::Dummy(ImVec2(0, Layout::spacingSm())); // --- Font Scale slider (always visible) --- { ImGui::PushFont(body2); ImGui::TextUnformatted(TR("font_scale")); float fontSliderW = std::max(S.drawElement("components.settings-page", "effects-input-min-width").size, availWidth - pad * 2); ImGui::SetNextItemWidth(fontSliderW); sp_font_scale = Layout::userFontScale(); float prev_font_scale = sp_font_scale; { char fs_fmt[16]; snprintf(fs_fmt, sizeof(fs_fmt), "%.2fx", sp_font_scale); ImGui::SliderFloat("##FontScale", &sp_font_scale, 1.0f, 1.5f, fs_fmt, ImGuiSliderFlags_AlwaysClamp); } sp_font_scale = std::max(1.0f, std::min(1.5f, std::round(sp_font_scale * 20.0f) / 20.0f)); if (sp_font_scale != prev_font_scale) Layout::setUserFontScaleVisual(sp_font_scale); if (ImGui::IsItemDeactivatedAfterEdit()) { Layout::setUserFontScale(sp_font_scale); saveSettingsPageState(app->settings()); } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_font_scale")); ImGui::PopFont(); } ImGui::Dummy(ImVec2(0, Layout::spacingSm())); // --- Collapsible: Advanced Effects... --- { const char* arrow = sp_effects_expanded ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE; ImGui::PushFont(body2); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0,0,0,0)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1,1,1,0.05f)); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1,1,1,0.08f)); { float narrowContentW = availWidth - pad * 2; ImVec2 hdrPos = ImGui::GetCursorScreenPos(); if (ImGui::Button("##EffectsToggleN", ImVec2(narrowContentW, ImGui::GetFrameHeight()))) { sp_effects_expanded = !sp_effects_expanded; } float textY = hdrPos.y + (ImGui::GetFrameHeight() - body2->LegacySize) * 0.5f; dl->AddText(body2, body2->LegacySize, ImVec2(hdrPos.x, textY), OnSurfaceMedium(), TR("advanced_effects")); ImFont* iconFont = Type().iconSmall(); if (!iconFont) iconFont = body2; float arrowW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, arrow).x; dl->AddText(iconFont, iconFont->LegacySize, ImVec2(hdrPos.x + narrowContentW - arrowW, textY), OnSurfaceMedium(), arrow); } ImGui::PopStyleColor(3); ImGui::PopFont(); } if (sp_effects_expanded) { ImGui::PushFont(body2); if (ImGui::Checkbox(TrId("low_spec_mode", "low_spec").c_str(), &sp_low_spec_mode)) { effects::setLowSpecMode(sp_low_spec_mode); if (sp_low_spec_mode) { s_lowSpecSnap.valid = true; s_lowSpecSnap.acrylic_enabled = sp_acrylic_enabled; s_lowSpecSnap.blur_amount = sp_blur_amount; s_lowSpecSnap.ui_opacity = sp_ui_opacity; s_lowSpecSnap.window_opacity = sp_window_opacity; s_lowSpecSnap.theme_effects_enabled = sp_theme_effects_enabled; s_lowSpecSnap.scanline_enabled = sp_scanline_enabled; sp_acrylic_enabled = false; sp_blur_amount = 0.0f; sp_ui_opacity = 1.0f; sp_window_opacity = 1.0f; sp_theme_effects_enabled = false; sp_scanline_enabled = false; effects::ImGuiAcrylic::ApplyBlurAmount(0.0f); effects::ImGuiAcrylic::SetUIOpacity(1.0f); effects::ThemeEffects::instance().setEnabled(false); ConsoleTab::s_scanline_enabled = false; } else if (s_lowSpecSnap.valid) { sp_blur_amount = s_lowSpecSnap.blur_amount; sp_acrylic_enabled = (sp_blur_amount > 0.001f); sp_ui_opacity = s_lowSpecSnap.ui_opacity; sp_window_opacity = s_lowSpecSnap.window_opacity; sp_theme_effects_enabled = s_lowSpecSnap.theme_effects_enabled; sp_scanline_enabled = s_lowSpecSnap.scanline_enabled; effects::ImGuiAcrylic::ApplyBlurAmount(sp_blur_amount); effects::ImGuiAcrylic::SetUIOpacity(sp_ui_opacity); effects::ThemeEffects::instance().setEnabled(sp_theme_effects_enabled); ConsoleTab::s_scanline_enabled = sp_scanline_enabled; s_lowSpecSnap.valid = false; } saveSettingsPageState(app->settings()); } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_low_spec")); if (ImGui::Checkbox(TrId("settings_gradient_bg", "gradient_bg").c_str(), &sp_gradient_background)) { schema::SkinManager::instance().setGradientMode(sp_gradient_background); saveSettingsPageState(app->settings()); } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_simple_bg_alt")); ImGui::BeginDisabled(sp_low_spec_mode); if (ImGui::Checkbox(TrId("console_scanline", "scanline").c_str(), &sp_scanline_enabled)) { ConsoleTab::s_scanline_enabled = sp_scanline_enabled; app->settings()->setScanlineEnabled(sp_scanline_enabled); app->settings()->save(); } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_scanline")); ImGui::SameLine(0, Layout::spacingLg()); if (ImGui::Checkbox(TrId("theme_effects", "theme_fx").c_str(), &sp_theme_effects_enabled)) { effects::ThemeEffects::instance().setEnabled(sp_theme_effects_enabled); saveSettingsPageState(app->settings()); } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_theme_effects")); float ctrlW = std::max(S.drawElement("components.settings-page", "effects-input-min-width").size, availWidth - pad * 2.0f); ImGui::TextUnformatted(TR("acrylic")); ImGui::SetNextItemWidth(ctrlW); { char blur_fmt[16]; if (sp_blur_amount < 0.01f) snprintf(blur_fmt, sizeof(blur_fmt), "Off"); else snprintf(blur_fmt, sizeof(blur_fmt), "%.0f%%%%", sp_blur_amount * 25.0f); if (ImGui::SliderFloat("##AcrylicBlur", &sp_blur_amount, 0.0f, 4.0f, blur_fmt, ImGuiSliderFlags_AlwaysClamp)) { if (sp_blur_amount > 0.0f && sp_blur_amount < 0.15f) sp_blur_amount = 0.0f; sp_acrylic_enabled = (sp_blur_amount > 0.001f); effects::ImGuiAcrylic::ApplyBlurAmount(sp_blur_amount); } } if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings()); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_blur")); ImGui::TextUnformatted(TR("noise")); ImGui::SetNextItemWidth(ctrlW); { char noise_fmt[16]; if (sp_noise_opacity < 0.01f) snprintf(noise_fmt, sizeof(noise_fmt), "Off"); else snprintf(noise_fmt, sizeof(noise_fmt), "%.0f%%%%", sp_noise_opacity * 100.0f); if (ImGui::SliderFloat("##NoiseOpacity", &sp_noise_opacity, 0.0f, 1.0f, noise_fmt, ImGuiSliderFlags_AlwaysClamp)) { effects::ImGuiAcrylic::SetNoiseOpacity(sp_noise_opacity); } } if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings()); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_noise")); ImGui::TextUnformatted(TR("ui_opacity")); ImGui::SetNextItemWidth(ctrlW); { char uiop_fmt[16]; snprintf(uiop_fmt, sizeof(uiop_fmt), "%.0f%%%%", sp_ui_opacity * 100.0f); if (ImGui::SliderFloat("##UIOpacity", &sp_ui_opacity, 0.3f, 1.0f, uiop_fmt, ImGuiSliderFlags_AlwaysClamp)) { effects::ImGuiAcrylic::SetUIOpacity(sp_ui_opacity); } } if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings()); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_ui_opacity")); ImGui::TextUnformatted(TR("window_opacity")); ImGui::SetNextItemWidth(ctrlW); { char winop_fmt[16]; snprintf(winop_fmt, sizeof(winop_fmt), "%.0f%%%%", sp_window_opacity * 100.0f); if (ImGui::SliderFloat("##WindowOpacity", &sp_window_opacity, 0.3f, 1.0f, winop_fmt, ImGuiSliderFlags_AlwaysClamp)) { } } if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings()); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_window_opacity")); ImGui::EndDisabled(); // low-spec ImGui::PopFont(); } // sp_effects_expanded } // Bottom padding ImGui::Dummy(ImVec2(0, bottomPad)); ImGui::Unindent(pad); // Draw glass panel behind content (auto-sized) ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y); dl->ChannelsSetCurrent(0); DrawGlassPanel(dl, cardMin, cardMax, glassSpec); dl->ChannelsMerge(); ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y)); ImGui::Dummy(ImVec2(availWidth, 0)); } ImGui::Dummy(ImVec2(0, gap)); // ==================================================================== // WALLET — card (privacy/daemon toggles + collapsible tools) // ==================================================================== { Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("wallet")); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); ImVec2 cardMin = ImGui::GetCursorScreenPos(); dl->ChannelsSplit(2); dl->ChannelsSetCurrent(1); ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad)); ImGui::Indent(pad); float contentW = availWidth - pad * 2; // Privacy, Network & Daemon checkboxes — all on one line, shrink text to fit { float cbSpacing = Layout::spacingMd(); float fh = ImGui::GetFrameHeight(); float inner = ImGui::GetStyle().ItemInnerSpacing.x; auto cbW = [&](const char* label) { return fh + inner + ImGui::CalcTextSize(label).x; }; float totalW = cbW(TR("save_z_transactions")) + cbSpacing + cbW(TR("auto_shield")) + cbSpacing + cbW(TR("use_tor")) + cbSpacing + cbW(TR("keep_daemon")) + cbSpacing + cbW(TR("stop_external")); float scale = (totalW > contentW) ? contentW / totalW : 1.0f; if (scale < 1.0f) ImGui::SetWindowFontScale(scale); float sp = cbSpacing * scale; ImGui::Checkbox(TrId("save_z_transactions", "save_ztx").c_str(), &sp_save_ztxs); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_save_ztx")); ImGui::SameLine(0, sp); ImGui::Checkbox(TrId("auto_shield", "auto_shld").c_str(), &sp_auto_shield); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_auto_shield")); ImGui::SameLine(0, sp); ImGui::Checkbox(TrId("use_tor", "tor").c_str(), &sp_use_tor); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_tor")); ImGui::SameLine(0, sp); if (ImGui::Checkbox(TrId("keep_daemon", "keep_dmn").c_str(), &sp_keep_daemon_running)) { saveSettingsPageState(app->settings()); } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_keep_daemon")); ImGui::SameLine(0, sp); if (ImGui::Checkbox(TrId("stop_external", "stop_ext").c_str(), &sp_stop_external_daemon)) { saveSettingsPageState(app->settings()); } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_stop_external")); ImGui::SameLine(0, sp); if (ImGui::Checkbox(TrId("verbose_logging", "verbose").c_str(), &sp_verbose_logging)) { dragonx::util::Logger::instance().setVerbose(sp_verbose_logging); saveSettingsPageState(app->settings()); } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_verbose")); if (scale < 1.0f) ImGui::SetWindowFontScale(1.0f); } ImGui::Dummy(ImVec2(0, Layout::spacingSm())); // --- Collapsible: Tools & Actions... --- { const char* arrow = sp_tools_expanded ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE; ImGui::PushFont(body2); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0,0,0,0)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1,1,1,0.05f)); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1,1,1,0.08f)); { ImVec2 hdrPos = ImGui::GetCursorScreenPos(); if (ImGui::Button("##ToolsToggle", ImVec2(contentW, ImGui::GetFrameHeight()))) { sp_tools_expanded = !sp_tools_expanded; } float textY = hdrPos.y + (ImGui::GetFrameHeight() - body2->LegacySize) * 0.5f; dl->AddText(body2, body2->LegacySize, ImVec2(hdrPos.x, textY), OnSurfaceMedium(), TR("tools_actions")); ImFont* iconFont = Type().iconSmall(); if (!iconFont) iconFont = body2; float arrowW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, arrow).x; dl->AddText(iconFont, iconFont->LegacySize, ImVec2(hdrPos.x + contentW - arrowW, textY), OnSurfaceMedium(), arrow); } ImGui::PopStyleColor(3); ImGui::PopFont(); } if (sp_tools_expanded) { float btnSpacing = Layout::spacingMd(); int btnsPerRow = (contentW >= 600.0f) ? 3 : 2; float bw = (contentW - btnSpacing * (btnsPerRow - 1)) / btnsPerRow; float minBtnW = S.drawElement("components.settings-page", "wallet-btn-min-width").sizeOr(100.0f); bw = std::max(minBtnW, bw); if (TactileButton(TR("settings_address_book"), ImVec2(bw, 0), S.resolveFont("button"))) AddressBookDialog::show(); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_address_book")); ImGui::SameLine(0, btnSpacing); if (TactileButton(TR("settings_validate_address"), ImVec2(bw, 0), S.resolveFont("button"))) ValidateAddressDialog::show(); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_validate")); if (btnsPerRow >= 3) { ImGui::SameLine(0, btnSpacing); } else { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); } if (TactileButton(TR("settings_request_payment"), ImVec2(bw, 0), S.resolveFont("button"))) RequestPaymentDialog::show(); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_request_payment")); if (btnsPerRow >= 3) { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); } else { ImGui::SameLine(0, btnSpacing); } if (TactileButton(TR("settings_shield_mining"), ImVec2(bw, 0), S.resolveFont("button"))) ShieldDialog::show(ShieldDialog::Mode::ShieldCoinbase); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_shield_mining")); ImGui::SameLine(0, btnSpacing); if (TactileButton(TR("settings_merge_to_address"), ImVec2(bw, 0), S.resolveFont("button"))) ShieldDialog::show(ShieldDialog::Mode::MergeToAddress); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_merge")); if (btnsPerRow >= 3) { ImGui::SameLine(0, btnSpacing); } else { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); } if (TactileButton(TR("settings_clear_ztx"), ImVec2(bw, 0), S.resolveFont("button"))) { sp_confirm_clear_ztx = true; } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_clear_ztx")); } ImGui::Dummy(ImVec2(0, bottomPad)); ImGui::Unindent(pad); ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y); dl->ChannelsSetCurrent(0); DrawGlassPanel(dl, cardMin, cardMax, glassSpec); dl->ChannelsMerge(); ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y)); ImGui::Dummy(ImVec2(availWidth, 0)); } ImGui::Dummy(ImVec2(0, gap)); // ==================================================================== // BACKUP & DATA — card // ==================================================================== { Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("backup_data")); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); ImVec2 cardMin = ImGui::GetCursorScreenPos(); dl->ChannelsSplit(2); dl->ChannelsSetCurrent(1); ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad)); ImGui::Indent(pad); float contentW = availWidth - pad * 2; float btnPad = S.drawElement("components.settings-page", "wallet-btn-padding").sizeOr(24.0f); { const char* r1[] = {TR("settings_import_key"), TR("settings_export_key"), TR("settings_export_all"), TR("settings_backup"), TR("settings_export_csv")}; const char* t1[] = { TR("tt_import_key"), TR("tt_export_key"), TR("tt_export_all"), TR("tt_backup"), TR("tt_export_csv") }; const char* wizLabel = TR("setup_wizard"); float sp = Layout::spacingSm(); ImFont* btnFont = S.resolveFont("button"); float btnPadX = btnPad * 2; float naturalW = 0; for (int i = 0; i < 5; i++) naturalW += ImGui::CalcTextSize(r1[i]).x + btnPadX; float wizW = ImGui::CalcTextSize(wizLabel).x + btnPadX; float totalW = naturalW + wizW + sp * 6; float scale = (totalW > contentW) ? contentW / totalW : 1.0f; if (scale < 1.0f) ImGui::SetWindowFontScale(scale); float scaledSp = sp * scale; if (TactileButton(r1[0], ImVec2(0, 0), btnFont)) app->showImportKeyDialog(); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", t1[0]); ImGui::SameLine(0, scaledSp); if (TactileButton(r1[1], ImVec2(0, 0), btnFont)) app->showExportKeyDialog(); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", t1[1]); ImGui::SameLine(0, scaledSp); if (TactileButton(r1[2], ImVec2(0, 0), btnFont)) ExportAllKeysDialog::show(); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", t1[2]); ImGui::SameLine(0, scaledSp); if (TactileButton(r1[3], ImVec2(0, 0), btnFont)) app->showBackupDialog(); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", t1[3]); ImGui::SameLine(0, scaledSp); if (TactileButton(r1[4], ImVec2(0, 0), btnFont)) ExportTransactionsDialog::show(); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", t1[4]); // Right-align Setup Wizard float curX = ImGui::GetCursorScreenPos().x; float wizBtnW = ImGui::CalcTextSize(wizLabel).x + btnPadX; if (scale < 1.0f) wizBtnW *= scale; float rightEdge = cardMin.x + availWidth - pad; float wizX = rightEdge - wizBtnW; if (wizX > curX) { ImGui::SameLine(0, 0); ImGui::SetCursorScreenPos(ImVec2(wizX, ImGui::GetCursorScreenPos().y)); } else { ImGui::SameLine(0, scaledSp); } if (TactileButton(wizLabel, ImVec2(0, 0), btnFont)) app->restartWizard(); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_wizard")); if (scale < 1.0f) ImGui::SetWindowFontScale(1.0f); } ImGui::Dummy(ImVec2(0, bottomPad)); ImGui::Unindent(pad); ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y); dl->ChannelsSetCurrent(0); DrawGlassPanel(dl, cardMin, cardMax, glassSpec); dl->ChannelsMerge(); ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y)); ImGui::Dummy(ImVec2(availWidth, 0)); } ImGui::Dummy(ImVec2(0, gap)); // ==================================================================== // NODE & SECURITY — card // ==================================================================== { Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("node_security")); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); ImVec2 cardMin = ImGui::GetCursorScreenPos(); dl->ChannelsSplit(2); dl->ChannelsSetCurrent(1); ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad)); ImGui::Indent(pad); float contentW = availWidth - pad * 2; float minBtnW = S.drawElement("components.settings-page", "wallet-btn-min-width").sizeOr(130.0f); float btnPad = S.drawElement("components.settings-page", "wallet-btn-padding").sizeOr(24.0f); auto rowBtnW = [&](std::initializer_list labels) -> float { float maxTextW = 0; for (auto* l : labels) maxTextW = std::max(maxTextW, ImGui::CalcTextSize(l).x); return std::max(minBtnW, maxTextW + btnPad * 2); }; // --- NODE (left) + SECURITY (right) side by side --- { float colGap = Layout::spacingLg(); float leftColW = (contentW - colGap) * 0.6f; float rightColW = (contentW - colGap) * 0.4f; ImVec2 sectionOrigin = ImGui::GetCursorScreenPos(); float leftX = sectionOrigin.x; float rightX = sectionOrigin.x + leftColW + colGap; // ---- LEFT COLUMN: NODE ---- ImGui::SetCursorScreenPos(ImVec2(leftX, sectionOrigin.y)); ImGui::PushClipRect(ImVec2(leftX, sectionOrigin.y), ImVec2(leftX + leftColW, sectionOrigin.y + 9999), true); Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("node")); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); { ImFont* body2Info = S.resolveFont("body2"); if (!body2Info) body2Info = Type().body2(); ImGui::PushFont(body2Info); float rpcLblW = std::max( S.drawElement("components.settings-page", "rpc-label-min-width").size, std::min(leftColW * 0.35f, S.drawElement("components.settings-page", "rpc-label-width").size * hs)); std::string wallet_path = util::Platform::getDragonXDataDir() + "wallet.dat"; uint64_t wallet_size = util::Platform::getFileSize(wallet_path); // Data Dir + Wallet Size — same line with "Label: value" format ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::AlignTextToFramePadding(); { // Measure the wallet size string first std::string size_str = (wallet_size > 0) ? util::Platform::formatFileSize(wallet_size) : TR("settings_not_found"); std::string dirPath = util::Platform::getDragonXDataDir(); // Calculate space taken by "Wallet Size: " on the right float walletSizeLabelW = ImGui::CalcTextSize(TR("settings_wallet_size_label")).x + Layout::spacingXs(); float walletSizeValW = ImGui::CalcTextSize(size_str.c_str()).x; float walletSizeTotalW = walletSizeLabelW + walletSizeValW + Layout::spacingLg(); // Available space for "Data Dir: " float dataDirLabelW = ImGui::CalcTextSize(TR("settings_data_dir")).x + Layout::spacingXs(); float availForPath = leftColW - walletSizeTotalW - dataDirLabelW; ImGui::TextUnformatted(TR("settings_data_dir")); ImGui::SameLine(0, Layout::spacingXs()); ImVec2 pathSize = ImGui::CalcTextSize(dirPath.c_str()); ImVec4 linkCol = ImGui::ColorConvertU32ToFloat4(Primary()); ImVec4 linkHoverCol = linkCol; linkHoverCol.w = std::min(1.0f, linkCol.w + 0.2f); bool hovered = false; if (pathSize.x > availForPath && availForPath > 0) { float scale = availForPath / pathSize.x; float condensedSize = body2Info->LegacySize * std::max(scale, 0.65f); ImGui::SetWindowFontScale(condensedSize / body2Info->LegacySize); ImGui::AlignTextToFramePadding(); ImGui::TextColored(linkCol, "%s", dirPath.c_str()); hovered = ImGui::IsItemHovered(); if (hovered) { ImVec2 tMin = ImGui::GetItemRectMin(); ImVec2 tMax = ImGui::GetItemRectMax(); dl->AddLine(ImVec2(tMin.x, tMax.y), ImVec2(tMax.x, tMax.y), ImGui::GetColorU32(linkHoverCol)); } ImGui::SetWindowFontScale(1.0f); } else { ImGui::TextColored(linkCol, "%s", dirPath.c_str()); hovered = ImGui::IsItemHovered(); if (hovered) { ImVec2 tMin = ImGui::GetItemRectMin(); ImVec2 tMax = ImGui::GetItemRectMax(); dl->AddLine(ImVec2(tMin.x, tMax.y), ImVec2(tMax.x, tMax.y), ImGui::GetColorU32(linkHoverCol)); } } if (hovered) { ImGui::SetTooltip("%s", TR("tt_open_dir")); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); } if (ImGui::IsItemClicked()) util::Platform::openFolder(dirPath); ImGui::SameLine(0, Layout::spacingLg()); ImGui::TextUnformatted(TR("settings_wallet_size_label")); ImGui::SameLine(0, Layout::spacingXs()); if (wallet_size > 0) { ImGui::TextUnformatted(size_str.c_str()); } else { ImGui::TextDisabled("%s", TR("settings_not_found")); } } ImGui::Dummy(ImVec2(0, Layout::spacingXs())); // RPC connection — two columns: (Host | Username) and (Port | Password) float rpcHalfW = (leftColW - Layout::spacingMd()) * 0.5f; float rpcHalfLblW = std::min(rpcLblW, rpcHalfW * 0.4f); float rpcHalfInputW = std::max(40.0f, rpcHalfW - rpcHalfLblW); float rpcRightColX = leftX + rpcHalfW + Layout::spacingMd(); // Row 1: RPC Host + Username float row1Y = ImGui::GetCursorScreenPos().y; ImGui::SetCursorScreenPos(ImVec2(leftX, row1Y)); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("rpc_host")); ImGui::SameLine(leftX - sectionOrigin.x + rpcHalfLblW); ImGui::SetNextItemWidth(rpcHalfInputW); ImGui::InputText("##RPCHost", sp_rpc_host, sizeof(sp_rpc_host)); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_rpc_host")); float afterRow1Y = ImGui::GetCursorScreenPos().y; ImGui::SetCursorScreenPos(ImVec2(rpcRightColX, row1Y)); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("rpc_user")); ImGui::SameLine(rpcRightColX - sectionOrigin.x + rpcHalfLblW); ImGui::SetNextItemWidth(rpcHalfInputW); ImGui::InputText("##RPCUser", sp_rpc_user, sizeof(sp_rpc_user)); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_rpc_user")); ImGui::SetCursorScreenPos(ImVec2(leftX, std::max(afterRow1Y, ImGui::GetCursorScreenPos().y))); // Row 2: Port + Password float row2Y = ImGui::GetCursorScreenPos().y; ImGui::SetCursorScreenPos(ImVec2(leftX, row2Y)); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("rpc_port")); ImGui::SameLine(leftX - sectionOrigin.x + rpcHalfLblW); ImGui::SetNextItemWidth(rpcHalfInputW); ImGui::InputText("##RPCPort", sp_rpc_port, sizeof(sp_rpc_port)); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_rpc_port")); float afterRow2Y = ImGui::GetCursorScreenPos().y; ImGui::SetCursorScreenPos(ImVec2(rpcRightColX, row2Y)); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("rpc_pass")); ImGui::SameLine(rpcRightColX - sectionOrigin.x + rpcHalfLblW); ImGui::SetNextItemWidth(rpcHalfInputW); ImGui::InputText("##RPCPassword", sp_rpc_password, sizeof(sp_rpc_password), ImGuiInputTextFlags_Password); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_rpc_pass")); ImGui::SetCursorScreenPos(ImVec2(leftX, std::max(afterRow2Y, ImGui::GetCursorScreenPos().y))); ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("settings_auto_detected")); ImGui::Dummy(ImVec2(0, Layout::spacingSm())); // Node maintenance buttons { ImFont* btnFont = S.resolveFont("button"); float nodeBtnW; { if (btnFont) ImGui::PushFont(btnFont); nodeBtnW = rowBtnW({TR("test_connection"), TR("rescan")}); if (btnFont) ImGui::PopFont(/* btnFont */); } ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); ImGui::BeginDisabled(!app->isConnected()); if (TactileButton(TR("test_connection"), ImVec2(nodeBtnW, 0), btnFont)) { if (app->rpc() && app->rpc()->isConnected() && app->worker()) { app->worker()->post([rpc = app->rpc()]() -> rpc::RPCWorker::MainCb { try { rpc->call("getinfo"); return []() { Notifications::instance().success("RPC connection OK"); }; } catch (const std::exception& e) { std::string err = e.what(); return [err]() { Notifications::instance().error("RPC error: " + err); }; } }); } else { Notifications::instance().warning("Not connected to daemon"); } } if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", TR("tt_test_conn")); ImGui::SameLine(0, Layout::spacingMd()); if (TactileButton(TR("rescan"), ImVec2(nodeBtnW, 0), btnFont)) { app->rescanBlockchain(); } if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", TR("tt_rescan")); ImGui::EndDisabled(); // Delete blockchain button (always available when using embedded daemon) ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y + Layout::spacingSm())); ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon()); if (TactileButton(TR("delete_blockchain"), ImVec2(0, 0), btnFont)) { sp_confirm_delete_blockchain = true; } if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) ImGui::SetTooltip("%s", TR("tt_delete_blockchain")); ImGui::EndDisabled(); } ImGui::PopFont(); } float leftBottom = ImGui::GetCursorScreenPos().y; ImGui::PopClipRect(); // ---- RIGHT COLUMN: SECURITY ---- ImGui::SetCursorScreenPos(ImVec2(rightX, sectionOrigin.y)); ImGui::PushClipRect(ImVec2(rightX, sectionOrigin.y), ImVec2(rightX + rightColW, sectionOrigin.y + 9999), true); Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("security")); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); { // Encrypt / Change passphrase / Lock buttons bool isEncrypted = app->state().isEncrypted(); bool isLocked = app->state().isLocked(); float secBtnW = std::min(rowBtnW({TR("settings_encrypt_wallet"), TR("settings_change_passphrase"), TR("settings_lock_now")}), (rightColW - Layout::spacingMd()) * 0.5f); ImGui::SetCursorScreenPos(ImVec2(rightX, ImGui::GetCursorScreenPos().y)); if (!isEncrypted) { if (TactileButton(TR("settings_encrypt_wallet"), ImVec2(secBtnW, 0), S.resolveFont("button"))) app->showEncryptDialog(); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_encrypt")); ImGui::SameLine(0, Layout::spacingMd()); ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", TR("settings_not_encrypted")); } else { if (TactileButton(TR("settings_change_passphrase"), ImVec2(secBtnW, 0), S.resolveFont("button"))) app->showChangePassphraseDialog(); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_change_pass")); ImGui::SameLine(0, Layout::spacingMd()); if (isLocked) { ImGui::PushFont(Type().iconSmall()); ImGui::TextColored(ImVec4(1,0.7f,0.3f,1.0f), ICON_MD_LOCK); ImGui::PopFont(); ImGui::SameLine(0, Layout::spacingXs()); ImGui::TextColored(ImVec4(1,0.7f,0.3f,1.0f), "%s", TR("settings_locked")); } else { if (TactileButton(TR("settings_lock_now"), ImVec2(secBtnW, 0), S.resolveFont("button"))) app->lockWallet(); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_lock")); ImGui::SameLine(0, Layout::spacingSm()); ImGui::PushFont(Type().iconSmall()); ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), ICON_MD_LOCK_OPEN); ImGui::PopFont(); ImGui::SameLine(0, Layout::spacingXs()); ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), "%s", TR("settings_unlocked")); } // Remove Encryption button ImGui::SetCursorScreenPos(ImVec2(rightX, ImGui::GetCursorScreenPos().y + Layout::spacingXs())); if (TactileButton(TR("settings_remove_encryption"), ImVec2(secBtnW, 0), S.resolveFont("button"))) app->showDecryptDialog(); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_remove_encrypt")); } ImGui::Dummy(ImVec2(0, Layout::spacingXs())); // Auto-Lock timeout { float comboW = S.drawElement("components.settings-page", "security-combo-width").sizeOr(120.0f); int timeout = app->settings()->getAutoLockTimeout(); const char* timeoutLabels[] = { "Off", "1 min", "5 min", "15 min", "30 min", "1 hour" }; int timeoutValues[] = { 0, 60, 300, 900, 1800, 3600 }; int selTimeout = 0; for (int i = 0; i < 6; i++) { if (timeoutValues[i] == timeout) { selTimeout = i; break; } } ImGui::SetCursorScreenPos(ImVec2(rightX, ImGui::GetCursorScreenPos().y)); Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("settings_auto_lock")); ImGui::SetCursorScreenPos(ImVec2(rightX, ImGui::GetCursorScreenPos().y)); ImGui::PushItemWidth(comboW); if (ImGui::Combo("##autolock", &selTimeout, timeoutLabels, 6)) { app->settings()->setAutoLockTimeout(timeoutValues[selTimeout]); app->settings()->save(); } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_auto_lock")); ImGui::PopItemWidth(); } ImGui::Dummy(ImVec2(0, Layout::spacingXs())); // PIN unlock section bool isEncryptedPIN = app->state().isEncrypted(); if (isEncryptedPIN) { bool hasPIN = app->hasPinVault(); float pinBtnW = std::min(rowBtnW({TR("settings_set_pin"), TR("settings_change_pin"), TR("settings_remove_pin")}), (rightColW - Layout::spacingSm()) * 0.5f); ImGui::SetCursorScreenPos(ImVec2(rightX, ImGui::GetCursorScreenPos().y)); if (!hasPIN) { if (TactileButton(TR("settings_set_pin"), ImVec2(pinBtnW, 0), S.resolveFont("button"))) app->showPinSetupDialog(); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_set_pin")); ImGui::SameLine(0, Layout::spacingMd()); ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", TR("settings_quick_unlock_pin")); } else { if (TactileButton(TR("settings_change_pin"), ImVec2(pinBtnW, 0), S.resolveFont("button"))) app->showPinChangeDialog(); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_change_pin")); ImGui::SameLine(0, Layout::spacingSm()); if (TactileButton(TR("settings_remove_pin"), ImVec2(pinBtnW, 0), S.resolveFont("button"))) app->showPinRemoveDialog(); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_remove_pin")); ImGui::SameLine(0, Layout::spacingMd()); ImGui::PushFont(Type().iconSmall()); ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), ICON_MD_DIALPAD); ImGui::PopFont(); ImGui::SameLine(0, Layout::spacingXs()); ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), "%s", TR("settings_pin_active")); } } else { ImGui::SetCursorScreenPos(ImVec2(rightX, ImGui::GetCursorScreenPos().y)); ImGui::TextColored(ImVec4(1,1,1,0.3f), "%s", TR("settings_encrypt_first_pin")); } } float rightBottom = ImGui::GetCursorScreenPos().y; ImGui::PopClipRect(); // Advance cursor past both columns float maxBottom = std::max(leftBottom, rightBottom); ImGui::SetCursorScreenPos(ImVec2(sectionOrigin.x, maxBottom)); } ImGui::Dummy(ImVec2(0, bottomPad)); ImGui::Unindent(pad); ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y); dl->ChannelsSetCurrent(0); DrawGlassPanel(dl, cardMin, cardMax, glassSpec); dl->ChannelsMerge(); ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y)); ImGui::Dummy(ImVec2(availWidth, 0)); } ImGui::Dummy(ImVec2(0, gap)); // ==================================================================== // EXPLORER & OPTIONS — full-width card // ==================================================================== { Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("explorer")); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); ImVec2 cardMin = ImGui::GetCursorScreenPos(); dl->ChannelsSplit(2); dl->ChannelsSetCurrent(1); ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad)); ImGui::Indent(pad); float contentW = availWidth - pad * 2; ImGui::PushFont(body2); // Row 1: Transaction URL | Address URL (side-by-side) float halfW = (contentW - Layout::spacingLg()) * 0.5f; float lblTxW = ImGui::CalcTextSize("Transaction URL").x + Layout::spacingXs(); float lblAddrW = ImGui::CalcTextSize("Address URL").x + Layout::spacingXs(); float inputTxW = std::max(80.0f, halfW - lblTxW); float inputAddrW = std::max(80.0f, halfW - lblAddrW); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("transaction_url")); ImGui::SameLine(0, Layout::spacingXs()); ImGui::SetNextItemWidth(inputTxW); ImGui::InputText("##TxExplorer", sp_tx_explorer, sizeof(sp_tx_explorer)); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_tx_url")); ImGui::SameLine(pad + halfW + Layout::spacingLg()); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("address_url")); ImGui::SameLine(0, Layout::spacingXs()); ImGui::SetNextItemWidth(inputAddrW); ImGui::InputText("##AddrExplorer", sp_addr_explorer, sizeof(sp_addr_explorer)); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_addr_url")); ImGui::PopFont(); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); // Row 2: Checkboxes + Block Explorer button (on one line) ImGui::Checkbox(TrId("custom_fees", "custom_fees").c_str(), &sp_allow_custom_fees); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_custom_fees")); ImGui::SameLine(0, Layout::spacingLg()); ImGui::Checkbox(TrId("fetch_prices", "fetch_prices").c_str(), &sp_fetch_prices); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_fetch_prices")); ImGui::SameLine(0, Layout::spacingLg()); if (TactileButton(TR("block_explorer"), ImVec2(0, 0), S.resolveFont("button"))) { util::Platform::openUrl("https://explorer.dragonx.is"); } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_block_explorer")); ImGui::Dummy(ImVec2(0, bottomPad)); ImGui::Unindent(pad); ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y); dl->ChannelsSetCurrent(0); DrawGlassPanel(dl, cardMin, cardMax, glassSpec); dl->ChannelsMerge(); ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y)); ImGui::Dummy(ImVec2(availWidth, 0)); } ImGui::Dummy(ImVec2(0, gap)); // ==================================================================== // ABOUT — card // ==================================================================== { Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("about")); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); ImVec2 cardMin = ImGui::GetCursorScreenPos(); dl->ChannelsSplit(2); dl->ChannelsSetCurrent(1); ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad)); ImGui::Indent(pad); // Logo on the left side of the about card ImTextureID logoTex = app->getLogoTexture(); float logoAreaW = 0; if (logoTex != 0) { float logoMaxH = schema::UI().drawElement("components.settings-page", "about-logo-size").sizeOr(64.0f); float logoH = logoMaxH; float aspect = (app->getLogoHeight() > 0) ? (float)app->getLogoWidth() / (float)app->getLogoHeight() : 1.0f; float logoW = logoH * aspect; ImVec2 logoPos = ImGui::GetCursorScreenPos(); dl->AddImage(logoTex, ImVec2(logoPos.x, logoPos.y), ImVec2(logoPos.x + logoW, logoPos.y + logoH)); logoAreaW = logoW + Layout::spacingLg(); ImGui::Indent(logoAreaW); } float contentW = availWidth - pad * 2 - logoAreaW; // App name + version on same line ImGui::PushFont(sub1); ImGui::TextUnformatted(DRAGONX_APP_NAME); ImGui::PopFont(); ImGui::SameLine(0, Layout::spacingLg()); ImGui::PushFont(body2); snprintf(buf, sizeof(buf), "v%s", DRAGONX_VERSION); ImGui::TextUnformatted(buf); ImGui::SameLine(0, Layout::spacingLg()); snprintf(buf, sizeof(buf), "ImGui %s", IMGUI_VERSION); ImGui::TextColored(ImVec4(1,1,1,0.4f), "%s", buf); ImGui::PopFont(); // Daemon version { const auto& st = app->state(); if (st.daemon_version > 0) { int dmaj = st.daemon_version / 1000000; int dmin = (st.daemon_version / 10000) % 100; int dpat = (st.daemon_version / 100) % 100; ImGui::PushFont(body2); snprintf(buf, sizeof(buf), "%s: %d.%d.%d", TR("daemon_version"), dmaj, dmin, dpat); ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", buf); ImGui::PopFont(); } } ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::PushFont(body2); ImGui::PushTextWrapPos(cardMin.x + availWidth - pad - logoAreaW); ImGui::TextUnformatted(TR("settings_about_text")); ImGui::PopTextWrapPos(); ImGui::PopFont(); ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::PushFont(capFont); ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", TR("settings_copyright")); ImGui::PopFont(); ImGui::Dummy(ImVec2(0, Layout::spacingMd())); // Buttons — consistent equal-width row (full card width) if (logoAreaW > 0) { ImGui::Unindent(logoAreaW); } { float fullContentW = availWidth - pad * 2; float aboutBtnW = (fullContentW - Layout::spacingMd() * 3) / 4.0f; if (TactileButton(TrId("website", "about_website").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) { util::Platform::openUrl("https://dragonx.is"); } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_website")); ImGui::SameLine(0, Layout::spacingMd()); if (TactileButton(TrId("report_bug", "about_bug").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) { util::Platform::openUrl("https://git.dragonx.is/dragonx/ObsidianDragon/issues"); } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_report_bug")); ImGui::SameLine(0, Layout::spacingMd()); if (TactileButton(TrId("save_settings", "about_save").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) { saveSettingsPageState(app->settings()); Notifications::instance().success("Settings saved"); } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_save_settings")); ImGui::SameLine(0, Layout::spacingMd()); if (TactileButton(TrId("reset_to_defaults", "about_reset").c_str(), ImVec2(aboutBtnW, 0), S.resolveFont("button"))) { if (app->settings()) { loadSettingsPageState(app->settings()); Notifications::instance().info("Settings reloaded from disk"); } } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_reset_settings")); } ImGui::Dummy(ImVec2(0, bottomPad)); ImGui::Unindent(pad); ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y); dl->ChannelsSetCurrent(0); DrawGlassPanel(dl, cardMin, cardMax, glassSpec); dl->ChannelsMerge(); ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y)); ImGui::Dummy(ImVec2(availWidth, 0)); } ImGui::Dummy(ImVec2(0, gap)); // ==================================================================== // DEBUG LOGGING — collapsible card with glass styling // ==================================================================== { // Clickable header row ImVec2 headerPos = ImGui::GetCursorScreenPos(); const char* arrow = sp_debug_expanded ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE; ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0,0,0,0)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(1,1,1,0.05f)); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(1,1,1,0.08f)); if (ImGui::Button("##DebugToggle", ImVec2(availWidth, ImGui::GetFrameHeight()))) { sp_debug_expanded = !sp_debug_expanded; } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", sp_debug_expanded ? TR("tt_debug_collapse") : TR("tt_debug_expand")); ImGui::PopStyleColor(3); // Draw overline label + arrow on top of the invisible button { ImFont* ovFont = Type().overline(); float textY = headerPos.y + (ImGui::GetFrameHeight() - ovFont->LegacySize) * 0.5f; dl->AddText(ovFont, ovFont->LegacySize, ImVec2(headerPos.x, textY), OnSurfaceMedium(), TR("debug_logging")); // Use the icon font for the expand/collapse arrow ImFont* iconFont = Type().iconSmall(); if (!iconFont) iconFont = ovFont; float arrowW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, arrow).x; dl->AddText(iconFont, iconFont->LegacySize, ImVec2(headerPos.x + availWidth - arrowW - pad, textY), OnSurfaceMedium(), arrow); } if (sp_debug_expanded) { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); ImVec2 cardMin = ImGui::GetCursorScreenPos(); dl->ChannelsSplit(2); dl->ChannelsSetCurrent(1); ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + pad)); ImGui::Indent(pad); ImGui::TextColored(ImVec4(1,1,1,0.5f), "%s", TR("settings_debug_select")); ImGui::TextColored(ImVec4(1,1,1,0.35f), "%s", TR("settings_debug_restart_note")); ImGui::Dummy(ImVec2(0, Layout::spacingSm())); // The 22 dragonxd debug categories static const char* debugCats[] = { "addrman", "alert", "bench", "coindb", "db", "estimatefee", "http", "libevent", "lock", "mempool", "net", "paymentdisclosure", "pow", "proxy", "prune", "rand", "reindex", "rpc", "selectcoins", "tor", "zmq", "zrpc" }; static const char* debugTips[] = { "Peer address tracking and management", "Alert system messages", "Benchmark timings for operations", "Coin database read/write operations", "Berkeley DB operations", "Fee estimation algorithm", "HTTP RPC server activity", "Libevent networking library", "Lock contention debugging", "Transaction memory pool activity", "Network connections and messages", "Payment disclosure protocol", "Proof-of-work mining activity", "SOCKS5 proxy connections", "Block pruning operations", "Random number generation", "Blockchain reindexing progress", "RPC command processing", "Coin selection for transactions", "Tor integration and circuit info", "ZeroMQ notification system", "Shielded (z-addr) RPC operations" }; constexpr int numCats = sizeof(debugCats) / sizeof(debugCats[0]); // Render as a 4-column grid of checkboxes int columns = 4; float dbgContentW = availWidth - pad * 2.0f; float colW = dbgContentW / columns; for (int i = 0; i < numCats; i++) { if (i > 0 && (i % columns) != 0) { ImGui::SameLine(pad + colW * (i % columns)); } bool enabled = sp_debug_categories.count(debugCats[i]) > 0; if (ImGui::Checkbox(debugCats[i], &enabled)) { if (enabled) { sp_debug_categories.insert(debugCats[i]); } else { sp_debug_categories.erase(debugCats[i]); } sp_debug_cats_dirty = true; saveSettingsPageState(app->settings()); } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", debugTips[i]); } ImGui::Dummy(ImVec2(0, Layout::spacingSm())); // "Restart daemon" button — only active when categories changed if (sp_debug_cats_dirty) { ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 218, 0, 255)); ImFont* iconFont = Type().iconSmall(); if (iconFont) { ImGui::PushFont(iconFont); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(ICON_MD_INFO); ImGui::PopFont(); ImGui::SameLine(0, Layout::spacingXs()); } ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("settings_debug_changed")); ImGui::PopStyleColor(); ImGui::SameLine(); if (TactileButton(TR("settings_restart_daemon"), ImVec2(0, 0), S.resolveFont("button"))) { sp_debug_cats_dirty = false; app->restartDaemon(); } if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_restart_daemon")); } ImGui::Dummy(ImVec2(0, bottomPad)); ImGui::Unindent(pad); ImVec2 cardMax(cardMin.x + availWidth, ImGui::GetCursorScreenPos().y); dl->ChannelsSetCurrent(0); DrawGlassPanel(dl, cardMin, cardMax, glassSpec); dl->ChannelsMerge(); ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y)); ImGui::Dummy(ImVec2(availWidth, 0)); } } // --- Shader-based scroll fade: unbind (restore ImGui's default shader) --- if (fadeH > 0.0f && !sp_low_spec_mode && s_fadeShader.ready) { effects::ScrollFadeShader::addUnbind(dl); } // --- Vertex-based alpha fade for ForegroundDrawList theme effects --- // DrawGlassPanel draws rainbow borders, shimmer, specular glare, and // edge traces on the ForegroundDrawList which bypasses the shader. // Apply the same fade boundaries via vertex alpha manipulation. if (fadeH > 0.0f) { int fgVtxEnd = fgDL->VtxBuffer.Size; float safeTop = settingsFadeTopY + settingsFadeZoneTop; float safeBot = settingsFadeBottomY - settingsFadeZoneBot; for (int vi = fgVtxStart; vi < fgVtxEnd; vi++) { ImDrawVert& v = fgDL->VtxBuffer[vi]; if (v.pos.y >= safeTop && v.pos.y <= safeBot) continue; float alpha = 1.0f; if (settingsFadeZoneTop > 0.0f) { float dTop = v.pos.y - settingsFadeTopY; if (dTop < settingsFadeZoneTop) alpha = std::min(alpha, std::max(0.0f, dTop / settingsFadeZoneTop)); } if (settingsFadeZoneBot > 0.0f) { float dBot = settingsFadeBottomY - v.pos.y; if (dBot < settingsFadeZoneBot) alpha = std::min(alpha, std::max(0.0f, dBot / settingsFadeZoneBot)); } if (alpha < 1.0f) { int a = (v.col >> IM_COL32_A_SHIFT) & 0xFF; a = static_cast(a * alpha); v.col = (v.col & ~IM_COL32_A_MASK) | (static_cast(a) << IM_COL32_A_SHIFT); } } } ImGui::EndChild(); // ##SettingsPageScroll // Confirmation dialog for clearing z-tx history if (sp_confirm_clear_ztx) { if (BeginOverlayDialog(TR("confirm_clear_ztx_title"), &sp_confirm_clear_ztx, 480.0f, 0.94f)) { ImGui::PushFont(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))) { sp_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_btn").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"); } sp_confirm_clear_ztx = false; } ImGui::PopStyleColor(2); EndOverlayDialog(); } } // Confirmation dialog for deleting blockchain data if (sp_confirm_delete_blockchain) { if (BeginOverlayDialog(TR("confirm_delete_blockchain_title"), &sp_confirm_delete_blockchain, 500.0f, 0.94f)) { ImGui::PushFont(Type().iconLarge()); ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), ICON_MD_WARNING); ImGui::PopFont(); ImGui::SameLine(); ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "%s", TR("warning")); ImGui::Spacing(); ImGui::TextWrapped("%s", TR("confirm_delete_blockchain_msg")); ImGui::Spacing(); ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("confirm_delete_blockchain_safe")); 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))) { sp_confirm_delete_blockchain = 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("delete_blockchain_confirm", "del_bc_btn").c_str(), ImVec2(btnW, 40))) { app->deleteBlockchainData(); sp_confirm_delete_blockchain = false; } ImGui::PopStyleColor(2); EndOverlayDialog(); } } } } // namespace ui } // namespace dragonx