From a338fac208f023930d0b23ff5f6616d9bc6f53bc Mon Sep 17 00:00:00 2001 From: DanS Date: Wed, 15 Jul 2026 08:53:05 -0500 Subject: [PATCH] fix(contacts): edit-dialog layout polish + per-row edit + hover address MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address five reported issues: - Address field is now full width with a centered Paste button beneath it (was a narrow field with Paste crammed alongside). - The pinned "Show in every wallet" checkbox no longer clips at the column bottom — the Notes fill reserves a clear margin for it. - Image mode: more spacing between the preview circle and the Choose/Remove row so the button isn't crowding the avatar. - Contact list: hovering a row now un-collapses the address to its full form inline (clipped to the text column) instead of popping a tooltip. - Per-row copy/edit/delete icons fire on the first click on an unselected row: the action lambdas validate s_selected_index freshly (via selValid()) instead of the frame-top has_selection bool, which was stale in the same frame the icon set the selection. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/ui/windows/contacts_tab.cpp | 46 +++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/src/ui/windows/contacts_tab.cpp b/src/ui/windows/contacts_tab.cpp index 8250dd3..3e876bf 100644 --- a/src/ui/windows/contacts_tab.cpp +++ b/src/ui/windows/contacts_tab.cpp @@ -187,9 +187,12 @@ void RenderContactsTab(App* app) bool has_selection = s_selected_index >= 0 && s_selected_index < static_cast(book.size()); - // Shared delete/copy actions (used by both the toolbar buttons and keyboard shortcuts). + // Shared delete/copy/edit actions (used by the toolbar, keyboard shortcuts, and per-row icons). + // Each validates s_selected_index FRESHLY rather than the frame-top has_selection bool: a per-row + // icon sets the selection then invokes the action in the SAME frame, so the cached bool is stale. + auto selValid = [&]() { return s_selected_index >= 0 && s_selected_index < static_cast(book.size()); }; auto doDelete = [&]() { - if (!has_selection) return; + if (!selValid()) return; if (s_confirm_delete_idx == s_selected_index) { book.removeEntry(s_selected_index); s_selected_index = -1; @@ -201,12 +204,12 @@ void RenderContactsTab(App* app) } }; auto doCopy = [&]() { - if (!has_selection) return; + if (!selValid()) return; ImGui::SetClipboardText(book.entries()[s_selected_index].address.c_str()); Notifications::instance().info(TR("address_copied")); }; auto openEdit = [&]() { - if (!has_selection) return; + if (!selValid()) return; loadEditFields(book.entries()[s_selected_index]); s_show_edit_dialog = true; s_focus_edit_field = true; @@ -327,16 +330,14 @@ void RenderContactsTab(App* app) ImGui::Spacing(); - // Address + inline Paste (available for both add and edit — handy when correcting an address). + // Address (full width) with a centered Paste button beneath it. + material::LabeledInput(TR("address_label"), isEdit ? "##EditAddress" : "##AddAddress", + s_edit_address, sizeof(s_edit_address), -1.0f); { float pasteW = btnFont->CalcTextSizeA(btnFont->LegacySize, FLT_MAX, 0, TR("paste")).x - + ImGui::GetStyle().FramePadding.x * 2.0f + 12.0f * dp; - float addrW = ImGui::GetContentRegionAvail().x - pasteW - ImGui::GetStyle().ItemSpacing.x; - material::LabeledInput(TR("address_label"), isEdit ? "##EditAddress" : "##AddAddress", - s_edit_address, sizeof(s_edit_address), addrW); - ImGui::SameLine(0, ImGui::GetStyle().ItemSpacing.x); - // Bottom-align the button with the input box (skip the label line above the input). - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ImGui::GetTextLineHeightWithSpacing()); + + ImGui::GetStyle().FramePadding.x * 2.0f + 24.0f * dp; + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + + std::max(0.0f, (ImGui::GetContentRegionAvail().x - pasteW) * 0.5f)); if (material::TactileButton(TR("paste"), ImVec2(pasteW, 0), btnFont)) { const char* clipboard = ImGui::GetClipboardText(); if (clipboard) copyEditField(s_edit_address, sizeof(s_edit_address), clipboard); @@ -346,8 +347,10 @@ void RenderContactsTab(App* app) ImGui::Spacing(); // Notes grows to fill the column above the Global checkbox, which is pinned to the bottom. + // Reserve the checkbox frame + the item spacing before it + a clear bottom margin so its + // descenders ("y"/"g") never clip against the child's edge. { - float globalReserve = ImGui::GetFrameHeight() + Layout::spacingSm(); + float globalReserve = ImGui::GetFrameHeight() + Layout::spacingLg() * 2.0f; float labelLine = ImGui::GetTextLineHeightWithSpacing(); float notesFill = std::max(60.0f * dp, ImGui::GetContentRegionAvail().y - globalReserve - labelLine); @@ -514,8 +517,8 @@ void RenderContactsTab(App* app) ImVec2 a = ImGui::GetContentRegionAvail(); ImVec2 origin = ImGui::GetCursorScreenPos(); float btnH = 40.0f * dp; - float blockH = prR * 2.0f + Layout::spacingSm() - + (hasImg ? capF->LegacySize + Layout::spacingSm() : 0.0f) + btnH; + float blockH = prR * 2.0f + Layout::spacingLg() + + (hasImg ? capF->LegacySize + Layout::spacingMd() : 0.0f) + btnH; float topPad = std::max(Layout::spacingSm(), (a.y - blockH) * 0.32f); float cx = origin.x + a.x * 0.5f; float cy = origin.y + topPad + prR; @@ -547,13 +550,13 @@ void RenderContactsTab(App* app) mdl->AddText(gf, gsz, ImVec2(cc.x - gs.x*0.5f, cc.y - gs.y*0.5f), material::OnSurfaceDisabled(), ICON_MD_ADD_PHOTO_ALTERNATE); } - float rowY = cy + prR + Layout::spacingSm(); + float rowY = cy + prR + Layout::spacingLg(); // clear gap between the preview and the row if (hasImg) { std::string fname = std::filesystem::path(s_edit_avatar.substr(4)).filename().string(); std::string fit = fitText(fname, capF, a.x - 8.0f * dp); ImVec2 fs = capF->CalcTextSizeA(capF->LegacySize, FLT_MAX, 0, fit.c_str()); mdl->AddText(capF, capF->LegacySize, ImVec2(cx - fs.x*0.5f, rowY), material::OnSurfaceMedium(), fit.c_str()); - rowY += capF->LegacySize + Layout::spacingSm(); + rowY += capF->LegacySize + Layout::spacingMd(); } // Buttons row, centered. ImGui::SetCursorScreenPos(ImVec2(origin.x, rowY)); @@ -915,9 +918,15 @@ void RenderContactsTab(App* app) dl->PushClipRect(ImVec2(tx, mn.y), ImVec2(textMaxX, mx.y), true); dl->AddText(lblF, lblF->LegacySize, ImVec2(tx, ty), material::OnSurface(), entry.label.c_str()); dl->PopClipRect(); - std::string addr = util::truncateMiddle(entry.address, addrFrontLbl.truncate, addrBackLbl.truncate); + // Un-collapse to the full address on hover (clipped to the text column so it never + // runs under the trailing actions); middle-truncated otherwise. + std::string addr = rowHovered + ? entry.address + : util::truncateMiddle(entry.address, addrFrontLbl.truncate, addrBackLbl.truncate); + dl->PushClipRect(ImVec2(tx, mn.y), ImVec2(textMaxX, mx.y), true); dl->AddText(adrF, adrF->LegacySize, ImVec2(tx, ty + lblF->LegacySize + 3.0f * dp), material::OnSurfaceMedium(), addr.c_str()); + dl->PopClipRect(); // Trailing: the globe badge stays pinned far-right (global contacts); per-row copy/edit/ // delete actions appear to its LEFT on hover/selection. float rightX = mx.x - pad; @@ -955,7 +964,6 @@ void RenderContactsTab(App* app) ax -= actHit + 2.0f * dp; } } - if (selHov) material::Tooltip("%s", entry.address.c_str()); ImGui::SetCursorScreenPos(afterRow); // undo the action buttons' cursor moves ImGui::PopID(); }