From fc5ce019e21d18a200b6c583760e8a8c850e4f01 Mon Sep 17 00:00:00 2001 From: DanS Date: Thu, 2 Jul 2026 00:55:52 -0500 Subject: [PATCH] feat(settings): 2x2 grid for Node & Security (fills the card width) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Single-column left every row hugging the left with a blank right half (and a split RPC row); two-column variants left a bottom-corner gap. Rework the card as a 2x2 grid on wide full-node windows so both halves carry real content: TOP-LEFT NODE/Data | TOP-RIGHT RPC BOTTOM-LEFT SECURITY | BOTTOM-RIGHT DAEMON BINARY with the two top cells sharing a row bottom, a thin vertical + horizontal divider, and the Daemon "Advanced" destructive buttons dropping to a full-width 4-button strip below the grid when expanded. Gated behind one `useGrid` flag (supportsFullNodeLifecycleActions() && availWidth >= node-grid-breakpoint, default 900). When false — narrow windows and lite — it falls through to the existing single full-width column, unchanged (one-flag-revertible). Wallet Size moved to its own row (removes a mid-row gap in both layouts). RPC / Security / Daemon rebased to per-cell X/width via aliases; Advanced buttons factored into one lambda so the disabled predicates live in a single place. Design produced + adversarially critiqued via a multi-agent pass. New: components.settings-page.node-grid-breakpoint (ui.toml), "rpc_connection" i18n string. Full-node + lite build clean; ctest green; hygiene clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- res/themes/ui.toml | 1 + src/ui/pages/settings_page.cpp | 255 ++++++++++++++++++++++----------- src/util/i18n.cpp | 1 + 3 files changed, 170 insertions(+), 87 deletions(-) diff --git a/res/themes/ui.toml b/res/themes/ui.toml index e32726f..09fd9a4 100644 --- a/res/themes/ui.toml +++ b/res/themes/ui.toml @@ -1325,6 +1325,7 @@ wallet-btn-padding = { size = 24.0 } rpc-label-min-width = { size = 70.0 } rpc-label-width = { size = 85.0 } security-combo-width = { size = 120.0 } +node-grid-breakpoint = { size = 900.0 } port-input-min-width = { size = 60.0 } port-input-width-ratio = { size = 0.4 } idle-combo-width = { size = 64.0 } diff --git a/src/ui/pages/settings_page.cpp b/src/ui/pages/settings_page.cpp index dc629ab..c6f6ec3 100644 --- a/src/ui/pages/settings_page.cpp +++ b/src/ui/pages/settings_page.cpp @@ -1508,18 +1508,28 @@ void RenderSettingsPage(App* app) { return std::max(minBtnW, maxTextW + btnPad * 2); }; - // --- NODE + SECURITY: side by side when wide, stacked when narrow --- + // --- NODE + SECURITY: 2x2 grid on wide full-node windows, stacked otherwise --- { - // Single full-width column: Node -> RPC -> Security -> Daemon Binary stack - // vertically, each at full card width. (Two-column variants left an unbalanced - // empty gap for this content, and the Daemon Binary button rows want the full - // width anyway.) `stacked` drives the existing vertical-flow path below. - bool stacked = true; - float leftColW = contentW; - float rightColW = contentW; + // 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 = sectionOrigin.x; + 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; // ---- LEFT COLUMN: NODE ---- ImGui::SetCursorScreenPos(ImVec2(leftX, sectionOrigin.y)); @@ -1896,14 +1906,10 @@ void RenderSettingsPage(App* app) { std::string size_str = (wallet_size > 0) ? util::Platform::formatFileSize(wallet_size) : TR("settings_not_found"); std::string dirPath = util::Platform::getDragonXDataDir(); - // Calculate space taken by "Wallet Size: " on the right - float walletSizeLabelW = ImGui::CalcTextSize(TR("settings_wallet_size_label")).x + Layout::spacingXs(); - float walletSizeValW = ImGui::CalcTextSize(size_str.c_str()).x; - float walletSizeTotalW = walletSizeLabelW + walletSizeValW + Layout::spacingLg(); - - // Available space for "Data Dir: " + // 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 - walletSizeTotalW - dataDirLabelW; + float availForPath = leftColW - dataDirLabelW; ImGui::TextUnformatted(TR("settings_data_dir")); ImGui::SameLine(0, Layout::spacingXs()); @@ -1941,7 +1947,9 @@ void RenderSettingsPage(App* app) { if (ImGui::IsItemClicked()) util::Platform::openFolder(dirPath); - ImGui::SameLine(0, Layout::spacingLg()); + // 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)); ImGui::TextUnformatted(TR("settings_wallet_size_label")); ImGui::SameLine(0, Layout::spacingXs()); if (wallet_size > 0) { @@ -1958,20 +1966,40 @@ void RenderSettingsPage(App* app) { } 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; + ImGui::Dummy(ImVec2(0, Layout::spacingXs())); // RPC connection — two columns: (Host | Username) and (Port | Password) - float rpcHalfW = (leftColW - Layout::spacingMd()) * 0.5f; + 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 = leftX + rpcHalfW + Layout::spacingMd(); + float rpcRightColX = rpcColX + rpcHalfW + Layout::spacingMd(); // Row 1: RPC Host + Username float row1Y = ImGui::GetCursorScreenPos().y; - ImGui::SetCursorScreenPos(ImVec2(leftX, row1Y)); + ImGui::SetCursorScreenPos(ImVec2(rpcColX, row1Y)); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("rpc_host")); - ImGui::SameLine(leftX - sectionOrigin.x + rpcHalfLblW); + 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")); @@ -1985,14 +2013,14 @@ void RenderSettingsPage(App* app) { ImGui::InputText("##RPCUser", s_settingsState.rpc_user, sizeof(s_settingsState.rpc_user)); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_user")); - ImGui::SetCursorScreenPos(ImVec2(leftX, std::max(afterRow1Y, ImGui::GetCursorScreenPos().y))); + ImGui::SetCursorScreenPos(ImVec2(rpcColX, std::max(afterRow1Y, ImGui::GetCursorScreenPos().y))); // Row 2: Port + Password float row2Y = ImGui::GetCursorScreenPos().y; - ImGui::SetCursorScreenPos(ImVec2(leftX, row2Y)); + ImGui::SetCursorScreenPos(ImVec2(rpcColX, row2Y)); ImGui::AlignTextToFramePadding(); ImGui::TextUnformatted(TR("rpc_port")); - ImGui::SameLine(leftX - sectionOrigin.x + rpcHalfLblW); + 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")); @@ -2007,33 +2035,45 @@ void RenderSettingsPage(App* app) { ImGuiInputTextFlags_Password); if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_rpc_pass")); - ImGui::SetCursorScreenPos(ImVec2(leftX, std::max(afterRow2Y, ImGui::GetCursorScreenPos().y))); + ImGui::SetCursorScreenPos(ImVec2(rpcColX, std::max(afterRow2Y, ImGui::GetCursorScreenPos().y))); - ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y)); + 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(leftX + leftColW); + 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(); } + if (useGrid) { + rpcBottomGrid = ImGui::GetCursorScreenPos().y; + } + } ImGui::PopFont(); } - float leftBottom = ImGui::GetCursorScreenPos().y; + float leftBottom = useGrid ? leftBottomGrid : ImGui::GetCursorScreenPos().y; + float rpcBottom = useGrid ? rpcBottomGrid : leftBottom; ImGui::PopClipRect(); - // ---- RIGHT COLUMN: SECURITY ---- - // Stacked: flows below the left column; side-by-side: aligned to the section top. - float rightTopY = stacked ? (leftBottom + Layout::spacingMd()) : sectionOrigin.y; - ImGui::SetCursorScreenPos(ImVec2(rightX, rightTopY)); - ImGui::PushClipRect(ImVec2(rightX, rightTopY), - ImVec2(rightX + rightColW, rightTopY + 9999), true); + // 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())); @@ -2046,9 +2086,9 @@ void RenderSettingsPage(App* app) { bool isEncrypted = app->state().isEncrypted(); bool isLocked = app->state().isLocked(); - float secBtnW = std::min(rowBtnW({TR("settings_encrypt_wallet"), TR("settings_change_passphrase"), TR("settings_lock_now")}), (rightColW - Layout::spacingMd()) * 0.5f); + 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(rightX, ImGui::GetCursorScreenPos().y)); + ImGui::SetCursorScreenPos(ImVec2(secX, ImGui::GetCursorScreenPos().y)); if (!isEncrypted) { if (TactileButton(TR("settings_encrypt_wallet"), ImVec2(secBtnW, 0), S.resolveFont("button"))) app->showEncryptDialog(); @@ -2078,7 +2118,7 @@ void RenderSettingsPage(App* app) { ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), "%s", TR("settings_unlocked")); } // Remove Encryption button - ImGui::SetCursorScreenPos(ImVec2(rightX, ImGui::GetCursorScreenPos().y + Layout::spacingXs())); + 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")); @@ -2098,9 +2138,9 @@ void RenderSettingsPage(App* app) { for (int i = 0; i < 6; i++) { if (timeoutValues[i] == timeout) { selTimeout = i; break; } } - ImGui::SetCursorScreenPos(ImVec2(rightX, ImGui::GetCursorScreenPos().y)); + ImGui::SetCursorScreenPos(ImVec2(secX, ImGui::GetCursorScreenPos().y)); Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("settings_auto_lock")); - ImGui::SetCursorScreenPos(ImVec2(rightX, ImGui::GetCursorScreenPos().y)); + ImGui::SetCursorScreenPos(ImVec2(secX, ImGui::GetCursorScreenPos().y)); ImGui::PushItemWidth(comboW); if (ImGui::Combo("##autolock", &selTimeout, timeoutLabels, 6)) { app->settings()->setAutoLockTimeout(timeoutValues[selTimeout]); @@ -2116,9 +2156,9 @@ void RenderSettingsPage(App* app) { bool isEncryptedPIN = app->state().isEncrypted(); if (isEncryptedPIN) { bool hasPIN = app->hasPinVault(); - float pinBtnW = std::min(rowBtnW({TR("settings_set_pin"), TR("settings_change_pin"), TR("settings_remove_pin")}), (rightColW - Layout::spacingSm()) * 0.5f); + 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(rightX, ImGui::GetCursorScreenPos().y)); + ImGui::SetCursorScreenPos(ImVec2(secX, ImGui::GetCursorScreenPos().y)); if (!hasPIN) { if (TactileButton(TR("settings_set_pin"), ImVec2(pinBtnW, 0), S.resolveFont("button"))) app->showPinSetupDialog(); @@ -2141,25 +2181,31 @@ void RenderSettingsPage(App* app) { ImGui::TextColored(ImVec4(0.3f,1.0f,0.5f,1.0f), "%s", TR("settings_pin_active")); } } else { - ImGui::SetCursorScreenPos(ImVec2(rightX, ImGui::GetCursorScreenPos().y)); + ImGui::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 — masonry-placed into the shorter column ---- - // Node/RPC (left) is taller than Security (right), so Daemon Binary drops under - // Security, filling the empty bottom-right and balancing the two columns. When - // the columns are stacked (narrow), it spans full width below both. + // ---- 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 = stacked; + const bool dbFull = useGrid ? false : stacked; const bool dbLeftCol = leftBottom <= rightBottom; - const float dbColW = dbFull ? contentW : (dbLeftCol ? leftColW : rightColW); - const float dbTopY = dbFull ? std::max(leftBottom, rightBottom) - : (dbLeftCol ? leftBottom : rightBottom); - const float dbLeftX = dbFull ? sectionOrigin.x : (dbLeftCol ? leftX : rightX); + 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); @@ -2196,7 +2242,7 @@ void RenderSettingsPage(App* app) { // 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 * 0.4f, 340.0f); + 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")); @@ -2271,6 +2317,40 @@ void RenderSettingsPage(App* app) { 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())); @@ -2292,47 +2372,48 @@ void RenderSettingsPage(App* app) { float arrowW = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, advArrow).x; dl->AddText(iconFont, iconFont->LegacySize, ImVec2(advPos.x + dbColW - arrowW, textY), OnSurfaceMedium(), advArrow); } - if (s_settingsState.node_advanced_expanded) { + // 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)); - 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, Layout::spacingMd()); - 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, Layout::spacingMd()); - 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, Layout::spacingMd()); - 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(); + renderDaemonAdvancedButtons(0.0f); } - float dbBottom = ImGui::GetCursorScreenPos().y; + dbBottom = ImGui::GetCursorScreenPos().y; ImGui::PopClipRect(); - if (dbFull || dbLeftCol) leftBottom = std::max(leftBottom, dbBottom); - if (dbFull || !dbLeftCol) rightBottom = std::max(rightBottom, dbBottom); + 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 column. - float maxBottom = std::max(leftBottom, rightBottom); + // Advance the card cursor past the taller cell/column. + float maxBottom = useGrid ? gridMaxBottom : std::max(leftBottom, rightBottom); ImGui::SetCursorScreenPos(ImVec2(sectionOrigin.x, maxBottom)); } diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 42731df..f3721e7 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -257,6 +257,7 @@ void I18n::loadBuiltinEnglish() strings_["setup_wizard"] = "Run Setup Wizard..."; // RPC / Explorer settings + strings_["rpc_connection"] = "RPC CONNECTION"; strings_["rpc_host"] = "Host"; strings_["rpc_port"] = "Port"; strings_["rpc_user"] = "Username";