fix(contacts): audit fixes — clip, tofu badge, plural, DPI, columns, toolbar
Address the confirmed findings from a workflow audit of the Contacts tab body:
- Label clip: the Label column was WidthFixed 150px and clipped long labels
mid-word ("drgx pool payout a…"). Make all three columns WidthStretch (label 1.5,
address 2.6, notes 1.0) so the label grows with the tab and the notes column no
longer reserves a fixed empty block on the right (notes = low weight, per request).
- Globe badge tofu: the global-contact ICON_MD_PUBLIC badge was drawn with the text
font (no Material glyphs) → rendered as "?". Push Type().iconSmall() around it.
- Plural: "1 addresses saved" -> add address_book_count_one ("%zu address saved",
8 langs, %zu kept so the format signature matches) and branch on count == 1.
- DPI: the SameLine badge gaps (6/8px) and the table-height floor (120px) are now
* Layout::dpiScale(); the WidthStretch columns are relative so need no scaling.
- Toolbar: give the four actions leading Material icons (person-add / edit / delete /
content-copy) and accent the primary "Add New" — via a small local icon+label
tactile-button helper (ImGui has no two-font button), icons inherit the
disabled-aware text alpha so they gray out with BeginDisabled().
Build + ctest + hygiene clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,7 @@
|
||||
"address_book_added": "Adresse zum Buch hinzugefügt",
|
||||
"address_book_confirm_delete": "Löschen bestätigen?",
|
||||
"address_book_count": "%zu Adressen gespeichert",
|
||||
"address_book_count_one": "%zu Adresse gespeichert",
|
||||
"address_book_deleted": "Eintrag gelöscht",
|
||||
"address_book_edit": "Adresse bearbeiten",
|
||||
"address_book_empty": "Keine gespeicherten Adressen. Klicken Sie auf 'Neue hinzufügen', um eine hinzuzufügen.",
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"address_book_added": "Dirección agregada a la libreta",
|
||||
"address_book_confirm_delete": "¿Confirmar eliminación?",
|
||||
"address_book_count": "%zu direcciones guardadas",
|
||||
"address_book_count_one": "%zu dirección guardada",
|
||||
"address_book_deleted": "Entrada eliminada",
|
||||
"address_book_edit": "Editar Dirección",
|
||||
"address_book_empty": "No hay direcciones guardadas. Haz clic en 'Agregar Nueva' para añadir una.",
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"address_book_added": "Adresse ajoutée au carnet",
|
||||
"address_book_confirm_delete": "Confirmer la suppression ?",
|
||||
"address_book_count": "%zu adresses enregistrées",
|
||||
"address_book_count_one": "%zu adresse enregistrée",
|
||||
"address_book_deleted": "Entrée supprimée",
|
||||
"address_book_edit": "Modifier l'adresse",
|
||||
"address_book_empty": "Aucune adresse enregistrée. Cliquez sur 'Ajouter' pour en créer une.",
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"address_book_added": "アドレスをアドレス帳に追加しました",
|
||||
"address_book_confirm_delete": "削除しますか?",
|
||||
"address_book_count": "%zu 件のアドレスを保存済み",
|
||||
"address_book_count_one": "%zu 件のアドレスを保存",
|
||||
"address_book_deleted": "エントリを削除しました",
|
||||
"address_book_edit": "アドレスを編集",
|
||||
"address_book_empty": "保存されたアドレスがありません。「新規追加」をクリックして追加してください。",
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"address_book_added": "주소록에 주소를 추가했습니다",
|
||||
"address_book_confirm_delete": "삭제하시겠습니까?",
|
||||
"address_book_count": "저장된 주소 %zu개",
|
||||
"address_book_count_one": "주소 %zu개 저장됨",
|
||||
"address_book_deleted": "항목이 삭제되었습니다",
|
||||
"address_book_edit": "주소 편집",
|
||||
"address_book_empty": "저장된 주소가 없습니다. '새로 추가'를 클릭하여 추가하세요.",
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"address_book_added": "Endereço adicionado ao livro",
|
||||
"address_book_confirm_delete": "Confirmar exclusão?",
|
||||
"address_book_count": "%zu endereços salvos",
|
||||
"address_book_count_one": "%zu endereço salvo",
|
||||
"address_book_deleted": "Entrada excluída",
|
||||
"address_book_edit": "Editar Endereço",
|
||||
"address_book_empty": "Nenhum endereço salvo. Clique em 'Adicionar Novo' para criar um.",
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"address_book_added": "Адрес добавлен в книгу",
|
||||
"address_book_confirm_delete": "Подтвердить удаление?",
|
||||
"address_book_count": "%zu адресов сохранено",
|
||||
"address_book_count_one": "Сохранён %zu адрес",
|
||||
"address_book_deleted": "Запись удалена",
|
||||
"address_book_edit": "Редактировать адрес",
|
||||
"address_book_empty": "Нет сохранённых адресов. Нажмите 'Добавить новый', чтобы создать.",
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"address_book_added": "地址已添加到通讯录",
|
||||
"address_book_confirm_delete": "确认删除?",
|
||||
"address_book_count": "已保存 %zu 个地址",
|
||||
"address_book_count_one": "已保存 %zu 个地址",
|
||||
"address_book_deleted": "条目已删除",
|
||||
"address_book_edit": "编辑地址",
|
||||
"address_book_empty": "没有保存的地址。点击'添加新地址'创建一个。",
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
#include "../notifications.h"
|
||||
#include "../schema/ui_schema.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
#include "../material/type.h"
|
||||
#include "../layout.h"
|
||||
#include "imgui.h"
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
@@ -215,8 +217,37 @@ void RenderContactsTab(App* app)
|
||||
ImGui::BeginChild("##ContactsScroll", avail, false,
|
||||
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar);
|
||||
|
||||
// Toolbar
|
||||
if (material::TactileButton(TR("address_book_add_new"), ImVec2(0,0), S.resolveFont(actionBtn.font))) {
|
||||
// Toolbar — icon + label actions; the primary "Add New" is accented so it stands out.
|
||||
const float dp = ui::Layout::dpiScale();
|
||||
ImFont* tbTxtF = S.resolveFont(actionBtn.font);
|
||||
ImFont* tbIcoF = material::Type().iconSmall();
|
||||
// Renders a tactile button with a leading Material icon (icon font) + label (text font). ImGui has no
|
||||
// native two-font button, so draw the styled rect via TactileButton (empty ##id label) then paint the
|
||||
// icon + label over it. `primary` accents the main action; the icon/label inherit the (disabled-aware)
|
||||
// text alpha so they gray out correctly inside BeginDisabled().
|
||||
auto toolbarBtn = [&](const char* id, const char* glyph, const char* label, bool primary) -> bool {
|
||||
ImGui::PushFont(tbIcoF); const float iw = ImGui::CalcTextSize(glyph).x; ImGui::PopFont();
|
||||
ImGui::PushFont(tbTxtF); const float tw = ImGui::CalcTextSize(label).x; ImGui::PopFont();
|
||||
const float gap = 6.0f * dp;
|
||||
const float w = ImGui::GetStyle().FramePadding.x * 2.0f + iw + gap + tw;
|
||||
const float h = ImGui::GetFrameHeight();
|
||||
if (primary) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(material::WithAlpha(material::Primary(), 205)));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(material::Primary()));
|
||||
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImGui::ColorConvertU32ToFloat4(material::WithAlpha(material::Primary(), 235)));
|
||||
}
|
||||
const ImVec2 p = ImGui::GetCursorScreenPos();
|
||||
const bool clicked = material::TactileButton(id, ImVec2(w, h), tbTxtF);
|
||||
if (primary) ImGui::PopStyleColor(3);
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
const ImU32 col = ImGui::GetColorU32(ImGuiCol_Text);
|
||||
const float sx = p.x + (w - (iw + gap + tw)) * 0.5f;
|
||||
dl->AddText(tbIcoF, tbIcoF->LegacySize, ImVec2(sx, p.y + (h - tbIcoF->LegacySize) * 0.5f), col, glyph);
|
||||
dl->AddText(tbTxtF, tbTxtF->LegacySize, ImVec2(sx + iw + gap, p.y + (h - tbTxtF->LegacySize) * 0.5f), col, label);
|
||||
return clicked;
|
||||
};
|
||||
|
||||
if (toolbarBtn("##ab_add", ICON_MD_PERSON_ADD, TR("address_book_add_new"), /*primary=*/true)) {
|
||||
s_show_add_dialog = true;
|
||||
s_focus_edit_field = true;
|
||||
clearEditFields();
|
||||
@@ -226,7 +257,7 @@ void RenderContactsTab(App* app)
|
||||
|
||||
if (!has_selection) ImGui::BeginDisabled();
|
||||
|
||||
if (material::TactileButton(TR("edit"), ImVec2(0,0), S.resolveFont(actionBtn.font)))
|
||||
if (toolbarBtn("##ab_edit", ICON_MD_EDIT, TR("edit"), false))
|
||||
openEdit();
|
||||
|
||||
ImGui::SameLine();
|
||||
@@ -234,12 +265,12 @@ void RenderContactsTab(App* app)
|
||||
// Delete — relabel to a visible confirm prompt while armed (not just a toast).
|
||||
bool armed = has_selection && s_confirm_delete_idx == s_selected_index;
|
||||
const char* delLabel = armed ? TR("address_book_confirm_delete") : TR("delete");
|
||||
if (material::TactileButton(delLabel, ImVec2(0,0), S.resolveFont(actionBtn.font)))
|
||||
if (toolbarBtn("##ab_del", ICON_MD_DELETE, delLabel, false))
|
||||
doDelete();
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (material::TactileButton(TR("copy_address"), ImVec2(0,0), S.resolveFont(actionBtn.font)))
|
||||
if (toolbarBtn("##ab_copy", ICON_MD_CONTENT_COPY, TR("copy_address"), false))
|
||||
doCopy();
|
||||
|
||||
if (!has_selection) ImGui::EndDisabled();
|
||||
@@ -271,17 +302,18 @@ void RenderContactsTab(App* app)
|
||||
// Address list — size the table to fill the tab, leaving room for the count footer.
|
||||
float footerH = ImGui::GetTextLineHeightWithSpacing() + ImGui::GetStyle().ItemSpacing.y;
|
||||
float tableH = ImGui::GetContentRegionAvail().y - footerH;
|
||||
if (tableH < 120.0f) tableH = 120.0f;
|
||||
if (tableH < 120.0f * dp) tableH = 120.0f * dp;
|
||||
if (ImGui::BeginTable("AddressBookTable", 3,
|
||||
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Sortable |
|
||||
ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY,
|
||||
ImVec2(0, tableH)))
|
||||
{
|
||||
float labelColW = (addrTable.columns.count("label") && addrTable.columns.at("label").width > 0) ? addrTable.columns.at("label").width : 150;
|
||||
float notesColW = (addrTable.columns.count("notes") && addrTable.columns.at("notes").width > 0) ? addrTable.columns.at("notes").width : 150;
|
||||
ImGui::TableSetupColumn(TR("label"), ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_DefaultSort, labelColW);
|
||||
ImGui::TableSetupColumn(TR("address_label"), ImGuiTableColumnFlags_WidthStretch);
|
||||
ImGui::TableSetupColumn(TR("notes"), ImGuiTableColumnFlags_WidthFixed, notesColW);
|
||||
// All columns stretch to share the tab width (address widest, notes a small share) so long labels
|
||||
// no longer clip mid-word and the notes column no longer reserves a fixed empty block on the right.
|
||||
// Stretch weights are relative, so no dpiScale() is needed here.
|
||||
ImGui::TableSetupColumn(TR("label"), ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_DefaultSort, 1.5f);
|
||||
ImGui::TableSetupColumn(TR("address_label"), ImGuiTableColumnFlags_WidthStretch, 2.6f);
|
||||
ImGui::TableSetupColumn(TR("notes"), ImGuiTableColumnFlags_WidthStretch, 1.0f);
|
||||
ImGui::TableSetupScrollFreeze(0, 1);
|
||||
ImGui::TableHeadersRow();
|
||||
|
||||
@@ -341,7 +373,7 @@ void RenderContactsTab(App* app)
|
||||
ImVec4 zCol = lightTheme ? ImVec4(0.10f, 0.55f, 0.38f, 1.0f) : ImVec4(0.35f, 0.80f, 0.60f, 1.0f);
|
||||
ImVec4 tCol = lightTheme ? ImVec4(0.72f, 0.48f, 0.05f, 1.0f) : ImVec4(0.95f, 0.72f, 0.30f, 1.0f);
|
||||
ImGui::TextColored(shielded ? zCol : tCol, "%s", shielded ? "Z" : "T");
|
||||
ImGui::SameLine(0.0f, 6.0f);
|
||||
ImGui::SameLine(0.0f, 6.0f * dp);
|
||||
std::string addr_display = entry.address;
|
||||
int addrTruncLen = (addrTable.columns.count("address") && addrTable.columns.at("address").truncate > 0) ? addrTable.columns.at("address").truncate : 40;
|
||||
if (addr_display.length() > static_cast<size_t>(addrTruncLen)) {
|
||||
@@ -351,11 +383,14 @@ void RenderContactsTab(App* app)
|
||||
if (ImGui::IsItemHovered()) {
|
||||
material::Tooltip("%s", entry.address.c_str());
|
||||
}
|
||||
// Globe badge marks a global contact (shown in every wallet).
|
||||
// Globe badge marks a global contact (shown in every wallet). Must be drawn in the icon
|
||||
// font — the text font has no Material glyphs, so it rendered as tofu ("?") before.
|
||||
if (entry.isGlobal()) {
|
||||
ImGui::SameLine(0.0f, 8.0f);
|
||||
ImGui::SameLine(0.0f, 8.0f * dp);
|
||||
ImGui::PushFont(material::Type().iconSmall());
|
||||
ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(material::OnSurfaceMedium()),
|
||||
"%s", ICON_MD_PUBLIC);
|
||||
ImGui::PopFont();
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("contact_global_badge_tt"));
|
||||
}
|
||||
|
||||
@@ -369,8 +404,8 @@ void RenderContactsTab(App* app)
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
// Status line
|
||||
ImGui::TextDisabled(TR("address_book_count"), book.size());
|
||||
// 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());
|
||||
|
||||
// Keyboard shortcuts — only when the tab owns focus (no field being edited, no modal up).
|
||||
if (!searchActive && !ImGui::IsAnyItemActive() &&
|
||||
|
||||
@@ -1225,6 +1225,7 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["contact_global_badge_tt"] = "Global contact — visible in every wallet";
|
||||
strings_["address_book_added"] = "Address added to book";
|
||||
strings_["address_book_count"] = "%zu addresses saved";
|
||||
strings_["address_book_count_one"] = "%zu address saved";
|
||||
strings_["address_book_deleted"] = "Entry deleted";
|
||||
strings_["address_book_edit"] = "Edit Address";
|
||||
strings_["address_book_empty"] = "No saved addresses. Click 'Add New' to add one.";
|
||||
|
||||
Reference in New Issue
Block a user