13 Commits

Author SHA1 Message Date
dan_s
96c27bb949 feat: Full UI internationalization, pool hashrate stats, and layout caching
- Replace all hardcoded English strings with TR() translation keys across
  every tab, dialog, and component (~20 UI files)
- Expand all 8 language files (de, es, fr, ja, ko, pt, ru, zh) with
  complete translations (~37k lines added)
- Improve i18n loader with exe-relative path fallback and English base
  fallback for missing keys
- Add pool-side hashrate polling via pool stats API in xmrig_manager
- Introduce Layout::beginFrame() per-frame caching and refresh balance
  layout config only on schema generation change
- Offload daemon output parsing to worker thread
- Add CJK subset fallback font for Chinese/Japanese/Korean glyphs
2026-03-11 00:40:50 -05:00
dan_s
cc617dd5be Add mine-when-idle, default banlist, and console parsing improvements
Mine-when-idle:
- Auto-start/stop mining based on system idle time detection
- Platform::getSystemIdleSeconds() via XScreenSaver (Linux) / GetLastInputInfo (Win)
- Settings: mine_when_idle toggle + configurable delay (30s–10m)
- Settings page UI with checkbox and delay combo

Console tab:
- Shell-like argument parsing with quote and JSON bracket support
- Pass JSON objects/arrays directly as RPC params
- Fix selection indices when lines are evicted from buffer

Connection & status bar:
- Reduce RPC connect timeout to 1s for localhost fast-fail
- Fast retry timer on daemon startup and external daemon detection
- Show pool mining hashrate in status bar; sidebar badge reflects pool state

UI polish:
- Add logo to About card in settings; expose logo dimensions on App
- Header title offset-y support; adjust content-area margins
- Fix banned peers row cursor position (rawRowPosB.x)

Branding:
- Update copyright to "DragonX Developers" in RC and About section
- Replace logo/icon assets with updated versions

Misc:
- setup.sh: checkout dragonx branch before pulling
- Remove stale prebuilt-binaries/xmrig/.gitkeep
2026-03-07 13:42:31 -06:00
dan_s
653a90de62 fix: Windows identity, async address creation, mining UI, and chart artifacts
Windows identity:
- Add VERSIONINFO resource (.rc) with ObsidianDragon file description
- Embed application manifest for DPI awareness and shell identity
- Patch libwinpthread/libpthread to remove competing VERSIONINFO
- Set AppUserModelID and HWND property store to override Task Manager cache
- Link patched pthread libs to eliminate "POSIX WinThreads" description

Address creation (+New button):
- Move z_getnewaddress/getnewaddress off UI thread to async worker
- Inject new address into state immediately for instant UI selection
- Trigger background refresh for balance updates

Mining tab:
- Add pool mining dropdown with saved URLs/workers and bookmarks
- Add solo mining log panel from daemon output with chart/log toggle
- Fix toggle button cursor (render after InputTextMultiline)
- Auto-restart miner on pool config change
- Migrate default pool URL to include stratum port

Transactions:
- Sort pending (0-conf) transactions to top of history
- Fall back to timereceived when timestamp is missing

Shutdown:
- Replace blocking sleep_for calls with 100ms polling loops
- Check shutting_down_ flag throughout daemon restart/bootstrap flows
- Reduce daemon stop timeout from 30s to 10s

Other:
- Fix market chart fill artifact (single concave polygon vs per-segment quads)
- Add bootstrap checksum verification state display
- Rename daemon client identifier to ObsidianDragon
2026-03-05 22:43:27 -06:00
dan_s
4b16a2a2c4 improve diagnostics, security UX, and network tab refresh
Diagnostics & logging:
- add verbose logging system (VERBOSE_LOGF) with toggle in Settings
- forward app-level log messages to Console tab for in-UI visibility
- add detailed connection attempt logging (attempt #, daemon state,
  config paths, auth failures, port owner identification)
- detect HTTP 401 auth failures and show actionable error messages
- identify port owner process (PID + name) on both Linux and Windows
- demote noisy acrylic/shader traces from DEBUG_LOGF to VERBOSE_LOGF
- persist verbose_logging preference in settings.json
- link iphlpapi on Windows for GetExtendedTcpTable

Security & encryption:
- update local encryption state immediately after encryptwallet RPC
  so Settings reflects the change before daemon restarts
- show notifications for encrypt success/failure and PIN skip
- use dedicated RPC client for z_importwallet during decrypt flow
  to avoid blocking main rpc_ curl_mutex (which starved peer/tx refresh)
- force full state refresh (addresses, transactions, peers) after
  successful wallet import

Network tab:
- redesign peers refresh button as glass-panel with icon + label,
  matching the mining button style
- add spinning arc animation while peer data is loading
  (peer_refresh_in_progress_ atomic flag set/cleared in refreshPeerInfo)
- prevent double-click spam during refresh
- add refresh-button size to ui.toml

Other:
- use fast_rpc_ for rescan polling to avoid blocking on main rpc_
- enable DRAGONX_DEBUG in all build configs (was debug-only)
- setup.sh: pull latest xmrig-hac when repo already exists
2026-03-05 05:26:04 -06:00
dan_s
c51d3dafff fix text shifting in status bar from font scale changes 2026-03-05 01:29:03 -06:00
dan_s
68c2a59d09 improved font scaling text and window adjustment, added ctrl + scroll hotkey for font scaling 2026-03-05 01:22:20 -06:00
dan_s
45a2ccd9f3 refresh network info instantly when switching to network tab 2026-03-04 15:16:32 -06:00
dan_s
0ca1caf148 feat: RPC caching, background decrypt import, fast-lane peers, mining fix
RPC client:
- Add call() overload with per-call timeout parameter
- z_exportwallet uses 300s, z_importwallet uses 1200s timeout

Decrypt wallet (app_security.cpp, app.cpp):
- Show per-step and overall elapsed timers during decrypt flow
- Reduce dialog to 5 steps; close before key import begins
- Run z_importwallet on detached background thread
- Add pulsing "Importing keys..." status bar indicator
- Report success/failure via notifications instead of dialog

RPC caching (app_network.cpp, app.h):
- Cache z_viewtransaction results in viewtx_cache_ across refresh cycles
- Skip RPC calls for already-cached txids (biggest perf win)
- Build confirmed_tx_cache_ for deeply-confirmed transactions
- Clear all caches on disconnect
- Remove unused refreshTransactions() dead code

