feat(settings): rebuild Node & Security as one fill-width card

Replace the column/grid/masonry machinery (which kept leaving empty gaps and
buried the daemon Advanced actions) with a single full-width card whose controls
are arranged horizontally to use the width:
- NODE: Data Dir + Wallet Size + Open-folder on one row.
- RPC: Host / Port / Username / Password four-across when wide (>=700), wrapping
  to 2x2 when narrow; auto-detected + plaintext warning kept.
- SECURITY: encryption buttons + Auto-lock combo + PIN controls on horizontal
  rows (factored into renderSecuritySection so lite gets auto-lock+PIN and
  full-node adds the RPC-backed encrypt/lock/remove block — same gating as before).
- DAEMON BINARY: inline Installed/Bundled/status; Check/Refresh/Test row; and the
  four maintenance actions (Install bundled / Rescan / Delete blockchain / Repair)
  behind a now-prominent, clearly-clickable "Advanced ▾" toggle instead of the
  easy-to-miss faint overline — fixing the "daemon options missing" report.

Lite keeps its existing wallet-lifecycle/backup/security block, full width.
Removed useGrid/stacked/column clips/dividers. Every control + state branch +
confirm flag + tooltip preserved (verified). Full-node + lite build clean; ctest
green; hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-02 01:28:14 -05:00
parent fc5ce019e2
commit 5180f74a68

View File

