diff --git a/res/lang/de.json b/res/lang/de.json index 9878903..dc9764a 100644 --- a/res/lang/de.json +++ b/res/lang/de.json @@ -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.", diff --git a/res/lang/es.json b/res/lang/es.json index fd33837..eff2afb 100644 --- a/res/lang/es.json +++ b/res/lang/es.json @@ -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.", diff --git a/res/lang/fr.json b/res/lang/fr.json index 2915389..31a84fe 100644 --- a/res/lang/fr.json +++ b/res/lang/fr.json @@ -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.", diff --git a/res/lang/ja.json b/res/lang/ja.json index 287d52e..773b9c0 100644 --- a/res/lang/ja.json +++ b/res/lang/ja.json @@ -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": "保存されたアドレスがありません。「新規追加」をクリックして追加してください。", diff --git a/res/lang/ko.json b/res/lang/ko.json index f61ee2f..d82a1ed 100644 --- a/res/lang/ko.json +++ b/res/lang/ko.json @@ -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": "저장된 주소가 없습니다. '새로 추가'를 클릭하여 추가하세요.", diff --git a/res/lang/pt.json b/res/lang/pt.json index 4c2b6ef..214e320 100644 --- a/res/lang/pt.json +++ b/res/lang/pt.json @@ -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.", diff --git a/res/lang/ru.json b/res/lang/ru.json index 225e723..48a91cc 100644 --- a/res/lang/ru.json +++ b/res/lang/ru.json @@ -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": "Нет сохранённых адресов. Нажмите 'Добавить новый', чтобы создать.", diff --git a/res/lang/zh.json b/res/lang/zh.json index 8c69376..ac7d8af 100644 --- a/res/lang/zh.json +++ b/res/lang/zh.json @@ -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": "没有保存的地址。点击'添加新地址'创建一个。", diff --git a/src/ui/windows/contacts_tab.cpp b/src/ui/windows/contacts_tab.cpp index fa94c45..aa499ce 100644 --- a/src/ui/windows/contacts_tab.cpp +++ b/src/ui/windows/contacts_tab.cpp @@ -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 #include @@ -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(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() && diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 9c79a4b..fae3d18 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -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.";