Peers (app_network.cpp, peers_tab.cpp):
- Route refreshPeerInfo() through fast_worker_ to avoid head-of-line blocking
- Replace footer "Refresh Peers" button with ICON_MD_REFRESH in toggle header
- Refresh button triggers both peer list and full blockchain data refresh

Mining (mining_tab.cpp):
- Allow pool mining toggle when blockchain is not synced
- Pool mining only needs xmrig, not local daemon sync
2026-03-04 15:12:24 -06:00
dan_s
7fb1f1de9d Rename hush→dragonx across wallet codebase
- Rename RESOURCE_HUSHD/HUSH_CLI/HUSH_TX to RESOURCE_DRAGONXD/DRAGONX_CLI/DRAGONX_TX
- Remove unused .bat resource constants (DRAGONXD_BAT, DRAGONX_CLI_BAT)
- Update INCBIN symbols: g_hushd_exe → g_dragonxd_exe, etc.
- Update daemon search paths, removing hush-arrakis-chain fallbacks
- Update process detection (Windows findProcessByName, Linux /proc/comm, macOS pgrep)
- Update build.sh: embed dragonxd.exe/dragonx-cli.exe/dragonx-tx.exe
- Overhaul setup.sh: fix binary names, release paths, add -j passthrough
- Update getDaemonPath/needsDaemonExtraction/hasDaemonAvailable for new names
2026-03-04 03:17:32 -06:00
dan_s
386cc857b0 setup script improvements, automatically clone xmrig-hac and build for multiple platforms 2026-03-03 01:47:44 -06:00
dan_s
3e6136983a update links 2026-03-03 01:20:03 -06:00
dan_s
2c1862aed3 change release output names 2026-02-28 15:28:40 -06:00
dan_s
4b815fc9d1 feat: blockchain rescan via daemon restart + status bar progress
- Fix z_importwallet to use full path instead of filename only
- Add rescanBlockchain() method that restarts daemon with -rescan flag
- Track rescan progress via daemon output parsing and getrescaninfo RPC
- Display rescan progress in status bar with animated indicator when starting
- Improve dark theme card contrast: lighter surface-variant, tinted borders, stronger rim-light
2026-02-28 15:06:35 -06:00
9 changed files with 224 additions and 380 deletions

109
build.sh
View File

