feat(settings): rebuild Node data-dir + Daemon binary on the new components

Step 3. Node "Data Dir" is now a clean key/value block: the directory on its own
row as a click-to-open accent link with a copy button, the wallet size below, and
the folder buttons on their own row (icon ActionButtons) — no more path
font-scaling or right-align overflow math. Daemon binary shows Installed / Bundled
as key/value rows plus a Success/Warning status chip (replacing the fragile
atom-by-atom wrapped status line); "Check for updates…" is now the accent Primary
action, and the maintenance actions are a separate row with Delete Blockchain
styled Destructive (Test/Rescan/Repair Secondary). All handlers, disabled-state
predicates and tooltips are preserved; the buttons wrap instead of overflowing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-21 15:53:28 -05:00
parent 95ff9ce9ae
commit fedfe3d60c

View File

@@ -1993,103 +1993,64 @@ void RenderSettingsPage(App* app) {
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("node")); Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("node"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs())); ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
{ {
std::string dirPath = util::Platform::getDragonXDataDir(); const std::string dirPath = util::Platform::getDragonXDataDir();
std::string walletPath = dirPath + "wallet.dat"; const std::string walletPath = dirPath + "wallet.dat";
uint64_t wallet_size = util::Platform::getFileSize(walletPath); const uint64_t wallet_size = util::Platform::getFileSize(walletPath);
std::string size_str = (wallet_size > 0) const std::string size_str = (wallet_size > 0)
? util::Platform::formatFileSize(wallet_size) : TR("settings_not_found"); ? util::Platform::formatFileSize(wallet_size) : std::string(TR("settings_not_found"));
const float leftX = ImGui::GetCursorPosX();
const float labelW = std::max(ImGui::CalcTextSize(TR("settings_data_dir")).x,
ImGui::CalcTextSize(TR("settings_wallet_size_label")).x)
+ Layout::spacingLg();
const ImU32 metaCol = OnSurfaceMedium();
// Right-aligned "Open data folder" button trailing the row; the path // Row 1: Data directory — a clickable link (opens the folder) + a copy button.
// gets whatever width is left after the data-dir label, wallet-size
// label+value, and the trailing button.
ImFont* nodeBtnFont = S.resolveFont("button");
if (!nodeBtnFont) nodeBtnFont = Type().button();
// Measure with the BUTTON font (not the current font) so the width matches
// the real TactileButton size — the old estimate mis-measured and the button
// overflowed the card's right edge, worse at high DPI.
float openBtnW = nodeBtnFont->CalcTextSizeA(nodeBtnFont->LegacySize, FLT_MAX, 0,
TR("settings_open_data_dir")).x + ImGui::GetStyle().FramePadding.x * 2.0f;
// "Open app folder" (the ObsidianDragon config folder), left of the data button.
float appBtnW = nodeBtnFont->CalcTextSizeA(nodeBtnFont->LegacySize, FLT_MAX, 0,
TR("settings_open_app_dir")).x + ImGui::GetStyle().FramePadding.x * 2.0f;
float openGroupW = appBtnW + spMd + openBtnW;
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 + both buttons.
float trailW = sizeLabelW + Layout::spacingXs() + sizeValueW + spMd + openGroupW;
float availForPath = contentW - dataDirLabelW - Layout::spacingXs() - spMd - trailW;
if (availForPath < 60.0f) availForPath = contentW - dataDirLabelW - Layout::spacingXs();
ImVec4 linkCol = ImGui::ColorConvertU32ToFloat4(Primary());
ImVec4 linkHoverCol = linkCol;
linkHoverCol.w = std::min(1.0f, linkCol.w + 0.2f);
float rowTopY = ImGui::GetCursorScreenPos().y; // top of the data-dir row
ImGui::AlignTextToFramePadding(); ImGui::AlignTextToFramePadding();
ImGui::PushStyleColor(ImGuiCol_Text, metaCol);
ImGui::TextUnformatted(TR("settings_data_dir")); ImGui::TextUnformatted(TR("settings_data_dir"));
ImGui::SameLine(0, Layout::spacingXs()); ImGui::PopStyleColor();
ImVec2 pathSize = ImGui::CalcTextSize(dirPath.c_str()); ImGui::SameLine(0, 0);
bool hovered = false; ImGui::SetCursorPosX(leftX + labelW);
ImGui::AlignTextToFramePadding(); ImGui::AlignTextToFramePadding();
if (pathSize.x > availForPath && availForPath > 0) { ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(Primary()), "%s", dirPath.c_str());
float scale = availForPath / pathSize.x; if (ImGui::IsItemHovered()) {
float condensedSize = body2Info->LegacySize * std::max(scale, 0.65f); const ImVec2 tmn = ImGui::GetItemRectMin(), tmx = ImGui::GetItemRectMax();
ImGui::SetWindowFontScale(condensedSize / body2Info->LegacySize); dl->AddLine(ImVec2(tmn.x, tmx.y), ImVec2(tmx.x, tmx.y), Primary());
ImGui::TextColored(linkCol, "%s", dirPath.c_str());
hovered = ImGui::IsItemHovered();
if (hovered) {
ImVec2 tMin = ImGui::GetItemRectMin();
ImVec2 tMax = ImGui::GetItemRectMax();
dl->AddLine(ImVec2(tMin.x, tMax.y), ImVec2(tMax.x, tMax.y), ImGui::GetColorU32(linkHoverCol));
}
ImGui::SetWindowFontScale(1.0f);
} else {
ImGui::TextColored(linkCol, "%s", dirPath.c_str());
hovered = ImGui::IsItemHovered();
if (hovered) {
ImVec2 tMin = ImGui::GetItemRectMin();
ImVec2 tMax = ImGui::GetItemRectMax();
dl->AddLine(ImVec2(tMin.x, tMax.y), ImVec2(tMax.x, tMax.y), ImGui::GetColorU32(linkHoverCol));
}
}
if (hovered) {
material::Tooltip("%s", TR("tt_open_dir"));
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s", TR("tt_open_dir"));
}
if (ImGui::IsItemClicked()) util::Platform::openFolder(dirPath);
ImGui::SameLine(0, Layout::spacingSm());
{
material::IconButtonStyle cs;
cs.hoverBg = material::WithAlpha(OnSurface(), 30);
cs.tooltip = TR("copy");
const float ih = ImGui::GetFrameHeight();
if (material::IconButton("##copydir", ICON_MD_CONTENT_COPY, Type().iconSmall(), ImVec2(ih, ih), cs))
ImGui::SetClipboardText(dirPath.c_str());
} }
if (ImGui::IsItemClicked())
util::Platform::openFolder(dirPath);
// Wallet Size label + value, trailing the path on the same row. // Row 2: Wallet size.
ImGui::SameLine(0, spMd);
ImGui::AlignTextToFramePadding(); ImGui::AlignTextToFramePadding();
ImGui::PushStyleColor(ImGuiCol_Text, metaCol);
ImGui::TextUnformatted(TR("settings_wallet_size_label")); ImGui::TextUnformatted(TR("settings_wallet_size_label"));
ImGui::SameLine(0, Layout::spacingXs()); ImGui::PopStyleColor();
ImGui::SameLine(0, 0);
ImGui::SetCursorPosX(leftX + labelW);
ImGui::AlignTextToFramePadding(); ImGui::AlignTextToFramePadding();
if (wallet_size > 0) if (wallet_size > 0) ImGui::TextUnformatted(size_str.c_str());
ImGui::TextUnformatted(size_str.c_str()); else ImGui::TextDisabled("%s", TR("settings_not_found"));
else
ImGui::TextDisabled("%s", TR("settings_not_found"));
// Folder buttons: [Open app folder] [Open data folder], right-aligned as a // Row 3: folder buttons (their own row so the path gets the full width).
// group on the data-dir row (rowTopY), each an EXPLICIT width so the group's ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// right edge lands exactly on the card edge (no overflow at any DPI). material::ButtonFlow ff(contentW);
float rowY = rowTopY; ff.next(material::ActionButtonWidth(TR("settings_open_app_dir"), ICON_MD_FOLDER));
float rightEdge = sectionOrigin.x + contentW; if (material::ActionButton("##openapp", TR("settings_open_app_dir"), ICON_MD_FOLDER, material::ActionTier::Secondary))
float groupX = rightEdge - openGroupW;
float afterSizeX = ImGui::GetItemRectMax().x + spMd;
ImGui::SameLine(0, spMd);
if (groupX > afterSizeX)
ImGui::SetCursorScreenPos(ImVec2(groupX, rowY));
// App (ObsidianDragon config) folder — opens ~/.config/ObsidianDragon etc.
if (TactileButton(TR("settings_open_app_dir"), ImVec2(appBtnW, 0), nodeBtnFont)) {
util::Platform::openFolder(util::Platform::getObsidianDragonDir()); util::Platform::openFolder(util::Platform::getObsidianDragonDir());
}
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_open_app_dir")); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_open_app_dir"));
ImGui::SameLine(0, spMd); ff.next(material::ActionButtonWidth(TR("settings_open_data_dir"), ICON_MD_FOLDER_OPEN));
if (TactileButton(TR("settings_open_data_dir"), ImVec2(openBtnW, 0), nodeBtnFont)) { if (material::ActionButton("##opendata", TR("settings_open_data_dir"), ICON_MD_FOLDER_OPEN, material::ActionTier::Secondary))
util::Platform::openFolder(dirPath); util::Platform::openFolder(dirPath);
}
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_open_data_dir")); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_open_data_dir"));
} }
@@ -2218,100 +2179,90 @@ void RenderSettingsPage(App* app) {
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("daemon_binary")); Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("daemon_binary"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs())); ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// Status line: Installed <v> (<size> · <date>) · Bundled <v> (<size>) · <status>. // Version info (Installed / Bundled) as key/value rows, then a status chip.
// Built as color-tagged atoms and laid out with manual wrapping — as one long inline const float dLeftX = ImGui::GetCursorPosX();
// row of SameLine() Text calls it overran the content width and clipped its colored const float dLabelW = std::max(ImGui::CalcTextSize(TR("daemon_installed")).x,
// tail at narrow windows / higher font scale (the status was effectively hidden). ImGui::CalcTextSize(TR("daemon_bundled")).x) + Layout::spacingLg();
struct DbAtom { std::string text; ImU32 col; bool sep; }; auto dkv = [&](const char* label, const std::string& value, bool dim) {
std::vector<DbAtom> dbAtoms; ImGui::AlignTextToFramePadding();
const ImU32 dbNorm = ImGui::GetColorU32(ImGuiCol_Text); ImGui::PushStyleColor(ImGuiCol_Text, OnSurfaceMedium());
const ImU32 dbDimU = OnSurfaceMedium(); ImGui::TextUnformatted(label);
ImGui::PopStyleColor();
dbAtoms.push_back({ TR("daemon_installed"), dbNorm, false }); ImGui::SameLine(0, 0);
ImGui::SetCursorPosX(dLeftX + dLabelW);
ImGui::AlignTextToFramePadding();
if (dim) ImGui::TextDisabled("%s", value.c_str());
else ImGui::TextUnformatted(value.c_str());
};
if (inst.exists) { if (inst.exists) {
dbAtoms.push_back({ inst.version.empty() ? std::string(TR("unknown")) : inst.version, dbNorm, false }); char ibuf[176];
char dbParen[96]; std::snprintf(ibuf, sizeof(ibuf), "%s \xC2\xB7 %s \xC2\xB7 %s",
std::snprintf(dbParen, sizeof(dbParen), "(%s · %s)", inst.version.empty() ? TR("unknown") : inst.version.c_str(),
util::Platform::formatFileSize(inst.size).c_str(), util::Platform::formatFileSize(inst.size).c_str(),
fmtDate(inst.modifiedEpoch).c_str()); fmtDate(inst.modifiedEpoch).c_str());
dbAtoms.push_back({ dbParen, dbDimU, false }); dkv(TR("daemon_installed"), ibuf, false);
} else { } else {
dbAtoms.push_back({ TR("daemon_not_installed"), dbDimU, false }); dkv(TR("daemon_installed"), TR("daemon_not_installed"), true);
} }
dbAtoms.push_back({ "·", dbDimU, true });
dbAtoms.push_back({ TR("daemon_bundled"), dbNorm, false });
if (bun.available) { if (bun.available) {
dbAtoms.push_back({ bun.version.empty() ? std::string(TR("unknown")) : bun.version, dbNorm, false }); char bbuf[144];
dbAtoms.push_back({ "(" + util::Platform::formatFileSize(bun.size) + ")", dbDimU, false }); std::snprintf(bbuf, sizeof(bbuf), "%s \xC2\xB7 %s",
bun.version.empty() ? TR("unknown") : bun.version.c_str(),
util::Platform::formatFileSize(bun.size).c_str());
dkv(TR("daemon_bundled"), bbuf, false);
} else { } else {
dbAtoms.push_back({ TR("daemon_none_bundled"), dbDimU, false }); dkv(TR("daemon_bundled"), TR("daemon_none_bundled"), true);
} }
if (bun.available) { if (bun.available) {
const bool sameSize = inst.exists && inst.size == bun.size; const bool sameSize = inst.exists && inst.size == bun.size;
dbAtoms.push_back({ "·", dbDimU, true }); const char* chipTxt = !inst.exists ? TR("daemon_status_missing")
if (!inst.exists) : sameSize ? TR("daemon_status_match")
dbAtoms.push_back({ TR("daemon_status_missing"), ImGui::ColorConvertFloat4ToU32(ImVec4(1.0f, 0.8f, 0.2f, 1.0f)), false }); : TR("daemon_status_differ");
else if (sameSize) const ImU32 chipCol = sameSize ? Success() : Warning();
dbAtoms.push_back({ TR("daemon_status_match"), ImGui::ColorConvertFloat4ToU32(ImVec4(0.3f, 0.8f, 0.3f, 1.0f)), false }); ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
else const float dp = Layout::dpiScale();
dbAtoms.push_back({ TR("daemon_status_differ"), ImGui::ColorConvertFloat4ToU32(ImVec4(1.0f, 0.8f, 0.2f, 1.0f)), false }); const ImVec2 cp = ImGui::GetCursorScreenPos();
const float chpad = 8.0f * dp, chh = ImGui::GetFrameHeight();
const ImVec2 cts = ImGui::CalcTextSize(chipTxt);
const float chw = cts.x + chpad * 2.0f;
dl->AddRectFilled(cp, ImVec2(cp.x + chw, cp.y + chh), material::WithAlpha(chipCol, 38), chh * 0.4f);
dl->AddRect(cp, ImVec2(cp.x + chw, cp.y + chh), material::WithAlpha(chipCol, 120), chh * 0.4f, 0, 1.0f);
dl->AddText(ImVec2(cp.x + chpad, cp.y + (chh - cts.y) * 0.5f), chipCol, chipTxt);
ImGui::Dummy(ImVec2(chw, chh));
} }
// Lay the atoms out left-to-right, wrapping to a new line when the next one would // Refresh the cached daemon info once an in-app install has completed.
// exceed the content width. A "·" separator is dropped when it (plus the atom after
// it) doesn't fit, so a wrapped line never starts or ends with a dangling separator.
const float dbAvail = ImGui::GetContentRegionAvail().x;
const float dbGap = Layout::spacingXs();
float dbX = 0.0f;
ImGui::AlignTextToFramePadding();
for (std::size_t i = 0; i < dbAtoms.size(); ++i) {
const float w = ImGui::CalcTextSize(dbAtoms[i].text.c_str()).x;
if (dbAtoms[i].sep) {
const float nextW = (i + 1 < dbAtoms.size())
? ImGui::CalcTextSize(dbAtoms[i + 1].text.c_str()).x : 0.0f;
if (dbX > 0.0f && dbX + dbGap + w + dbGap + nextW > dbAvail) continue;
}
const bool wrap = (dbX > 0.0f) && (dbX + dbGap + w > dbAvail);
if (dbX > 0.0f && !wrap) { ImGui::SameLine(0, dbGap); dbX += dbGap; }
else if (wrap) { dbX = 0.0f; }
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(dbAtoms[i].col), "%s", dbAtoms[i].text.c_str());
dbX += w;
}
// 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()) if (ui::DaemonUpdateDialog::consumeInstalled())
s_settingsState.daemon_info_loaded = false; s_settingsState.daemon_info_loaded = false;
// Action row: Check for updates | Refresh | Install bundled on the left, // Update actions: Check for updates (primary) | Refresh | Install bundled.
// with the four maintenance actions right-aligned on the same row.
ImGui::Dummy(ImVec2(0, Layout::spacingXs())); ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
float daemonBtnRowY = ImGui::GetCursorScreenPos().y; {
if (TactileButton(TR("daemon_update_check"), ImVec2(0, 0), dbBtnFont)) { using AT = material::ActionTier;
ui::DaemonUpdateDialog::show(app, inst.exists ? inst.version : std::string()); material::ButtonFlow uf(contentW);
uf.next(material::ActionButtonWidth(TR("daemon_update_check"), ICON_MD_SYSTEM_UPDATE));
if (material::ActionButton("##dupd", TR("daemon_update_check"), ICON_MD_SYSTEM_UPDATE, AT::Primary))
ui::DaemonUpdateDialog::show(app, inst.exists ? inst.version : std::string());
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_daemon_update_check"));
uf.next(material::ActionButtonWidth(TR("refresh"), ICON_MD_REFRESH));
if (material::ActionButton("##drefresh", TR("refresh"), ICON_MD_REFRESH, AT::Secondary))
s_settingsState.daemon_info_loaded = false;
ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon() || !bun.available);
uf.next(material::ActionButtonWidth(TR("daemon_install_bundled"), ICON_MD_DOWNLOAD));
if (material::ActionButton("##dinst", TR("daemon_install_bundled"), ICON_MD_DOWNLOAD, AT::Secondary))
s_settingsState.confirm_reinstall_daemon = true;
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_daemon_install_bundled"));
ImGui::EndDisabled();
} }
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_daemon_update_check"));
ImGui::SameLine(0, spMd);
// Unique ImGui ID (label still displays "Refresh") — the theme sections
// also have "Refresh" buttons, so a bare label collides on this page.
if (TactileButton((std::string(TR("refresh")) + "##daemonRefresh").c_str(), ImVec2(0, 0), dbBtnFont)) {
s_settingsState.daemon_info_loaded = false;
}
ImGui::SameLine(0, spMd);
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();
// The four maintenance actions (Test connection / Rescan / Delete blockchain // Maintenance actions: Test / Rescan / Repair | Delete blockchain (destructive).
// / Repair wallet), right-aligned on the SAME row as Install bundled — no ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// separate "Advanced" toggle. Every BeginDisabled/EndDisabled predicate is {
// preserved exactly. Wraps to a right-aligned next row when too narrow. using AT = material::ActionTier;
auto renderDaemonMaintenanceButtons = [&]() { material::ButtonFlow mf(contentW);
ImGui::BeginDisabled(!app->isConnected()); ImGui::BeginDisabled(!app->isConnected());
if (TactileButton(TR("test_connection"), ImVec2(0, 0), dbBtnFont)) { mf.next(material::ActionButtonWidth(TR("test_connection"), ICON_MD_LINK));
if (material::ActionButton("##dtest", TR("test_connection"), ICON_MD_LINK, AT::Secondary)) {
if (app->rpc() && app->rpc()->isConnected() && app->worker()) { if (app->rpc() && app->rpc()->isConnected() && app->worker()) {
app->worker()->post([rpc = app->rpc()]() -> rpc::RPCWorker::MainCb { app->worker()->post([rpc = app->rpc()]() -> rpc::RPCWorker::MainCb {
try { try {
@@ -2328,52 +2279,24 @@ void RenderSettingsPage(App* app) {
} }
} }
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_test_conn")); if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_test_conn"));
ImGui::EndDisabled(); mf.next(material::ActionButtonWidth(TR("rescan"), ICON_MD_REFRESH));
ImGui::SameLine(0, spMd); if (material::ActionButton("##drescan", TR("rescan"), ICON_MD_REFRESH, AT::Secondary)) {
ImGui::BeginDisabled(!app->isConnected());
if (TactileButton(TR("rescan"), ImVec2(0, 0), dbBtnFont)) {
s_settingsState.confirm_rescan = true; 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_detecting = false;
s_settingsState.rescan_height_detected = false; s_settingsState.rescan_height_detected = false;
} }
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_rescan")); if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_rescan"));
ImGui::EndDisabled(); ImGui::EndDisabled();
ImGui::SameLine(0, spMd);
ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon()); ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon());
if (TactileButton(TR("delete_blockchain"), ImVec2(0, 0), dbBtnFont)) { mf.next(material::ActionButtonWidth(TR("repair_wallet"), ICON_MD_HEALING));
s_settingsState.confirm_delete_blockchain = true; if (material::ActionButton("##drepair", TR("repair_wallet"), ICON_MD_HEALING, AT::Secondary))
}
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; s_settingsState.confirm_repair_wallet = true;
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_repair_wallet")); if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_repair_wallet"));
mf.next(material::ActionButtonWidth(TR("delete_blockchain"), ICON_MD_DELETE));
if (material::ActionButton("##ddelete", TR("delete_blockchain"), ICON_MD_DELETE, AT::Destructive))
s_settingsState.confirm_delete_blockchain = true;
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_delete_blockchain"));
ImGui::EndDisabled(); ImGui::EndDisabled();
};
{
// Group width (button font) to right-align the four maintenance actions.
const char* mLabels[] = { TR("test_connection"), TR("rescan"),
TR("delete_blockchain"), TR("repair_wallet") };
float groupW = spMd * 3.0f;
for (const char* l : mLabels)
groupW += dbBtnFont->CalcTextSizeA(dbBtnFont->LegacySize, FLT_MAX, 0, l).x
+ ImGui::GetStyle().FramePadding.x * 2.0f;
float afterTestX = ImGui::GetItemRectMax().x + spMd; // right edge of Install bundled
float rightEdge = sectionOrigin.x + contentW;
float groupX = rightEdge - groupW;
if (groupX >= afterTestX) {
// Fits on the Install-bundled row, right-aligned.
ImGui::SameLine(0, 0);
ImGui::SetCursorScreenPos(ImVec2(groupX, daemonBtnRowY));
} else {
// Too narrow: wrap to the next row, right-aligned (clamped to left edge).
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
float wrapX = std::max(sectionOrigin.x, rightEdge - groupW);
ImGui::SetCursorScreenPos(ImVec2(wrapX, ImGui::GetCursorScreenPos().y));
}
renderDaemonMaintenanceButtons();
} }
} }