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::BeginChild("##pfCard", ImVec2(cardW, cardH), ImGuiChildFlags_None, ImGuiWindowFlags_NoScrollbar);
|
||||||
ImGui::PopStyleVar(); // pop WindowPadding — the 28x20 applies to ##pfCard only, not nested children
|
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;
|
if (ImGui::IsKeyPressed(ImGuiKey_Escape)) s_portfolio_editor_open = false;
|
||||||
|
|
||||||
{ // clamp/validate the selection against the current entry list
|
{ // clamp/validate the selection against the current entry list
|
||||||
@@ -455,16 +455,19 @@ static void RenderPortfolioEditor(App* app)
|
|||||||
if (entries.empty())
|
if (entries.empty())
|
||||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("portfolio_no_entries"));
|
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("portfolio_no_entries"));
|
||||||
float rowH = 46.0f * dp;
|
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++) {
|
for (int i = 0; i < (int)entries.size(); i++) {
|
||||||
ImGui::PushID(i);
|
ImGui::PushID(i);
|
||||||
const auto& en = entries[i];
|
const auto& en = entries[i];
|
||||||
bool selRow = (s_pf_sel == i);
|
bool selRow = (s_pf_sel == i);
|
||||||
ImVec2 rmn = ImGui::GetCursorScreenPos();
|
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 (ImGui::Selectable("##pfgrp", selRow, ImGuiSelectableFlags_SpanAvailWidth, ImVec2(0, rowH))) {
|
||||||
if (i != s_pf_sel) clickedSel = i;
|
if (i != s_pf_sel) clickedSel = i;
|
||||||
}
|
}
|
||||||
ImVec2 rmx(rmn.x + ImGui::GetItemRectSize().x, rmn.y + rowH);
|
ImVec2 rmx(rmn.x + ImGui::GetItemRectSize().x, rmn.y + rowH);
|
||||||
|
bool rowHov = ImGui::IsItemHovered();
|
||||||
ImU32 accent = en.color ? (ImU32)en.color : WithAlpha(OnSurface(), 60);
|
ImU32 accent = en.color ? (ImU32)en.color : WithAlpha(OnSurface(), 60);
|
||||||
mdl->AddRectFilled(ImVec2(rmn.x, rmn.y + 4.0f * dp),
|
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);
|
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);
|
en.color ? (ImU32)en.color : OnSurfaceMedium(), iconFont, sub1f->LegacySize);
|
||||||
ix += sub1f->LegacySize + Layout::spacingSm();
|
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;
|
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))
|
while (nm.size() > 1 && body2f->CalcTextSizeA(body2f->LegacySize, FLT_MAX, 0, nm.c_str()).x > (txtRight - ix))
|
||||||
nm.pop_back();
|
nm.pop_back();
|
||||||
@@ -484,14 +487,46 @@ static void RenderPortfolioEditor(App* app)
|
|||||||
double bal = data::SumPortfolioBalance(en.addresses, state.addresses);
|
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());
|
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);
|
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();
|
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::EndChild();
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
// Add button lives outside the scroll child (a normal widget) so it can never be clipped.
|
// Add immediately creates a persisted "Untitled" group and selects it for editing.
|
||||||
if (ImGui::Button(TR("portfolio_add_entry"), ImVec2(masterW, addH))) { commitIfNeeded(); PortfolioBeginEdit(app, -2); }
|
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();
|
ImGui::EndGroup();
|
||||||
|
|
||||||
@@ -868,33 +903,23 @@ static void RenderPortfolioEditor(App* app)
|
|||||||
}
|
}
|
||||||
ImGui::EndChild(); // ##pfDetail
|
ImGui::EndChild(); // ##pfDetail
|
||||||
|
|
||||||
// ================= FOOTER: delete / revert / save =================
|
// ================= FOOTER: revert / save / close (right-aligned, large) =================
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
{
|
{
|
||||||
ImGui::BeginDisabled(s_pf_sel < 0);
|
float bh = 40.0f * dp, bw = 112.0f * dp, sp = Layout::spacingSm();
|
||||||
if (ImGui::Button(TR("portfolio_delete"))) {
|
float aw = bw * 3 + sp * 2;
|
||||||
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 availF = ImGui::GetContentRegionAvail().x;
|
float availF = ImGui::GetContentRegionAvail().x;
|
||||||
if (availF > aw) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + availF - aw);
|
if (availF > aw) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + availF - aw);
|
||||||
ImGui::BeginDisabled(s_pf_sel == -1);
|
ImGui::BeginDisabled(s_pf_sel == -1);
|
||||||
if (ImGui::Button(TR("portfolio_revert"), ImVec2(bw, 0)))
|
if (ImGui::Button(TR("portfolio_revert"), ImVec2(bw, bh)))
|
||||||
PortfolioBeginEdit(app, s_pf_sel); // reload from the stored entry (or defaults for a draft)
|
PortfolioBeginEdit(app, s_pf_sel); // reload the working set from the stored entry
|
||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
ImGui::SameLine();
|
ImGui::SameLine(0, sp);
|
||||||
ImGui::BeginDisabled(s_pf_label[0] == '\0');
|
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::EndDisabled();
|
||||||
|
ImGui::SameLine(0, sp);
|
||||||
|
if (ImGui::Button(TR("portfolio_close"), ImVec2(bw, bh))) { commitIfNeeded(); s_portfolio_editor_open = false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::EndChild(); // ##pfCard
|
ImGui::EndChild(); // ##pfCard
|
||||||
@@ -944,6 +969,11 @@ void RenderMarketTab(App* app)
|
|||||||
const auto& currentExchange = registry[s_exchange_idx];
|
const auto& currentExchange = registry[s_exchange_idx];
|
||||||
if (s_pair_idx >= (int)currentExchange.pairs.size()) s_pair_idx = 0;
|
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();
|
ImVec2 marketAvail = ImGui::GetContentRegionAvail();
|
||||||
// Scrollable (the portfolio grid can extend past the window) but with no visible scrollbar —
|
// Scrollable (the portfolio grid can extend past the window) but with no visible scrollbar —
|
||||||
// scrolling still works via the mouse wheel.
|
// scrolling still works via the mouse wheel.
|
||||||
@@ -1540,8 +1570,8 @@ void RenderMarketTab(App* app)
|
|||||||
float availX = ImGui::GetContentRegionAvail().x;
|
float availX = ImGui::GetContentRegionAvail().x;
|
||||||
if (availX > mBtnW) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + availX - mBtnW);
|
if (availX > mBtnW) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + availX - mBtnW);
|
||||||
if (ImGui::Button(ml, ImVec2(mBtnW, 0))) {
|
if (ImGui::Button(ml, ImVec2(mBtnW, 0))) {
|
||||||
// Open the combined editor with a valid selection: first group, or a blank draft.
|
// Open the combined editor selecting the first group (or the empty state if none).
|
||||||
PortfolioBeginEdit(app, app->settings()->getPortfolioEntries().empty() ? -2 : 0);
|
PortfolioBeginEdit(app, app->settings()->getPortfolioEntries().empty() ? -1 : 0);
|
||||||
s_portfolio_editor_open = true;
|
s_portfolio_editor_open = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1875,9 +1905,7 @@ void RenderMarketTab(App* app)
|
|||||||
}
|
}
|
||||||
|
|
||||||
ImGui::EndChild(); // ##MarketScroll
|
ImGui::EndChild(); // ##MarketScroll
|
||||||
|
// (When the editor is open the tab body is skipped above and the modal is rendered there.)
|
||||||
// Portfolio editor modal (rendered outside the scroll child so it overlays the page).
|
|
||||||
RenderPortfolioEditor(app);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace ui
|
} // namespace ui
|
||||||
|
|||||||
@@ -1166,6 +1166,8 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["portfolio_save"] = "Save";
|
strings_["portfolio_save"] = "Save";
|
||||||
strings_["portfolio_cancel"] = "Cancel";
|
strings_["portfolio_cancel"] = "Cancel";
|
||||||
strings_["portfolio_revert"] = "Revert";
|
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_detail_empty"] = "Select a group on the left, or add one, to edit it.";
|
||||||
strings_["portfolio_add_to"] = "Add to portfolio";
|
strings_["portfolio_add_to"] = "Add to portfolio";
|
||||||
strings_["portfolio_remove_from"] = "Remove from portfolio";
|
strings_["portfolio_remove_from"] = "Remove from portfolio";
|
||||||
|
|||||||
Reference in New Issue
Block a user