@@ -5,7 +5,7 @@
# #
# Usage: # Usage:
# ./build.sh # Dev build (Linux, debug-friendly) # ./build.sh # Dev build (Linux, debug-friendly)
# ./build.sh --linux-release # Linux release (zip + AppImage) # ./build.sh --linux-release # Linux release + AppImage
# ./build.sh --win-release # Windows cross-compile (mingw-w64) # ./build.sh --win-release # Windows cross-compile (mingw-w64)
# ./build.sh --mac-release # macOS .app bundle + DMG # ./build.sh --mac-release # macOS .app bundle + DMG
# ./build.sh --linux-release --win-release # Multiple targets # ./build.sh --linux-release --win-release # Multiple targets
@@ -51,7 +51,7 @@ DragonX Wallet — Unified Build Script
Usage: $0 [options] Usage: $0 [options]
Targets (at least one required, or none for dev build): Targets (at least one required, or none for dev build):
--linux-release Linux release (zip + AppImage) -> release/linux/ --linux-release Linux release build + AppImage -> release/linux/
--win-release Windows cross-compile (mingw-w64) -> release/windows/ --win-release Windows cross-compile (mingw-w64) -> release/windows/
--mac-release macOS .app bundle + DMG -> release/mac/ --mac-release macOS .app bundle + DMG -> release/mac/
@@ -71,7 +71,7 @@ Cross-compiling from Linux:
Examples: Examples:
$0 # Quick dev build (Linux) $0 # Quick dev build (Linux)
$0 --linux-release # Linux release (zip + AppImage) $0 --linux-release # Linux release + AppImage
$0 --win-release # Windows cross-compile $0 --win-release # Windows cross-compile
$0 --mac-release # macOS bundle + DMG (native or osxcross) $0 --mac-release # macOS bundle + DMG (native or osxcross)
$0 --clean --linux-release --win-release # Clean + both $0 --clean --linux-release --win-release # Clean + both
@@ -218,7 +218,7 @@ build_dev() {
} }
# ═══════════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════════
# RELEASE: LINUX — build + strip + bundle daemon + zip + AppImage # RELEASE: LINUX — build + strip + bundle daemon + AppImage
# ═══════════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════════
build_release_linux() { build_release_linux() {
header "Release: Linux x86_64" header "Release: Linux x86_64"
@@ -249,41 +249,17 @@ build_release_linux() {
# ── Bundle daemon ──────────────────────────────────────────────────────── # ── Bundle daemon ────────────────────────────────────────────────────────
bundle_linux_daemon "bin" || warn "Daemon not bundled — wallet-only build" bundle_linux_daemon "bin" || warn "Daemon not bundled — wallet-only build"
# ── Bundle Sapling params ────────────────────────────────────────────────
SAPLING_SPEND="" SAPLING_OUTPUT=""
find_sapling_params && {
cp -f "$SAPLING_SPEND" "bin/sapling-spend.params"
cp -f "$SAPLING_OUTPUT" "bin/sapling-output.params"
info "Bundled Sapling params"
} || warn "Sapling params not found — not bundled"
# ── Package: release/linux/ ────────────────────────────────────────────── # ── Package: release/linux/ ──────────────────────────────────────────────
rm -rf "$out" rm -rf "$out"
mkdir -p "$out" mkdir -p "$out"
local DIST="ObsidianDragon-Linux-x64" cp bin/ObsidianDragon "$out/"
local dist_dir="$out/$DIST" [[ -f bin/dragonxd ]] && cp bin/dragonxd "$out/"
mkdir -p "$dist_dir" [[ -f bin/dragonx-cli ]] && cp bin/dragonx-cli "$out/"
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$out/"
cp -r bin/res "$out/" 2>/dev/null || true
cp bin/ObsidianDragon "$dist_dir/" # ── AppImage ─────────────────────────────────────────────────────────────
[[ -f bin/dragonxd ]] && cp bin/dragonxd "$dist_dir/"
[[ -f bin/dragonx-cli ]] && cp bin/dragonx-cli "$dist_dir/"
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$dist_dir/"
[[ -f bin/sapling-spend.params ]] && cp bin/sapling-spend.params "$dist_dir/"
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$dist_dir/"
# Bundle xmrig for mining support
local XMRIG_LINUX="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig"
[[ -f "$XMRIG_LINUX" ]] && { cp "$XMRIG_LINUX" "$dist_dir/"; chmod +x "$dist_dir/xmrig"; info "Bundled xmrig"; } || warn "xmrig not found — mining unavailable in zip"
cp -r bin/res "$dist_dir/" 2>/dev/null || true
# ── Zip ──────────────────────────────────────────────────────────────────
if command -v zip &>/dev/null; then
(cd "$out" && zip -r "$DIST.zip" "$DIST")
info "Zip: $out/$DIST.zip ($(du -h "$out/$DIST.zip" | cut -f1))"
fi
rm -rf "$dist_dir"
# ── AppImage (single-file) ───────────────────────────────────────────────
info "Creating AppImage ..." info "Creating AppImage ..."
local APPDIR="$bd/AppDir" local APPDIR="$bd/AppDir"
rm -rf "$APPDIR" rm -rf "$APPDIR"
@@ -295,16 +271,10 @@ build_release_linux() {
cp bin/ObsidianDragon "$APPDIR/usr/bin/" cp bin/ObsidianDragon "$APPDIR/usr/bin/"
cp -r bin/res/* "$APPDIR/usr/share/ObsidianDragon/res/" 2>/dev/null || true cp -r bin/res/* "$APPDIR/usr/share/ObsidianDragon/res/" 2>/dev/null || true
# Daemon inside AppImage
[[ -f bin/dragonxd ]] && cp bin/dragonxd "$APPDIR/usr/bin/" [[ -f bin/dragonxd ]] && cp bin/dragonxd "$APPDIR/usr/bin/"
[[ -f bin/dragonx-cli ]] && cp bin/dragonx-cli "$APPDIR/usr/bin/" [[ -f bin/dragonx-cli ]] && cp bin/dragonx-cli "$APPDIR/usr/bin/"
# Daemon data files must be alongside the daemon binary (usr/bin/) [[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$APPDIR/usr/share/ObsidianDragon/"
# because dragonxd searches relative to its own directory.
[[ -f bin/asmap.dat ]] && cp bin/asmap.dat "$APPDIR/usr/bin/"
[[ -f bin/sapling-spend.params ]] && cp bin/sapling-spend.params "$APPDIR/usr/bin/"
[[ -f bin/sapling-output.params ]] && cp bin/sapling-output.params "$APPDIR/usr/bin/"
# Bundle xmrig for mining support
local XMRIG_LINUX_AI="$SCRIPT_DIR/prebuilt-binaries/xmrig-hac/xmrig"
[[ -f "$XMRIG_LINUX_AI" ]] && { cp "$XMRIG_LINUX_AI" "$APPDIR/usr/bin/"; chmod +x "$APPDIR/usr/bin/xmrig"; }
# Desktop entry # Desktop entry
cat > "$APPDIR/usr/share/applications/ObsidianDragon.desktop" <<'DESK' cat > "$APPDIR/usr/share/applications/ObsidianDragon.desktop" <<'DESK'
@@ -378,11 +348,32 @@ APPRUN
local ARCH local ARCH
ARCH=$(uname -m) ARCH=$(uname -m)
local IMG_NAME="ObsidianDragon-${ARCH}.AppImage"
cd "$bd" cd "$bd"
ARCH="$ARCH" "$APPIMAGETOOL" "$APPDIR" "ObsidianDragon-${ARCH}.AppImage" 2>/dev/null && { ARCH="$ARCH" "$APPIMAGETOOL" "$APPDIR" "$IMG_NAME" 2>/dev/null && {
cp "ObsidianDragon-${ARCH}.AppImage" "$out/ObsidianDragon.AppImage" cp "$IMG_NAME" "$out/"
# Rename to match Windows convention: ObsidianDragon.AppImage
mv "$out/$IMG_NAME" "$out/ObsidianDragon.AppImage"
info "AppImage: $out/ObsidianDragon.AppImage ($(du -h "$out/ObsidianDragon.AppImage" | cut -f1))" info "AppImage: $out/ObsidianDragon.AppImage ($(du -h "$out/ObsidianDragon.AppImage" | cut -f1))"
} || warn "AppImage creation failed — binaries zip still in release/linux/" } || warn "AppImage creation failed (appimagetool issue) — raw binary still in release/linux/"
# Clean up: keep only AppImage + zip in release/linux/
if [[ -f "$out/ObsidianDragon.AppImage" ]]; then
# AppImage succeeded — remove everything except AppImage
find "$out" -maxdepth 1 -type f ! -name 'ObsidianDragon.AppImage' -delete
rm -rf "$out/res" 2>/dev/null
# Create zip matching Windows naming convention
local DIST="ObsidianDragon-Linux-x64"
local dist_dir="$out/$DIST"
mkdir -p "$dist_dir"
cp "$out/ObsidianDragon.AppImage" "$dist_dir/"
if command -v zip &>/dev/null; then
(cd "$out" && zip -r "$DIST.zip" "$DIST")
info "Zip: $out/$DIST.zip ($(du -h "$out/$DIST.zip" | cut -f1))"
fi
rm -rf "$dist_dir"
fi
info "Linux release artifacts: $out/" info "Linux release artifacts: $out/"
ls -lh "$out/" ls -lh "$out/"
@@ -610,25 +601,31 @@ HDR
[[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/" [[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/"
done done
# Bundle Sapling params + asmap for the zip distribution cat > "$dist_dir/README.txt" <<'README'
# (The single-file exe has these embedded via INCBIN, but the zip DragonX Wallet - Windows Edition
# needs them on disk so the daemon can find them in its work dir.) ================================
for f in sapling-spend.params sapling-output.params asmap.dat; do
[[ -f "$DD/$f" ]] && cp "$DD/$f" "$dist_dir/"
done
cp -r bin/res "$dist_dir/" 2>/dev/null || true SINGLE-FILE DISTRIBUTION
========================
This wallet is a true single-file executable with all resources embedded.
Just run ObsidianDragon.exe — no additional files needed!
# ── Single-file exe (all resources embedded) ──────────────────────────── On first run, the wallet will automatically extract:
- Sapling parameters to %APPDATA%\ZcashParams\
- asmap.dat to %APPDATA%\Hush\DRAGONX\
For support: https://git.dragonx.is/dragonx/ObsidianDragon
README
# Copy single-file exe to release dir
cp bin/ObsidianDragon.exe "$out/" cp bin/ObsidianDragon.exe "$out/"
info "Single-file exe: $out/ObsidianDragon.exe ($(du -h "$out/ObsidianDragon.exe" | cut -f1))"
# ── Zip ──────────────────────────────────────────────────────────────────
if command -v zip &>/dev/null; then if command -v zip &>/dev/null; then
(cd "$out" && zip -r "$DIST.zip" "$DIST") (cd "$out" && zip -r "$DIST.zip" "$DIST")
info "Zip: $out/$DIST.zip ($(du -h "$out/$DIST.zip" | cut -f1))" info "Zip: $out/$DIST.zip ($(du -h "$out/$DIST.zip" | cut -f1))"
fi # Clean up: keep .zip + single-file exe, remove loose directory
rm -rf "$dist_dir" rm -rf "$dist_dir"
fi
info "Windows release artifacts: $out/" info "Windows release artifacts: $out/"
ls -lh "$out/" ls -lh "$out/"

View File

@@ -627,13 +627,8 @@ XMRIG_SRC="$PROJECT_DIR/external/xmrig-hac"
XMRIG_PREBUILT="$PROJECT_DIR/prebuilt-binaries/xmrig-hac" XMRIG_PREBUILT="$PROJECT_DIR/prebuilt-binaries/xmrig-hac"
# Clean previous prebuilt xmrig binaries so we always rebuild # Clean previous prebuilt xmrig binaries so we always rebuild
# Only clean the binary for the platform(s) we are actually building,
# otherwise a plain ./setup.sh deletes xmrig.exe without rebuilding it.
if ! $CHECK_ONLY; then if ! $CHECK_ONLY; then
rm -f "$XMRIG_PREBUILT/xmrig" 2>/dev/null || true rm -f "$XMRIG_PREBUILT/xmrig" "$XMRIG_PREBUILT/xmrig.exe" 2>/dev/null || true
if $SETUP_WIN; then
rm -f "$XMRIG_PREBUILT/xmrig.exe" 2>/dev/null || true
fi
fi fi
# Helper: clone xmrig-hac if not present # Helper: clone xmrig-hac if not present

View File

@@ -1854,29 +1854,6 @@ bool App::startEmbeddedDaemon()
return false; return false;
} }
DEBUG_LOGF("Sapling params extracted successfully\n"); DEBUG_LOGF("Sapling params extracted successfully\n");
} else {
// Fallback: check for params bundled alongside the executable
// (zip distributions bundle sapling-*.params next to the binary)
namespace fs = std::filesystem;
std::string exe_dir = util::Platform::getExecutableDirectory();
std::string daemon_dir = resources::getDaemonDirectory();
const char* paramFiles[] = { "sapling-spend.params", "sapling-output.params", "asmap.dat" };
bool copied = false;
if (!exe_dir.empty()) {
std::error_code ec;
fs::create_directories(daemon_dir, ec);
for (const char* name : paramFiles) {
fs::path src = fs::path(exe_dir) / name;
fs::path dst = fs::path(daemon_dir) / name;
if (fs::exists(src) && !fs::exists(dst)) {
DEBUG_LOGF("Copying bundled %s from exe dir to %s\n", name, daemon_dir.c_str());
fs::copy_file(src, dst, ec);
if (!ec) copied = true;
}
}
}
if (copied && rpc::Connection::verifySaplingParams()) {
DEBUG_LOGF("Sapling params copied from exe directory successfully\n");
} else { } else {
daemon_status_ = "Sapling parameters not found. They should be in: " + rpc::Connection::getSaplingParamsDir(); daemon_status_ = "Sapling parameters not found. They should be in: " + rpc::Connection::getSaplingParamsDir();
DEBUG_LOGF("Sapling params not found and no embedded resources available!\n"); DEBUG_LOGF("Sapling params not found and no embedded resources available!\n");
@@ -1884,7 +1861,6 @@ bool App::startEmbeddedDaemon()
return false; return false;
} }
} }
}
// Create daemon manager if needed // Create daemon manager if needed
if (!embedded_daemon_) { if (!embedded_daemon_) {

View File

@@ -466,18 +466,6 @@ bool XmrigManager::startProcess(const std::string& xmrigPath, const std::string&
dup2(pipefd[1], STDERR_FILENO); dup2(pipefd[1], STDERR_FILENO);
close(pipefd[1]); close(pipefd[1]);
// Detach from controlling terminal's stdin to prevent SIGTTIN/SIGTTOU
// when running in a new process group (setpgid below).
int devnull = open("/dev/null", O_RDONLY);
if (devnull >= 0) {
dup2(devnull, STDIN_FILENO);
close(devnull);
}
// Ignore job-control signals that a background process group may receive
signal(SIGTTIN, SIG_IGN);
signal(SIGTTOU, SIG_IGN);
// New process group so we can kill the whole group // New process group so we can kill the whole group
setpgid(0, 0); setpgid(0, 0);

View File

@@ -509,18 +509,6 @@ std::string getXmrigPath()
return xmrigName; return xmrigName;
} }
// Check alongside the wallet executable (zip / AppImage distributions)
{
std::string exeDir = util::Platform::getExecutableDirectory();
if (!exeDir.empty()) {
std::string exeDirPath = exeDir + "/" + xmrigName;
if (std::filesystem::exists(exeDirPath)) {
DEBUG_LOGF("[INFO] getXmrigPath: found alongside wallet at %s\n", exeDirPath.c_str());
return exeDirPath;
}
}
}
// Check development paths // Check development paths
#ifdef _WIN32 #ifdef _WIN32
// Not applicable for cross-compiled Windows builds // Not applicable for cross-compiled Windows builds

View File

@@ -757,20 +757,10 @@ inline void ApplySmoothScroll(float speed = 12.0f)
s.current = actualY; s.current = actualY;
} }
// Capture mouse wheel when the cursor is inside this child window. // Capture mouse wheel when hovered, but not when a popup (combo dropdown
// Use a direct position check instead of IsWindowHovered() which can // etc.) is open — let the popup handle its own scrolling exclusively.
// return false when an ActiveId exists (focused input, held button) or
// when tooltip/popup windows from row hovers interfere with hover
// routing. Still skip when a real popup (combo dropdown, context menu)
// is open so the popup handles its own scrolling.
{
bool popupOpen = ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel); bool popupOpen = ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel);
ImVec2 mpos = ImGui::GetIO().MousePos; if (!popupOpen && ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows)) {
ImVec2 wmin = win->Pos;
ImVec2 wmax = ImVec2(wmin.x + win->Size.x, wmin.y + win->Size.y);
bool inside = (mpos.x >= wmin.x && mpos.x < wmax.x &&
mpos.y >= wmin.y && mpos.y < wmax.y);
if (!popupOpen && inside) {
float wheel = ImGui::GetIO().MouseWheel; float wheel = ImGui::GetIO().MouseWheel;
if (wheel != 0.0f) { if (wheel != 0.0f) {
float step = ImGui::GetTextLineHeightWithSpacing() * 3.0f; float step = ImGui::GetTextLineHeightWithSpacing() * 3.0f;
@@ -778,7 +768,6 @@ inline void ApplySmoothScroll(float speed = 12.0f)
s.target = ImClamp(s.target, 0.0f, scrollMaxY); s.target = ImClamp(s.target, 0.0f, scrollMaxY);
} }
} }
}
// Clamp target if scrollMax changed (e.g., content resized) // Clamp target if scrollMax changed (e.g., content resized)
if (s.target > scrollMaxY) s.target = scrollMaxY; if (s.target > scrollMaxY) s.target = scrollMaxY;

View File

@@ -122,8 +122,6 @@ static bool sp_verbose_logging = false;
static std::set<std::string> sp_debug_categories; static std::set<std::string> sp_debug_categories;
static bool sp_debug_cats_dirty = false; // true when changed but daemon not yet restarted 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_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_clear_ztx = false; // confirmation dialog for clearing z-tx history
// (APPEARANCE card now uses ChannelsSplit like all other cards) // (APPEARANCE card now uses ChannelsSplit like all other cards)
@@ -379,11 +377,11 @@ void RenderSettingsPage(App* app) {
ImFont* sub1 = Type().subtitle1(); ImFont* sub1 = Type().subtitle1();
// ==================================================================== // ====================================================================
// THEME & LANGUAGE — card (draw-first approach; avoids ChannelsSplit // APPEARANCE — card (draw-first approach; avoids ChannelsSplit which
// which breaks BeginCombo popup rendering in some ImGui versions) // breaks BeginCombo popup rendering in some ImGui versions)
// ==================================================================== // ====================================================================
{ {
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("theme_language")); Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("appearance"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs())); ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImVec2 cardMin = ImGui::GetCursorScreenPos(); ImVec2 cardMin = ImGui::GetCursorScreenPos();
@@ -563,59 +561,9 @@ void RenderSettingsPage(App* app) {
ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// --- Font Scale slider (always visible) --- // --- Visual Effects (checkboxes on one row, Quality+Blur paired) ---
{ {
ImGui::PushFont(body2); 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 // 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)) { if (ImGui::Checkbox(TrId("low_spec_mode", "low_spec").c_str(), &sp_low_spec_mode)) {
@@ -655,6 +603,7 @@ void RenderSettingsPage(App* app) {
} }
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_low_spec")); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_low_spec"));
// Simple background is lightweight — always interactive, even in low-spec mode
ImGui::SameLine(0, Layout::spacingLg()); ImGui::SameLine(0, Layout::spacingLg());
if (ImGui::Checkbox(TrId("simple_background", "simple_bg").c_str(), &sp_gradient_background)) { if (ImGui::Checkbox(TrId("simple_background", "simple_bg").c_str(), &sp_gradient_background)) {
schema::SkinManager::instance().setGradientMode(sp_gradient_background); schema::SkinManager::instance().setGradientMode(sp_gradient_background);
@@ -686,10 +635,12 @@ void RenderSettingsPage(App* app) {
float baseX = ImGui::GetCursorScreenPos().x; float baseX = ImGui::GetCursorScreenPos().x;
float rightX = baseX + ctrlW + Layout::spacingLg(); float rightX = baseX + ctrlW + Layout::spacingLg();
// Acrylic label + slider (left column)
ImGui::TextUnformatted(TR("acrylic")); ImGui::TextUnformatted(TR("acrylic"));
float row1Y = ImGui::GetCursorScreenPos().y; float row1Y = ImGui::GetCursorScreenPos().y;
ImGui::SetNextItemWidth(ctrlW); ImGui::SetNextItemWidth(ctrlW);
{ {
// Build display format: "Off" at zero, percentage otherwise
char blur_fmt[16]; char blur_fmt[16];
if (sp_blur_amount < 0.01f) if (sp_blur_amount < 0.01f)
snprintf(blur_fmt, sizeof(blur_fmt), "Off"); snprintf(blur_fmt, sizeof(blur_fmt), "Off");
@@ -697,6 +648,7 @@ void RenderSettingsPage(App* app) {
snprintf(blur_fmt, sizeof(blur_fmt), "%.0f%%%%", sp_blur_amount * 25.0f); 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, if (ImGui::SliderFloat("##AcrylicBlur", &sp_blur_amount, 0.0f, 4.0f, blur_fmt,
ImGuiSliderFlags_AlwaysClamp)) { ImGuiSliderFlags_AlwaysClamp)) {
// Snap to off when dragged near 0%
if (sp_blur_amount > 0.0f && sp_blur_amount < 0.15f) sp_blur_amount = 0.0f; if (sp_blur_amount > 0.0f && sp_blur_amount < 0.15f) sp_blur_amount = 0.0f;
sp_acrylic_enabled = (sp_blur_amount > 0.001f); sp_acrylic_enabled = (sp_blur_amount > 0.001f);
effects::ImGuiAcrylic::ApplyBlurAmount(sp_blur_amount); effects::ImGuiAcrylic::ApplyBlurAmount(sp_blur_amount);
@@ -706,6 +658,7 @@ void RenderSettingsPage(App* app) {
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_blur")); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_blur"));
float afterRow1Y = ImGui::GetCursorScreenPos().y; float afterRow1Y = ImGui::GetCursorScreenPos().y;
// Noise label + slider (right column, same row)
float lblH = ImGui::GetTextLineHeight() + ImGui::GetStyle().ItemSpacing.y; float lblH = ImGui::GetTextLineHeight() + ImGui::GetStyle().ItemSpacing.y;
ImGui::SetCursorScreenPos(ImVec2(rightX, row1Y - lblH)); ImGui::SetCursorScreenPos(ImVec2(rightX, row1Y - lblH));
ImGui::TextUnformatted(TR("noise")); ImGui::TextUnformatted(TR("noise"));
@@ -725,8 +678,10 @@ void RenderSettingsPage(App* app) {
} }
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_noise")); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_noise"));
// Reset cursor to left column, past row 1
ImGui::SetCursorScreenPos(ImVec2(baseX, afterRow1Y)); ImGui::SetCursorScreenPos(ImVec2(baseX, afterRow1Y));
// Row 2: UI Opacity + Window Opacity (labels above)
ImGui::TextUnformatted(TR("ui_opacity")); ImGui::TextUnformatted(TR("ui_opacity"));
float row2Y = ImGui::GetCursorScreenPos().y; float row2Y = ImGui::GetCursorScreenPos().y;
ImGui::SetNextItemWidth(ctrlW); ImGui::SetNextItemWidth(ctrlW);
@@ -742,6 +697,7 @@ void RenderSettingsPage(App* app) {
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_ui_opacity")); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_ui_opacity"));
float afterRow2Y = ImGui::GetCursorScreenPos().y; float afterRow2Y = ImGui::GetCursorScreenPos().y;
// Window label + slider (right column, same row)
ImGui::SetCursorScreenPos(ImVec2(rightX, row2Y - lblH)); ImGui::SetCursorScreenPos(ImVec2(rightX, row2Y - lblH));
ImGui::TextUnformatted(TR("window_opacity")); ImGui::TextUnformatted(TR("window_opacity"));
ImGui::SetCursorScreenPos(ImVec2(rightX, row2Y)); ImGui::SetCursorScreenPos(ImVec2(rightX, row2Y));
@@ -756,11 +712,12 @@ void RenderSettingsPage(App* app) {
} }
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_window_opacity")); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_window_opacity"));
// Reset cursor to left column, past row 2
ImGui::SetCursorScreenPos(ImVec2(baseX, afterRow2Y)); ImGui::SetCursorScreenPos(ImVec2(baseX, afterRow2Y));
ImGui::EndDisabled(); // low-spec ImGui::EndDisabled(); // low-spec
ImGui::PopFont(); ImGui::PopFont();
} // sp_effects_expanded }
} else { } else {
// ============================================================ // ============================================================
// Narrow: stacked combos + 2-column effects (original layout) // Narrow: stacked combos + 2-column effects (original layout)
@@ -853,62 +810,11 @@ void RenderSettingsPage(App* app) {
ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// --- Font Scale slider (always visible) --- // --- Visual Effects (checkboxes + controls) ---
{ {
ImGui::PushFont(body2); 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);
// Checkbox row 1: Low-spec
if (ImGui::Checkbox(TrId("low_spec_mode", "low_spec").c_str(), &sp_low_spec_mode)) { if (ImGui::Checkbox(TrId("low_spec_mode", "low_spec").c_str(), &sp_low_spec_mode)) {
effects::setLowSpecMode(sp_low_spec_mode); effects::setLowSpecMode(sp_low_spec_mode);
if (sp_low_spec_mode) { if (sp_low_spec_mode) {
@@ -946,6 +852,7 @@ void RenderSettingsPage(App* app) {
} }
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_low_spec")); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_low_spec"));
// Simple background is lightweight — always interactive, even in low-spec mode
if (ImGui::Checkbox(TrId("settings_gradient_bg", "gradient_bg").c_str(), &sp_gradient_background)) { if (ImGui::Checkbox(TrId("settings_gradient_bg", "gradient_bg").c_str(), &sp_gradient_background)) {
schema::SkinManager::instance().setGradientMode(sp_gradient_background); schema::SkinManager::instance().setGradientMode(sp_gradient_background);
saveSettingsPageState(app->settings()); saveSettingsPageState(app->settings());
@@ -954,6 +861,7 @@ void RenderSettingsPage(App* app) {
ImGui::BeginDisabled(sp_low_spec_mode); ImGui::BeginDisabled(sp_low_spec_mode);
// Checkbox row 2: Console scanline | Theme effects
if (ImGui::Checkbox(TrId("console_scanline", "scanline").c_str(), &sp_scanline_enabled)) { if (ImGui::Checkbox(TrId("console_scanline", "scanline").c_str(), &sp_scanline_enabled)) {
ConsoleTab::s_scanline_enabled = sp_scanline_enabled; ConsoleTab::s_scanline_enabled = sp_scanline_enabled;
app->settings()->setScanlineEnabled(sp_scanline_enabled); app->settings()->setScanlineEnabled(sp_scanline_enabled);
@@ -968,8 +876,9 @@ void RenderSettingsPage(App* app) {
} }
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_theme_effects")); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_theme_effects"));
// Acrylic blur slider (label above)
float ctrlW = std::max(S.drawElement("components.settings-page", "effects-input-min-width").size, float ctrlW = std::max(S.drawElement("components.settings-page", "effects-input-min-width").size,
availWidth - pad * 2.0f); contentW);
ImGui::TextUnformatted(TR("acrylic")); ImGui::TextUnformatted(TR("acrylic"));
ImGui::SetNextItemWidth(ctrlW); ImGui::SetNextItemWidth(ctrlW);
{ {
@@ -988,6 +897,7 @@ void RenderSettingsPage(App* app) {
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings()); if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_blur")); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_blur"));
// Noise opacity slider (label above)
ImGui::TextUnformatted(TR("noise")); ImGui::TextUnformatted(TR("noise"));
ImGui::SetNextItemWidth(ctrlW); ImGui::SetNextItemWidth(ctrlW);
{ {
@@ -1004,6 +914,7 @@ void RenderSettingsPage(App* app) {
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings()); if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_noise")); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_noise"));
// UI Opacity slider (label above)
ImGui::TextUnformatted(TR("ui_opacity")); ImGui::TextUnformatted(TR("ui_opacity"));
ImGui::SetNextItemWidth(ctrlW); ImGui::SetNextItemWidth(ctrlW);
{ {
@@ -1017,6 +928,7 @@ void RenderSettingsPage(App* app) {
if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings()); if (ImGui::IsItemDeactivatedAfterEdit()) saveSettingsPageState(app->settings());
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_ui_opacity")); if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_ui_opacity"));
// Window Opacity slider (label above)
ImGui::TextUnformatted(TR("window_opacity")); ImGui::TextUnformatted(TR("window_opacity"));
ImGui::SetNextItemWidth(ctrlW); ImGui::SetNextItemWidth(ctrlW);
{ {
@@ -1031,7 +943,44 @@ void RenderSettingsPage(App* app) {
ImGui::EndDisabled(); // low-spec ImGui::EndDisabled(); // low-spec
ImGui::PopFont(); ImGui::PopFont();
} // sp_effects_expanded }
}
// ============================================================
// Font Scale slider (always enabled, not affected by low-spec)
// ============================================================
{
ImGui::PushFont(body2);
ImGui::Spacing();
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);
// Sync from Layout so Alt+scroll hotkey changes are reflected
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);
}
// Snap to 0.05 increments for consistent stepping.
// Visual scaling uses FontScaleMain (no atlas rebuild),
// atlas rebuild is deferred to slider release for crisp text.
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) {
// While dragging: update layout scale without atlas rebuild
Layout::setUserFontScaleVisual(sp_font_scale);
}
if (ImGui::IsItemDeactivatedAfterEdit()) {
// On release: rebuild font atlas at final size
Layout::setUserFontScale(sp_font_scale);
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_font_scale"));
ImGui::PopFont();
} }
// Bottom padding // Bottom padding
@@ -1051,7 +1000,7 @@ void RenderSettingsPage(App* app) {
ImGui::Dummy(ImVec2(0, gap)); ImGui::Dummy(ImVec2(0, gap));
// ==================================================================== // ====================================================================
// WALLET — card (privacy/daemon toggles + collapsible tools) // WALLET — card (Keys, Backup, Tools, Maintenance, Node/RPC)
// ==================================================================== // ====================================================================
{ {
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("wallet")); Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("wallet"));
@@ -1064,6 +1013,56 @@ void RenderSettingsPage(App* app) {
ImGui::Indent(pad); ImGui::Indent(pad);
float contentW = availWidth - pad * 2; float contentW = availWidth - pad * 2;
float btnSpacing = Layout::spacingMd();
float btnPad = S.drawElement("components.settings-page", "wallet-btn-padding").sizeOr(24.0f);
// Calculate button width that fits available space
// 6 buttons total: on wide screens 3+3, on narrow screens 2+2+2
int btnsPerRow = (contentW >= 600.0f) ? 3 : 2;
float bw = (contentW - btnSpacing * (btnsPerRow - 1)) / btnsPerRow;
// Clamp to reasonable size
float minBtnW = S.drawElement("components.settings-page", "wallet-btn-min-width").sizeOr(100.0f);
bw = std::max(minBtnW, bw);
// Row 1 — Tools & Actions
{
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"));
}
// Thin divider
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
{
float divAlpha = S.drawElement("components.settings-page", "section-divider-alpha").opacity;
if (divAlpha <= 0.0f) divAlpha = 0.08f;
ImU32 baseDivCol = S.resolveColor("var(--status-divider)", IM_COL32(255, 255, 255, 20));
ImU32 divCol = material::ScaleAlpha(baseDivCol, divAlpha / 0.08f);
ImVec2 p = ImGui::GetCursorScreenPos();
dl->AddLine(ImVec2(p.x, p.y), ImVec2(p.x + contentW, p.y), divCol);
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Privacy, Network & Daemon checkboxes — all on one line, shrink text to fit // Privacy, Network & Daemon checkboxes — all on one line, shrink text to fit
{ {
@@ -1112,94 +1111,47 @@ void RenderSettingsPage(App* app) {
if (scale < 1.0f) ImGui::SetWindowFontScale(1.0f); if (scale < 1.0f) ImGui::SetWindowFontScale(1.0f);
} }
ImGui::Dummy(ImVec2(0, Layout::spacingSm())); // Mine when idle — checkbox + delay combo
// --- Collapsible: Tools & Actions... ---
{ {
const char* arrow = sp_tools_expanded ? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE; if (ImGui::Checkbox(TrId("mine_when_idle", "mine_idle").c_str(), &sp_mine_when_idle)) {
ImGui::PushFont(body2); saveSettingsPageState(app->settings());
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; if (ImGui::IsItemHovered())
dl->AddText(body2, body2->LegacySize, ImVec2(hdrPos.x, textY), OnSurfaceMedium(), TR("tools_actions")); ImGui::SetTooltip("%s", TR("tt_mine_idle"));
ImFont* iconFont = Type().iconSmall();
if (!iconFont) iconFont = body2; if (sp_mine_when_idle) {
float arrowW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, arrow).x; ImGui::SameLine(0, Layout::spacingMd());
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(hdrPos.x + contentW - arrowW, textY), OnSurfaceMedium(), arrow); ImGui::AlignTextToFramePadding();
} ImGui::TextColored(ImVec4(1, 1, 1, 0.5f), "%s", TR("settings_idle_after"));
ImGui::PopStyleColor(3); ImGui::SameLine(0, Layout::spacingSm());
ImGui::PopFont();
struct DelayOption { int seconds; const char* label; };
static const DelayOption delays[] = {
{30, "30s"}, {60, "1m"}, {120, "2m"}, {300, "5m"}, {600, "10m"}
};
const char* previewLabel = "2m";
for (const auto& d : delays) {
if (d.seconds == sp_mine_idle_delay) { previewLabel = d.label; break; }
} }
if (sp_tools_expanded) { ImGui::SetNextItemWidth(schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f));
float btnSpacing = Layout::spacingMd(); if (ImGui::BeginCombo("##IdleDelay", previewLabel, ImGuiComboFlags_NoArrowButton)) {
int btnsPerRow = (contentW >= 600.0f) ? 3 : 2; for (const auto& d : delays) {
float bw = (contentW - btnSpacing * (btnsPerRow - 1)) / btnsPerRow; bool selected = (d.seconds == sp_mine_idle_delay);
float minBtnW = S.drawElement("components.settings-page", "wallet-btn-min-width").sizeOr(100.0f); if (ImGui::Selectable(d.label, selected)) {
bw = std::max(minBtnW, bw); sp_mine_idle_delay = d.seconds;
saveSettingsPageState(app->settings());
if (TactileButton(TR("settings_address_book"), ImVec2(bw, 0), S.resolveFont("button"))) }
AddressBookDialog::show(); if (selected) ImGui::SetItemDefaultFocus();
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_address_book")); }
ImGui::SameLine(0, btnSpacing); ImGui::EndCombo();
if (TactileButton(TR("settings_validate_address"), ImVec2(bw, 0), S.resolveFont("button"))) }
ValidateAddressDialog::show(); if (ImGui::IsItemHovered())
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_validate")); ImGui::SetTooltip("%s", TR("tt_idle_delay"));
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)); // Bottom row — Keys & Data left-aligned, Setup Wizard right-aligned
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* r1[] = {TR("settings_import_key"), TR("settings_export_key"), TR("settings_export_all"), TR("settings_backup"), TR("settings_export_csv")};
const char* t1[] = { const char* t1[] = {
@@ -1213,12 +1165,13 @@ void RenderSettingsPage(App* app) {
float sp = Layout::spacingSm(); float sp = Layout::spacingSm();
ImFont* btnFont = S.resolveFont("button"); ImFont* btnFont = S.resolveFont("button");
// Measure natural widths
float btnPadX = btnPad * 2; float btnPadX = btnPad * 2;
float naturalW = 0; float naturalW = 0;
for (int i = 0; i < 5; i++) for (int i = 0; i < 5; i++)
naturalW += ImGui::CalcTextSize(r1[i]).x + btnPadX; naturalW += ImGui::CalcTextSize(r1[i]).x + btnPadX;
float wizW = ImGui::CalcTextSize(wizLabel).x + btnPadX; float wizW = ImGui::CalcTextSize(wizLabel).x + btnPadX;
float totalW = naturalW + wizW + sp * 6; float totalW = naturalW + wizW + sp * 6; // 5 gaps between data btns + 1 gap before wizard
float scale = (totalW > contentW) ? contentW / totalW : 1.0f; float scale = (totalW > contentW) ? contentW / totalW : 1.0f;
if (scale < 1.0f) ImGui::SetWindowFontScale(scale); if (scale < 1.0f) ImGui::SetWindowFontScale(scale);

View File

@@ -153,15 +153,10 @@ void RenderMiningTab(App* app)
s_pool_mode = app->settings()->getPoolMode(); s_pool_mode = app->settings()->getPoolMode();
strncpy(s_pool_url, app->settings()->getPoolUrl().c_str(), sizeof(s_pool_url) - 1); strncpy(s_pool_url, app->settings()->getPoolUrl().c_str(), sizeof(s_pool_url) - 1);
strncpy(s_pool_worker, app->settings()->getPoolWorker().c_str(), sizeof(s_pool_worker) - 1); strncpy(s_pool_worker, app->settings()->getPoolWorker().c_str(), sizeof(s_pool_worker) - 1);
s_pool_state_loaded = true;
}
// Default pool worker to user's first shielded address once addresses are available // If pool worker is empty or placeholder, default to user's first address
{
static bool s_pool_worker_defaulted = false;
std::string workerStr(s_pool_worker); std::string workerStr(s_pool_worker);
if (!s_pool_worker_defaulted && !state.addresses.empty() && if (workerStr.empty() || workerStr == "x") {
(workerStr.empty() || workerStr == "x")) {
std::string defaultAddr; std::string defaultAddr;
for (const auto& addr : state.addresses) { for (const auto& addr : state.addresses) {
if (addr.type == "shielded" && !addr.address.empty()) { if (addr.type == "shielded" && !addr.address.empty()) {
@@ -180,10 +175,9 @@ void RenderMiningTab(App* app)
if (!defaultAddr.empty()) { if (!defaultAddr.empty()) {
strncpy(s_pool_worker, defaultAddr.c_str(), sizeof(s_pool_worker) - 1); strncpy(s_pool_worker, defaultAddr.c_str(), sizeof(s_pool_worker) - 1);
s_pool_worker[sizeof(s_pool_worker) - 1] = '\0'; s_pool_worker[sizeof(s_pool_worker) - 1] = '\0';
s_pool_settings_dirty = true;
} }
s_pool_worker_defaulted = true;
} }
s_pool_state_loaded = true;
} }
// Persist pool settings when dirty and no field is active // Persist pool settings when dirty and no field is active
@@ -872,42 +866,9 @@ void RenderMiningTab(App* app)
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("%s", idleOn ? TR("mining_idle_on_tooltip") : TR("mining_idle_off_tooltip")); ImGui::SetTooltip("%s", idleOn ? TR("mining_idle_on_tooltip") : TR("mining_idle_off_tooltip"));
} }
ImGui::SetCursorScreenPos(savedCur);
idleRightEdge = btnX - 4.0f * dp; idleRightEdge = btnX - 4.0f * dp;
// Idle delay combo (to the left of the icon when enabled)
if (idleOn) {
struct DelayOption { int seconds; const char* label; };
static const DelayOption delays[] = {
{30, "30s"}, {60, "1m"}, {120, "2m"}, {300, "5m"}, {600, "10m"}
};
int curDelay = app->settings()->getMineIdleDelay();
const char* previewLabel = "2m";
for (const auto& d : delays) {
if (d.seconds == curDelay) { previewLabel = d.label; break; }
}
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f);
float comboX = idleRightEdge - comboW;
float comboY = curY + (headerH - ImGui::GetFrameHeight()) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(comboX, comboY));
ImGui::SetNextItemWidth(comboW);
if (ImGui::BeginCombo("##IdleDelay", previewLabel, ImGuiComboFlags_NoArrowButton)) {
for (const auto& d : delays) {
bool selected = (d.seconds == curDelay);
if (ImGui::Selectable(d.label, selected)) {
app->settings()->setMineIdleDelay(d.seconds);
app->settings()->save();
}
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", TR("tt_idle_delay"));
idleRightEdge = comboX - 4.0f * dp;
}
ImGui::SetCursorScreenPos(savedCur);
} }
// Active mining indicator (left of idle toggle) // Active mining indicator (left of idle toggle)

View File

@@ -174,9 +174,6 @@ void I18n::loadBuiltinEnglish()
// Settings sections // Settings sections
strings_["appearance"] = "APPEARANCE"; strings_["appearance"] = "APPEARANCE";
strings_["theme_language"] = "THEME & LANGUAGE";
strings_["advanced_effects"] = "Advanced Effects...";
strings_["tools_actions"] = "Tools & Actions...";
strings_["wallet"] = "WALLET"; strings_["wallet"] = "WALLET";
strings_["node_security"] = "NODE & SECURITY"; strings_["node_security"] = "NODE & SECURITY";
strings_["node"] = "NODE"; strings_["node"] = "NODE";