@@ -1508,46 +1508,142 @@ void RenderSettingsPage(App* app) {
return std::max(minBtnW, maxTextW + btnPad * 2);
};
// --- NODE + SECURITY: 2x2 grid on wide full-node windows, stacked otherwise ---
// --- NODE & SECURITY: one full-width card, controls laid out horizontally ---
// A single vertical flow of sub-sections (Node, RPC, Security, Daemon binary),
// each with its overline header and its controls flowing left-to-right to fill
// the card width. No 2x2 grid, no column clips, no masonry placement.
{
// Wide full-node: a 2x2 grid fills the card width with no empty gap —
// TOP-LEFT = NODE/Data, TOP-RIGHT = RPC,
// BOTTOM-LEFT = SECURITY, BOTTOM-RIGHT = DAEMON BINARY head,
// with the Advanced destructive buttons dropping to a full-width strip below.
// Narrow windows (or lite, which has no daemon) fall through to the original
// single full-width column: Node -> RPC -> Security -> Daemon Binary stack.
// `stacked` still drives the existing vertical-flow path below.
float gridBP = S.drawElement("components.settings-page", "node-grid-breakpoint").sizeOr(900.0f);
bool useGrid = app->supportsFullNodeLifecycleActions() && (availWidth >= gridBP);
bool stacked = !useGrid;
float gutter = Layout::spacingLg();
float leftColW = useGrid ? std::floor((contentW - gutter) * 0.5f) : contentW;
float rightColW = useGrid ? (contentW - gutter - leftColW) : contentW;
ImVec2 sectionOrigin = ImGui::GetCursorScreenPos();
float leftX = sectionOrigin.x;
float rightX = useGrid ? (sectionOrigin.x + leftColW + gutter) : sectionOrigin.x;
// Grid path: NODE (left) and RPC (right) live in separate clips, so their
// bottoms are captured inside the content block below and read out afterward.
float leftBottomGrid = sectionOrigin.y;
float rpcBottomGrid = sectionOrigin.y;
// The lite WALLET block below still positions its rows against a "left"
// column origin/width; in the single-column layout that is simply the full
// content column at the section origin.
const float leftX = sectionOrigin.x;
const float leftColW = contentW;
// ---- LEFT COLUMN: NODE ----
ImGui::SetCursorScreenPos(ImVec2(leftX, sectionOrigin.y));
ImGui::PushClipRect(ImVec2(leftX, sectionOrigin.y),
ImVec2(leftX + leftColW, sectionOrigin.y + 9999), true);
ImFont* body2Info = S.resolveFont("body2");
if (!body2Info) body2Info = Type().body2();
// Lite has no daemon — this column holds wallet lifecycle/encryption, so label
// it "WALLET" rather than "NODE".
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(),
app->isLiteBuild() ? TR("wallet") : TR("node"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// Shared SECURITY controls, rendered full-width with horizontal rows. Used by
// both the lite branch (auto-lock + PIN only) and the full-node branch (which
// also gets the RPC-backed encrypt/change/lock/remove block, includeRpcEncrypt).
// `secX` is the row start X (== content-column left); `secColW` its width.
auto renderSecuritySection = [&](float secX, float secColW, bool includeRpcEncrypt) {
// Encrypt / Change passphrase / Lock / Remove — RPC-backed, full-node only.
// In lite the wallet's encryption controls live in the WALLET block above.
if (includeRpcEncrypt) {
bool isEncrypted = app->state().isEncrypted();
bool isLocked = app->state().isLocked();
{
ImFont* body2Info = S.resolveFont("body2");
if (!body2Info) body2Info = Type().body2();
float secBtnW = std::min(rowBtnW({TR("settings_encrypt_wallet"), TR("settings_change_passphrase"), TR("settings_lock_now")}), (secColW - Layout::spacingMd()) * 0.5f);
ImGui::SetCursorScreenPos(ImVec2(secX, ImGui::GetCursorScreenPos().y));
if (!isEncrypted) {
if (TactileButton(TR("settings_encrypt_wallet"), ImVec2(secBtnW, 0), S.resolveFont("button")))
app->showEncryptDialog();
if (ImGui::IsItemHovered()) material::Tooltip("%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()) material::Tooltip("%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()) material::Tooltip("%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 — trails the row.
ImGui::SameLine(0, Layout::spacingMd());
if (TactileButton(TR("settings_remove_encryption"), ImVec2(secBtnW, 0), S.resolveFont("button")))
app->showDecryptDialog();
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_remove_encrypt"));
}
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
}
// Auto-Lock timeout + PIN controls on one horizontal row.
{
float comboW = S.drawElement("components.settings-page", "security-combo-width").sizeOr(120.0f);
int timeout = app->settings()->getAutoLockTimeout();
const char* timeoutLabels[] = { TR("timeout_off"), TR("timeout_1min"), TR("timeout_5min"), TR("timeout_15min"), TR("timeout_30min"), TR("timeout_1hour") };
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(secX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("settings_auto_lock"));
ImGui::SameLine(0, Layout::spacingXs());
ImGui::PushItemWidth(comboW);
if (ImGui::Combo("##autolock", &selTimeout, timeoutLabels, 6)) {
app->settings()->setAutoLockTimeout(timeoutValues[selTimeout]);
app->settings()->save();
}
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_auto_lock"));
ImGui::PopItemWidth();
// PIN unlock controls, trailing the auto-lock combo on the same row.
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")}), (secColW - Layout::spacingSm()) * 0.5f);
ImGui::SameLine(0, Layout::spacingMd());
if (!hasPIN) {
if (TactileButton(TR("settings_set_pin"), ImVec2(pinBtnW, 0), S.resolveFont("button")))
app->showPinSetupDialog();
if (ImGui::IsItemHovered()) material::Tooltip("%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()) material::Tooltip("%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()) material::Tooltip("%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::SameLine(0, Layout::spacingMd());
ImGui::AlignTextToFramePadding();
ImGui::TextColored(ImVec4(1,1,1,0.3f), "%s", TR("settings_encrypt_first_pin"));
}
}
};
if (app->isLiteBuild()) {
// ============================ LITE ============================
// Lite has no daemon: this card holds wallet lifecycle/encryption
// (the "WALLET" block) followed by the shared SECURITY block, both
// rendered full-width in a single column exactly as before.
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("wallet"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
{
ImGui::PushFont(body2Info);
if (app->isLiteBuild()) {
{
float liteLabelW = std::min(leftColW * 0.35f, 132.0f);
float liteInputW = std::max(80.0f, leftColW - liteLabelW - Layout::spacingSm());
@@ -1889,40 +1985,65 @@ void RenderSettingsPage(App* app) {
sodium_memzero(s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass));
sodium_memzero(s_settingsState.lite_dec_pass, sizeof(s_settingsState.lite_dec_pass));
}
} else {
} // lite WALLET block
ImGui::PopFont();
} // lite WALLET push-font scope
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));
// ---- SECURITY (lite): auto-lock + PIN only ----
// The wallet's passphrase encryption controls live in the WALLET block
// above (lite backend), so this section shows only the shared auto-lock
// timeout + PIN controls.
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
renderSecuritySection(sectionOrigin.x, contentW, /*includeRpcEncrypt=*/false);
std::string wallet_path = util::Platform::getDragonXDataDir() + "wallet.dat";
uint64_t wallet_size = util::Platform::getFileSize(wallet_path);
// Advance to the true bottom of the single column.
ImGui::SetCursorScreenPos(ImVec2(sectionOrigin.x, ImGui::GetCursorScreenPos().y));
// Data Dir + Wallet Size — same line with "Label: value" format
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
} else {
// ========================= FULL NODE =========================
// One full-width column; each sub-section's controls flow left-to-right.
ImGui::PushFont(body2Info);
const float spMd = Layout::spacingMd();
// -------------------- NODE / DATA --------------------
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("node"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
{
// 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();
std::string dirPath = util::Platform::getDragonXDataDir();
std::string walletPath = dirPath + "wallet.dat";
uint64_t wallet_size = util::Platform::getFileSize(walletPath);
std::string size_str = (wallet_size > 0)
? util::Platform::formatFileSize(wallet_size) : TR("settings_not_found");
// Wallet Size now lives on its own row below (so the grid's left cell
// has no mid-row gap), so the path gets the full column width.
float dataDirLabelW = ImGui::CalcTextSize(TR("settings_data_dir")).x + Layout::spacingXs();
float availForPath = leftColW - dataDirLabelW;
// Right-aligned "Open data folder" button trailing the row; the path
// gets whatever width is left after the data-dir label, wallet-size
// label+value, and the trailing button.
ImFont* nodeBtnFont = S.resolveFont("button");
float openBtnW = ImGui::CalcTextSize(TR("settings_open_data_dir")).x +
ImGui::GetStyle().FramePadding.x * 2.0f + btnPad;
float dataDirLabelW = ImGui::CalcTextSize(TR("settings_data_dir")).x;
float sizeLabelW = ImGui::CalcTextSize(TR("settings_wallet_size_label")).x;
float sizeValueW = ImGui::CalcTextSize(size_str.c_str()).x;
// Space reserved to the right of the path for the size block + button.
float trailW = sizeLabelW + Layout::spacingXs() + sizeValueW + spMd + openBtnW;
float availForPath = contentW - dataDirLabelW - Layout::spacingXs() - spMd - trailW;
if (availForPath < 60.0f) availForPath = contentW - dataDirLabelW - Layout::spacingXs();
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);
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(TR("settings_data_dir"));
ImGui::SameLine(0, Layout::spacingXs());
ImVec2 pathSize = ImGui::CalcTextSize(dirPath.c_str());
bool hovered = false;
ImGui::AlignTextToFramePadding();
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) {
@@ -1947,474 +2068,288 @@ void RenderSettingsPage(App* app) {
if (ImGui::IsItemClicked())
util::Platform::openFolder(dirPath);
// Wallet Size on its own row (no SameLine) so the left grid cell has
// no mid-row gap; reads fine full-width too.
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
// Wallet Size label + value, trailing the path on the same row.
ImGui::SameLine(0, spMd);
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(TR("settings_wallet_size_label"));
ImGui::SameLine(0, Layout::spacingXs());
if (wallet_size > 0) {
ImGui::AlignTextToFramePadding();
if (wallet_size > 0)
ImGui::TextUnformatted(size_str.c_str());
} else {
else
ImGui::TextDisabled("%s", TR("settings_not_found"));
// Open-data-folder button: right-aligned when there is room, else trailing.
float rowY = ImGui::GetCursorScreenPos().y;
float rightEdge = sectionOrigin.x + contentW;
float actualOpenW = ImGui::CalcTextSize(TR("settings_open_data_dir")).x +
ImGui::GetStyle().FramePadding.x * 2.0f;
float openX = rightEdge - actualOpenW;
float afterSizeX = ImGui::GetItemRectMax().x + spMd;
ImGui::SameLine(0, spMd);
if (openX > afterSizeX)
ImGui::SetCursorScreenPos(ImVec2(openX, rowY));
if (TactileButton(TR("settings_open_data_dir"), ImVec2(0, 0), nodeBtnFont)) {
util::Platform::openFolder(dirPath);
}
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_open_data_dir"));
}
// -------------------- RPC --------------------
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("rpc_connection"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
{
// Four fields (Host, Port, Username, Password) across one row when the
// card is wide enough; otherwise 2x2. Each field is "label [input]".
const char* hostLbl = TR("rpc_host");
const char* portLbl = TR("rpc_port");
const char* userLbl = TR("rpc_user");
const char* passLbl = TR("rpc_pass");
float labelsW = ImGui::CalcTextSize(hostLbl).x + ImGui::CalcTextSize(portLbl).x +
ImGui::CalcTextSize(userLbl).x + ImGui::CalcTextSize(passLbl).x;
const bool fourAcross = contentW >= 700.0f;
auto field = [&](const char* label, const char* id, char* buf, size_t bufSz,
float inputW, bool password) {
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(label);
ImGui::SameLine(0, Layout::spacingXs());
ImGui::SetNextItemWidth(inputW);
ImGui::InputText(id, buf, bufSz,
password ? ImGuiInputTextFlags_Password : 0);
};
if (fourAcross) {
// fieldW = (contentW - labels - per-field label gaps - 3 inter-field gaps) / 4
float inputTotal = contentW - labelsW - Layout::spacingXs() * 4 - spMd * 3;
float inputW = std::max(60.0f, std::floor(inputTotal / 4.0f));
field(hostLbl, "##RPCHost", s_settingsState.rpc_host, sizeof(s_settingsState.rpc_host), inputW, false);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_host"));
ImGui::SameLine(0, spMd);
field(portLbl, "##RPCPort", s_settingsState.rpc_port, sizeof(s_settingsState.rpc_port), inputW, false);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_port"));
ImGui::SameLine(0, spMd);
field(userLbl, "##RPCUser", s_settingsState.rpc_user, sizeof(s_settingsState.rpc_user), inputW, false);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_user"));
ImGui::SameLine(0, spMd);
field(passLbl, "##RPCPassword", s_settingsState.rpc_password, sizeof(s_settingsState.rpc_password), inputW, true);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_pass"));
} else {
// 2x2: two fields per row.
float halfLabelsW = ImGui::CalcTextSize(hostLbl).x + ImGui::CalcTextSize(userLbl).x;
float inputW = std::max(60.0f, std::floor(
(contentW - halfLabelsW - Layout::spacingXs() * 2 - spMd) / 2.0f));
field(hostLbl, "##RPCHost", s_settingsState.rpc_host, sizeof(s_settingsState.rpc_host), inputW, false);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_host"));
ImGui::SameLine(0, spMd);
field(userLbl, "##RPCUser", s_settingsState.rpc_user, sizeof(s_settingsState.rpc_user), inputW, false);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_user"));
field(portLbl, "##RPCPort", s_settingsState.rpc_port, sizeof(s_settingsState.rpc_port), inputW, false);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_port"));
ImGui::SameLine(0, spMd);
field(passLbl, "##RPCPassword", s_settingsState.rpc_password, sizeof(s_settingsState.rpc_password), inputW, true);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_pass"));
}
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("settings_auto_detected"));
if (s_settingsState.rpc_plaintext_remote) {
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning()));
ImGui::PushTextWrapPos(sectionOrigin.x + contentW);
ImGui::TextWrapped("Remote RPC is using plaintext HTTP. Add rpctls=1 to DRAGONX.conf if your daemon supports TLS.");
ImGui::PopTextWrapPos();
ImGui::PopStyleColor();
}
}
// Explicit button to open the wallet + blockchain data folder in the OS file manager.
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
if (TactileButton(TR("settings_open_data_dir"), ImVec2(0, 0), S.resolveFont("button"))) {
util::Platform::openFolder(util::Platform::getDragonXDataDir());
}
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_open_data_dir"));
// When gridded, RPC moves into the TOP-RIGHT cell: close the LEFT clip here
// and open the RIGHT cell rebased to rightX/rightColW. When stacked, RPC
// stays inline in the left column exactly as before.
if (useGrid) {
ImGui::PopFont(); // body2 re-pushed below for the right cell
leftBottomGrid = ImGui::GetCursorScreenPos().y;
ImGui::PopClipRect();
float rpcTopY = sectionOrigin.y;
ImGui::SetCursorScreenPos(ImVec2(rightX, rpcTopY));
ImGui::PushClipRect(ImVec2(rightX, rpcTopY),
ImVec2(rightX + rightColW, rpcTopY + 9999), true);
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("rpc_connection"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImGui::PushFont(body2Info);
}
// RPC cell geometry: rebased to the right cell when gridded, else the left.
const float rpcColX = useGrid ? rightX : leftX;
const float rpcColW = useGrid ? rightColW : leftColW;
// -------------------- SECURITY --------------------
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("security"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
renderSecuritySection(sectionOrigin.x, contentW, /*includeRpcEncrypt=*/true);
// RPC connection — two columns: (Host | Username) and (Port | Password)
float rpcHalfW = (rpcColW - Layout::spacingMd()) * 0.5f;
float rpcHalfLblW = std::min(rpcLblW, rpcHalfW * 0.4f);
float rpcHalfInputW = std::max(40.0f, rpcHalfW - rpcHalfLblW);
float rpcRightColX = rpcColX + rpcHalfW + Layout::spacingMd();
// -------------------- DAEMON BINARY --------------------
if (app->supportsFullNodeLifecycleActions()) {
ImFont* dbBtnFont = S.resolveFont("button");
if (!s_settingsState.daemon_info_loaded) {
s_settingsState.installed_daemon = dragonx::resources::getInstalledDaemonInfo();
s_settingsState.bundled_daemon = dragonx::resources::getBundledDaemonInfo();
s_settingsState.daemon_info_loaded = true;
}
const auto& inst = s_settingsState.installed_daemon;
const auto& bun = s_settingsState.bundled_daemon;
// Row 1: RPC Host + Username
float row1Y = ImGui::GetCursorScreenPos().y;
ImGui::SetCursorScreenPos(ImVec2(rpcColX, row1Y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(TR("rpc_host"));
ImGui::SameLine(rpcColX - sectionOrigin.x + rpcHalfLblW);
ImGui::SetNextItemWidth(rpcHalfInputW);
ImGui::InputText("##RPCHost", s_settingsState.rpc_host, sizeof(s_settingsState.rpc_host));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_host"));
auto fmtDate = [](std::int64_t epoch) -> std::string {
if (epoch <= 0) return "";
std::time_t t = static_cast<std::time_t>(epoch);
std::tm tmv{};
#ifdef _WIN32
localtime_s(&tmv, &t);
#else
localtime_r(&t, &tmv);
#endif
char buf[32];
std::strftime(buf, sizeof(buf), "%Y-%m-%d", &tmv);
return std::string(buf);
};
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", s_settingsState.rpc_user, sizeof(s_settingsState.rpc_user));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_user"));
const ImVec4 dbDim = ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium());
ImGui::SetCursorScreenPos(ImVec2(rpcColX, std::max(afterRow1Y, ImGui::GetCursorScreenPos().y)));
ImGui::Dummy(ImVec2(0, Layout::spacingLg()));
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("daemon_binary"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// Row 2: Port + Password
float row2Y = ImGui::GetCursorScreenPos().y;
ImGui::SetCursorScreenPos(ImVec2(rpcColX, row2Y));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(TR("rpc_port"));
ImGui::SameLine(rpcColX - sectionOrigin.x + rpcHalfLblW);
ImGui::SetNextItemWidth(rpcHalfInputW);
ImGui::InputText("##RPCPort", s_settingsState.rpc_port, sizeof(s_settingsState.rpc_port));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_port"));
// Inline single line: Installed <v> (<size> · <date>) · Bundled <v> (<size>) · <status>
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(TR("daemon_installed"));
ImGui::SameLine(0, Layout::spacingXs());
if (inst.exists) {
ImGui::TextUnformatted(inst.version.empty() ? TR("unknown") : inst.version.c_str());
ImGui::SameLine(0, Layout::spacingXs());
ImGui::TextColored(dbDim, "(%s · %s)",
util::Platform::formatFileSize(inst.size).c_str(),
fmtDate(inst.modifiedEpoch).c_str());
} else {
ImGui::TextColored(dbDim, "%s", TR("daemon_not_installed"));
}
ImGui::SameLine(0, Layout::spacingXs());
ImGui::TextColored(dbDim, "·");
ImGui::SameLine(0, Layout::spacingXs());
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(TR("daemon_bundled"));
ImGui::SameLine(0, Layout::spacingXs());
if (bun.available) {
ImGui::TextUnformatted(bun.version.empty() ? TR("unknown") : bun.version.c_str());
ImGui::SameLine(0, Layout::spacingXs());
ImGui::TextColored(dbDim, "(%s)", util::Platform::formatFileSize(bun.size).c_str());
} else {
ImGui::TextColored(dbDim, "%s", TR("daemon_none_bundled"));
}
if (bun.available) {
const bool sameSize = inst.exists && inst.size == bun.size;
ImGui::SameLine(0, Layout::spacingXs());
ImGui::TextColored(dbDim, "·");
ImGui::SameLine(0, Layout::spacingXs());
if (!inst.exists)
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("daemon_status_missing"));
else if (sameSize)
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("daemon_status_match"));
else
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("daemon_status_differ"));
}
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", s_settingsState.rpc_password, sizeof(s_settingsState.rpc_password),
ImGuiInputTextFlags_Password);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_pass"));
// In-app node update: download + verify the latest dragonxd from the project Gitea.
// Refresh the cached daemon info once an install has completed.
if (ui::DaemonUpdateDialog::consumeInstalled())
s_settingsState.daemon_info_loaded = false;
ImGui::SetCursorScreenPos(ImVec2(rpcColX, std::max(afterRow2Y, ImGui::GetCursorScreenPos().y)));
// Common action row: Check for updates | Refresh | Test connection.
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
if (TactileButton(TR("daemon_update_check"), ImVec2(0, 0), dbBtnFont)) {
ui::DaemonUpdateDialog::show(app, inst.exists ? inst.version : std::string());
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_daemon_update_check"));
ImGui::SameLine(0, spMd);
if (TactileButton(TR("refresh"), ImVec2(0, 0), dbBtnFont)) {
s_settingsState.daemon_info_loaded = false;
}
ImGui::SameLine(0, spMd);
ImGui::BeginDisabled(!app->isConnected());
if (TactileButton(TR("test_connection"), ImVec2(0, 0), dbBtnFont)) {
if (app->rpc() && app->rpc()->isConnected() && app->worker()) {
app->worker()->post([rpc = app->rpc()]() -> rpc::RPCWorker::MainCb {
try {
rpc::RPCClient::TraceScope trace("Settings / Test connection");
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)) material::Tooltip("%s", TR("tt_test_conn"));
ImGui::EndDisabled();
ImGui::SetCursorScreenPos(ImVec2(rpcColX, ImGui::GetCursorScreenPos().y));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
TR("settings_auto_detected"));
if (s_settingsState.rpc_plaintext_remote) {
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning()));
ImGui::PushTextWrapPos(rpcColX + rpcColW);
ImGui::TextWrapped("Remote RPC is using plaintext HTTP. Add rpctls=1 to DRAGONX.conf if your daemon supports TLS.");
ImGui::PopTextWrapPos();
ImGui::PopStyleColor();
}
// The FOUR destructive maintenance actions, in one horizontal row prefixed
// by a visible "Advanced" toggle. Every BeginDisabled/EndDisabled predicate
// is preserved exactly.
auto renderDaemonAdvancedButtons = [&]() {
ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon() || !bun.available);
if (TactileButton(TR("daemon_install_bundled"), ImVec2(0, 0), dbBtnFont)) {
s_settingsState.confirm_reinstall_daemon = true;
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_daemon_install_bundled"));
ImGui::EndDisabled();
ImGui::SameLine(0, spMd);
ImGui::BeginDisabled(!app->isConnected());
if (TactileButton(TR("rescan"), ImVec2(0, 0), dbBtnFont)) {
s_settingsState.confirm_rescan = true;
// Re-probe the available block range each time the dialog is opened.
s_settingsState.rescan_height_detecting = false;
s_settingsState.rescan_height_detected = false;
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_rescan"));
ImGui::EndDisabled();
ImGui::SameLine(0, spMd);
ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon());
if (TactileButton(TR("delete_blockchain"), ImVec2(0, 0), dbBtnFont)) {
s_settingsState.confirm_delete_blockchain = true;
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_delete_blockchain"));
ImGui::SameLine(0, spMd);
if (TactileButton(TR("repair_wallet"), ImVec2(0, 0), dbBtnFont)) {
s_settingsState.confirm_repair_wallet = true;
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_repair_wallet"));
ImGui::EndDisabled();
};
if (useGrid) {
rpcBottomGrid = ImGui::GetCursorScreenPos().y;
}
// Advanced — a clearly-clickable toggle (label + arrow) on its own line,
// controlling the rare + destructive daemon maintenance actions. When
// expanded the four buttons render on one horizontal row: inline with the
// toggle when there is room, otherwise wrapped to the next full-width row.
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
{
char advBuf[64];
const char* advArrow = s_settingsState.node_advanced_expanded
? ICON_MD_EXPAND_LESS : ICON_MD_EXPAND_MORE;
snprintf(advBuf, sizeof(advBuf), "%s %s##NodeAdvancedToggle",
TR("advanced"), advArrow);
// Width of the four Advanced buttons + gaps, to decide inline vs. wrap.
float advBtnsW = spMd * 3;
const char* advLabels[] = { TR("daemon_install_bundled"), TR("rescan"),
TR("delete_blockchain"), TR("repair_wallet") };
for (const char* l : advLabels)
advBtnsW += ImGui::CalcTextSize(l).x + ImGui::GetStyle().FramePadding.x * 2.0f;
float advToggleW = ImGui::CalcTextSize(advBuf).x +
ImGui::GetStyle().FramePadding.x * 2.0f;
if (TactileButton(advBuf, ImVec2(0, 0), dbBtnFont)) {
s_settingsState.node_advanced_expanded = !s_settingsState.node_advanced_expanded;
}
if (s_settingsState.node_advanced_expanded) {
const bool inline_ = (advToggleW + spMd + advBtnsW) <= contentW;
if (inline_) {
ImGui::SameLine(0, spMd);
} else {
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImGui::SetCursorScreenPos(ImVec2(sectionOrigin.x, ImGui::GetCursorScreenPos().y));
}
renderDaemonAdvancedButtons();
}
}
}
ImGui::PopFont();
// Advance to the true bottom of the single column.
ImGui::SetCursorScreenPos(ImVec2(sectionOrigin.x, ImGui::GetCursorScreenPos().y));
}
float leftBottom = useGrid ? leftBottomGrid : ImGui::GetCursorScreenPos().y;
float rpcBottom = useGrid ? rpcBottomGrid : leftBottom;
ImGui::PopClipRect();
// Grid: the two top cells share a row bottom; the bottom cells hang below it.
float row1Bottom = std::max(leftBottom, rpcBottom);
// ---- SECURITY ----
// Grid: BOTTOM-LEFT cell, under NODE. Stacked: flows below the left column.
// Rebased to the LEFT column (X=leftX, width=leftColW) when gridded.
const float secX = useGrid ? leftX : rightX;
const float secColW = useGrid ? leftColW : rightColW;
float rightTopY = useGrid ? (row1Bottom + Layout::spacingMd())
: (stacked ? (leftBottom + Layout::spacingMd()) : sectionOrigin.y);
ImGui::SetCursorScreenPos(ImVec2(secX, rightTopY));
ImGui::PushClipRect(ImVec2(secX, rightTopY),
ImVec2(secX + secColW, rightTopY + 9999), true);
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("security"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
{
// Encrypt / Change passphrase / Lock / Remove — RPC-backed, so full-node
// only. In lite the wallet's encryption controls live in the NODE column
// above (via the lite backend), so this duplicate is hidden.
if (!app->isLiteBuild()) {
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")}), (secColW - Layout::spacingMd()) * 0.5f);
ImGui::SetCursorScreenPos(ImVec2(secX, ImGui::GetCursorScreenPos().y));
if (!isEncrypted) {
if (TactileButton(TR("settings_encrypt_wallet"), ImVec2(secBtnW, 0), S.resolveFont("button")))
app->showEncryptDialog();
if (ImGui::IsItemHovered()) material::Tooltip("%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()) material::Tooltip("%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()) material::Tooltip("%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(secX, ImGui::GetCursorScreenPos().y + Layout::spacingXs()));
if (TactileButton(TR("settings_remove_encryption"), ImVec2(secBtnW, 0), S.resolveFont("button")))
app->showDecryptDialog();
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_remove_encrypt"));
}
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
} // !isLiteBuild — RPC-backed encrypt/change/lock/remove block
// Auto-Lock timeout
{
float comboW = S.drawElement("components.settings-page", "security-combo-width").sizeOr(120.0f);
int timeout = app->settings()->getAutoLockTimeout();
const char* timeoutLabels[] = { TR("timeout_off"), TR("timeout_1min"), TR("timeout_5min"), TR("timeout_15min"), TR("timeout_30min"), TR("timeout_1hour") };
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(secX, ImGui::GetCursorScreenPos().y));
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("settings_auto_lock"));
ImGui::SetCursorScreenPos(ImVec2(secX, ImGui::GetCursorScreenPos().y));
ImGui::PushItemWidth(comboW);
if (ImGui::Combo("##autolock", &selTimeout, timeoutLabels, 6)) {
app->settings()->setAutoLockTimeout(timeoutValues[selTimeout]);
app->settings()->save();
}
if (ImGui::IsItemHovered()) material::Tooltip("%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")}), (secColW - Layout::spacingSm()) * 0.5f);
ImGui::SetCursorScreenPos(ImVec2(secX, ImGui::GetCursorScreenPos().y));
if (!hasPIN) {
if (TactileButton(TR("settings_set_pin"), ImVec2(pinBtnW, 0), S.resolveFont("button")))
app->showPinSetupDialog();
if (ImGui::IsItemHovered()) material::Tooltip("%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()) material::Tooltip("%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()) material::Tooltip("%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(secX, ImGui::GetCursorScreenPos().y));
ImGui::TextColored(ImVec4(1,1,1,0.3f), "%s", TR("settings_encrypt_first_pin"));
}
}
float rightBottom = ImGui::GetCursorScreenPos().y;
float secBottom = rightBottom;
ImGui::PopClipRect();
// ---- DAEMON BINARY ----
// Grid: BOTTOM-RIGHT cell (under RPC), sharing the bottom-row top with SECURITY.
// Stacked (narrow): spans full width below everything. (The old masonry pick —
// dropping it into the shorter column — only applied to the abandoned 2-col path.)
float dbBottom = 0.0f;
float gridMaxBottom = row1Bottom; // grid path: set in the daemon block below
if (app->supportsFullNodeLifecycleActions()) {
const bool dbFull = useGrid ? false : stacked;
const bool dbLeftCol = leftBottom <= rightBottom;
const float dbColW = useGrid ? rightColW
: (dbFull ? contentW : (dbLeftCol ? leftColW : rightColW));
const float dbTopY = useGrid ? (row1Bottom + Layout::spacingMd())
: (dbFull ? std::max(leftBottom, rightBottom)
: (dbLeftCol ? leftBottom : rightBottom));
const float dbLeftX = useGrid ? rightX
: (dbFull ? sectionOrigin.x : (dbLeftCol ? leftX : rightX));
ImGui::SetCursorScreenPos(ImVec2(dbLeftX, dbTopY));
ImGui::PushClipRect(ImVec2(dbLeftX, dbTopY),
ImVec2(dbLeftX + dbColW, dbTopY + 9999.0f), true);
ImFont* dbBtnFont = S.resolveFont("button");
if (!s_settingsState.daemon_info_loaded) {
s_settingsState.installed_daemon = dragonx::resources::getInstalledDaemonInfo();
s_settingsState.bundled_daemon = dragonx::resources::getBundledDaemonInfo();
s_settingsState.daemon_info_loaded = true;
}
const auto& inst = s_settingsState.installed_daemon;
const auto& bun = s_settingsState.bundled_daemon;
auto fmtDate = [](std::int64_t epoch) -> std::string {
if (epoch <= 0) return "";
std::time_t t = static_cast<std::time_t>(epoch);
std::tm tmv{};
#ifdef _WIN32
localtime_s(&tmv, &t);
#else
localtime_r(&t, &tmv);
#endif
char buf[32];
std::strftime(buf, sizeof(buf), "%Y-%m-%d", &tmv);
return std::string(buf);
};
const ImVec4 dbDim = ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium());
ImGui::Dummy(ImVec2(0, Layout::spacingLg()));
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("daemon_binary"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// Installed | Bundled side by side across the full container width.
const float dbStartY = ImGui::GetCursorScreenPos().y;
const float dbLineH = ImGui::GetTextLineHeightWithSpacing();
const float dbCol2X = dbLeftX + std::min(dbColW * (useGrid ? 0.5f : 0.4f), useGrid ? 200.0f : 340.0f);
ImGui::SetCursorScreenPos(ImVec2(dbLeftX, dbStartY));
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("daemon_installed"));
ImGui::SetCursorScreenPos(ImVec2(dbLeftX, dbStartY + dbLineH));
if (inst.exists) {
ImGui::TextUnformatted(inst.version.empty() ? TR("unknown") : inst.version.c_str());
ImGui::SetCursorScreenPos(ImVec2(dbLeftX, dbStartY + dbLineH * 2));
ImGui::TextColored(dbDim, "%s · %s",
util::Platform::formatFileSize(inst.size).c_str(),
fmtDate(inst.modifiedEpoch).c_str());
} else {
ImGui::TextColored(dbDim, "%s", TR("daemon_not_installed"));
}
ImGui::SetCursorScreenPos(ImVec2(dbCol2X, dbStartY));
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("daemon_bundled"));
ImGui::SetCursorScreenPos(ImVec2(dbCol2X, dbStartY + dbLineH));
if (bun.available) {
ImGui::TextUnformatted(bun.version.empty() ? TR("unknown") : bun.version.c_str());
ImGui::SetCursorScreenPos(ImVec2(dbCol2X, dbStartY + dbLineH * 2));
ImGui::TextColored(dbDim, "%s", util::Platform::formatFileSize(bun.size).c_str());
} else {
ImGui::TextColored(dbDim, "%s", TR("daemon_none_bundled"));
}
ImGui::SetCursorScreenPos(ImVec2(dbLeftX, dbStartY + dbLineH * 3 + Layout::spacingXs()));
if (bun.available) {
const bool sameSize = inst.exists && inst.size == bun.size;
if (!inst.exists)
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("daemon_status_missing"));
else if (sameSize)
ImGui::TextColored(ImVec4(0.3f, 0.8f, 0.3f, 1.0f), "%s", TR("daemon_status_match"));
else
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "%s", TR("daemon_status_differ"));
}
// In-app node update: download + verify the latest dragonxd from the project Gitea.
// Refresh the cached daemon info once an install has completed.
if (ui::DaemonUpdateDialog::consumeInstalled())
s_settingsState.daemon_info_loaded = false;
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImGui::SetCursorScreenPos(ImVec2(dbLeftX, ImGui::GetCursorScreenPos().y));
if (TactileButton(TR("daemon_update_check"), ImVec2(0, 0), dbBtnFont)) {
ui::DaemonUpdateDialog::show(app, inst.exists ? inst.version : std::string());
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_daemon_update_check"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// Common actions, always visible: Refresh | Test connection.
ImGui::SetCursorScreenPos(ImVec2(dbLeftX, ImGui::GetCursorScreenPos().y));
if (TactileButton(TR("refresh"), ImVec2(0, 0), dbBtnFont)) {
s_settingsState.daemon_info_loaded = false;
}
ImGui::SameLine(0, Layout::spacingMd());
ImGui::BeginDisabled(!app->isConnected());
if (TactileButton(TR("test_connection"), ImVec2(0, 0), dbBtnFont)) {
if (app->rpc() && app->rpc()->isConnected() && app->worker()) {
app->worker()->post([rpc = app->rpc()]() -> rpc::RPCWorker::MainCb {
try {
rpc::RPCClient::TraceScope trace("Settings / Test connection");
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)) material::Tooltip("%s", TR("tt_test_conn"));
ImGui::EndDisabled();
// The FOUR destructive Advanced buttons, factored so they can render inline
// (stacked) or as a full-width strip (grid). btnW==0 → auto width. Every
// BeginDisabled/EndDisabled predicate is preserved exactly.
auto renderDaemonAdvancedButtons = [&](float btnW) {
ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon() || !bun.available);
if (TactileButton(TR("daemon_install_bundled"), ImVec2(btnW, 0), dbBtnFont)) {
s_settingsState.confirm_reinstall_daemon = true;
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_daemon_install_bundled"));
ImGui::EndDisabled();
ImGui::SameLine(0, Layout::spacingMd());
ImGui::BeginDisabled(!app->isConnected());
if (TactileButton(TR("rescan"), ImVec2(btnW, 0), dbBtnFont)) {
s_settingsState.confirm_rescan = true;
// Re-probe the available block range each time the dialog is opened.
s_settingsState.rescan_height_detecting = false;
s_settingsState.rescan_height_detected = false;
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_rescan"));
ImGui::EndDisabled();
ImGui::SameLine(0, Layout::spacingMd());
ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon());
if (TactileButton(TR("delete_blockchain"), ImVec2(btnW, 0), dbBtnFont)) {
s_settingsState.confirm_delete_blockchain = true;
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_delete_blockchain"));
ImGui::SameLine(0, Layout::spacingMd());
if (TactileButton(TR("repair_wallet"), ImVec2(btnW, 0), dbBtnFont)) {
s_settingsState.confirm_repair_wallet = true;
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_repair_wallet"));
ImGui::EndDisabled();
};
// Advanced — rare + destructive daemon actions behind a collapsible header
// (reuses the DEBUG LOGGING expander idiom).
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
{
ImVec2 advPos = ImGui::GetCursorScreenPos();
const char* advArrow = s_settingsState.node_advanced_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("##NodeAdvancedToggle", ImVec2(dbColW, ImGui::GetFrameHeight()))) {
s_settingsState.node_advanced_expanded = !s_settingsState.node_advanced_expanded;
}
ImGui::PopStyleColor(3);
ImFont* ovFont = Type().overline();
float textY = advPos.y + (ImGui::GetFrameHeight() - ovFont->LegacySize) * 0.5f;
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(advPos.x, textY), OnSurfaceMedium(), TR("advanced"));
ImFont* iconFont = Type().iconSmall();
if (!iconFont) iconFont = ovFont;
float arrowW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, advArrow).x;
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(advPos.x + dbColW - arrowW, textY), OnSurfaceMedium(), advArrow);
}
// Stacked: render the Advanced buttons inline under the header. Grid: they
// drop to a full-width strip below the grid (rendered after the grid closes).
if (!useGrid && s_settingsState.node_advanced_expanded) {
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImGui::SetCursorScreenPos(ImVec2(dbLeftX, ImGui::GetCursorScreenPos().y));
renderDaemonAdvancedButtons(0.0f);
}
dbBottom = ImGui::GetCursorScreenPos().y;
ImGui::PopClipRect();
if (!useGrid) {
if (dbFull || dbLeftCol) leftBottom = std::max(leftBottom, dbBottom);
if (dbFull || !dbLeftCol) rightBottom = std::max(rightBottom, dbBottom);
}
// Grid finalization: the Advanced buttons drop to a full-width strip below
// the 2x2 grid, then thin dividers separate the four cells. Done inside the
// daemon block so renderDaemonAdvancedButtons (which captures bun/dbBtnFont)
// is still in scope. useGrid implies this block ran, so this covers it.
if (useGrid) {
float gridBottom = std::max(secBottom, dbBottom);
float advBottom = gridBottom;
if (s_settingsState.node_advanced_expanded) {
float advTopY = gridBottom + gutter;
ImGui::SetCursorScreenPos(ImVec2(sectionOrigin.x, advTopY));
float advBtnW = std::floor((contentW - 3 * Layout::spacingMd()) / 4.0f);
renderDaemonAdvancedButtons(advBtnW);
advBottom = ImGui::GetCursorScreenPos().y;
}
gridMaxBottom = std::max(gridBottom, advBottom);
// Thin, low-alpha dividers separating the four grid cells.
const ImU32 divCol = OnSurfaceDisabled();
float vDivX = leftX + leftColW + gutter * 0.5f;
dl->AddLine(ImVec2(vDivX, sectionOrigin.y), ImVec2(vDivX, gridBottom), divCol);
dl->AddLine(ImVec2(sectionOrigin.x, row1Bottom),
ImVec2(sectionOrigin.x + contentW, row1Bottom), divCol);
}
}
// Advance the card cursor past the taller cell/column.
float maxBottom = useGrid ? gridMaxBottom : std::max(leftBottom, rightBottom);
ImGui::SetCursorScreenPos(ImVec2(sectionOrigin.x, maxBottom));
}
ImGui::Dummy(ImVec2(0, bottomPad));