feat(ui): promote the address book to a Contacts sidebar tab
Phase 0b. The address book was buried in Settings behind a modal dialog.
Make it a first-class "Contacts" tab (which will also become the chat
roster), rendered inline in the main content area.
- New NavPage::Contacts (after History; ICON_MD_CONTACTS) +
WalletUiSurface::Contacts. isUiSurfaceAvailable's `default: return true`
shows it in BOTH variants; uiSurfaceNeedsWalletData's default keeps it
usable before wallet data loads. All the touchpoints wired: NavPageSurface,
GetNavIconMD, the app.cpp dispatch case, the app_network.cpp tracePageName
case, and the `contacts` i18n label.
- New src/ui/windows/contacts_tab.{h,cpp}: RenderContactsTab lifts the
toolbar + table + add/edit modal out of address_book_dialog, rendered in a
BeginChild scroll region (peers_tab pattern) instead of an overlay; the
add/edit form stays a modal layered over the tab. Reuses the existing
address_book_* i18n keys and the dialogs.address-book schema.
- Delete address_book_dialog.{h,cpp}; remove its app.cpp render pump and the
dead App::show_address_book_. The Settings "Address Book…" button now
navigates to the tab (setCurrentPage) instead of opening the modal, so the
Tools & Actions grid layout is untouched.
- CMake: swap the dialog sources for contacts_tab.
Behavior-preserving move; search/sort/keyboard/contrast land in 0d. Visual
check pending the per-theme screenshot sweep.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
10
src/app.cpp
10
src/app.cpp
@@ -41,7 +41,7 @@
|
||||
#include "ui/windows/transaction_details_dialog.h"
|
||||
#include "ui/windows/qr_popup_dialog.h"
|
||||
#include "ui/windows/validate_address_dialog.h"
|
||||
#include "ui/windows/address_book_dialog.h"
|
||||
#include "ui/windows/contacts_tab.h"
|
||||
#include "ui/windows/shield_dialog.h"
|
||||
#include "ui/windows/request_payment_dialog.h"
|
||||
#include "ui/windows/block_info_dialog.h"
|
||||
@@ -1691,6 +1691,9 @@ void App::render()
|
||||
case ui::NavPage::History:
|
||||
ui::RenderTransactionsTab(this);
|
||||
break;
|
||||
case ui::NavPage::Contacts:
|
||||
ui::RenderContactsTab(this);
|
||||
break;
|
||||
case ui::NavPage::Mining:
|
||||
ui::RenderMiningTab(this);
|
||||
break;
|
||||
@@ -1888,10 +1891,7 @@ void App::render()
|
||||
|
||||
// Validate address dialog (triggered from Edit menu)
|
||||
ui::ValidateAddressDialog::render(this);
|
||||
|
||||
// Address book dialog (triggered from Edit menu)
|
||||
ui::AddressBookDialog::render(this);
|
||||
|
||||
|
||||
// Shield/merge dialog (triggered from Wallet menu)
|
||||
ui::ShieldDialog::render(this);
|
||||
|
||||
|
||||
@@ -621,8 +621,7 @@ private:
|
||||
bool show_import_key_ = false;
|
||||
bool show_export_key_ = false;
|
||||
bool show_backup_ = false;
|
||||
bool show_address_book_ = false;
|
||||
|
||||
|
||||
// Embedded daemon state
|
||||
bool use_embedded_daemon_ = wallet::supportsEmbeddedDaemon(wallet::currentWalletCapabilities());
|
||||
std::string daemon_status_;
|
||||
|
||||
@@ -100,6 +100,7 @@ const char* tracePageName(ui::NavPage page)
|
||||
case ui::NavPage::Send: return "Send tab";
|
||||
case ui::NavPage::Receive: return "Receive tab";
|
||||
case ui::NavPage::History: return "History tab";
|
||||
case ui::NavPage::Contacts: return "Contacts tab";
|
||||
case ui::NavPage::Mining: return "Mining tab";
|
||||
case ui::NavPage::Market: return "Market tab";
|
||||
case ui::NavPage::Console: return "Console tab";
|
||||
|
||||
@@ -33,7 +33,6 @@
|
||||
#include "../material/type.h"
|
||||
#include "../material/colors.h"
|
||||
#include "../windows/validate_address_dialog.h"
|
||||
#include "../windows/address_book_dialog.h"
|
||||
#include "../windows/shield_dialog.h"
|
||||
#include "../windows/request_payment_dialog.h"
|
||||
#include "../windows/block_info_dialog.h"
|
||||
@@ -1260,7 +1259,7 @@ void RenderSettingsPage(App* app) {
|
||||
bw = std::max(minBtnW, bw);
|
||||
|
||||
if (TactileButton(TR("settings_address_book"), ImVec2(bw, 0), S.resolveFont("button")))
|
||||
AddressBookDialog::show();
|
||||
app->setCurrentPage(ui::NavPage::Contacts); // now a top-level tab
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_address_book"));
|
||||
ImGui::SameLine(0, btnSpacing);
|
||||
if (TactileButton(TR("settings_validate_address"), ImVec2(bw, 0), S.resolveFont("button")))
|
||||
|
||||
@@ -25,6 +25,7 @@ enum class NavPage {
|
||||
Send,
|
||||
Receive,
|
||||
History,
|
||||
Contacts,
|
||||
// --- separator ---
|
||||
Mining,
|
||||
Market,
|
||||
@@ -51,6 +52,7 @@ inline const NavItem kNavItems[] = {
|
||||
{ "Send", NavPage::Send, nullptr, "send", nullptr },
|
||||
{ "Receive", NavPage::Receive, nullptr, "receive", nullptr },
|
||||
{ "History", NavPage::History, nullptr, "history", nullptr },
|
||||
{ "Contacts", NavPage::Contacts, nullptr, "contacts", nullptr },
|
||||
{ "Mining", NavPage::Mining, "TOOLS", "mining", "tools" },
|
||||
{ "Market", NavPage::Market, nullptr, "market", nullptr },
|
||||
{ "Console", NavPage::Console, "ADVANCED","console", "advanced" },
|
||||
@@ -78,6 +80,7 @@ inline wallet::WalletUiSurface NavPageSurface(NavPage page)
|
||||
case NavPage::Send: return wallet::WalletUiSurface::Send;
|
||||
case NavPage::Receive: return wallet::WalletUiSurface::Receive;
|
||||
case NavPage::History: return wallet::WalletUiSurface::History;
|
||||
case NavPage::Contacts: return wallet::WalletUiSurface::Contacts;
|
||||
case NavPage::Mining: return wallet::WalletUiSurface::Mining;
|
||||
case NavPage::Market: return wallet::WalletUiSurface::Market;
|
||||
case NavPage::Console: return wallet::WalletUiSurface::Console;
|
||||
@@ -103,6 +106,7 @@ inline const char* GetNavIconMD(NavPage page)
|
||||
case NavPage::Send: return ICON_MD_CALL_MADE;
|
||||
case NavPage::Receive: return ICON_MD_CALL_RECEIVED;
|
||||
case NavPage::History: return ICON_MD_HISTORY;
|
||||
case NavPage::Contacts: return ICON_MD_CONTACTS;
|
||||
case NavPage::Mining: return ICON_MD_CONSTRUCTION;
|
||||
case NavPage::Market: return ICON_MD_TRENDING_UP;
|
||||
case NavPage::Console: return ICON_MD_TERMINAL;
|
||||
|
||||
@@ -1,279 +0,0 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
|
||||
#include "address_book_dialog.h"
|
||||
#include "../../app.h"
|
||||
#include "../../data/address_book.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../../util/text_format.h"
|
||||
#include "../notifications.h"
|
||||
#include "../schema/ui_schema.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
#include "imgui.h"
|
||||
#include <cstring>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
// Static member initialization
|
||||
bool AddressBookDialog::s_open = false;
|
||||
int AddressBookDialog::s_selected_index = -1;
|
||||
bool AddressBookDialog::s_show_add_dialog = false;
|
||||
bool AddressBookDialog::s_show_edit_dialog = false;
|
||||
char AddressBookDialog::s_edit_label[128] = "";
|
||||
char AddressBookDialog::s_edit_address[512] = "";
|
||||
char AddressBookDialog::s_edit_notes[512] = "";
|
||||
|
||||
static void copyEditField(char* dest, size_t destSize, const std::string& source) {
|
||||
if (destSize == 0) return;
|
||||
std::strncpy(dest, source.c_str(), destSize - 1);
|
||||
dest[destSize - 1] = '\0';
|
||||
}
|
||||
|
||||
void AddressBookDialog::show()
|
||||
{
|
||||
s_open = true;
|
||||
s_selected_index = -1;
|
||||
s_show_add_dialog = false;
|
||||
s_show_edit_dialog = false;
|
||||
// The address book is App-owned and loaded once at startup (App::init); it stays
|
||||
// authoritative for the app lifetime and self-saves on every mutation, so there is
|
||||
// nothing to reload here. (show() is static and has no App* to reach it anyway.)
|
||||
}
|
||||
|
||||
bool AddressBookDialog::isOpen()
|
||||
{
|
||||
return s_open;
|
||||
}
|
||||
|
||||
void AddressBookDialog::render(App* app)
|
||||
{
|
||||
if (!s_open) return;
|
||||
|
||||
auto& S = schema::UI();
|
||||
auto win = S.window("dialogs.address-book");
|
||||
auto addrTable = S.table("dialogs.address-book", "address-table");
|
||||
auto addrFrontLbl = S.label("dialogs.address-book", "address-front-label");
|
||||
auto addrBackLbl = S.label("dialogs.address-book", "address-back-label");
|
||||
auto addrInput = S.input("dialogs.address-book", "address-input");
|
||||
auto notesInput = S.input("dialogs.address-book", "notes-input");
|
||||
auto actionBtn = S.button("dialogs.address-book", "action-button");
|
||||
|
||||
auto clearEditFields = []() {
|
||||
s_edit_label[0] = '\0';
|
||||
s_edit_address[0] = '\0';
|
||||
s_edit_notes[0] = '\0';
|
||||
};
|
||||
|
||||
auto loadEditFields = [](const data::AddressBookEntry& entry) {
|
||||
copyEditField(s_edit_label, sizeof(s_edit_label), entry.label);
|
||||
copyEditField(s_edit_address, sizeof(s_edit_address), entry.address);
|
||||
copyEditField(s_edit_notes, sizeof(s_edit_notes), entry.notes);
|
||||
};
|
||||
|
||||
auto renderEntryDialog = [&]() {
|
||||
bool isEdit = s_show_edit_dialog;
|
||||
bool* open = isEdit ? &s_show_edit_dialog : &s_show_add_dialog;
|
||||
if (!*open) return;
|
||||
|
||||
const char* title = isEdit ? TR("address_book_edit") : TR("address_book_add");
|
||||
const char* id = isEdit ? "AddressBookEdit" : "AddressBookAdd";
|
||||
float dialogW = std::max(Layout::kDialogMinWidth(), Layout::kDialogDefaultWidth());
|
||||
float formW = addrInput.width > 0 ? addrInput.width : Layout::kDialogFormWidth();
|
||||
float actionW = actionBtn.width > 0 ? actionBtn.width : Layout::kDialogActionWidth();
|
||||
float actionGap = actionBtn.gap > 0 ? actionBtn.gap : Layout::kDialogActionGap();
|
||||
float notesH = notesInput.height > 0 ? notesInput.height : 60.0f;
|
||||
|
||||
if (material::BeginOverlayDialog(title, open, dialogW, 0.94f,
|
||||
Layout::kDialogCompactBottomRatio(), id)) {
|
||||
material::LabeledInput(TR("label"), isEdit ? "##EditLabel" : "##AddLabel",
|
||||
s_edit_label, sizeof(s_edit_label), formW);
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
material::LabeledInput(TR("address_label"), isEdit ? "##EditAddress" : "##AddAddress",
|
||||
s_edit_address, sizeof(s_edit_address), formW);
|
||||
if (!isEdit) {
|
||||
ImGui::SameLine();
|
||||
if (material::StyledButton(TR("paste"), ImVec2(0,0), S.resolveFont(actionBtn.font))) {
|
||||
const char* clipboard = ImGui::GetClipboardText();
|
||||
if (clipboard) copyEditField(s_edit_address, sizeof(s_edit_address), clipboard);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
material::LabeledInputMultiline(TR("notes_optional"), isEdit ? "##EditNotes" : "##AddNotes",
|
||||
s_edit_notes, sizeof(s_edit_notes), ImVec2(formW, notesH));
|
||||
|
||||
bool canSubmit = std::strlen(s_edit_label) > 0 && std::strlen(s_edit_address) > 0;
|
||||
float totalActionsW = actionW * 2.0f + actionGap;
|
||||
material::BeginOverlayDialogFooter(totalActionsW);
|
||||
|
||||
if (!canSubmit) ImGui::BeginDisabled();
|
||||
|
||||
const char* primaryLabel = isEdit ? TR("save") : TR("add");
|
||||
if (material::StyledButton(primaryLabel, ImVec2(actionW, 0), S.resolveFont(actionBtn.font))) {
|
||||
// Trim the label/address (a pasted address often carries a trailing newline); keep notes as-is.
|
||||
auto trimAB = [](std::string s) {
|
||||
while (!s.empty() && (s.front()==' '||s.front()=='\t'||s.front()=='\n'||s.front()=='\r')) s.erase(s.begin());
|
||||
while (!s.empty() && (s.back()==' '||s.back()=='\t'||s.back()=='\n'||s.back()=='\r')) s.pop_back();
|
||||
return s;
|
||||
};
|
||||
data::AddressBookEntry entry(trimAB(s_edit_label), trimAB(s_edit_address), s_edit_notes);
|
||||
if (isEdit) {
|
||||
if (app->addressBook().updateEntry(s_selected_index, entry)) {
|
||||
Notifications::instance().success(TR("address_book_updated"));
|
||||
s_show_edit_dialog = false;
|
||||
} else {
|
||||
Notifications::instance().error(TR("address_book_update_failed"));
|
||||
}
|
||||
} else {
|
||||
if (app->addressBook().addEntry(entry)) {
|
||||
Notifications::instance().success(TR("address_book_added"));
|
||||
s_show_add_dialog = false;
|
||||
} else {
|
||||
Notifications::instance().error(TR("address_book_exists"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!canSubmit) ImGui::EndDisabled();
|
||||
|
||||
ImGui::SameLine(0, actionGap);
|
||||
if (material::StyledButton(TR("cancel"), ImVec2(actionW, 0), S.resolveFont(actionBtn.font))) {
|
||||
*open = false;
|
||||
}
|
||||
|
||||
material::EndOverlayDialog();
|
||||
}
|
||||
};
|
||||
|
||||
if (material::BeginOverlayDialog(TR("address_book_title"), &s_open, win.width, 0.94f)) {
|
||||
auto& book = app->addressBook();
|
||||
|
||||
// Toolbar
|
||||
if (material::StyledButton(TR("address_book_add_new"), ImVec2(0,0), S.resolveFont(actionBtn.font))) {
|
||||
s_show_add_dialog = true;
|
||||
clearEditFields();
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
bool has_selection = s_selected_index >= 0 && s_selected_index < static_cast<int>(book.size());
|
||||
|
||||
if (!has_selection) ImGui::BeginDisabled();
|
||||
|
||||
if (material::StyledButton(TR("edit"), ImVec2(0,0), S.resolveFont(actionBtn.font))) {
|
||||
if (has_selection) {
|
||||
const auto& entry = book.entries()[s_selected_index];
|
||||
loadEditFields(entry);
|
||||
s_show_edit_dialog = true;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
static int s_confirmDeleteIdx = -1;
|
||||
if (material::StyledButton(TR("delete"), ImVec2(0,0), S.resolveFont(actionBtn.font))) {
|
||||
if (has_selection) {
|
||||
if (s_confirmDeleteIdx == s_selected_index) {
|
||||
book.removeEntry(s_selected_index);
|
||||
s_selected_index = -1;
|
||||
s_confirmDeleteIdx = -1;
|
||||
Notifications::instance().success(TR("address_book_deleted"));
|
||||
} else {
|
||||
// Require a second click to confirm (no undo for a removed contact).
|
||||
s_confirmDeleteIdx = s_selected_index;
|
||||
Notifications::instance().warning("Click Delete again to remove this entry.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (material::StyledButton(TR("copy_address"), ImVec2(0,0), S.resolveFont(actionBtn.font))) {
|
||||
if (has_selection) {
|
||||
ImGui::SetClipboardText(book.entries()[s_selected_index].address.c_str());
|
||||
Notifications::instance().info(TR("address_copied"));
|
||||
}
|
||||
}
|
||||
|
||||
if (!has_selection) ImGui::EndDisabled();
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Address list
|
||||
if (ImGui::BeginTable("AddressBookTable", 3,
|
||||
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
|
||||
ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY,
|
||||
ImVec2(0, addrTable.bottomReserve > 0 ? -addrTable.bottomReserve : -35)))
|
||||
{
|
||||
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, labelColW);
|
||||
ImGui::TableSetupColumn(TR("address_label"), ImGuiTableColumnFlags_WidthStretch);
|
||||
ImGui::TableSetupColumn(TR("notes"), ImGuiTableColumnFlags_WidthFixed, notesColW);
|
||||
ImGui::TableSetupScrollFreeze(0, 1);
|
||||
ImGui::TableHeadersRow();
|
||||
|
||||
if (book.empty()) {
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextDisabled("%s", TR("address_book_empty"));
|
||||
} else {
|
||||
for (size_t i = 0; i < book.size(); i++) {
|
||||
const auto& entry = book.entries()[i];
|
||||
|
||||
ImGui::TableNextRow();
|
||||
ImGui::PushID(static_cast<int>(i));
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
bool is_selected = (s_selected_index == static_cast<int>(i));
|
||||
if (ImGui::Selectable(entry.label.c_str(), is_selected,
|
||||
ImGuiSelectableFlags_SpanAllColumns)) {
|
||||
s_selected_index = static_cast<int>(i);
|
||||
}
|
||||
|
||||
// Double-click to edit
|
||||
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) {
|
||||
s_selected_index = static_cast<int>(i);
|
||||
loadEditFields(entry);
|
||||
s_show_edit_dialog = true;
|
||||
}
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
// Truncate long addresses
|
||||
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)) {
|
||||
addr_display = util::truncateMiddle(addr_display, addrFrontLbl.truncate, addrBackLbl.truncate);
|
||||
}
|
||||
ImGui::TextDisabled("%s", addr_display.c_str());
|
||||
if (ImGui::IsItemHovered()) {
|
||||
material::Tooltip("%s", entry.address.c_str());
|
||||
}
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextDisabled("%s", entry.notes.c_str());
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
// Status line
|
||||
ImGui::TextDisabled(TR("address_book_count"), book.size());
|
||||
material::EndOverlayDialog();
|
||||
}
|
||||
|
||||
renderEntryDialog();
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
@@ -1,45 +0,0 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace dragonx {
|
||||
|
||||
class App;
|
||||
|
||||
namespace ui {
|
||||
|
||||
/**
|
||||
* @brief Address book dialog for managing saved addresses
|
||||
*/
|
||||
class AddressBookDialog {
|
||||
public:
|
||||
/**
|
||||
* @brief Show the address book dialog
|
||||
*/
|
||||
static void show();
|
||||
|
||||
/**
|
||||
* @brief Render the dialog (call every frame)
|
||||
* @param app Pointer to app instance
|
||||
*/
|
||||
static void render(App* app);
|
||||
|
||||
/**
|
||||
* @brief Check if dialog is currently open
|
||||
*/
|
||||
static bool isOpen();
|
||||
|
||||
private:
|
||||
static bool s_open;
|
||||
static int s_selected_index;
|
||||
static bool s_show_add_dialog;
|
||||
static bool s_show_edit_dialog;
|
||||
static char s_edit_label[128];
|
||||
static char s_edit_address[512];
|
||||
static char s_edit_notes[512];
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
269
src/ui/windows/contacts_tab.cpp
Normal file
269
src/ui/windows/contacts_tab.cpp
Normal file
@@ -0,0 +1,269 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
|
||||
#include "contacts_tab.h"
|
||||
#include "../../app.h"
|
||||
#include "../../data/address_book.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../../util/text_format.h"
|
||||
#include "../notifications.h"
|
||||
#include "../schema/ui_schema.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
#include "imgui.h"
|
||||
#include <cstring>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
// Tab state (formerly AddressBookDialog's class statics). One selection/edit
|
||||
// buffer set for the whole tab; the add/edit form is a modal layered on top.
|
||||
static int s_selected_index = -1;
|
||||
static bool s_show_add_dialog = false;
|
||||
static bool s_show_edit_dialog = false;
|
||||
static char s_edit_label[128] = "";
|
||||
static char s_edit_address[512] = "";
|
||||
static char s_edit_notes[512] = "";
|
||||
|
||||
static void copyEditField(char* dest, size_t destSize, const std::string& source) {
|
||||
if (destSize == 0) return;
|
||||
std::strncpy(dest, source.c_str(), destSize - 1);
|
||||
dest[destSize - 1] = '\0';
|
||||
}
|
||||
|
||||
void RenderContactsTab(App* app)
|
||||
{
|
||||
auto& S = schema::UI();
|
||||
// Reuse the existing address-book schema/column config for the table + add/edit form.
|
||||
auto addrTable = S.table("dialogs.address-book", "address-table");
|
||||
auto addrFrontLbl = S.label("dialogs.address-book", "address-front-label");
|
||||
auto addrBackLbl = S.label("dialogs.address-book", "address-back-label");
|
||||
auto addrInput = S.input("dialogs.address-book", "address-input");
|
||||
auto notesInput = S.input("dialogs.address-book", "notes-input");
|
||||
auto actionBtn = S.button("dialogs.address-book", "action-button");
|
||||
|
||||
auto clearEditFields = []() {
|
||||
s_edit_label[0] = '\0';
|
||||
s_edit_address[0] = '\0';
|
||||
s_edit_notes[0] = '\0';
|
||||
};
|
||||
|
||||
auto loadEditFields = [](const data::AddressBookEntry& entry) {
|
||||
copyEditField(s_edit_label, sizeof(s_edit_label), entry.label);
|
||||
copyEditField(s_edit_address, sizeof(s_edit_address), entry.address);
|
||||
copyEditField(s_edit_notes, sizeof(s_edit_notes), entry.notes);
|
||||
};
|
||||
|
||||
// Add/edit form — a modal popup layered over the tab.
|
||||
auto renderEntryDialog = [&]() {
|
||||
bool isEdit = s_show_edit_dialog;
|
||||
bool* open = isEdit ? &s_show_edit_dialog : &s_show_add_dialog;
|
||||
if (!*open) return;
|
||||
|
||||
const char* title = isEdit ? TR("address_book_edit") : TR("address_book_add");
|
||||
const char* id = isEdit ? "AddressBookEdit" : "AddressBookAdd";
|
||||
float dialogW = std::max(Layout::kDialogMinWidth(), Layout::kDialogDefaultWidth());
|
||||
float formW = addrInput.width > 0 ? addrInput.width : Layout::kDialogFormWidth();
|
||||
float actionW = actionBtn.width > 0 ? actionBtn.width : Layout::kDialogActionWidth();
|
||||
float actionGap = actionBtn.gap > 0 ? actionBtn.gap : Layout::kDialogActionGap();
|
||||
float notesH = notesInput.height > 0 ? notesInput.height : 60.0f;
|
||||
|
||||
if (material::BeginOverlayDialog(title, open, dialogW, 0.94f,
|
||||
Layout::kDialogCompactBottomRatio(), id)) {
|
||||
material::LabeledInput(TR("label"), isEdit ? "##EditLabel" : "##AddLabel",
|
||||
s_edit_label, sizeof(s_edit_label), formW);
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
material::LabeledInput(TR("address_label"), isEdit ? "##EditAddress" : "##AddAddress",
|
||||
s_edit_address, sizeof(s_edit_address), formW);
|
||||
if (!isEdit) {
|
||||
ImGui::SameLine();
|
||||
if (material::StyledButton(TR("paste"), ImVec2(0,0), S.resolveFont(actionBtn.font))) {
|
||||
const char* clipboard = ImGui::GetClipboardText();
|
||||
if (clipboard) copyEditField(s_edit_address, sizeof(s_edit_address), clipboard);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
material::LabeledInputMultiline(TR("notes_optional"), isEdit ? "##EditNotes" : "##AddNotes",
|
||||
s_edit_notes, sizeof(s_edit_notes), ImVec2(formW, notesH));
|
||||
|
||||
bool canSubmit = std::strlen(s_edit_label) > 0 && std::strlen(s_edit_address) > 0;
|
||||
float totalActionsW = actionW * 2.0f + actionGap;
|
||||
material::BeginOverlayDialogFooter(totalActionsW);
|
||||
|
||||
if (!canSubmit) ImGui::BeginDisabled();
|
||||
|
||||
const char* primaryLabel = isEdit ? TR("save") : TR("add");
|
||||
if (material::StyledButton(primaryLabel, ImVec2(actionW, 0), S.resolveFont(actionBtn.font))) {
|
||||
// Trim the label/address (a pasted address often carries a trailing newline); keep notes as-is.
|
||||
auto trimAB = [](std::string s) {
|
||||
while (!s.empty() && (s.front()==' '||s.front()=='\t'||s.front()=='\n'||s.front()=='\r')) s.erase(s.begin());
|
||||
while (!s.empty() && (s.back()==' '||s.back()=='\t'||s.back()=='\n'||s.back()=='\r')) s.pop_back();
|
||||
return s;
|
||||
};
|
||||
data::AddressBookEntry entry(trimAB(s_edit_label), trimAB(s_edit_address), s_edit_notes);
|
||||
if (isEdit) {
|
||||
if (app->addressBook().updateEntry(s_selected_index, entry)) {
|
||||
Notifications::instance().success(TR("address_book_updated"));
|
||||
s_show_edit_dialog = false;
|
||||
} else {
|
||||
Notifications::instance().error(TR("address_book_update_failed"));
|
||||
}
|
||||
} else {
|
||||
if (app->addressBook().addEntry(entry)) {
|
||||
Notifications::instance().success(TR("address_book_added"));
|
||||
s_show_add_dialog = false;
|
||||
} else {
|
||||
Notifications::instance().error(TR("address_book_exists"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!canSubmit) ImGui::EndDisabled();
|
||||
|
||||
ImGui::SameLine(0, actionGap);
|
||||
if (material::StyledButton(TR("cancel"), ImVec2(actionW, 0), S.resolveFont(actionBtn.font))) {
|
||||
*open = false;
|
||||
}
|
||||
|
||||
material::EndOverlayDialog();
|
||||
}
|
||||
};
|
||||
|
||||
// Inline tab content lives in a scroll child (mirrors peers_tab / explorer_tab).
|
||||
ImVec2 avail = ImGui::GetContentRegionAvail();
|
||||
ImGui::BeginChild("##ContactsScroll", avail, false,
|
||||
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar);
|
||||
|
||||
auto& book = app->addressBook();
|
||||
|
||||
// Toolbar
|
||||
if (material::StyledButton(TR("address_book_add_new"), ImVec2(0,0), S.resolveFont(actionBtn.font))) {
|
||||
s_show_add_dialog = true;
|
||||
clearEditFields();
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
bool has_selection = s_selected_index >= 0 && s_selected_index < static_cast<int>(book.size());
|
||||
|
||||
if (!has_selection) ImGui::BeginDisabled();
|
||||
|
||||
if (material::StyledButton(TR("edit"), ImVec2(0,0), S.resolveFont(actionBtn.font))) {
|
||||
if (has_selection) {
|
||||
const auto& entry = book.entries()[s_selected_index];
|
||||
loadEditFields(entry);
|
||||
s_show_edit_dialog = true;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
static int s_confirmDeleteIdx = -1;
|
||||
if (material::StyledButton(TR("delete"), ImVec2(0,0), S.resolveFont(actionBtn.font))) {
|
||||
if (has_selection) {
|
||||
if (s_confirmDeleteIdx == s_selected_index) {
|
||||
book.removeEntry(s_selected_index);
|
||||
s_selected_index = -1;
|
||||
s_confirmDeleteIdx = -1;
|
||||
Notifications::instance().success(TR("address_book_deleted"));
|
||||
} else {
|
||||
// Require a second click to confirm (no undo for a removed contact).
|
||||
s_confirmDeleteIdx = s_selected_index;
|
||||
Notifications::instance().warning("Click Delete again to remove this entry.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
if (material::StyledButton(TR("copy_address"), ImVec2(0,0), S.resolveFont(actionBtn.font))) {
|
||||
if (has_selection) {
|
||||
ImGui::SetClipboardText(book.entries()[s_selected_index].address.c_str());
|
||||
Notifications::instance().info(TR("address_copied"));
|
||||
}
|
||||
}
|
||||
|
||||
if (!has_selection) ImGui::EndDisabled();
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// 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 (ImGui::BeginTable("AddressBookTable", 3,
|
||||
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
|
||||
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, labelColW);
|
||||
ImGui::TableSetupColumn(TR("address_label"), ImGuiTableColumnFlags_WidthStretch);
|
||||
ImGui::TableSetupColumn(TR("notes"), ImGuiTableColumnFlags_WidthFixed, notesColW);
|
||||
ImGui::TableSetupScrollFreeze(0, 1);
|
||||
ImGui::TableHeadersRow();
|
||||
|
||||
if (book.empty()) {
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextDisabled("%s", TR("address_book_empty"));
|
||||
} else {
|
||||
for (size_t i = 0; i < book.size(); i++) {
|
||||
const auto& entry = book.entries()[i];
|
||||
|
||||
ImGui::TableNextRow();
|
||||
ImGui::PushID(static_cast<int>(i));
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
bool is_selected = (s_selected_index == static_cast<int>(i));
|
||||
if (ImGui::Selectable(entry.label.c_str(), is_selected,
|
||||
ImGuiSelectableFlags_SpanAllColumns)) {
|
||||
s_selected_index = static_cast<int>(i);
|
||||
}
|
||||
|
||||
// Double-click to edit
|
||||
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) {
|
||||
s_selected_index = static_cast<int>(i);
|
||||
loadEditFields(entry);
|
||||
s_show_edit_dialog = true;
|
||||
}
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
// Truncate long addresses
|
||||
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)) {
|
||||
addr_display = util::truncateMiddle(addr_display, addrFrontLbl.truncate, addrBackLbl.truncate);
|
||||
}
|
||||
ImGui::TextDisabled("%s", addr_display.c_str());
|
||||
if (ImGui::IsItemHovered()) {
|
||||
material::Tooltip("%s", entry.address.c_str());
|
||||
}
|
||||
|
||||
ImGui::TableNextColumn();
|
||||
ImGui::TextDisabled("%s", entry.notes.c_str());
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndTable();
|
||||
}
|
||||
|
||||
// Status line
|
||||
ImGui::TextDisabled(TR("address_book_count"), book.size());
|
||||
|
||||
ImGui::EndChild();
|
||||
|
||||
renderEntryDialog();
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
25
src/ui/windows/contacts_tab.h
Normal file
25
src/ui/windows/contacts_tab.h
Normal file
@@ -0,0 +1,25 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace dragonx {
|
||||
|
||||
class App;
|
||||
|
||||
namespace ui {
|
||||
|
||||
/**
|
||||
* @brief Render the Contacts tab (the address book, promoted out of Settings).
|
||||
*
|
||||
* Doubles as the future chat roster. Reads/writes the App-owned AddressBook
|
||||
* (App::addressBook()). Renders inline in the main content area; the add/edit
|
||||
* form is a modal popup layered over the tab.
|
||||
*
|
||||
* @param app Pointer to the app instance.
|
||||
*/
|
||||
void RenderContactsTab(App* app);
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
@@ -210,6 +210,7 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["receive"] = "Receive";
|
||||
strings_["transactions"] = "Transactions";
|
||||
strings_["history"] = "History";
|
||||
strings_["contacts"] = "Contacts";
|
||||
strings_["mining"] = "Mining";
|
||||
strings_["peers"] = "Peers";
|
||||
strings_["market"] = "Market";
|
||||
|
||||
@@ -34,6 +34,7 @@ enum class WalletUiSurface {
|
||||
Send,
|
||||
Receive,
|
||||
History,
|
||||
Contacts, // address book / chat roster; available in both variants (default: return true)
|
||||
Mining,
|
||||
Market,
|
||||
Console,
|
||||
|
||||
Reference in New Issue
Block a user