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:
@@ -470,7 +470,7 @@ set(APP_SOURCES
|
|||||||
src/ui/windows/transaction_details_dialog.cpp
|
src/ui/windows/transaction_details_dialog.cpp
|
||||||
src/ui/windows/qr_popup_dialog.cpp
|
src/ui/windows/qr_popup_dialog.cpp
|
||||||
src/ui/windows/validate_address_dialog.cpp
|
src/ui/windows/validate_address_dialog.cpp
|
||||||
src/ui/windows/address_book_dialog.cpp
|
src/ui/windows/contacts_tab.cpp
|
||||||
src/ui/windows/shield_dialog.cpp
|
src/ui/windows/shield_dialog.cpp
|
||||||
src/ui/windows/request_payment_dialog.cpp
|
src/ui/windows/request_payment_dialog.cpp
|
||||||
src/ui/windows/block_info_dialog.cpp
|
src/ui/windows/block_info_dialog.cpp
|
||||||
@@ -594,7 +594,7 @@ set(APP_HEADERS
|
|||||||
src/ui/windows/transaction_details_dialog.h
|
src/ui/windows/transaction_details_dialog.h
|
||||||
src/ui/windows/qr_popup_dialog.h
|
src/ui/windows/qr_popup_dialog.h
|
||||||
src/ui/windows/validate_address_dialog.h
|
src/ui/windows/validate_address_dialog.h
|
||||||
src/ui/windows/address_book_dialog.h
|
src/ui/windows/contacts_tab.h
|
||||||
src/ui/windows/shield_dialog.h
|
src/ui/windows/shield_dialog.h
|
||||||
src/ui/windows/request_payment_dialog.h
|
src/ui/windows/request_payment_dialog.h
|
||||||
src/ui/windows/block_info_dialog.h
|
src/ui/windows/block_info_dialog.h
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
#include "ui/windows/transaction_details_dialog.h"
|
#include "ui/windows/transaction_details_dialog.h"
|
||||||
#include "ui/windows/qr_popup_dialog.h"
|
#include "ui/windows/qr_popup_dialog.h"
|
||||||
#include "ui/windows/validate_address_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/shield_dialog.h"
|
||||||
#include "ui/windows/request_payment_dialog.h"
|
#include "ui/windows/request_payment_dialog.h"
|
||||||
#include "ui/windows/block_info_dialog.h"
|
#include "ui/windows/block_info_dialog.h"
|
||||||
@@ -1691,6 +1691,9 @@ void App::render()
|
|||||||
case ui::NavPage::History:
|
case ui::NavPage::History:
|
||||||
ui::RenderTransactionsTab(this);
|
ui::RenderTransactionsTab(this);
|
||||||
break;
|
break;
|
||||||
|
case ui::NavPage::Contacts:
|
||||||
|
ui::RenderContactsTab(this);
|
||||||
|
break;
|
||||||
case ui::NavPage::Mining:
|
case ui::NavPage::Mining:
|
||||||
ui::RenderMiningTab(this);
|
ui::RenderMiningTab(this);
|
||||||
break;
|
break;
|
||||||
@@ -1889,9 +1892,6 @@ void App::render()
|
|||||||
// Validate address dialog (triggered from Edit menu)
|
// Validate address dialog (triggered from Edit menu)
|
||||||
ui::ValidateAddressDialog::render(this);
|
ui::ValidateAddressDialog::render(this);
|
||||||
|
|
||||||
// Address book dialog (triggered from Edit menu)
|
|
||||||
ui::AddressBookDialog::render(this);
|
|
||||||
|
|
||||||
// Shield/merge dialog (triggered from Wallet menu)
|
// Shield/merge dialog (triggered from Wallet menu)
|
||||||
ui::ShieldDialog::render(this);
|
ui::ShieldDialog::render(this);
|
||||||
|
|
||||||
|
|||||||
@@ -621,7 +621,6 @@ private:
|
|||||||
bool show_import_key_ = false;
|
bool show_import_key_ = false;
|
||||||
bool show_export_key_ = false;
|
bool show_export_key_ = false;
|
||||||
bool show_backup_ = false;
|
bool show_backup_ = false;
|
||||||
bool show_address_book_ = false;
|
|
||||||
|
|
||||||
// Embedded daemon state
|
// Embedded daemon state
|
||||||
bool use_embedded_daemon_ = wallet::supportsEmbeddedDaemon(wallet::currentWalletCapabilities());
|
bool use_embedded_daemon_ = wallet::supportsEmbeddedDaemon(wallet::currentWalletCapabilities());
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ const char* tracePageName(ui::NavPage page)
|
|||||||
case ui::NavPage::Send: return "Send tab";
|
case ui::NavPage::Send: return "Send tab";
|
||||||
case ui::NavPage::Receive: return "Receive tab";
|
case ui::NavPage::Receive: return "Receive tab";
|
||||||
case ui::NavPage::History: return "History 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::Mining: return "Mining tab";
|
||||||
case ui::NavPage::Market: return "Market tab";
|
case ui::NavPage::Market: return "Market tab";
|
||||||
case ui::NavPage::Console: return "Console tab";
|
case ui::NavPage::Console: return "Console tab";
|
||||||
|
|||||||
@@ -33,7 +33,6 @@
|
|||||||
#include "../material/type.h"
|
#include "../material/type.h"
|
||||||
#include "../material/colors.h"
|
#include "../material/colors.h"
|
||||||
#include "../windows/validate_address_dialog.h"
|
#include "../windows/validate_address_dialog.h"
|
||||||
#include "../windows/address_book_dialog.h"
|
|
||||||
#include "../windows/shield_dialog.h"
|
#include "../windows/shield_dialog.h"
|
||||||
#include "../windows/request_payment_dialog.h"
|
#include "../windows/request_payment_dialog.h"
|
||||||
#include "../windows/block_info_dialog.h"
|
#include "../windows/block_info_dialog.h"
|
||||||
@@ -1260,7 +1259,7 @@ void RenderSettingsPage(App* app) {
|
|||||||
bw = std::max(minBtnW, bw);
|
bw = std::max(minBtnW, bw);
|
||||||
|
|
||||||
if (TactileButton(TR("settings_address_book"), ImVec2(bw, 0), S.resolveFont("button")))
|
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"));
|
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_address_book"));
|
||||||
ImGui::SameLine(0, btnSpacing);
|
ImGui::SameLine(0, btnSpacing);
|
||||||
if (TactileButton(TR("settings_validate_address"), ImVec2(bw, 0), S.resolveFont("button")))
|
if (TactileButton(TR("settings_validate_address"), ImVec2(bw, 0), S.resolveFont("button")))
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ enum class NavPage {
|
|||||||
Send,
|
Send,
|
||||||
Receive,
|
Receive,
|
||||||
History,
|
History,
|
||||||
|
Contacts,
|
||||||
// --- separator ---
|
// --- separator ---
|
||||||
Mining,
|
Mining,
|
||||||
Market,
|
Market,
|
||||||
@@ -51,6 +52,7 @@ inline const NavItem kNavItems[] = {
|
|||||||
{ "Send", NavPage::Send, nullptr, "send", nullptr },
|
{ "Send", NavPage::Send, nullptr, "send", nullptr },
|
||||||
{ "Receive", NavPage::Receive, nullptr, "receive", nullptr },
|
{ "Receive", NavPage::Receive, nullptr, "receive", nullptr },
|
||||||
{ "History", NavPage::History, nullptr, "history", nullptr },
|
{ "History", NavPage::History, nullptr, "history", nullptr },
|
||||||
|
{ "Contacts", NavPage::Contacts, nullptr, "contacts", nullptr },
|
||||||
{ "Mining", NavPage::Mining, "TOOLS", "mining", "tools" },
|
{ "Mining", NavPage::Mining, "TOOLS", "mining", "tools" },
|
||||||
{ "Market", NavPage::Market, nullptr, "market", nullptr },
|
{ "Market", NavPage::Market, nullptr, "market", nullptr },
|
||||||
{ "Console", NavPage::Console, "ADVANCED","console", "advanced" },
|
{ "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::Send: return wallet::WalletUiSurface::Send;
|
||||||
case NavPage::Receive: return wallet::WalletUiSurface::Receive;
|
case NavPage::Receive: return wallet::WalletUiSurface::Receive;
|
||||||
case NavPage::History: return wallet::WalletUiSurface::History;
|
case NavPage::History: return wallet::WalletUiSurface::History;
|
||||||
|
case NavPage::Contacts: return wallet::WalletUiSurface::Contacts;
|
||||||
case NavPage::Mining: return wallet::WalletUiSurface::Mining;
|
case NavPage::Mining: return wallet::WalletUiSurface::Mining;
|
||||||
case NavPage::Market: return wallet::WalletUiSurface::Market;
|
case NavPage::Market: return wallet::WalletUiSurface::Market;
|
||||||
case NavPage::Console: return wallet::WalletUiSurface::Console;
|
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::Send: return ICON_MD_CALL_MADE;
|
||||||
case NavPage::Receive: return ICON_MD_CALL_RECEIVED;
|
case NavPage::Receive: return ICON_MD_CALL_RECEIVED;
|
||||||
case NavPage::History: return ICON_MD_HISTORY;
|
case NavPage::History: return ICON_MD_HISTORY;
|
||||||
|
case NavPage::Contacts: return ICON_MD_CONTACTS;
|
||||||
case NavPage::Mining: return ICON_MD_CONSTRUCTION;
|
case NavPage::Mining: return ICON_MD_CONSTRUCTION;
|
||||||
case NavPage::Market: return ICON_MD_TRENDING_UP;
|
case NavPage::Market: return ICON_MD_TRENDING_UP;
|
||||||
case NavPage::Console: return ICON_MD_TERMINAL;
|
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_["receive"] = "Receive";
|
||||||
strings_["transactions"] = "Transactions";
|
strings_["transactions"] = "Transactions";
|
||||||
strings_["history"] = "History";
|
strings_["history"] = "History";
|
||||||
|
strings_["contacts"] = "Contacts";
|
||||||
strings_["mining"] = "Mining";
|
strings_["mining"] = "Mining";
|
||||||
strings_["peers"] = "Peers";
|
strings_["peers"] = "Peers";
|
||||||
strings_["market"] = "Market";
|
strings_["market"] = "Market";
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ enum class WalletUiSurface {
|
|||||||
Send,
|
Send,
|
||||||
Receive,
|
Receive,
|
||||||
History,
|
History,
|
||||||
|
Contacts, // address book / chat roster; available in both variants (default: return true)
|
||||||
Mining,
|
Mining,
|
||||||
Market,
|
Market,
|
||||||
Console,
|
Console,
|
||||||
|
|||||||
Reference in New Issue
Block a user