feat(market): portfolio modal — per-row delete, footer Close, instant Add, no bleed-through
- 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) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user