diff --git a/src/ui/windows/market_tab.cpp b/src/ui/windows/market_tab.cpp index 21d4a2b..7a1fafa 100644 --- a/src/ui/windows/market_tab.cpp +++ b/src/ui/windows/market_tab.cpp @@ -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 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; } } diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index b2386d5..d951975 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -1147,6 +1147,7 @@ void I18n::loadBuiltinEnglish() strings_["portfolio_appearance"] = "Appearance"; strings_["portfolio_addresses_hdr"] = "Addresses"; strings_["portfolio_search"] = "Search addresses\xE2\x80\xA6"; + strings_["portfolio_search_icons"] = "Search icons\xE2\x80\xA6"; strings_["portfolio_funded"] = "Funded"; strings_["portfolio_select_all"] = "All"; strings_["portfolio_select_shown"] = "Select all";