feat(contacts): deselect on empty click, actions left of globe, right-click menu
Three interaction refinements to the address list: - The globe badge now stays pinned far-right; the per-row copy/edit/delete actions appear to its LEFT on hover/selection instead of replacing it. - A left-click on empty space in the Cards/List area clears the current selection (no row/action hovered -> deselect). - Right-clicking a row (any view) selects it and opens a shared context menu (Copy address / Edit / Delete), rendered once after the list. Build + hygiene clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -352,6 +352,10 @@ void RenderContactsTab(App* app)
|
||||
material::Type().textColored(material::TypeStyle::Body2, material::OnSurfaceMedium(), msg);
|
||||
};
|
||||
|
||||
// Right-click a row (any view) -> select it and open the shared context menu (rendered after the
|
||||
// list). A left-click on empty list space clears the selection (handled in the Cards/List child).
|
||||
bool openContextMenu = false;
|
||||
|
||||
if (viewMode == 2) {
|
||||
// ── TABLE mode (material-ized): no outer/grid borders, row backgrounds, interactive sort. ──
|
||||
if (ImGui::BeginTable("AddressBookTable", 3,
|
||||
@@ -393,6 +397,9 @@ void RenderContactsTab(App* app)
|
||||
s_selected_index = static_cast<int>(i);
|
||||
if (ImGui::IsMouseDoubleClicked(0)) openEdit();
|
||||
}
|
||||
if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
|
||||
s_selected_index = static_cast<int>(i); s_confirm_delete_idx = -1; openContextMenu = true;
|
||||
}
|
||||
ImGui::TableNextColumn();
|
||||
bool shielded = isShieldedAddr(entry.address);
|
||||
ImGui::PushFont(material::Type().subtitle2());
|
||||
@@ -458,6 +465,9 @@ void RenderContactsTab(App* app)
|
||||
if (ImGui::IsMouseDoubleClicked(0)) openEdit();
|
||||
}
|
||||
const bool selHov = ImGui::IsItemHovered();
|
||||
if (selHov && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
|
||||
s_selected_index = static_cast<int>(i); s_confirm_delete_idx = -1; openContextMenu = true;
|
||||
}
|
||||
const ImVec2 afterRow = ImGui::GetCursorScreenPos(); // restore this for the next row
|
||||
const ImVec2 mn = p, mx(p.x + w, p.y + rowH);
|
||||
const bool rowHovered = ImGui::IsMouseHoveringRect(mn, mx); // whole-row hover (survives action-icon hover)
|
||||
@@ -483,26 +493,36 @@ void RenderContactsTab(App* app)
|
||||
const char* letter = shielded ? "Z" : "T";
|
||||
const ImVec2 ls = lblF->CalcTextSizeA(lblF->LegacySize, FLT_MAX, 0, letter);
|
||||
dl->AddText(lblF, lblF->LegacySize, ImVec2(avC.x - ls.x * 0.5f, avC.y - ls.y * 0.5f), tu, letter);
|
||||
// Label (line 1) + muted address (line 2) — reserve trailing room for the actions/globe.
|
||||
// Label (line 1) + muted address (line 2) — reserve trailing room for the globe + actions.
|
||||
const float actHit = icoF->LegacySize + 8.0f * dp; // per-action square hit area
|
||||
const float trailW = 3.0f * actHit + 6.0f * dp;
|
||||
const float globeW = entry.isGlobal() ? (icoF->LegacySize + 8.0f * dp) : 0.0f;
|
||||
const float trailW = 3.0f * actHit + 6.0f * dp + globeW;
|
||||
const float cy = mn.y + rowH * 0.5f;
|
||||
const float tx = mn.x + pad + avR * 2.0f + pad;
|
||||
const float textMaxX = mx.x - pad - trailW;
|
||||
const float blockH = lblF->LegacySize + adrF->LegacySize + 3.0f * dp;
|
||||
const float ty = mn.y + rowH * 0.5f - blockH * 0.5f;
|
||||
const float ty = cy - blockH * 0.5f;
|
||||
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);
|
||||
dl->AddText(adrF, adrF->LegacySize, ImVec2(tx, ty + lblF->LegacySize + 3.0f * dp),
|
||||
material::OnSurfaceMedium(), addr.c_str());
|
||||
// Trailing: per-row copy/edit/delete actions on hover/selection; else the globe badge.
|
||||
// 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;
|
||||
if (entry.isGlobal()) {
|
||||
const ImVec2 gs = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, ICON_MD_PUBLIC);
|
||||
dl->AddText(icoF, icoF->LegacySize, ImVec2(rightX - gs.x, cy - gs.y * 0.5f),
|
||||
material::OnSurfaceMedium(), ICON_MD_PUBLIC);
|
||||
rightX -= gs.x + 8.0f * dp;
|
||||
}
|
||||
if (rowHovered || selected) {
|
||||
const char* actGlyph[3] = { ICON_MD_CONTENT_COPY, ICON_MD_EDIT, ICON_MD_DELETE };
|
||||
const char* actTip[3] = { TR("copy_address"), TR("edit"), TR("delete") };
|
||||
float ax = mx.x - pad - actHit; // rightmost action's left edge (delete)
|
||||
float ax = rightX - actHit; // rightmost action (delete), just left of the globe
|
||||
for (int a = 2; a >= 0; --a) {
|
||||
ImGui::SetCursorScreenPos(ImVec2(ax, mn.y + rowH * 0.5f - actHit * 0.5f));
|
||||
ImGui::SetCursorScreenPos(ImVec2(ax, cy - actHit * 0.5f));
|
||||
ImGui::PushID(a + 100);
|
||||
const bool aClk = ImGui::InvisibleButton("##act", ImVec2(actHit, actHit));
|
||||
const bool aHov = ImGui::IsItemHovered();
|
||||
@@ -519,16 +539,11 @@ void RenderContactsTab(App* app)
|
||||
: material::OnSurfaceMedium();
|
||||
const ImVec2 gsz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, actGlyph[a]);
|
||||
dl->AddText(icoF, icoF->LegacySize,
|
||||
ImVec2(ax + (actHit - gsz.x) * 0.5f, mn.y + rowH * 0.5f - gsz.y * 0.5f),
|
||||
ImVec2(ax + (actHit - gsz.x) * 0.5f, cy - gsz.y * 0.5f),
|
||||
acol, actGlyph[a]);
|
||||
ImGui::PopID();
|
||||
ax -= actHit + 2.0f * dp;
|
||||
}
|
||||
} else if (entry.isGlobal()) {
|
||||
const ImVec2 gs = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, ICON_MD_PUBLIC);
|
||||
dl->AddText(icoF, icoF->LegacySize,
|
||||
ImVec2(mx.x - pad - gs.x, mn.y + rowH * 0.5f - gs.y * 0.5f),
|
||||
material::OnSurfaceMedium(), ICON_MD_PUBLIC);
|
||||
}
|
||||
if (selHov) material::Tooltip("%s", entry.address.c_str());
|
||||
ImGui::SetCursorScreenPos(afterRow); // undo the action buttons' cursor moves
|
||||
@@ -540,10 +555,25 @@ void RenderContactsTab(App* app)
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopStyleColor(3);
|
||||
}
|
||||
// A left-click on empty list space (no row/action hovered) clears the selection.
|
||||
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !ImGui::IsAnyItemHovered()) {
|
||||
s_selected_index = -1;
|
||||
s_confirm_delete_idx = -1;
|
||||
}
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
// Shared right-click context menu (opened by either view's row right-click; acts on the selection).
|
||||
if (openContextMenu) ImGui::OpenPopup("##contactCtx");
|
||||
if (ImGui::BeginPopup("##contactCtx")) {
|
||||
if (ImGui::MenuItem(TR("copy_address"))) doCopy();
|
||||
if (ImGui::MenuItem(TR("edit"))) openEdit();
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem(TR("delete"))) doDelete();
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
// Status line — singular form for exactly one contact ("1 address saved", not "1 addresses saved").
|
||||
ImGui::TextDisabled(TR(book.size() == 1 ? "address_book_count_one" : "address_book_count"), book.size());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user