fix(contacts): edit-dialog layout polish + per-row edit + hover address
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) <noreply@anthropic.com>
This commit is contained in:
@@ -187,9 +187,12 @@ void RenderContactsTab(App* app)
|
||||
|
||||
bool has_selection = s_selected_index >= 0 && s_selected_index < static_cast<int>(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<int>(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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user