feat: modernize address list with drag-transfer, labels, and UX polish
- Rewrite RenderSharedAddressList with two-pass layout architecture - Add drag-to-transfer: drag address onto another to open transfer dialog - Add AddressLabelDialog with custom label text and 20-icon picker - Add AddressTransferDialog with amount input, fee, and balance preview - Add AddressMeta persistence (label, icon, sortOrder) in settings.json - Gold favorite border inset 2dp from container edge - Show hide button on all addresses, not just zero-balance - Smaller star/hide buttons to clear favorite border - Semi-transparent dragged row with context-aware tooltip - Copy-to-clipboard deferred to mouse-up (no copy on drag) - Themed colors via resolveColor() with CSS variable fallbacks - Keyboard nav (Up/Down/J/K, Enter to copy, F2 to edit label) - Add i18n keys for all new UI strings
This commit is contained in:
184
src/ui/windows/address_label_dialog.h
Normal file
184
src/ui/windows/address_label_dialog.h
Normal file
@@ -0,0 +1,184 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <cstring>
|
||||
#include "../../app.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
#include "../theme.h"
|
||||
#include "../../embedded/IconsMaterialDesign.h"
|
||||
#include "imgui.h"
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
class AddressLabelDialog {
|
||||
public:
|
||||
static void show(App* app, const std::string& address, bool isZ) {
|
||||
s_open = true;
|
||||
s_app = app;
|
||||
s_address = address;
|
||||
s_isZ = isZ;
|
||||
s_selectedIcon = -1;
|
||||
|
||||
// Pre-fill from existing metadata
|
||||
std::string existing = app->getAddressLabel(address);
|
||||
std::strncpy(s_label, existing.c_str(), sizeof(s_label) - 1);
|
||||
s_label[sizeof(s_label) - 1] = '\0';
|
||||
|
||||
std::string existingIcon = app->getAddressIcon(address);
|
||||
for (int i = 0; i < kIconCount; ++i) {
|
||||
if (kIconNames[i] == existingIcon) {
|
||||
s_selectedIcon = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void render() {
|
||||
if (!s_open || !s_app) return;
|
||||
|
||||
using namespace material;
|
||||
|
||||
if (BeginOverlayDialog(TR("set_label"), &s_open, 420.0f, 0.92f)) {
|
||||
float dp = Layout::dpiScale();
|
||||
|
||||
// Address preview
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(),
|
||||
s_isZ ? TR("shielded_address") : TR("transparent_address"));
|
||||
ImGui::Spacing();
|
||||
{
|
||||
std::string display = s_address;
|
||||
if (display.size() > 42) display = display.substr(0, 20) + "..." + display.substr(display.size() - 16);
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), display.c_str());
|
||||
}
|
||||
ImGui::Spacing();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Label input
|
||||
Type().text(TypeStyle::Subtitle2, TR("label"));
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
ImGui::InputTextWithHint("##AddrLabel", TR("label_placeholder"), s_label, sizeof(s_label));
|
||||
ImGui::Spacing();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Icon picker grid
|
||||
Type().text(TypeStyle::Subtitle2, TR("choose_icon"));
|
||||
ImGui::Spacing();
|
||||
|
||||
ImFont* iconFont = Type().iconMed();
|
||||
float iconFsz = ScaledFontSize(iconFont);
|
||||
float cellSz = iconFsz + 16.0f * dp;
|
||||
float avail = ImGui::GetContentRegionAvail().x;
|
||||
int cols = std::max(1, (int)(avail / (cellSz + 4.0f * dp)));
|
||||
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
|
||||
for (int i = 0; i < kIconCount; ++i) {
|
||||
if (i % cols != 0) ImGui::SameLine(0, 4.0f * dp);
|
||||
ImVec2 pos = ImGui::GetCursorScreenPos();
|
||||
ImVec2 mn = pos;
|
||||
ImVec2 mx(pos.x + cellSz, pos.y + cellSz);
|
||||
bool hov = ImGui::IsMouseHoveringRect(mn, mx);
|
||||
bool sel = (s_selectedIcon == i);
|
||||
|
||||
// Background
|
||||
if (sel) {
|
||||
dl->AddRectFilled(mn, mx, WithAlpha(Primary(), 40), 6.0f * dp);
|
||||
dl->AddRect(mn, mx, WithAlpha(Primary(), 120), 6.0f * dp, 0, 1.5f * dp);
|
||||
} else if (hov) {
|
||||
dl->AddRectFilled(mn, mx, IM_COL32(255, 255, 255, 20), 6.0f * dp);
|
||||
}
|
||||
|
||||
// Icon centered in cell
|
||||
ImVec2 iSz = iconFont->CalcTextSizeA(iconFsz, 1000.0f, 0.0f, kIconGlyphs[i]);
|
||||
dl->AddText(iconFont, iconFsz,
|
||||
ImVec2(mn.x + (cellSz - iSz.x) * 0.5f, mn.y + (cellSz - iSz.y) * 0.5f),
|
||||
sel ? Primary() : (hov ? OnSurface() : OnSurfaceMedium()),
|
||||
kIconGlyphs[i]);
|
||||
|
||||
ImGui::PushID(i);
|
||||
ImGui::InvisibleButton("##icon", ImVec2(cellSz, cellSz));
|
||||
if (ImGui::IsItemClicked()) s_selectedIcon = i;
|
||||
if (hov) ImGui::SetTooltip("%s", kIconNames[i]);
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
// "No icon" option
|
||||
ImGui::Spacing();
|
||||
if (s_selectedIcon >= 0) {
|
||||
if (ImGui::SmallButton(TR("clear_icon"))) {
|
||||
s_selectedIcon = -1;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Buttons
|
||||
float btnW = 120.0f;
|
||||
float totalW = btnW * 2 + Layout::spacingMd();
|
||||
ImGui::SetCursorPosX((ImGui::GetContentRegionAvail().x - totalW) * 0.5f);
|
||||
|
||||
if (TactileButton(TR("cancel"), ImVec2(btnW, 0))) {
|
||||
s_open = false;
|
||||
}
|
||||
ImGui::SameLine(0, Layout::spacingMd());
|
||||
if (TactileButton(TR("save"), ImVec2(btnW, 0))) {
|
||||
// Apply changes
|
||||
s_app->setAddressLabel(s_address, s_label);
|
||||
if (s_selectedIcon >= 0)
|
||||
s_app->setAddressIcon(s_address, kIconNames[s_selectedIcon]);
|
||||
else
|
||||
s_app->setAddressIcon(s_address, "");
|
||||
s_open = false;
|
||||
}
|
||||
|
||||
EndOverlayDialog();
|
||||
}
|
||||
}
|
||||
|
||||
static bool isOpen() { return s_open; }
|
||||
|
||||
private:
|
||||
static inline bool s_open = false;
|
||||
static inline App* s_app = nullptr;
|
||||
static inline std::string s_address;
|
||||
static inline bool s_isZ = false;
|
||||
static inline char s_label[128] = {};
|
||||
static inline int s_selectedIcon = -1;
|
||||
|
||||
// Icon palette — wallet-relevant Material Design icons
|
||||
static constexpr int kIconCount = 20;
|
||||
static inline const char* kIconNames[kIconCount] = {
|
||||
"savings", "account_balance", "wallet", "payments",
|
||||
"diamond", "shield", "lock", "swap_horiz",
|
||||
"store", "home", "work", "rocket_launch",
|
||||
"favorite", "bolt", "token", "category",
|
||||
"label", "coffee", "volunteer", "star"
|
||||
};
|
||||
static inline const char* kIconGlyphs[kIconCount] = {
|
||||
ICON_MD_SAVINGS, ICON_MD_ACCOUNT_BALANCE, ICON_MD_WALLET, ICON_MD_PAYMENTS,
|
||||
ICON_MD_DIAMOND, ICON_MD_SHIELD, ICON_MD_LOCK, ICON_MD_SWAP_HORIZ,
|
||||
ICON_MD_STORE, ICON_MD_HOME, ICON_MD_WORK, ICON_MD_ROCKET_LAUNCH,
|
||||
ICON_MD_FAVORITE, ICON_MD_BOLT, ICON_MD_TOKEN, ICON_MD_CATEGORY,
|
||||
ICON_MD_LABEL, ICON_MD_LOCAL_CAFE, ICON_MD_VOLUNTEER_ACTIVISM, ICON_MD_STAR
|
||||
};
|
||||
|
||||
public:
|
||||
// Expose for the address list to look up icon glyphs by name
|
||||
static const char* iconGlyphForName(const std::string& name) {
|
||||
for (int i = 0; i < kIconCount; ++i)
|
||||
if (kIconNames[i] == name) return kIconGlyphs[i];
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
Reference in New Issue
Block a user