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
|
// Glass Card Scope (RAII) — channel-split glass-card scaffold
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -94,7 +94,10 @@ static std::string FormatPrice(double price)
|
|||||||
|
|
||||||
// ---- Portfolio editor (modal) state ----
|
// ---- Portfolio editor (modal) state ----
|
||||||
static bool s_portfolio_editor_open = false;
|
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 char s_pf_label[64] = {0};
|
||||||
static std::vector<std::string> s_pf_addrs;
|
static std::vector<std::string> s_pf_addrs;
|
||||||
static std::string s_pf_icon; // selected wallet-icon name ("" = none)
|
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)
|
static void PortfolioBeginEdit(App* app, int index)
|
||||||
{
|
{
|
||||||
const auto& entries = app->settings()->getPortfolioEntries();
|
const auto& entries = app->settings()->getPortfolioEntries();
|
||||||
s_pf_editing = index;
|
s_pf_sel = index;
|
||||||
if (index >= 0 && index < (int)entries.size()) {
|
if (index >= 0 && index < (int)entries.size()) {
|
||||||
const auto& src = entries[index];
|
const auto& src = entries[index];
|
||||||
snprintf(s_pf_label, sizeof(s_pf_label), "%s", src.label.c_str());
|
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)
|
static void RenderPortfolioEditor(App* app)
|
||||||
{
|
{
|
||||||
if (!s_portfolio_editor_open) return;
|
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();
|
config::Settings* settings = app->settings();
|
||||||
const auto& state = app->getWalletState();
|
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) {
|
auto persist = [&](const std::vector<config::Settings::PortfolioEntry>& entries) {
|
||||||
settings->setPortfolioEntries(entries);
|
settings->setPortfolioEntries(entries);
|
||||||
settings->save();
|
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) {
|
// ---------------- Full-window overlay frame ----------------
|
||||||
// ---- LIST MODE ----
|
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();
|
const auto& entries = settings->getPortfolioEntries();
|
||||||
if (entries.empty()) {
|
float addH = 30.0f * dp;
|
||||||
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("portfolio_no_entries"));
|
ImGui::BeginChild("##pfMasterList",
|
||||||
}
|
ImVec2(0, std::max(48.0f, ImGui::GetContentRegionAvail().y - addH - Layout::spacingXs())), false);
|
||||||
for (int i = 0; i < (int)entries.size(); i++) {
|
{
|
||||||
ImGui::PushID(i);
|
ImDrawList* mdl = ImGui::GetWindowDrawList();
|
||||||
double bal = data::SumPortfolioBalance(entries[i].addresses, state.addresses);
|
if (entries.empty())
|
||||||
char row[160];
|
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("portfolio_no_entries"));
|
||||||
snprintf(row, sizeof(row), "%s \xE2\x80\x94 %.4f %s (%d)",
|
float rowH = 46.0f * dp;
|
||||||
entries[i].label.c_str(), bal, DRAGONX_TICKER, (int)entries[i].addresses.size());
|
int clickedSel = -999; // deferred: don't mutate `entries` mid-loop
|
||||||
ImGui::TextUnformatted(row);
|
for (int i = 0; i < (int)entries.size(); i++) {
|
||||||
ImGui::SameLine();
|
ImGui::PushID(i);
|
||||||
float bw = 72.0f;
|
const auto& en = entries[i];
|
||||||
float avail = ImGui::GetContentRegionAvail().x;
|
bool selRow = (s_pf_sel == i);
|
||||||
if (avail > bw * 2 + Layout::spacingSm())
|
ImVec2 rmn = ImGui::GetCursorScreenPos();
|
||||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + avail - bw * 2 - Layout::spacingSm());
|
if (ImGui::Selectable("##pfgrp", selRow, ImGuiSelectableFlags_SpanAvailWidth, ImVec2(0, rowH))) {
|
||||||
if (ImGui::Button(TR("portfolio_edit"), ImVec2(bw, 0))) PortfolioBeginEdit(app, i);
|
if (i != s_pf_sel) clickedSel = i;
|
||||||
ImGui::SameLine();
|
}
|
||||||
if (ImGui::Button(TR("portfolio_delete"), ImVec2(bw, 0))) {
|
ImVec2 rmx(rmn.x + ImGui::GetItemRectSize().x, rmn.y + rowH);
|
||||||
auto e = entries; e.erase(e.begin() + i); persist(e);
|
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();
|
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()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
if (ImGui::Button(TR("portfolio_add_entry"))) PortfolioBeginEdit(app, -2);
|
ImGui::SetNextItemWidth(-1);
|
||||||
} else {
|
ImGui::InputTextWithHint("##pfLabel", TR("portfolio_group_name"), s_pf_label, sizeof(s_pf_label));
|
||||||
// ---- EDIT MODE: two columns (Appearance | Addresses) ----
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
float dp = Layout::dpiScale();
|
|
||||||
ImFont* body2f = Type().body2();
|
|
||||||
ImFont* capF = Type().caption();
|
|
||||||
ImFont* sub1f = Type().subtitle1();
|
|
||||||
ImFont* iconFont = Type().iconSmall();
|
|
||||||
float iconFsz = iconFont->LegacySize;
|
|
||||||
|
|
||||||
float contentW = ImGui::GetContentRegionAvail().x;
|
// Accordion: exactly one section expanded at a time.
|
||||||
float colGap = Layout::spacingLg();
|
auto section = [&](const char* label, bool& openFlag) -> bool {
|
||||||
float leftW = std::max(220.0f, (contentW - colGap) * 0.40f);
|
ImGui::SetNextItemOpen(openFlag, ImGuiCond_Always);
|
||||||
float rightW = contentW - colGap - leftW;
|
bool now = ImGui::CollapsingHeader(label);
|
||||||
float colH = std::min(ImGui::GetMainViewport()->Size.y * 0.52f, 460.0f * dp);
|
if (now != openFlag) { s_pf_sec_appearance = s_pf_sec_price = s_pf_sec_addresses = false; openFlag = now; }
|
||||||
|
return openFlag;
|
||||||
// ===================== 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()));
|
|
||||||
|
|
||||||
|
// ---- 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.
|
// Color picker: preset swatches + a custom-color circle that opens a picker popup.
|
||||||
{
|
{
|
||||||
float sw = 22.0f * dp;
|
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];
|
if (ImGui::IsItemClicked()) s_pf_color = isDefault ? 0u : kPfPalette[c - 1];
|
||||||
ImGui::PopID();
|
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);
|
bool isCustom = (s_pf_color != 0u);
|
||||||
for (int c = 0; c < nColors; c++)
|
for (int c = 0; c < nColors; c++)
|
||||||
if (s_pf_color == kPfPalette[c]) isCustom = false;
|
if (s_pf_color == kPfPalette[c]) isCustom = false;
|
||||||
@@ -541,7 +643,6 @@ static void RenderPortfolioEditor(App* app)
|
|||||||
ImGui::EndPopup();
|
ImGui::EndPopup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accent-outline opacity (only meaningful when an accent color is set).
|
// Accent-outline opacity (only meaningful when an accent color is set).
|
||||||
ImGui::BeginDisabled(s_pf_color == 0u);
|
ImGui::BeginDisabled(s_pf_color == 0u);
|
||||||
ImGui::TextUnformatted(TR("portfolio_outline_opacity"));
|
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;
|
if (s_pf_outline_opacity > 100) s_pf_outline_opacity = 100;
|
||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||||
|
// Icon picker grid (fixed height, scrolls internally).
|
||||||
// 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.
|
|
||||||
{
|
{
|
||||||
float cell = 28.0f * dp, cellGap = 4.0f * dp;
|
float cell = 28.0f * dp, cellGap = 4.0f * dp;
|
||||||
ImGui::TextUnformatted(TR("portfolio_icon"));
|
ImGui::TextUnformatted(TR("portfolio_icon"));
|
||||||
float iconGridH = std::max(cell * 2.0f + cellGap, ImGui::GetContentRegionAvail().y);
|
ImGui::BeginChild("##pfIconGrid", ImVec2(0, 132.0f * dp), true);
|
||||||
ImGui::BeginChild("##pfIconGrid", ImVec2(0, iconGridH), true);
|
|
||||||
ImDrawList* gdl = ImGui::GetWindowDrawList();
|
ImDrawList* gdl = ImGui::GetWindowDrawList();
|
||||||
float availW = ImGui::GetContentRegionAvail().x;
|
float availW = ImGui::GetContentRegionAvail().x;
|
||||||
int cols = std::max(1, (int)((availW + cellGap) / (cell + cellGap)));
|
int cols = std::max(1, (int)((availW + cellGap) / (cell + cellGap)));
|
||||||
@@ -639,32 +695,59 @@ static void RenderPortfolioEditor(App* app)
|
|||||||
}
|
}
|
||||||
ImGui::EndChild();
|
ImGui::EndChild();
|
||||||
}
|
}
|
||||||
|
ImGui::Unindent(Layout::spacingSm());
|
||||||
}
|
}
|
||||||
ImGui::EndChild();
|
|
||||||
|
|
||||||
ImGui::SameLine(0, colGap);
|
// ---- PRICE (basis, manual price, shown fields, sparkline) ----
|
||||||
|
if (section(TR("portfolio_price"), s_pf_sec_price)) {
|
||||||
// ===================== RIGHT: ADDRESSES =====================
|
ImGui::Indent(Layout::spacingSm());
|
||||||
ImGui::BeginChild("##pfRight", ImVec2(rightW, colH), false);
|
const char* basisItems[4] = { TR("portfolio_price_usd"), TR("portfolio_price_btc"),
|
||||||
{
|
TR("portfolio_price_drgx"), TR("portfolio_price_manual") };
|
||||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("portfolio_addresses_hdr"));
|
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::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];
|
char selc[48];
|
||||||
snprintf(selc, sizeof(selc), TR("portfolio_addresses_sel"), (int)s_pf_addrs.size());
|
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);
|
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), selc);
|
||||||
}
|
}
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
||||||
|
|
||||||
// Search
|
|
||||||
ImGui::SetNextItemWidth(-1);
|
ImGui::SetNextItemWidth(-1);
|
||||||
ImGui::InputTextWithHint("##pfSearch", TR("portfolio_search"), s_pf_search, sizeof(s_pf_search));
|
ImGui::InputTextWithHint("##pfSearch", TR("portfolio_search"), s_pf_search, sizeof(s_pf_search));
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||||
|
|
||||||
// Type filter chips + funded toggle
|
|
||||||
{
|
{
|
||||||
const char* flabels[3] = { TR("portfolio_select_all"), TR("shielded"), TR("transparent") };
|
const char* flabels[3] = { TR("portfolio_select_all"), TR("shielded"), TR("transparent") };
|
||||||
for (int f = 0; f < 3; f++) {
|
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::Checkbox(TR("portfolio_funded"), &s_pf_funded_only);
|
||||||
}
|
}
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||||
|
|
||||||
// Build the filtered address list (selected first, then by balance desc).
|
|
||||||
std::string search = s_pf_search;
|
std::string search = s_pf_search;
|
||||||
std::vector<const AddressInfo*> filtered;
|
std::vector<const AddressInfo*> filtered;
|
||||||
for (const auto& a : state.addresses) {
|
for (const auto& a : state.addresses) {
|
||||||
@@ -699,17 +780,12 @@ static void RenderPortfolioEditor(App* app)
|
|||||||
if (sx != sy) return sx;
|
if (sx != sy) return sx;
|
||||||
return x->balance > y->balance;
|
return x->balance > y->balance;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Select-shown / clear
|
|
||||||
if (ImGui::SmallButton(TR("portfolio_select_shown")))
|
if (ImGui::SmallButton(TR("portfolio_select_shown")))
|
||||||
for (const AddressInfo* a : filtered) data::PortfolioEntryAdd(s_pf_addrs, a->address);
|
for (const AddressInfo* a : filtered) data::PortfolioEntryAdd(s_pf_addrs, a->address);
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ImGui::SmallButton(TR("portfolio_clear_sel"))) s_pf_addrs.clear();
|
if (ImGui::SmallButton(TR("portfolio_clear_sel"))) s_pf_addrs.clear();
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||||
|
ImGui::BeginChild("##pfAddrList", ImVec2(0, 300.0f * dp), true);
|
||||||
// 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);
|
|
||||||
ImDrawList* ldl = ImGui::GetWindowDrawList();
|
ImDrawList* ldl = ImGui::GetWindowDrawList();
|
||||||
float rowH = std::max(26.0f * dp, body2f->LegacySize + Layout::spacingSm());
|
float rowH = std::max(26.0f * dp, body2f->LegacySize + Layout::spacingSm());
|
||||||
float lw = ImGui::GetContentRegionAvail().x;
|
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);
|
else if (rhov) ldl->AddRectFilled(rmn, rmx, IM_COL32(255, 255, 255, 14), 4.0f);
|
||||||
float rcy = rmn.y + rowH * 0.5f;
|
float rcy = rmn.y + rowH * 0.5f;
|
||||||
float rx = rmn.x + Layout::spacingSm();
|
float rx = rmn.x + Layout::spacingSm();
|
||||||
|
|
||||||
// Checkbox indicator
|
|
||||||
float cb = 15.0f * dp;
|
float cb = 15.0f * dp;
|
||||||
ImVec2 cbMin(rx, rcy - cb * 0.5f), cbMax(rx + cb, rcy + cb * 0.5f);
|
ImVec2 cbMin(rx, rcy - cb * 0.5f), cbMax(rx + cb, rcy + cb * 0.5f);
|
||||||
if (inSet) {
|
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);
|
ldl->AddRect(cbMin, cbMax, WithAlpha(OnSurface(), 120), 3.0f * dp, 0, 1.2f * dp);
|
||||||
}
|
}
|
||||||
rx += cb + Layout::spacingSm();
|
rx += cb + Layout::spacingSm();
|
||||||
|
|
||||||
// Type chip (Z / T)
|
|
||||||
bool isZ = (a.type == "shielded");
|
bool isZ = (a.type == "shielded");
|
||||||
const char* chip = isZ ? "Z" : "T";
|
const char* chip = isZ ? "Z" : "T";
|
||||||
ImU32 chipCol = isZ ? Success() : Warning();
|
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);
|
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);
|
ldl->AddText(capF, capF->LegacySize, ImVec2(rx + 4.0f * dp, rcy - capF->LegacySize * 0.5f), chipCol, chip);
|
||||||
rx += chipW + Layout::spacingSm();
|
rx += chipW + Layout::spacingSm();
|
||||||
|
|
||||||
// Address's own icon (if set)
|
|
||||||
std::string aicon = app->getAddressIcon(a.address);
|
std::string aicon = app->getAddressIcon(a.address);
|
||||||
if (!aicon.empty()) {
|
if (!aicon.empty()) {
|
||||||
material::project_icons::drawByName(ldl, aicon, ImVec2(rx + iconFsz * 0.5f, rcy), OnSurfaceMedium(), iconFont, iconFsz);
|
material::project_icons::drawByName(ldl, aicon, ImVec2(rx + iconFsz * 0.5f, rcy), OnSurfaceMedium(), iconFont, iconFsz);
|
||||||
rx += iconFsz + Layout::spacingSm();
|
rx += iconFsz + Layout::spacingSm();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Balance (right) then primary text (label or truncated address).
|
|
||||||
char balbuf[48];
|
char balbuf[48];
|
||||||
snprintf(balbuf, sizeof(balbuf), "%.4f", a.balance);
|
snprintf(balbuf, sizeof(balbuf), "%.4f", a.balance);
|
||||||
float balW = capF->CalcTextSizeA(capF->LegacySize, FLT_MAX, 0, balbuf).x;
|
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());
|
inSet ? OnSurface() : OnSurfaceMedium(), primary.c_str());
|
||||||
ldl->AddText(capF, capF->LegacySize, ImVec2(rmx.x - Layout::spacingSm() - balW, rcy - capF->LegacySize * 0.5f),
|
ldl->AddText(capF, capF->LegacySize, ImVec2(rmx.x - Layout::spacingSm() - balW, rcy - capF->LegacySize * 0.5f),
|
||||||
a.balance > 1e-9 ? OnSurface() : OnSurfaceDisabled(), balbuf);
|
a.balance > 1e-9 ? OnSurface() : OnSurfaceDisabled(), balbuf);
|
||||||
|
|
||||||
// Whole row toggles selection.
|
|
||||||
ImGui::PushID(a.address.c_str());
|
ImGui::PushID(a.address.c_str());
|
||||||
ImGui::InvisibleButton("##pfrow", ImVec2(lw, rowH));
|
ImGui::InvisibleButton("##pfrow", ImVec2(lw, rowH));
|
||||||
if (ImGui::IsItemClicked()) {
|
if (ImGui::IsItemClicked()) {
|
||||||
@@ -783,43 +849,54 @@ static void RenderPortfolioEditor(App* app)
|
|||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
}
|
}
|
||||||
ImGui::EndChild();
|
ImGui::EndChild();
|
||||||
|
ImGui::Unindent(Layout::spacingSm());
|
||||||
}
|
}
|
||||||
ImGui::EndChild();
|
}
|
||||||
|
ImGui::EndChild(); // ##pfDetail
|
||||||
|
|
||||||
// ===================== ACTIONS =====================
|
// ================= FOOTER: delete / revert / save =================
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
ImGui::Separator();
|
||||||
bool canSave = s_pf_label[0] != '\0';
|
{
|
||||||
ImGui::BeginDisabled(!canSave);
|
ImGui::BeginDisabled(s_pf_sel < 0);
|
||||||
if (ImGui::Button(TR("portfolio_save"))) {
|
if (ImGui::Button(TR("portfolio_delete"))) {
|
||||||
auto entries = settings->getPortfolioEntries();
|
auto entries = settings->getPortfolioEntries();
|
||||||
// Start from the existing entry when editing so its grid placement/size (and any
|
if (s_pf_sel >= 0 && s_pf_sel < (int)entries.size()) {
|
||||||
// other fields not on this form) are preserved — editing must not reset the card.
|
entries.erase(entries.begin() + s_pf_sel);
|
||||||
config::Settings::PortfolioEntry e;
|
persist(entries);
|
||||||
if (s_pf_editing >= 0 && s_pf_editing < (int)entries.size()) e = entries[s_pf_editing];
|
if (entries.empty()) s_pf_sel = -1;
|
||||||
e.label = s_pf_label;
|
else { s_pf_sel = std::min(s_pf_sel, (int)entries.size() - 1); PortfolioBeginEdit(app, s_pf_sel); }
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
ImGui::EndDisabled();
|
ImGui::EndDisabled();
|
||||||
|
|
||||||
|
float bw = 92.0f * dp, aw = bw * 2 + Layout::spacingSm();
|
||||||
ImGui::SameLine();
|
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)
|
void RenderMarketTab(App* app)
|
||||||
@@ -1448,7 +1525,11 @@ void RenderMarketTab(App* app)
|
|||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
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))) 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()));
|
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||||
|
|
||||||
|
|||||||
@@ -1165,6 +1165,8 @@ void I18n::loadBuiltinEnglish()
|
|||||||
strings_["portfolio_delete"] = "Delete";
|
strings_["portfolio_delete"] = "Delete";
|
||||||
strings_["portfolio_save"] = "Save";
|
strings_["portfolio_save"] = "Save";
|
||||||
strings_["portfolio_cancel"] = "Cancel";
|
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_add_to"] = "Add to portfolio";
|
||||||
strings_["portfolio_remove_from"] = "Remove from portfolio";
|
strings_["portfolio_remove_from"] = "Remove from portfolio";
|
||||||
strings_["market_24h_change"] = "24h";
|
strings_["market_24h_change"] = "24h";
|
||||||
|
|||||||
Reference in New Issue
Block a user