feat(market): rework portfolio-modal actions, addresses row, price & unsaved state
- Move the group-scoped Revert/Save buttons into the detail container (sized to the Add-entry height) so they read as lower-hierarchy than the modal-level Close, which now stands alone in the footer. The detail body is wrapped in a child that reserves the bottom row. - Add an amber "unsaved changes" dot on the selected group row while its working copy differs from the stored entry; also gate the detail Revert/Save on that dirty state. - Addresses: narrow the All/Shielded/Transparent segmented control (360px) and right-align Select all / Clear / Funded on one line; give the Select all / Clear pills more text padding. - Appearance: shrink the icon search field and right-align it on the heading line, with a "Search icons…" hint. - Price: move the sparkline toggle onto the 24h line; the interval radios stay below, enabled only when the sparkline is shown. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -551,6 +551,15 @@ static void RenderPortfolioEditor(App* app)
|
||||
float masterW = std::min(std::max(contentW * 0.28f, 240.0f * dp), 320.0f * dp);
|
||||
float detailW = contentW - masterW - gap;
|
||||
|
||||
// Does the selected group have uncommitted edits? (Only the selected group can differ from the
|
||||
// stored copy — others were committed on deselect.) Drives the master-list "unsaved" dot and the
|
||||
// detail-pane Revert/Save enable state.
|
||||
bool selDirty = false;
|
||||
{
|
||||
const auto& es = settings->getPortfolioEntries();
|
||||
if (s_pf_sel >= 0 && s_pf_sel < (int)es.size()) selDirty = !workingMatches(es[s_pf_sel]);
|
||||
}
|
||||
|
||||
// ================= MASTER: scrollable group list + a pinned Add button below it =================
|
||||
float addH = 34.0f * dp;
|
||||
ImGui::BeginGroup();
|
||||
@@ -584,6 +593,13 @@ static void RenderPortfolioEditor(App* app)
|
||||
en.color ? (ImU32)en.color : OnSurfaceMedium(), iconFont, sub1f->LegacySize);
|
||||
ix += sub1f->LegacySize + Layout::spacingSm();
|
||||
}
|
||||
// Unsaved-changes dot: shown on the selected row while its edits aren't committed.
|
||||
if (selRow && selDirty) {
|
||||
float dr = 3.5f * dp;
|
||||
mdl->AddCircleFilled(ImVec2(ix + dr, rmn.y + rowH * 0.28f + body2f->LegacySize * 0.5f),
|
||||
dr, Warning());
|
||||
ix += dr * 2.0f + Layout::spacingXs();
|
||||
}
|
||||
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))
|
||||
@@ -651,6 +667,12 @@ static void RenderPortfolioEditor(App* app)
|
||||
ImGui::SetCursorPos(ImVec2(std::max(0.0f, (av.x - ts.x) * 0.5f), av.y * 0.42f));
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), msg);
|
||||
} else {
|
||||
// Body fills the detail column minus a bottom row reserved for the group-scoped
|
||||
// Revert/Save buttons (kept inside this container, below the modal-level Close in the footer).
|
||||
float pfBtnRowH = addH + Layout::spacingMd();
|
||||
ImGui::BeginChild("##pfDetailBody",
|
||||
ImVec2(0, std::max(80.0f * dp, ImGui::GetContentRegionAvail().y - pfBtnRowH)),
|
||||
false, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
// ---- Header: live preview + label (always visible above the accordion) ----
|
||||
{
|
||||
ImDrawList* pdl = ImGui::GetWindowDrawList();
|
||||
@@ -848,9 +870,14 @@ static void RenderPortfolioEditor(App* app)
|
||||
ImFont* gIconFont = Type().iconXL();
|
||||
ImGui::TextUnformatted(TR("portfolio_icon"));
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
|
||||
ImGui::InputTextWithHint("##pfIconSearch", TR("portfolio_search"),
|
||||
s_pf_icon_search, sizeof(s_pf_icon_search));
|
||||
{
|
||||
float searchW = std::min(ImGui::GetContentRegionAvail().x, 200.0f * dp);
|
||||
float off = ImGui::GetContentRegionAvail().x - searchW; // right-align the field
|
||||
if (off > 0.0f) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + off);
|
||||
ImGui::SetNextItemWidth(searchW);
|
||||
ImGui::InputTextWithHint("##pfIconSearch", TR("portfolio_search_icons"),
|
||||
s_pf_icon_search, sizeof(s_pf_icon_search));
|
||||
}
|
||||
// Filtered icon list (-1 = "None", shown only when not searching).
|
||||
std::string isearch = s_pf_icon_search;
|
||||
std::vector<int> iconIdx;
|
||||
@@ -932,20 +959,20 @@ static void RenderPortfolioEditor(App* app)
|
||||
ImGui::Checkbox(TR("portfolio_show_value"), &s_pf_show_value);
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled(!(s_pf_price_basis == 0 || s_pf_price_basis == 1)); // 24h: live-market only
|
||||
// 24h + sparkline toggles share one line (both live-market only).
|
||||
ImGui::BeginDisabled(!(s_pf_price_basis == 0 || s_pf_price_basis == 1));
|
||||
ImGui::Checkbox(TR("portfolio_show_24h"), &s_pf_show_24h);
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox(TR("portfolio_show_sparkline"), &s_pf_show_sparkline);
|
||||
ImGui::EndDisabled();
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
// Sparkline + its interval — radio group.
|
||||
ImGui::BeginDisabled(!(s_pf_price_basis == 0 || s_pf_price_basis == 1));
|
||||
ImGui::Checkbox(TR("portfolio_show_sparkline"), &s_pf_show_sparkline);
|
||||
ImGui::BeginDisabled(!s_pf_show_sparkline);
|
||||
// Sparkline interval — radio group (enabled only when the sparkline is shown).
|
||||
ImGui::BeginDisabled(!s_pf_show_sparkline || !(s_pf_price_basis == 0 || s_pf_price_basis == 1));
|
||||
const char* spItems[5] = { TR("portfolio_spark_min"), TR("portfolio_spark_hour"),
|
||||
TR("portfolio_spark_day"), TR("portfolio_spark_week"),
|
||||
TR("portfolio_spark_month") };
|
||||
for (int s = 0; s < 5; s++) { if (s) ImGui::SameLine(); ImGui::RadioButton(spItems[s], &s_pf_spark_interval, s); }
|
||||
ImGui::EndDisabled();
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
|
||||
// ---- ADDRESSES (search, filter, select) ----
|
||||
@@ -958,12 +985,12 @@ static void RenderPortfolioEditor(App* app)
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
ImGui::InputTextWithHint("##pfSearch", TR("portfolio_search"), s_pf_search, sizeof(s_pf_search));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
// Type filter — segmented control on its own row, capped at 580px wide.
|
||||
// Type filter — segmented control on its own row (compact).
|
||||
{
|
||||
const char* flabels[3] = { TR("portfolio_select_all"), TR("shielded"), TR("transparent") };
|
||||
float segH = 26.0f * dp;
|
||||
float availW = ImGui::GetContentRegionAvail().x;
|
||||
float segW = std::min(availW, 580.0f * dp);
|
||||
float segW = std::min(availW, 360.0f * dp);
|
||||
float cellW = segW / 3.0f;
|
||||
ImVec2 sMin = ImGui::GetCursorScreenPos();
|
||||
ImDrawList* fdl = ImGui::GetWindowDrawList();
|
||||
@@ -1009,21 +1036,34 @@ static void RenderPortfolioEditor(App* app)
|
||||
if (sx != sy) return sx;
|
||||
return x->balance > y->balance;
|
||||
});
|
||||
// Select all / Clear (rounded pills) + Funded toggle — one row, left-aligned.
|
||||
// Select all / Clear (rounded pills) + Funded toggle — one row, right-aligned.
|
||||
{
|
||||
const char* selLbl = TR("portfolio_select_shown");
|
||||
const char* clrLbl = TR("portfolio_clear_sel");
|
||||
ImGuiStyle& st = ImGui::GetStyle();
|
||||
ImVec2 pillPad(Layout::spacingLg(), Layout::spacingSm()); // roomier text padding
|
||||
// Measure the checkbox at default padding and the pills at their (larger) padding, so
|
||||
// the whole group can be right-aligned before it's drawn.
|
||||
float defFrameH = ImGui::GetFrameHeight();
|
||||
float fundedW = defFrameH + st.ItemInnerSpacing.x + ImGui::CalcTextSize(TR("portfolio_funded")).x;
|
||||
float pillH = ImGui::GetTextLineHeight() + pillPad.y * 2.0f;
|
||||
float selW = ImGui::CalcTextSize(selLbl).x + pillPad.x * 2.0f;
|
||||
float clrW = ImGui::CalcTextSize(clrLbl).x + pillPad.x * 2.0f;
|
||||
float groupW = selW + clrW + fundedW + st.ItemSpacing.x * 2.0f;
|
||||
float rowH = std::max(pillH, defFrameH);
|
||||
float rowY = ImGui::GetCursorPosY();
|
||||
float availW = ImGui::GetContentRegionAvail().x;
|
||||
if (availW > groupW) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + availW - groupW);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 12.0f * dp);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(Layout::spacingMd(), Layout::spacingXs()));
|
||||
float pillH = ImGui::GetFrameHeight();
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, pillPad);
|
||||
ImGui::SetCursorPosY(rowY + (rowH - pillH) * 0.5f);
|
||||
if (ImGui::Button(selLbl))
|
||||
for (const AddressInfo* a : filtered) data::PortfolioEntryAdd(s_pf_addrs, a->address);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(clrLbl)) s_pf_addrs.clear();
|
||||
ImGui::PopStyleVar(2);
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosY(rowY + (pillH - ImGui::GetFrameHeight()) * 0.5f); // center vs. pills
|
||||
ImGui::SetCursorPosY(rowY + (rowH - defFrameH) * 0.5f); // center vs. pills
|
||||
ImGui::Checkbox(TR("portfolio_funded"), &s_pf_funded_only);
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
@@ -1092,24 +1132,31 @@ static void RenderPortfolioEditor(App* app)
|
||||
}
|
||||
pfEndScrollChild(s_pf_addr_fade, ldl);
|
||||
}
|
||||
ImGui::EndChild(); // ##pfDetailBody
|
||||
// Group-scoped actions (Add-entry height → lower hierarchy than the modal Close in the footer).
|
||||
{
|
||||
float bw = 104.0f * dp, sp = Layout::spacingSm();
|
||||
float grp = bw * 2 + sp;
|
||||
float availD = ImGui::GetContentRegionAvail().x;
|
||||
if (availD > grp) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + availD - grp);
|
||||
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + Layout::spacingSm());
|
||||
ImGui::BeginDisabled(!selDirty);
|
||||
if (ImGui::Button(TR("portfolio_revert"), ImVec2(bw, addH)))
|
||||
PortfolioBeginEdit(app, s_pf_sel); // reload the working set from the stored entry
|
||||
ImGui::SameLine(0, sp);
|
||||
ImGui::BeginDisabled(s_pf_label[0] == '\0');
|
||||
if (ImGui::Button(TR("portfolio_save"), ImVec2(bw, addH))) commitIfNeeded();
|
||||
ImGui::EndDisabled();
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
}
|
||||
ImGui::EndChild(); // ##pfDetail
|
||||
|
||||
// ================= FOOTER: revert / save / close (right-aligned, large) =================
|
||||
// ================= FOOTER: close (modal-level action, larger → higher hierarchy) =================
|
||||
{
|
||||
float bh = 40.0f * dp, bw = 112.0f * dp, sp = Layout::spacingSm();
|
||||
float aw = bw * 3 + sp * 2;
|
||||
float bh = 40.0f * dp, bw = 120.0f * dp;
|
||||
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, bh)))
|
||||
PortfolioBeginEdit(app, s_pf_sel); // reload the working set from the stored entry
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine(0, sp);
|
||||
ImGui::BeginDisabled(s_pf_label[0] == '\0');
|
||||
if (ImGui::Button(TR("portfolio_save"), ImVec2(bw, bh))) commitIfNeeded();
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine(0, sp);
|
||||
if (availF > bw) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + availF - bw);
|
||||
if (ImGui::Button(TR("portfolio_close"), ImVec2(bw, bh))) { commitIfNeeded(); s_portfolio_editor_open = false; }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user