From d007f3de3949f8cc402841812a4e3edf5962a7ba Mon Sep 17 00:00:00 2001 From: DanS Date: Fri, 3 Jul 2026 20:02:25 -0500 Subject: [PATCH] =?UTF-8?q?feat(market):=20portfolio=20modal=20=E2=80=94?= =?UTF-8?q?=20per-row=20delete,=20footer=20Close,=20instant=20Add,=20no=20?= =?UTF-8?q?bleed-through?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Delete moved from the footer to a per-row trash icon on each group (shown on hover/selected), with deferred deletion + selection fix-up. - Removed the title-bar X; the footer is now right-aligned Revert / Save / Close (larger buttons), Close commits + dismisses. Esc and outside-click still close. - Add entry now immediately creates a persisted "Untitled" group and selects it (no more invisible draft state); the -2 draft path is gone. - Backdrop bleed-through fixed: when the modal is open the Market tab body is not rendered, so its content and foreground theme-effect borders no longer show over the overlay — the modal floats on the app's blurred/opaque backdrop. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/ui/windows/market_tab.cpp | 88 +++++++++++++++++++++++------------ src/util/i18n.cpp | 2 + 2 files changed, 60 insertions(+), 30 deletions(-) diff --git a/src/ui/windows/market_tab.cpp b/src/ui/windows/market_tab.cpp index 4074483..7a89a3c 100644 --- a/src/ui/windows/market_tab.cpp +++ b/src/ui/windows/market_tab.cpp @@ -429,7 +429,7 @@ static void RenderPortfolioEditor(App* app) ImGui::BeginChild("##pfCard", ImVec2(cardW, cardH), ImGuiChildFlags_None, ImGuiWindowFlags_NoScrollbar); ImGui::PopStyleVar(); // pop WindowPadding — the 28x20 applies to ##pfCard only, not nested children - material::DrawDialogTitleBar(TR("portfolio_manage_title"), &s_portfolio_editor_open); + material::DrawDialogTitleBar(TR("portfolio_manage_title"), nullptr); // no X — Close is in the footer if (ImGui::IsKeyPressed(ImGuiKey_Escape)) s_portfolio_editor_open = false; { // clamp/validate the selection against the current entry list @@ -455,16 +455,19 @@ static void RenderPortfolioEditor(App* app) if (entries.empty()) Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("portfolio_no_entries")); float rowH = 46.0f * dp; - int clickedSel = -999; // deferred: don't mutate `entries` mid-loop + float delSlot = 26.0f * dp; + int clickedSel = -999, delRow = -1; // deferred: don't mutate `entries` mid-loop for (int i = 0; i < (int)entries.size(); i++) { ImGui::PushID(i); const auto& en = entries[i]; bool selRow = (s_pf_sel == i); ImVec2 rmn = ImGui::GetCursorScreenPos(); + ImGui::SetNextItemAllowOverlap(); // let the per-row delete icon sit on top if (ImGui::Selectable("##pfgrp", selRow, ImGuiSelectableFlags_SpanAvailWidth, ImVec2(0, rowH))) { if (i != s_pf_sel) clickedSel = i; } ImVec2 rmx(rmn.x + ImGui::GetItemRectSize().x, rmn.y + rowH); + bool rowHov = ImGui::IsItemHovered(); ImU32 accent = en.color ? (ImU32)en.color : WithAlpha(OnSurface(), 60); mdl->AddRectFilled(ImVec2(rmn.x, rmn.y + 4.0f * dp), ImVec2(rmn.x + 3.0f * dp, rmx.y - 4.0f * dp), accent, 1.5f * dp); @@ -475,7 +478,7 @@ static void RenderPortfolioEditor(App* app) en.color ? (ImU32)en.color : OnSurfaceMedium(), iconFont, sub1f->LegacySize); ix += sub1f->LegacySize + Layout::spacingSm(); } - float txtRight = rmx.x - Layout::spacingSm(); + float txtRight = rmx.x - delSlot; // reserve the trailing delete-icon slot std::string nm = en.label.empty() ? std::string(TR("portfolio_new_entry")) : en.label; while (nm.size() > 1 && body2f->CalcTextSizeA(body2f->LegacySize, FLT_MAX, 0, nm.c_str()).x > (txtRight - ix)) nm.pop_back(); @@ -484,14 +487,46 @@ static void RenderPortfolioEditor(App* app) double bal = data::SumPortfolioBalance(en.addresses, state.addresses); char vl[80]; snprintf(vl, sizeof(vl), "%.4f %s \xC2\xB7 %d", bal, DRAGONX_TICKER, (int)en.addresses.size()); mdl->AddText(capF, capF->LegacySize, ImVec2(ix, rmn.y + rowH * 0.56f), OnSurfaceMedium(), vl); + // Per-row delete icon (shown on hover or when selected). + if (rowHov || selRow) { + float ds = 22.0f * dp; + ImVec2 dmn(rmx.x - ds - Layout::spacingXs(), rmn.y + (rowH - ds) * 0.5f); + ImGui::SetCursorScreenPos(dmn); + if (ImGui::InvisibleButton("##pfdel", ImVec2(ds, ds))) delRow = i; + bool dhov = ImGui::IsItemHovered(); + if (dhov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + ImVec2 isz = iconFont->CalcTextSizeA(iconFsz, FLT_MAX, 0, ICON_MD_DELETE_OUTLINE); + mdl->AddText(iconFont, iconFsz, ImVec2(dmn.x + (ds - isz.x) * 0.5f, dmn.y + (ds - isz.y) * 0.5f), + dhov ? Error() : OnSurfaceMedium(), ICON_MD_DELETE_OUTLINE); + } ImGui::PopID(); } - if (clickedSel != -999) { commitIfNeeded(); PortfolioBeginEdit(app, clickedSel); } + if (delRow >= 0) { + commitIfNeeded(); // persist edits to the current group first + auto es = settings->getPortfolioEntries(); + if (delRow < (int)es.size()) { + es.erase(es.begin() + delRow); + persist(es); + if (es.empty()) s_pf_sel = -1; + else if (delRow < s_pf_sel) s_pf_sel -= 1; + else if (delRow == s_pf_sel) s_pf_sel = std::min(delRow, (int)es.size() - 1); + PortfolioBeginEdit(app, s_pf_sel); + } + } else if (clickedSel != -999) { commitIfNeeded(); PortfolioBeginEdit(app, clickedSel); } } ImGui::EndChild(); ImGui::Dummy(ImVec2(0, Layout::spacingSm())); - // Add button lives outside the scroll child (a normal widget) so it can never be clipped. - if (ImGui::Button(TR("portfolio_add_entry"), ImVec2(masterW, addH))) { commitIfNeeded(); PortfolioBeginEdit(app, -2); } + // Add immediately creates a persisted "Untitled" group and selects it for editing. + if (ImGui::Button(TR("portfolio_add_entry"), ImVec2(masterW, addH))) { + commitIfNeeded(); + auto es = settings->getPortfolioEntries(); + config::Settings::PortfolioEntry ne; + ne.label = TR("portfolio_untitled"); + es.push_back(ne); + persist(es); + s_pf_sel = (int)es.size() - 1; + PortfolioBeginEdit(app, s_pf_sel); + } } ImGui::EndGroup(); @@ -868,33 +903,23 @@ static void RenderPortfolioEditor(App* app) } ImGui::EndChild(); // ##pfDetail - // ================= FOOTER: delete / revert / save ================= + // ================= FOOTER: revert / save / close (right-aligned, large) ================= ImGui::Separator(); { - ImGui::BeginDisabled(s_pf_sel < 0); - if (ImGui::Button(TR("portfolio_delete"))) { - auto entries = settings->getPortfolioEntries(); - if (s_pf_sel >= 0 && s_pf_sel < (int)entries.size()) { - entries.erase(entries.begin() + s_pf_sel); - persist(entries); - if (entries.empty()) s_pf_sel = -1; - else { s_pf_sel = std::min(s_pf_sel, (int)entries.size() - 1); PortfolioBeginEdit(app, s_pf_sel); } - } - } - ImGui::EndDisabled(); - - float bw = 92.0f * dp, aw = bw * 2 + Layout::spacingSm(); - ImGui::SameLine(); + float bh = 40.0f * dp, bw = 112.0f * dp, sp = Layout::spacingSm(); + float aw = bw * 3 + sp * 2; float availF = ImGui::GetContentRegionAvail().x; if (availF > aw) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + availF - aw); ImGui::BeginDisabled(s_pf_sel == -1); - if (ImGui::Button(TR("portfolio_revert"), ImVec2(bw, 0))) - PortfolioBeginEdit(app, s_pf_sel); // reload from the stored entry (or defaults for a draft) + if (ImGui::Button(TR("portfolio_revert"), ImVec2(bw, bh))) + PortfolioBeginEdit(app, s_pf_sel); // reload the working set from the stored entry ImGui::EndDisabled(); - ImGui::SameLine(); + ImGui::SameLine(0, sp); ImGui::BeginDisabled(s_pf_label[0] == '\0'); - if (ImGui::Button(TR("portfolio_save"), ImVec2(bw, 0))) commitIfNeeded(); + if (ImGui::Button(TR("portfolio_save"), ImVec2(bw, bh))) commitIfNeeded(); ImGui::EndDisabled(); + ImGui::SameLine(0, sp); + if (ImGui::Button(TR("portfolio_close"), ImVec2(bw, bh))) { commitIfNeeded(); s_portfolio_editor_open = false; } } ImGui::EndChild(); // ##pfCard @@ -944,6 +969,11 @@ void RenderMarketTab(App* app) const auto& currentExchange = registry[s_exchange_idx]; if (s_pair_idx >= (int)currentExchange.pairs.size()) s_pair_idx = 0; + // When the Manage-portfolio modal is open it fully covers the tab, so skip rendering the tab + // body (and its foreground theme effects, which would otherwise bleed over the overlay) — the + // modal floats on the app's blurred backdrop instead of the sharp tab content. + if (s_portfolio_editor_open) { RenderPortfolioEditor(app); return; } + ImVec2 marketAvail = ImGui::GetContentRegionAvail(); // Scrollable (the portfolio grid can extend past the window) but with no visible scrollbar — // scrolling still works via the mouse wheel. @@ -1540,8 +1570,8 @@ void RenderMarketTab(App* app) float availX = ImGui::GetContentRegionAvail().x; if (availX > mBtnW) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + availX - mBtnW); if (ImGui::Button(ml, ImVec2(mBtnW, 0))) { - // Open the combined editor with a valid selection: first group, or a blank draft. - PortfolioBeginEdit(app, app->settings()->getPortfolioEntries().empty() ? -2 : 0); + // Open the combined editor selecting the first group (or the empty state if none). + PortfolioBeginEdit(app, app->settings()->getPortfolioEntries().empty() ? -1 : 0); s_portfolio_editor_open = true; } } @@ -1875,9 +1905,7 @@ void RenderMarketTab(App* app) } ImGui::EndChild(); // ##MarketScroll - - // Portfolio editor modal (rendered outside the scroll child so it overlays the page). - RenderPortfolioEditor(app); + // (When the editor is open the tab body is skipped above and the modal is rendered there.) } } // namespace ui diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 1d361a0..5dde156 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -1166,6 +1166,8 @@ void I18n::loadBuiltinEnglish() strings_["portfolio_save"] = "Save"; strings_["portfolio_cancel"] = "Cancel"; strings_["portfolio_revert"] = "Revert"; + strings_["portfolio_close"] = "Close"; + strings_["portfolio_untitled"] = "Untitled"; strings_["portfolio_detail_empty"] = "Select a group on the left, or add one, to edit it."; strings_["portfolio_add_to"] = "Add to portfolio"; strings_["portfolio_remove_from"] = "Remove from portfolio";