feat(market): rework Manage-Portfolio as a full-window master-detail modal
Combine the old list/edit modes into one master-detail view rendered as a full-window overlay instead of a floating dialog. - Backdrop: new material::DrawFullWindowBlurBackdrop draws an opaque base first (so the sheet is opaque regardless of the UI-opacity slider — the acrylic path multiplies its alpha by GetUIOpacity), then a strong full-window blur of the backdrop, gated exactly like DrawGlassPanel so low-performance mode skips the blur entirely (just the opaque scrim). - Frame: bespoke full-window overlay (not BeginOverlayDialog) that mirrors the scrim plumbing and preserves both overlay popup gotchas verbatim (no focus-steal and no outside-click dismiss while a combo / color picker is open). Title-bar X, Esc, and outside-click all dismiss. - Left (master): selectable group list with accent bar + icon + name + value line, and a pinned "Add group" button. - Right (detail): live preview + label header, then a single-open accordion (Appearance = color + outline opacity + icon; Price; Addresses) so only one control group shows at a time — far less cluttered than the old two-column form. - Footer: Delete / Revert / Save. Editing auto-commits named groups on navigation and on close (preserving grid geometry); unnamed drafts are dropped. - s_pf_editing tri-state replaced by s_pf_sel (+ accordion open-state); the Manage button and grid-card click open with a valid selection. Design + adversarial review done via multi-agent workflows (understand/design, then a 3-lens ImGui-lifecycle / state-safety / backdrop review; zero confirmed defects). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -476,6 +476,24 @@ inline void DrawGlassPanel(ImDrawList* dl, const ImVec2& pMin,
|
||||
}
|
||||
}
|
||||
|
||||
// Full-window backdrop for a modal overlay. Lays an OPAQUE base first (so the sheet reads as
|
||||
// opaque regardless of the UI-opacity slider — the acrylic path multiplies its alpha by
|
||||
// GetUIOpacity(), so it can't guarantee opacity on its own), then — only when the acrylic backdrop
|
||||
// is active and we are NOT in low-performance mode — draws a strong blur of the (static) window
|
||||
// backdrop on top plus a dim. In low-perf mode only the opaque base is drawn (no blur cost),
|
||||
// mirroring DrawGlassPanel's gating.
|
||||
inline void DrawFullWindowBlurBackdrop(ImDrawList* dl, const ImVec2& pMin, const ImVec2& pMax)
|
||||
{
|
||||
dl->AddRectFilled(pMin, pMax, IM_COL32(9, 10, 16, 255)); // opaque base — guarantees opacity
|
||||
if (IsBackdropActive() && !dragonx::ui::effects::isLowSpecMode()
|
||||
&& effects::ImGuiAcrylic::IsEnabled() && effects::ImGuiAcrylic::IsAvailable()) {
|
||||
auto params = GetCurrentAcrylicTheme().card;
|
||||
params.blurRadius = 40.0f; // strong — spans the whole window
|
||||
effects::ImGuiAcrylic::DrawAcrylicRect(dl, pMin, pMax, params, 0.0f);
|
||||
dl->AddRectFilled(pMin, pMax, IM_COL32(6, 8, 18, 90)); // dim so the card reads as foreground
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Glass Card Scope (RAII) — channel-split glass-card scaffold
|
||||
// ============================================================================
|
||||
|
||||
@@ -94,7 +94,10 @@ static std::string FormatPrice(double price)
|
||||
|
||||
// ---- Portfolio editor (modal) state ----
|
||||
static bool s_portfolio_editor_open = false;
|
||||
static int s_pf_editing = -1; // -1 = list mode; -2 = adding new; >=0 = editing that index
|
||||
static int s_pf_sel = -1; // selected group index in the master list; -1 = none, -2 = unsaved draft
|
||||
static bool s_pf_sec_appearance = true; // right-pane accordion: which section is expanded
|
||||
static bool s_pf_sec_price = false;
|
||||
static bool s_pf_sec_addresses = false;
|
||||
static char s_pf_label[64] = {0};
|
||||
static std::vector<std::string> s_pf_addrs;
|
||||
static std::string s_pf_icon; // selected wallet-icon name ("" = none)
|
||||
@@ -302,7 +305,7 @@ static const ImU32 kPfPalette[] = {
|
||||
static void PortfolioBeginEdit(App* app, int index)
|
||||
{
|
||||
const auto& entries = app->settings()->getPortfolioEntries();
|
||||
s_pf_editing = index;
|
||||
s_pf_sel = index;
|
||||
if (index >= 0 && index < (int)entries.size()) {
|
||||
const auto& src = entries[index];
|
||||
snprintf(s_pf_label, sizeof(s_pf_label), "%s", src.label.c_str());
|
||||
@@ -343,144 +346,246 @@ static void PortfolioBeginEdit(App* app, int index)
|
||||
static void RenderPortfolioEditor(App* app)
|
||||
{
|
||||
if (!s_portfolio_editor_open) return;
|
||||
// Edit mode is a wider two-column layout; list mode stays compact.
|
||||
float dialogW = std::min((s_pf_editing == -1 ? 560.0f : 780.0f),
|
||||
ImGui::GetMainViewport()->Size.x * 0.92f);
|
||||
if (!material::BeginOverlayDialog(TR("portfolio_manage_title"), &s_portfolio_editor_open, dialogW)) {
|
||||
return;
|
||||
}
|
||||
|
||||
config::Settings* settings = app->settings();
|
||||
const auto& state = app->getWalletState();
|
||||
float dp = Layout::dpiScale();
|
||||
ImFont* body2f = Type().body2();
|
||||
ImFont* capF = Type().caption();
|
||||
ImFont* sub1f = Type().subtitle1();
|
||||
ImFont* iconFont = Type().iconSmall();
|
||||
float iconFsz = iconFont->LegacySize;
|
||||
|
||||
auto persist = [&](const std::vector<config::Settings::PortfolioEntry>& entries) {
|
||||
settings->setPortfolioEntries(entries);
|
||||
settings->save();
|
||||
};
|
||||
// Build a PortfolioEntry from the working set, preserving off-form fields (grid geometry) from base.
|
||||
auto buildWorking = [&](const config::Settings::PortfolioEntry& base) {
|
||||
config::Settings::PortfolioEntry e = base;
|
||||
e.label = s_pf_label; e.addresses = s_pf_addrs; e.icon = s_pf_icon;
|
||||
e.color = s_pf_color; e.outlineOpacity = s_pf_outline_opacity;
|
||||
e.priceBasis = s_pf_price_basis; e.manualPrice = s_pf_manual_price; e.manualCurrency = s_pf_manual_ccy;
|
||||
e.showDrgx = s_pf_show_drgx; e.showValue = s_pf_show_value; e.show24h = s_pf_show_24h;
|
||||
e.showSparkline = s_pf_show_sparkline; e.sparklineInterval = s_pf_spark_interval;
|
||||
return e;
|
||||
};
|
||||
auto workingMatches = [&](const config::Settings::PortfolioEntry& e) {
|
||||
return e.label == std::string(s_pf_label) && e.addresses == s_pf_addrs && e.icon == s_pf_icon
|
||||
&& e.color == s_pf_color && e.outlineOpacity == s_pf_outline_opacity
|
||||
&& e.priceBasis == s_pf_price_basis && e.manualPrice == s_pf_manual_price
|
||||
&& e.manualCurrency == std::string(s_pf_manual_ccy)
|
||||
&& e.showDrgx == s_pf_show_drgx && e.showValue == s_pf_show_value && e.show24h == s_pf_show_24h
|
||||
&& e.showSparkline == s_pf_show_sparkline && e.sparklineInterval == s_pf_spark_interval;
|
||||
};
|
||||
// Persist the working group when it is named and actually changed; unnamed drafts are dropped.
|
||||
auto commitIfNeeded = [&]() {
|
||||
if (s_pf_label[0] == '\0') return;
|
||||
auto entries = settings->getPortfolioEntries();
|
||||
bool existing = (s_pf_sel >= 0 && s_pf_sel < (int)entries.size());
|
||||
config::Settings::PortfolioEntry base;
|
||||
if (existing) { base = entries[s_pf_sel]; if (workingMatches(base)) return; }
|
||||
auto e = buildWorking(base);
|
||||
if (existing) entries[s_pf_sel] = e;
|
||||
else { entries.push_back(e); s_pf_sel = (int)entries.size() - 1; }
|
||||
persist(entries);
|
||||
};
|
||||
|
||||
if (s_pf_editing == -1) {
|
||||
// ---- LIST MODE ----
|
||||
// ---------------- Full-window overlay frame ----------------
|
||||
material::MarkOverlayDialogActive();
|
||||
ImGuiViewport* vp = ImGui::GetMainViewport();
|
||||
ImVec2 vpPos = vp->Pos, vpSize = vp->Size;
|
||||
// Don't force-focus the scrim while a popup (basis/interval combo, color picker) is open, or it
|
||||
// would close the popup; the same guard gates the outside-click dismiss below.
|
||||
const bool popupOpen = ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel);
|
||||
if (!popupOpen) ImGui::SetNextWindowFocus();
|
||||
ImGui::SetNextWindowPos(vpPos);
|
||||
ImGui::SetNextWindowSize(vpSize);
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0, 0, 0, 0)); // transparent — we draw the backdrop
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
|
||||
ImGui::Begin("##PortfolioOverlay", nullptr,
|
||||
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
||||
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse |
|
||||
ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoBringToFrontOnFocus);
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
material::DrawFullWindowBlurBackdrop(dl, vpPos, ImVec2(vpPos.x + vpSize.x, vpPos.y + vpSize.y));
|
||||
ImGui::SetCursorScreenPos(vpPos);
|
||||
ImGui::InvisibleButton("##pfInputBlocker", vpSize,
|
||||
ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_MouseButtonRight | ImGuiButtonFlags_MouseButtonMiddle);
|
||||
|
||||
// Card: large, centered, fixed size.
|
||||
float cardW = std::min(vpSize.x - 64.0f * dp, 1120.0f * dp);
|
||||
float cardH = std::min(vpSize.y - 64.0f * dp, 760.0f * dp);
|
||||
ImVec2 cardMin(vpPos.x + (vpSize.x - cardW) * 0.5f, vpPos.y + (vpSize.y - cardH) * 0.5f);
|
||||
ImVec2 cardMax(cardMin.x + cardW, cardMin.y + cardH);
|
||||
{ GlassPanelSpec cs; cs.rounding = 20.0f; cs.fillAlpha = 40; cs.borderAlpha = 60; cs.borderWidth = 1.0f;
|
||||
DrawGlassPanel(dl, cardMin, cardMax, cs); }
|
||||
|
||||
ImGui::SetCursorScreenPos(cardMin);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 20.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(28.0f, 20.0f));
|
||||
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0));
|
||||
ImGui::BeginChild("##pfCard", ImVec2(cardW, cardH), ImGuiChildFlags_None, ImGuiWindowFlags_NoScrollbar);
|
||||
|
||||
material::DrawDialogTitleBar(TR("portfolio_manage_title"), &s_portfolio_editor_open);
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_Escape)) s_portfolio_editor_open = false;
|
||||
|
||||
{ // clamp/validate the selection against the current entry list
|
||||
const auto& entriesRO = settings->getPortfolioEntries();
|
||||
if (s_pf_sel >= (int)entriesRO.size()) s_pf_sel = entriesRO.empty() ? -1 : 0;
|
||||
}
|
||||
|
||||
float footerH = 40.0f * dp;
|
||||
float bodyH = std::max(120.0f, ImGui::GetContentRegionAvail().y - footerH - Layout::spacingSm());
|
||||
float contentW = ImGui::GetContentRegionAvail().x;
|
||||
float gap = Layout::spacingLg();
|
||||
float masterW = std::min(std::max(contentW * 0.28f, 240.0f * dp), 320.0f * dp);
|
||||
float detailW = contentW - masterW - gap;
|
||||
|
||||
// ================= MASTER: group list =================
|
||||
ImGui::BeginChild("##pfMaster", ImVec2(masterW, bodyH), false, ImGuiWindowFlags_NoScrollbar);
|
||||
{
|
||||
const auto& entries = settings->getPortfolioEntries();
|
||||
if (entries.empty()) {
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("portfolio_no_entries"));
|
||||
}
|
||||
for (int i = 0; i < (int)entries.size(); i++) {
|
||||
ImGui::PushID(i);
|
||||
double bal = data::SumPortfolioBalance(entries[i].addresses, state.addresses);
|
||||
char row[160];
|
||||
snprintf(row, sizeof(row), "%s \xE2\x80\x94 %.4f %s (%d)",
|
||||
entries[i].label.c_str(), bal, DRAGONX_TICKER, (int)entries[i].addresses.size());
|
||||
ImGui::TextUnformatted(row);
|
||||
ImGui::SameLine();
|
||||
float bw = 72.0f;
|
||||
float avail = ImGui::GetContentRegionAvail().x;
|
||||
if (avail > bw * 2 + Layout::spacingSm())
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + avail - bw * 2 - Layout::spacingSm());
|
||||
if (ImGui::Button(TR("portfolio_edit"), ImVec2(bw, 0))) PortfolioBeginEdit(app, i);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(TR("portfolio_delete"), ImVec2(bw, 0))) {
|
||||
auto e = entries; e.erase(e.begin() + i); persist(e);
|
||||
float addH = 30.0f * dp;
|
||||
ImGui::BeginChild("##pfMasterList",
|
||||
ImVec2(0, std::max(48.0f, ImGui::GetContentRegionAvail().y - addH - Layout::spacingXs())), false);
|
||||
{
|
||||
ImDrawList* mdl = ImGui::GetWindowDrawList();
|
||||
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
|
||||
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();
|
||||
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);
|
||||
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);
|
||||
float ix = rmn.x + Layout::spacingSm() + 4.0f * dp;
|
||||
if (!en.icon.empty()) {
|
||||
material::project_icons::drawByName(mdl, en.icon,
|
||||
ImVec2(ix + sub1f->LegacySize * 0.5f, rmn.y + rowH * 0.5f),
|
||||
en.color ? (ImU32)en.color : OnSurfaceMedium(), iconFont, sub1f->LegacySize);
|
||||
ix += sub1f->LegacySize + Layout::spacingSm();
|
||||
}
|
||||
float txtRight = rmx.x - Layout::spacingSm();
|
||||
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();
|
||||
mdl->AddText(body2f, body2f->LegacySize, ImVec2(ix, rmn.y + rowH * 0.28f),
|
||||
en.label.empty() ? OnSurfaceDisabled() : OnSurface(), nm.c_str());
|
||||
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);
|
||||
ImGui::PopID();
|
||||
break;
|
||||
}
|
||||
ImGui::PopID();
|
||||
if (clickedSel != -999) { commitIfNeeded(); PortfolioBeginEdit(app, clickedSel); }
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
if (ImGui::Button(TR("portfolio_add_entry"), ImVec2(-1, addH))) { commitIfNeeded(); PortfolioBeginEdit(app, -2); }
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::SameLine(0, gap);
|
||||
|
||||
// ================= DETAIL: options for the selected group =================
|
||||
ImGui::BeginChild("##pfDetail", ImVec2(detailW, bodyH), false);
|
||||
if (s_pf_sel == -1) {
|
||||
const char* msg = TR("portfolio_detail_empty");
|
||||
ImVec2 av = ImGui::GetContentRegionAvail();
|
||||
ImVec2 ts = capF->CalcTextSizeA(capF->LegacySize, FLT_MAX, 0, msg);
|
||||
ImGui::SetCursorPos(ImVec2(std::max(0.0f, (av.x - ts.x) * 0.5f), av.y * 0.42f));
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), msg);
|
||||
} else {
|
||||
// ---- Header: live preview + label (always visible above the accordion) ----
|
||||
{
|
||||
ImDrawList* pdl = ImGui::GetWindowDrawList();
|
||||
float ppad = Layout::spacingMd();
|
||||
float pw = ImGui::GetContentRegionAvail().x;
|
||||
float ph = ppad * 2.0f + sub1f->LegacySize + Layout::spacingXs() + capF->LegacySize;
|
||||
ImVec2 pMin = ImGui::GetCursorScreenPos();
|
||||
ImVec2 pMax(pMin.x + pw, pMin.y + ph);
|
||||
ImU32 accent = s_pf_color ? (ImU32)s_pf_color : 0;
|
||||
GlassPanelSpec g; g.rounding = 10.0f; g.fillAlpha = 22; g.borderAlpha = 40;
|
||||
DrawGlassPanel(pdl, pMin, pMax, g);
|
||||
if (accent) {
|
||||
int oa = (int)(std::max(0, std::min(100, s_pf_outline_opacity)) * 2.55f + 0.5f);
|
||||
pdl->AddRect(pMin, pMax, WithAlpha(accent, oa), 10.0f, 0, 2.0f);
|
||||
}
|
||||
if (s_pf_show_sparkline && (s_pf_price_basis == 0 || s_pf_price_basis == 1)) {
|
||||
std::vector<double> h = pfSparklineSeries(state.market, s_pf_spark_interval);
|
||||
if (h.size() >= 2) {
|
||||
ImU32 spCol = WithAlpha(h.back() >= h.front() ? Success() : Error(), 80);
|
||||
ImVec2 spMin(pMin.x + ppad, pMin.y + ph * 0.46f);
|
||||
ImVec2 spMax(pMax.x - ppad, pMax.y - ppad * 0.6f);
|
||||
pfDrawSparkline(pdl, spMin, spMax, h, spCol);
|
||||
}
|
||||
}
|
||||
float rowY = pMin.y + ppad, nameX = pMin.x + ppad;
|
||||
if (!s_pf_icon.empty()) {
|
||||
material::project_icons::drawByName(pdl, s_pf_icon,
|
||||
ImVec2(nameX + sub1f->LegacySize * 0.5f, rowY + sub1f->LegacySize * 0.5f),
|
||||
accent ? accent : OnSurface(), iconFont, sub1f->LegacySize);
|
||||
nameX += sub1f->LegacySize + Layout::spacingSm();
|
||||
}
|
||||
const char* nm = s_pf_label[0] ? s_pf_label : TR("portfolio_group_name");
|
||||
pdl->AddText(sub1f, sub1f->LegacySize, ImVec2(nameX, rowY),
|
||||
s_pf_label[0] ? OnSurface() : OnSurfaceDisabled(), nm);
|
||||
double pbal = data::SumPortfolioBalance(s_pf_addrs, state.addresses);
|
||||
config::Settings::PortfolioEntry tmp;
|
||||
tmp.priceBasis = s_pf_price_basis;
|
||||
tmp.manualPrice = s_pf_manual_price;
|
||||
tmp.manualCurrency = s_pf_manual_ccy;
|
||||
tmp.showDrgx = s_pf_show_drgx;
|
||||
tmp.showValue = s_pf_show_value;
|
||||
tmp.show24h = s_pf_show_24h;
|
||||
PfDisplay pd = pfBuildDisplay(tmp, pbal, state.market);
|
||||
float primW = pd.primary.empty() ? 0.0f
|
||||
: body2f->CalcTextSizeA(body2f->LegacySize, FLT_MAX, 0, pd.primary.c_str()).x;
|
||||
if (!pd.primary.empty())
|
||||
pdl->AddText(body2f, body2f->LegacySize,
|
||||
ImVec2(pMax.x - ppad - primW, rowY + (sub1f->LegacySize - body2f->LegacySize) * 0.5f),
|
||||
OnSurface(), pd.primary.c_str());
|
||||
{
|
||||
float belowY = rowY + sub1f->LegacySize + Layout::spacingXs();
|
||||
float cursorR = pMax.x - ppad;
|
||||
if (!pd.change.empty()) {
|
||||
float cw = capF->CalcTextSizeA(capF->LegacySize, FLT_MAX, 0, pd.change.c_str()).x;
|
||||
pdl->AddText(capF, capF->LegacySize, ImVec2(cursorR - cw, belowY), pd.changeCol, pd.change.c_str());
|
||||
cursorR -= cw + Layout::spacingSm();
|
||||
}
|
||||
if (!pd.secondary.empty()) {
|
||||
float sw2 = capF->CalcTextSizeA(capF->LegacySize, FLT_MAX, 0, pd.secondary.c_str()).x;
|
||||
pdl->AddText(capF, capF->LegacySize, ImVec2(cursorR - sw2, belowY), OnSurfaceDisabled(), pd.secondary.c_str());
|
||||
}
|
||||
}
|
||||
ImGui::Dummy(ImVec2(pw, ph));
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
if (ImGui::Button(TR("portfolio_add_entry"))) PortfolioBeginEdit(app, -2);
|
||||
} else {
|
||||
// ---- EDIT MODE: two columns (Appearance | Addresses) ----
|
||||
float dp = Layout::dpiScale();
|
||||
ImFont* body2f = Type().body2();
|
||||
ImFont* capF = Type().caption();
|
||||
ImFont* sub1f = Type().subtitle1();
|
||||
ImFont* iconFont = Type().iconSmall();
|
||||
float iconFsz = iconFont->LegacySize;
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
ImGui::InputTextWithHint("##pfLabel", TR("portfolio_group_name"), s_pf_label, sizeof(s_pf_label));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
float contentW = ImGui::GetContentRegionAvail().x;
|
||||
float colGap = Layout::spacingLg();
|
||||
float leftW = std::max(220.0f, (contentW - colGap) * 0.40f);
|
||||
float rightW = contentW - colGap - leftW;
|
||||
float colH = std::min(ImGui::GetMainViewport()->Size.y * 0.52f, 460.0f * dp);
|
||||
|
||||
// ===================== LEFT: APPEARANCE =====================
|
||||
ImGui::BeginChild("##pfLeft", ImVec2(leftW, colH), false);
|
||||
{
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("portfolio_appearance"));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
// Live preview of the group card (icon + color + name + summed balance).
|
||||
{
|
||||
ImDrawList* pdl = ImGui::GetWindowDrawList();
|
||||
float ppad = Layout::spacingMd();
|
||||
float pw = ImGui::GetContentRegionAvail().x;
|
||||
float ph = ppad * 2.0f + sub1f->LegacySize + Layout::spacingXs() + capF->LegacySize;
|
||||
ImVec2 pMin = ImGui::GetCursorScreenPos();
|
||||
ImVec2 pMax(pMin.x + pw, pMin.y + ph);
|
||||
ImU32 accent = s_pf_color ? (ImU32)s_pf_color : 0;
|
||||
GlassPanelSpec g; g.rounding = 10.0f; g.fillAlpha = 22; g.borderAlpha = 40;
|
||||
DrawGlassPanel(pdl, pMin, pMax, g);
|
||||
// Colored outline around the whole preview card (matches the live group cards).
|
||||
if (accent) {
|
||||
int oa = (int)(std::max(0, std::min(100, s_pf_outline_opacity)) * 2.55f + 0.5f);
|
||||
pdl->AddRect(pMin, pMax, WithAlpha(accent, oa), 10.0f, 0, 2.0f);
|
||||
}
|
||||
if (s_pf_show_sparkline && (s_pf_price_basis == 0 || s_pf_price_basis == 1)) {
|
||||
std::vector<double> h = pfSparklineSeries(state.market, s_pf_spark_interval);
|
||||
if (h.size() >= 2) {
|
||||
ImU32 spCol = WithAlpha(h.back() >= h.front() ? Success() : Error(), 80);
|
||||
ImVec2 spMin(pMin.x + ppad, pMin.y + ph * 0.46f);
|
||||
ImVec2 spMax(pMax.x - ppad, pMax.y - ppad * 0.6f);
|
||||
pfDrawSparkline(pdl, spMin, spMax, h, spCol);
|
||||
}
|
||||
}
|
||||
float rowY = pMin.y + ppad, nameX = pMin.x + ppad;
|
||||
if (!s_pf_icon.empty()) {
|
||||
material::project_icons::drawByName(pdl, s_pf_icon,
|
||||
ImVec2(nameX + sub1f->LegacySize * 0.5f, rowY + sub1f->LegacySize * 0.5f),
|
||||
accent ? accent : OnSurface(), iconFont, sub1f->LegacySize);
|
||||
nameX += sub1f->LegacySize + Layout::spacingSm();
|
||||
}
|
||||
const char* nm = s_pf_label[0] ? s_pf_label : TR("portfolio_group_name");
|
||||
pdl->AddText(sub1f, sub1f->LegacySize, ImVec2(nameX, rowY),
|
||||
s_pf_label[0] ? OnSurface() : OnSurfaceDisabled(), nm);
|
||||
double pbal = data::SumPortfolioBalance(s_pf_addrs, state.addresses);
|
||||
config::Settings::PortfolioEntry tmp;
|
||||
tmp.priceBasis = s_pf_price_basis;
|
||||
tmp.manualPrice = s_pf_manual_price;
|
||||
tmp.manualCurrency = s_pf_manual_ccy;
|
||||
tmp.showDrgx = s_pf_show_drgx;
|
||||
tmp.showValue = s_pf_show_value;
|
||||
tmp.show24h = s_pf_show_24h;
|
||||
PfDisplay pd = pfBuildDisplay(tmp, pbal, state.market);
|
||||
float primW = pd.primary.empty() ? 0.0f
|
||||
: body2f->CalcTextSizeA(body2f->LegacySize, FLT_MAX, 0, pd.primary.c_str()).x;
|
||||
if (!pd.primary.empty())
|
||||
pdl->AddText(body2f, body2f->LegacySize,
|
||||
ImVec2(pMax.x - ppad - primW, rowY + (sub1f->LegacySize - body2f->LegacySize) * 0.5f),
|
||||
OnSurface(), pd.primary.c_str());
|
||||
{
|
||||
float belowY = rowY + sub1f->LegacySize + Layout::spacingXs();
|
||||
float cursorR = pMax.x - ppad;
|
||||
if (!pd.change.empty()) {
|
||||
float cw = capF->CalcTextSizeA(capF->LegacySize, FLT_MAX, 0, pd.change.c_str()).x;
|
||||
pdl->AddText(capF, capF->LegacySize, ImVec2(cursorR - cw, belowY), pd.changeCol, pd.change.c_str());
|
||||
cursorR -= cw + Layout::spacingSm();
|
||||
}
|
||||
if (!pd.secondary.empty()) {
|
||||
float sw2 = capF->CalcTextSizeA(capF->LegacySize, FLT_MAX, 0, pd.secondary.c_str()).x;
|
||||
pdl->AddText(capF, capF->LegacySize, ImVec2(cursorR - sw2, belowY), OnSurfaceDisabled(), pd.secondary.c_str());
|
||||
}
|
||||
}
|
||||
ImGui::Dummy(ImVec2(pw, ph));
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
|
||||
|
||||
// Label
|
||||
ImGui::TextUnformatted(TR("portfolio_label"));
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
ImGui::InputText("##pfLabel", s_pf_label, sizeof(s_pf_label));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
// Accordion: exactly one section expanded at a time.
|
||||
auto section = [&](const char* label, bool& openFlag) -> bool {
|
||||
ImGui::SetNextItemOpen(openFlag, ImGuiCond_Always);
|
||||
bool now = ImGui::CollapsingHeader(label);
|
||||
if (now != openFlag) { s_pf_sec_appearance = s_pf_sec_price = s_pf_sec_addresses = false; openFlag = now; }
|
||||
return openFlag;
|
||||
};
|
||||
|
||||
// ---- APPEARANCE (color + outline opacity + icon) ----
|
||||
if (section(TR("portfolio_appearance"), s_pf_sec_appearance)) {
|
||||
ImGui::Indent(Layout::spacingSm());
|
||||
// Color picker: preset swatches + a custom-color circle that opens a picker popup.
|
||||
{
|
||||
float sw = 22.0f * dp;
|
||||
@@ -503,9 +608,6 @@ static void RenderPortfolioEditor(App* app)
|
||||
if (ImGui::IsItemClicked()) s_pf_color = isDefault ? 0u : kPfPalette[c - 1];
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
// Custom-color circle: shows the current color when it isn't a preset, else a "+";
|
||||
// click opens a color-picker popup.
|
||||
bool isCustom = (s_pf_color != 0u);
|
||||
for (int c = 0; c < nColors; c++)
|
||||
if (s_pf_color == kPfPalette[c]) isCustom = false;
|
||||
@@ -541,7 +643,6 @@ static void RenderPortfolioEditor(App* app)
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
|
||||
// Accent-outline opacity (only meaningful when an accent color is set).
|
||||
ImGui::BeginDisabled(s_pf_color == 0u);
|
||||
ImGui::TextUnformatted(TR("portfolio_outline_opacity"));
|
||||
@@ -551,56 +652,11 @@ static void RenderPortfolioEditor(App* app)
|
||||
if (s_pf_outline_opacity > 100) s_pf_outline_opacity = 100;
|
||||
ImGui::EndDisabled();
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
// Price data: how this group's value is priced + which fields the card shows.
|
||||
{
|
||||
ImGui::TextUnformatted(TR("portfolio_price"));
|
||||
const char* basisItems[4] = { TR("portfolio_price_usd"), TR("portfolio_price_btc"),
|
||||
TR("portfolio_price_drgx"), TR("portfolio_price_manual") };
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
ImGui::Combo("##pfBasis", &s_pf_price_basis, basisItems, 4);
|
||||
|
||||
if (s_pf_price_basis == 3) { // Manual: price-per-DRGX + currency label
|
||||
float half = (ImGui::GetContentRegionAvail().x - Layout::spacingSm()) * 0.62f;
|
||||
ImGui::SetNextItemWidth(half);
|
||||
ImGui::InputDouble("##pfManPrice", &s_pf_manual_price, 0.0, 0.0, "%.6f");
|
||||
if (s_pf_manual_price < 0.0) s_pf_manual_price = 0.0;
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
ImGui::InputTextWithHint("##pfManCcy", TR("portfolio_currency"), s_pf_manual_ccy, sizeof(s_pf_manual_ccy));
|
||||
}
|
||||
|
||||
// Field toggles (which price fields appear on the card).
|
||||
ImGui::Checkbox(DRAGONX_TICKER "##pfShowDrgx", &s_pf_show_drgx);
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled(s_pf_price_basis == 2); // DRGX-only -> no value field
|
||||
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
|
||||
ImGui::Checkbox(TR("portfolio_show_24h"), &s_pf_show_24h);
|
||||
ImGui::EndDisabled();
|
||||
// Sparkline on its own line (also live-market only) + its interval.
|
||||
ImGui::BeginDisabled(!(s_pf_price_basis == 0 || s_pf_price_basis == 1));
|
||||
ImGui::Checkbox(TR("portfolio_show_sparkline"), &s_pf_show_sparkline);
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled(!s_pf_show_sparkline);
|
||||
const char* spItems[5] = { TR("portfolio_spark_min"), TR("portfolio_spark_hour"),
|
||||
TR("portfolio_spark_day"), TR("portfolio_spark_week"),
|
||||
TR("portfolio_spark_month") };
|
||||
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
|
||||
ImGui::Combo("##pfSparkInterval", &s_pf_spark_interval, spItems, 5);
|
||||
ImGui::EndDisabled();
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
// Icon picker — fills the remaining height of the left column.
|
||||
// Icon picker grid (fixed height, scrolls internally).
|
||||
{
|
||||
float cell = 28.0f * dp, cellGap = 4.0f * dp;
|
||||
ImGui::TextUnformatted(TR("portfolio_icon"));
|
||||
float iconGridH = std::max(cell * 2.0f + cellGap, ImGui::GetContentRegionAvail().y);
|
||||
ImGui::BeginChild("##pfIconGrid", ImVec2(0, iconGridH), true);
|
||||
ImGui::BeginChild("##pfIconGrid", ImVec2(0, 132.0f * dp), true);
|
||||
ImDrawList* gdl = ImGui::GetWindowDrawList();
|
||||
float availW = ImGui::GetContentRegionAvail().x;
|
||||
int cols = std::max(1, (int)((availW + cellGap) / (cell + cellGap)));
|
||||
@@ -639,32 +695,59 @@ static void RenderPortfolioEditor(App* app)
|
||||
}
|
||||
ImGui::EndChild();
|
||||
}
|
||||
ImGui::Unindent(Layout::spacingSm());
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::SameLine(0, colGap);
|
||||
|
||||
// ===================== RIGHT: ADDRESSES =====================
|
||||
ImGui::BeginChild("##pfRight", ImVec2(rightW, colH), false);
|
||||
{
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("portfolio_addresses_hdr"));
|
||||
// ---- PRICE (basis, manual price, shown fields, sparkline) ----
|
||||
if (section(TR("portfolio_price"), s_pf_sec_price)) {
|
||||
ImGui::Indent(Layout::spacingSm());
|
||||
const char* basisItems[4] = { TR("portfolio_price_usd"), TR("portfolio_price_btc"),
|
||||
TR("portfolio_price_drgx"), TR("portfolio_price_manual") };
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
ImGui::Combo("##pfBasis", &s_pf_price_basis, basisItems, 4);
|
||||
if (s_pf_price_basis == 3) { // Manual: price-per-DRGX + currency label
|
||||
float half = (ImGui::GetContentRegionAvail().x - Layout::spacingSm()) * 0.62f;
|
||||
ImGui::SetNextItemWidth(half);
|
||||
ImGui::InputDouble("##pfManPrice", &s_pf_manual_price, 0.0, 0.0, "%.6f");
|
||||
if (s_pf_manual_price < 0.0) s_pf_manual_price = 0.0;
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
ImGui::InputTextWithHint("##pfManCcy", TR("portfolio_currency"), s_pf_manual_ccy, sizeof(s_pf_manual_ccy));
|
||||
}
|
||||
ImGui::Checkbox(DRAGONX_TICKER "##pfShowDrgx", &s_pf_show_drgx);
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled(s_pf_price_basis == 2); // DRGX-only -> no value field
|
||||
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
|
||||
ImGui::Checkbox(TR("portfolio_show_24h"), &s_pf_show_24h);
|
||||
ImGui::EndDisabled();
|
||||
ImGui::BeginDisabled(!(s_pf_price_basis == 0 || s_pf_price_basis == 1));
|
||||
ImGui::Checkbox(TR("portfolio_show_sparkline"), &s_pf_show_sparkline);
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled(!s_pf_show_sparkline);
|
||||
const char* spItems[5] = { TR("portfolio_spark_min"), TR("portfolio_spark_hour"),
|
||||
TR("portfolio_spark_day"), TR("portfolio_spark_week"),
|
||||
TR("portfolio_spark_month") };
|
||||
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
|
||||
ImGui::Combo("##pfSparkInterval", &s_pf_spark_interval, spItems, 5);
|
||||
ImGui::EndDisabled();
|
||||
ImGui::EndDisabled();
|
||||
ImGui::Unindent(Layout::spacingSm());
|
||||
}
|
||||
|
||||
// ---- ADDRESSES (search, filter, select) ----
|
||||
if (section(TR("portfolio_addresses_hdr"), s_pf_sec_addresses)) {
|
||||
ImGui::Indent(Layout::spacingSm());
|
||||
{
|
||||
char selc[48];
|
||||
snprintf(selc, sizeof(selc), TR("portfolio_addresses_sel"), (int)s_pf_addrs.size());
|
||||
float w = capF->CalcTextSizeA(capF->LegacySize, FLT_MAX, 0, selc).x;
|
||||
float ax = ImGui::GetContentRegionAvail().x;
|
||||
if (ax > w) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ax - w);
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), selc);
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
// Search
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
ImGui::InputTextWithHint("##pfSearch", TR("portfolio_search"), s_pf_search, sizeof(s_pf_search));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
// Type filter chips + funded toggle
|
||||
{
|
||||
const char* flabels[3] = { TR("portfolio_select_all"), TR("shielded"), TR("transparent") };
|
||||
for (int f = 0; f < 3; f++) {
|
||||
@@ -679,8 +762,6 @@ static void RenderPortfolioEditor(App* app)
|
||||
ImGui::Checkbox(TR("portfolio_funded"), &s_pf_funded_only);
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
// Build the filtered address list (selected first, then by balance desc).
|
||||
std::string search = s_pf_search;
|
||||
std::vector<const AddressInfo*> filtered;
|
||||
for (const auto& a : state.addresses) {
|
||||
@@ -699,17 +780,12 @@ static void RenderPortfolioEditor(App* app)
|
||||
if (sx != sy) return sx;
|
||||
return x->balance > y->balance;
|
||||
});
|
||||
|
||||
// Select-shown / clear
|
||||
if (ImGui::SmallButton(TR("portfolio_select_shown")))
|
||||
for (const AddressInfo* a : filtered) data::PortfolioEntryAdd(s_pf_addrs, a->address);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton(TR("portfolio_clear_sel"))) s_pf_addrs.clear();
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
// Address list fills the remaining height in the right column.
|
||||
float listH = std::max(60.0f, ImGui::GetContentRegionAvail().y);
|
||||
ImGui::BeginChild("##pfAddrList", ImVec2(0, listH), true);
|
||||
ImGui::BeginChild("##pfAddrList", ImVec2(0, 300.0f * dp), true);
|
||||
ImDrawList* ldl = ImGui::GetWindowDrawList();
|
||||
float rowH = std::max(26.0f * dp, body2f->LegacySize + Layout::spacingSm());
|
||||
float lw = ImGui::GetContentRegionAvail().x;
|
||||
@@ -725,8 +801,6 @@ static void RenderPortfolioEditor(App* app)
|
||||
else if (rhov) ldl->AddRectFilled(rmn, rmx, IM_COL32(255, 255, 255, 14), 4.0f);
|
||||
float rcy = rmn.y + rowH * 0.5f;
|
||||
float rx = rmn.x + Layout::spacingSm();
|
||||
|
||||
// Checkbox indicator
|
||||
float cb = 15.0f * dp;
|
||||
ImVec2 cbMin(rx, rcy - cb * 0.5f), cbMax(rx + cb, rcy + cb * 0.5f);
|
||||
if (inSet) {
|
||||
@@ -739,8 +813,6 @@ static void RenderPortfolioEditor(App* app)
|
||||
ldl->AddRect(cbMin, cbMax, WithAlpha(OnSurface(), 120), 3.0f * dp, 0, 1.2f * dp);
|
||||
}
|
||||
rx += cb + Layout::spacingSm();
|
||||
|
||||
// Type chip (Z / T)
|
||||
bool isZ = (a.type == "shielded");
|
||||
const char* chip = isZ ? "Z" : "T";
|
||||
ImU32 chipCol = isZ ? Success() : Warning();
|
||||
@@ -749,15 +821,11 @@ static void RenderPortfolioEditor(App* app)
|
||||
ImVec2(rx + chipW, rcy + capF->LegacySize * 0.5f + 2.0f), WithAlpha(chipCol, 40), 4.0f * dp);
|
||||
ldl->AddText(capF, capF->LegacySize, ImVec2(rx + 4.0f * dp, rcy - capF->LegacySize * 0.5f), chipCol, chip);
|
||||
rx += chipW + Layout::spacingSm();
|
||||
|
||||
// Address's own icon (if set)
|
||||
std::string aicon = app->getAddressIcon(a.address);
|
||||
if (!aicon.empty()) {
|
||||
material::project_icons::drawByName(ldl, aicon, ImVec2(rx + iconFsz * 0.5f, rcy), OnSurfaceMedium(), iconFont, iconFsz);
|
||||
rx += iconFsz + Layout::spacingSm();
|
||||
}
|
||||
|
||||
// Balance (right) then primary text (label or truncated address).
|
||||
char balbuf[48];
|
||||
snprintf(balbuf, sizeof(balbuf), "%.4f", a.balance);
|
||||
float balW = capF->CalcTextSizeA(capF->LegacySize, FLT_MAX, 0, balbuf).x;
|
||||
@@ -772,8 +840,6 @@ static void RenderPortfolioEditor(App* app)
|
||||
inSet ? OnSurface() : OnSurfaceMedium(), primary.c_str());
|
||||
ldl->AddText(capF, capF->LegacySize, ImVec2(rmx.x - Layout::spacingSm() - balW, rcy - capF->LegacySize * 0.5f),
|
||||
a.balance > 1e-9 ? OnSurface() : OnSurfaceDisabled(), balbuf);
|
||||
|
||||
// Whole row toggles selection.
|
||||
ImGui::PushID(a.address.c_str());
|
||||
ImGui::InvisibleButton("##pfrow", ImVec2(lw, rowH));
|
||||
if (ImGui::IsItemClicked()) {
|
||||
@@ -783,43 +849,54 @@ static void RenderPortfolioEditor(App* app)
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::Unindent(Layout::spacingSm());
|
||||
}
|
||||
ImGui::EndChild();
|
||||
}
|
||||
ImGui::EndChild(); // ##pfDetail
|
||||
|
||||
// ===================== ACTIONS =====================
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
bool canSave = s_pf_label[0] != '\0';
|
||||
ImGui::BeginDisabled(!canSave);
|
||||
if (ImGui::Button(TR("portfolio_save"))) {
|
||||
// ================= FOOTER: delete / revert / save =================
|
||||
ImGui::Separator();
|
||||
{
|
||||
ImGui::BeginDisabled(s_pf_sel < 0);
|
||||
if (ImGui::Button(TR("portfolio_delete"))) {
|
||||
auto entries = settings->getPortfolioEntries();
|
||||
// Start from the existing entry when editing so its grid placement/size (and any
|
||||
// other fields not on this form) are preserved — editing must not reset the card.
|
||||
config::Settings::PortfolioEntry e;
|
||||
if (s_pf_editing >= 0 && s_pf_editing < (int)entries.size()) e = entries[s_pf_editing];
|
||||
e.label = s_pf_label;
|
||||
e.addresses = s_pf_addrs;
|
||||
e.icon = s_pf_icon;
|
||||
e.color = s_pf_color;
|
||||
e.outlineOpacity = s_pf_outline_opacity;
|
||||
e.priceBasis = s_pf_price_basis;
|
||||
e.manualPrice = s_pf_manual_price;
|
||||
e.manualCurrency = s_pf_manual_ccy;
|
||||
e.showDrgx = s_pf_show_drgx;
|
||||
e.showValue = s_pf_show_value;
|
||||
e.show24h = s_pf_show_24h;
|
||||
e.showSparkline = s_pf_show_sparkline;
|
||||
e.sparklineInterval = s_pf_spark_interval;
|
||||
if (s_pf_editing >= 0 && s_pf_editing < (int)entries.size()) entries[s_pf_editing] = e;
|
||||
else entries.push_back(e);
|
||||
persist(entries);
|
||||
s_pf_editing = -1;
|
||||
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();
|
||||
if (ImGui::Button(TR("portfolio_cancel"))) s_pf_editing = -1;
|
||||
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)
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled(s_pf_label[0] == '\0');
|
||||
if (ImGui::Button(TR("portfolio_save"), ImVec2(bw, 0))) commitIfNeeded();
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
|
||||
material::EndOverlayDialog();
|
||||
ImGui::EndChild(); // ##pfCard
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopStyleVar(2);
|
||||
|
||||
// Outside-click dismiss (guarded against the appearing frame and open popups).
|
||||
if (!popupOpen && !ImGui::IsWindowAppearing() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)
|
||||
&& !ImGui::IsMouseHoveringRect(cardMin, cardMax))
|
||||
s_portfolio_editor_open = false;
|
||||
// Commit named edits on any close path (X / Esc / outside-click); unnamed drafts are dropped.
|
||||
if (!s_portfolio_editor_open) commitIfNeeded();
|
||||
|
||||
ImGui::End();
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopStyleVar(3);
|
||||
}
|
||||
|
||||
void RenderMarketTab(App* app)
|
||||
@@ -1448,7 +1525,11 @@ void RenderMarketTab(App* app)
|
||||
ImGui::SameLine();
|
||||
float availX = ImGui::GetContentRegionAvail().x;
|
||||
if (availX > mBtnW) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + availX - mBtnW);
|
||||
if (ImGui::Button(ml, ImVec2(mBtnW, 0))) s_portfolio_editor_open = true;
|
||||
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);
|
||||
s_portfolio_editor_open = true;
|
||||
}
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
|
||||
@@ -1165,6 +1165,8 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["portfolio_delete"] = "Delete";
|
||||
strings_["portfolio_save"] = "Save";
|
||||
strings_["portfolio_cancel"] = "Cancel";
|
||||
strings_["portfolio_revert"] = "Revert";
|
||||
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";
|
||||
strings_["market_24h_change"] = "24h";
|
||||
|
||||
Reference in New Issue
Block a user