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:
10
src/app.cpp
10
src/app.cpp
@@ -35,6 +35,8 @@
|
||||
#include "ui/windows/block_info_dialog.h"
|
||||
#include "ui/windows/export_all_keys_dialog.h"
|
||||
#include "ui/windows/export_transactions_dialog.h"
|
||||
#include "ui/windows/address_label_dialog.h"
|
||||
#include "ui/windows/address_transfer_dialog.h"
|
||||
#include "ui/windows/console_tab.h"
|
||||
#include "ui/pages/settings_page.h"
|
||||
#include "ui/theme.h"
|
||||
@@ -1347,7 +1349,13 @@ void App::render()
|
||||
|
||||
// Export transactions to CSV dialog (triggered from File menu)
|
||||
ui::ExportTransactionsDialog::render(this);
|
||||
|
||||
|
||||
// Address label/icon editor
|
||||
ui::AddressLabelDialog::render();
|
||||
|
||||
// Address-to-address transfer confirmation
|
||||
ui::AddressTransferDialog::render();
|
||||
|
||||
// Windows Defender antivirus help dialog
|
||||
renderAntivirusHelpDialog();
|
||||
|
||||
|
||||
10
src/app.h
10
src/app.h
@@ -202,6 +202,16 @@ public:
|
||||
void unfavoriteAddress(const std::string& addr);
|
||||
bool isAddressFavorite(const std::string& addr) const;
|
||||
|
||||
// Address metadata (labels, icons, custom ordering)
|
||||
void setAddressLabel(const std::string& addr, const std::string& label);
|
||||
void setAddressIcon(const std::string& addr, const std::string& icon);
|
||||
std::string getAddressLabel(const std::string& addr) const;
|
||||
std::string getAddressIcon(const std::string& addr) const;
|
||||
int getAddressSortOrder(const std::string& addr) const;
|
||||
void setAddressSortOrder(const std::string& addr, int order);
|
||||
int getNextSortOrder() const;
|
||||
void swapAddressOrder(const std::string& a, const std::string& b);
|
||||
|
||||
// Key export/import
|
||||
void exportPrivateKey(const std::string& address, std::function<void(const std::string&)> callback);
|
||||
void exportAllKeys(std::function<void(const std::string&)> callback);
|
||||
|
||||
@@ -1640,6 +1640,61 @@ bool App::isAddressFavorite(const std::string& addr) const
|
||||
return settings_ && settings_->isAddressFavorite(addr);
|
||||
}
|
||||
|
||||
void App::setAddressLabel(const std::string& addr, const std::string& label)
|
||||
{
|
||||
if (settings_) {
|
||||
settings_->setAddressLabel(addr, label);
|
||||
settings_->save();
|
||||
}
|
||||
}
|
||||
|
||||
void App::setAddressIcon(const std::string& addr, const std::string& icon)
|
||||
{
|
||||
if (settings_) {
|
||||
settings_->setAddressIcon(addr, icon);
|
||||
settings_->save();
|
||||
}
|
||||
}
|
||||
|
||||
std::string App::getAddressLabel(const std::string& addr) const
|
||||
{
|
||||
if (!settings_) return "";
|
||||
return settings_->getAddressMeta(addr).label;
|
||||
}
|
||||
|
||||
std::string App::getAddressIcon(const std::string& addr) const
|
||||
{
|
||||
if (!settings_) return "";
|
||||
return settings_->getAddressMeta(addr).icon;
|
||||
}
|
||||
|
||||
int App::getAddressSortOrder(const std::string& addr) const
|
||||
{
|
||||
if (!settings_) return -1;
|
||||
return settings_->getAddressMeta(addr).sortOrder;
|
||||
}
|
||||
|
||||
void App::setAddressSortOrder(const std::string& addr, int order)
|
||||
{
|
||||
if (settings_) {
|
||||
settings_->setAddressSortOrder(addr, order);
|
||||
settings_->save();
|
||||
}
|
||||
}
|
||||
|
||||
int App::getNextSortOrder() const
|
||||
{
|
||||
return settings_ ? settings_->getNextSortOrder() : 0;
|
||||
}
|
||||
|
||||
void App::swapAddressOrder(const std::string& a, const std::string& b)
|
||||
{
|
||||
if (settings_) {
|
||||
settings_->swapAddressOrder(a, b);
|
||||
settings_->save();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Key Export/Import Operations
|
||||
// ============================================================================
|
||||
|
||||
@@ -126,6 +126,19 @@ bool Settings::load(const std::string& path)
|
||||
for (const auto& a : j["favorite_addresses"])
|
||||
if (a.is_string()) favorite_addresses_.insert(a.get<std::string>());
|
||||
}
|
||||
if (j.contains("address_meta") && j["address_meta"].is_object()) {
|
||||
address_meta_.clear();
|
||||
for (auto& [addr, meta] : j["address_meta"].items()) {
|
||||
AddressMeta m;
|
||||
if (meta.contains("label") && meta["label"].is_string())
|
||||
m.label = meta["label"].get<std::string>();
|
||||
if (meta.contains("icon") && meta["icon"].is_string())
|
||||
m.icon = meta["icon"].get<std::string>();
|
||||
if (meta.contains("order") && meta["order"].is_number_integer())
|
||||
m.sortOrder = meta["order"].get<int>();
|
||||
address_meta_[addr] = m;
|
||||
}
|
||||
}
|
||||
if (j.contains("wizard_completed")) wizard_completed_ = j["wizard_completed"].get<bool>();
|
||||
if (j.contains("auto_lock_timeout")) auto_lock_timeout_ = j["auto_lock_timeout"].get<int>();
|
||||
if (j.contains("unlock_duration")) unlock_duration_ = j["unlock_duration"].get<int>();
|
||||
@@ -230,6 +243,18 @@ bool Settings::save(const std::string& path)
|
||||
j["favorite_addresses"] = json::array();
|
||||
for (const auto& addr : favorite_addresses_)
|
||||
j["favorite_addresses"].push_back(addr);
|
||||
{
|
||||
json meta_obj = json::object();
|
||||
for (const auto& [addr, m] : address_meta_) {
|
||||
if (m.label.empty() && m.icon.empty() && m.sortOrder < 0) continue;
|
||||
json entry = json::object();
|
||||
if (!m.label.empty()) entry["label"] = m.label;
|
||||
if (!m.icon.empty()) entry["icon"] = m.icon;
|
||||
if (m.sortOrder >= 0) entry["order"] = m.sortOrder;
|
||||
meta_obj[addr] = entry;
|
||||
}
|
||||
j["address_meta"] = meta_obj;
|
||||
}
|
||||
j["wizard_completed"] = wizard_completed_;
|
||||
j["auto_lock_timeout"] = auto_lock_timeout_;
|
||||
j["unlock_duration"] = unlock_duration_;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
@@ -141,6 +142,39 @@ public:
|
||||
void unfavoriteAddress(const std::string& addr) { favorite_addresses_.erase(addr); }
|
||||
int getFavoriteAddressCount() const { return (int)favorite_addresses_.size(); }
|
||||
|
||||
// Address metadata (labels, icons, custom ordering)
|
||||
struct AddressMeta {
|
||||
std::string label;
|
||||
std::string icon; // material icon name, e.g. "savings"
|
||||
int sortOrder = -1; // -1 = auto (use default sort)
|
||||
};
|
||||
const AddressMeta& getAddressMeta(const std::string& addr) const {
|
||||
static const AddressMeta empty{};
|
||||
auto it = address_meta_.find(addr);
|
||||
return it != address_meta_.end() ? it->second : empty;
|
||||
}
|
||||
void setAddressLabel(const std::string& addr, const std::string& label) {
|
||||
address_meta_[addr].label = label;
|
||||
}
|
||||
void setAddressIcon(const std::string& addr, const std::string& icon) {
|
||||
address_meta_[addr].icon = icon;
|
||||
}
|
||||
void setAddressSortOrder(const std::string& addr, int order) {
|
||||
address_meta_[addr].sortOrder = order;
|
||||
}
|
||||
int getNextSortOrder() const {
|
||||
int mx = -1;
|
||||
for (const auto& [k, v] : address_meta_)
|
||||
if (v.sortOrder > mx) mx = v.sortOrder;
|
||||
return mx + 1;
|
||||
}
|
||||
void swapAddressOrder(const std::string& a, const std::string& b) {
|
||||
int oa = address_meta_[a].sortOrder;
|
||||
int ob = address_meta_[b].sortOrder;
|
||||
address_meta_[a].sortOrder = ob;
|
||||
address_meta_[b].sortOrder = oa;
|
||||
}
|
||||
|
||||
// First-run wizard
|
||||
bool getWizardCompleted() const { return wizard_completed_; }
|
||||
void setWizardCompleted(bool v) { wizard_completed_ = v; }
|
||||
@@ -301,6 +335,7 @@ private:
|
||||
bool scanline_enabled_ = true;
|
||||
std::set<std::string> hidden_addresses_;
|
||||
std::set<std::string> favorite_addresses_;
|
||||
std::map<std::string, AddressMeta> address_meta_;
|
||||
bool wizard_completed_ = false;
|
||||
int auto_lock_timeout_ = 900; // 15 minutes
|
||||
int unlock_duration_ = 600; // 10 minutes
|
||||
|
||||
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
|
||||
293
src/ui/windows/address_transfer_dialog.h
Normal file
293
src/ui/windows/address_transfer_dialog.h
Normal file
@@ -0,0 +1,293 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cmath>
|
||||
#include "../../app.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
#include "../theme.h"
|
||||
#include "imgui.h"
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
/**
|
||||
* @brief Modal dialog for transferring funds between two addresses.
|
||||
*
|
||||
* Shown when a user drags one address row onto another in the address list.
|
||||
* Displays from/to addresses, balances, amount input, fee, and a preview of
|
||||
* the resulting balances after transfer.
|
||||
*/
|
||||
struct AddressTransferInfo {
|
||||
std::string fromAddr;
|
||||
std::string toAddr;
|
||||
double fromBalance = 0.0;
|
||||
double toBalance = 0.0;
|
||||
bool fromIsZ = false;
|
||||
bool toIsZ = false;
|
||||
};
|
||||
|
||||
class AddressTransferDialog {
|
||||
public:
|
||||
using TransferInfo = AddressTransferInfo;
|
||||
|
||||
static void show(App* app, const TransferInfo& info) {
|
||||
s_open = true;
|
||||
s_app = app;
|
||||
s_info = info;
|
||||
s_sending = false;
|
||||
s_resultMsg.clear();
|
||||
s_success = false;
|
||||
|
||||
// Pre-fill amount with full source balance
|
||||
snprintf(s_amount, sizeof(s_amount), "%.8f", info.fromBalance);
|
||||
|
||||
// Default fee
|
||||
s_fee = 0.0001;
|
||||
}
|
||||
|
||||
static void render() {
|
||||
if (!s_open || !s_app) return;
|
||||
|
||||
using namespace material;
|
||||
|
||||
if (BeginOverlayDialog(TR("transfer_funds"), &s_open, 480.0f, 0.94f)) {
|
||||
float dp = Layout::dpiScale();
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
|
||||
// If we got a result, show it
|
||||
if (!s_resultMsg.empty()) {
|
||||
renderResult();
|
||||
EndOverlayDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
// Shielding / deshielding warning
|
||||
if (s_info.fromIsZ && !s_info.toIsZ) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning()));
|
||||
ImGui::TextWrapped("%s", TR("deshielding_warning"));
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::Spacing();
|
||||
} else if (!s_info.fromIsZ && s_info.toIsZ) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Success()));
|
||||
ImGui::TextWrapped("%s", TR("shielding_notice"));
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::Spacing();
|
||||
}
|
||||
|
||||
// From address
|
||||
Type().text(TypeStyle::Overline, TR("from"));
|
||||
renderAddressRow(s_info.fromAddr, s_info.fromBalance, s_info.fromIsZ, dp);
|
||||
ImGui::Spacing();
|
||||
|
||||
// Arrow
|
||||
{
|
||||
float arrowCX = ImGui::GetContentRegionAvail().x * 0.5f;
|
||||
ImGui::SetCursorPosX(arrowCX - 8.0f);
|
||||
ImFont* iconFont = Type().iconMed();
|
||||
float fsz = ScaledFontSize(iconFont);
|
||||
ImVec2 pos = ImGui::GetCursorScreenPos();
|
||||
dl->AddText(iconFont, fsz, pos, OnSurfaceMedium(), ICON_MD_ARROW_DOWNWARD);
|
||||
ImGui::Dummy(ImVec2(fsz, fsz));
|
||||
}
|
||||
ImGui::Spacing();
|
||||
|
||||
// To address
|
||||
Type().text(TypeStyle::Overline, TR("to"));
|
||||
renderAddressRow(s_info.toAddr, s_info.toBalance, s_info.toIsZ, dp);
|
||||
ImGui::Spacing();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Amount input
|
||||
Type().text(TypeStyle::Subtitle2, TR("amount"));
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
ImGui::InputText("##TransferAmt", s_amount, sizeof(s_amount),
|
||||
ImGuiInputTextFlags_CharsDecimal);
|
||||
|
||||
// Max button
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton(TR("max"))) {
|
||||
double maxAmt = s_info.fromBalance - s_fee;
|
||||
if (maxAmt < 0) maxAmt = 0;
|
||||
snprintf(s_amount, sizeof(s_amount), "%.8f", maxAmt);
|
||||
}
|
||||
ImGui::Spacing();
|
||||
|
||||
// Fee display
|
||||
{
|
||||
char feeBuf[64];
|
||||
snprintf(feeBuf, sizeof(feeBuf), "%s: %.4f DRGX", TR("fee"), s_fee);
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), feeBuf);
|
||||
}
|
||||
ImGui::Spacing();
|
||||
|
||||
// Preview after transfer
|
||||
double amount = atof(s_amount);
|
||||
double totalDeduct = amount + s_fee;
|
||||
double newFromBal = s_info.fromBalance - totalDeduct;
|
||||
double newToBal = s_info.toBalance + amount;
|
||||
bool amountValid = amount > 0 && totalDeduct <= s_info.fromBalance;
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
Type().text(TypeStyle::Subtitle2, TR("result_preview"));
|
||||
ImGui::Spacing();
|
||||
{
|
||||
char buf[128];
|
||||
snprintf(buf, sizeof(buf), "%s: %.8f DRGX → %.8f DRGX",
|
||||
TR("sender_balance"), s_info.fromBalance, amountValid ? newFromBal : s_info.fromBalance);
|
||||
Type().textColored(TypeStyle::Caption,
|
||||
(amountValid && newFromBal < 1e-9) ? Warning() : OnSurfaceMedium(), buf);
|
||||
}
|
||||
{
|
||||
char buf[128];
|
||||
snprintf(buf, sizeof(buf), "%s: %.8f DRGX → %.8f DRGX",
|
||||
TR("recipient_balance"), s_info.toBalance, amountValid ? newToBal : s_info.toBalance);
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), buf);
|
||||
}
|
||||
|
||||
// Warnings
|
||||
if (!amountValid && amount > 0) {
|
||||
ImGui::Spacing();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Error()));
|
||||
ImGui::TextWrapped("%s", TR("insufficient_funds"));
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
if (amountValid && newFromBal < 1e-9) {
|
||||
ImGui::Spacing();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning()));
|
||||
ImGui::TextWrapped("%s", TR("sends_full_balance_warning"));
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
|
||||
// Buttons
|
||||
float btnW = 140.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());
|
||||
|
||||
ImGui::BeginDisabled(!amountValid || s_sending);
|
||||
if (TactileButton(s_sending ? TR("sending") : TR("confirm_transfer"), ImVec2(btnW, 0))) {
|
||||
s_sending = true;
|
||||
s_app->sendTransaction(s_info.fromAddr, s_info.toAddr,
|
||||
amount, s_fee, "",
|
||||
[](bool ok, const std::string& result) {
|
||||
s_sending = false;
|
||||
s_success = ok;
|
||||
if (ok)
|
||||
s_resultMsg = result; // opid
|
||||
else
|
||||
s_resultMsg = result; // error message
|
||||
});
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
|
||||
EndOverlayDialog();
|
||||
}
|
||||
}
|
||||
|
||||
static bool isOpen() { return s_open; }
|
||||
static void close() { s_open = false; }
|
||||
|
||||
private:
|
||||
static void renderAddressRow(const std::string& addr, double balance, bool isZ, float dp) {
|
||||
using namespace material;
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
ImVec2 pos = ImGui::GetCursorScreenPos();
|
||||
float w = ImGui::GetContentRegionAvail().x;
|
||||
float h = 40.0f * dp;
|
||||
ImVec2 mn = pos, mx(pos.x + w, pos.y + h);
|
||||
|
||||
GlassPanelSpec g;
|
||||
g.rounding = 8.0f * dp;
|
||||
g.fillAlpha = 18;
|
||||
DrawGlassPanel(dl, mn, mx, g);
|
||||
|
||||
float pad = 12.0f * dp;
|
||||
// Type icon
|
||||
ImFont* iconFont = Type().iconSmall();
|
||||
const char* icon = isZ ? ICON_MD_SHIELD : ICON_MD_CIRCLE;
|
||||
ImU32 col = isZ ? Success() : Warning();
|
||||
float iconFsz = ScaledFontSize(iconFont);
|
||||
ImVec2 iSz = iconFont->CalcTextSizeA(iconFsz, 1000.0f, 0.0f, icon);
|
||||
dl->AddText(iconFont, iconFsz, ImVec2(mn.x + pad, mn.y + (h - iSz.y) * 0.5f), col, icon);
|
||||
|
||||
// Address (truncated)
|
||||
float textX = mn.x + pad + iSz.x + 8.0f * dp;
|
||||
std::string display = addr;
|
||||
if (display.size() > 42) display = display.substr(0, 18) + "..." + display.substr(display.size() - 14);
|
||||
ImFont* capFont = Type().caption();
|
||||
float capFsz = ScaledFontSize(capFont);
|
||||
dl->AddText(capFont, capFsz, ImVec2(textX, mn.y + (h * 0.5f - capFsz)), OnSurfaceMedium(), display.c_str());
|
||||
|
||||
// Balance (right-aligned)
|
||||
char balBuf[32];
|
||||
snprintf(balBuf, sizeof(balBuf), "%.8f DRGX", balance);
|
||||
ImFont* body = Type().body2();
|
||||
float bodyFsz = ScaledFontSize(body);
|
||||
ImVec2 balSz = body->CalcTextSizeA(bodyFsz, 1000.0f, 0.0f, balBuf);
|
||||
dl->AddText(body, bodyFsz, ImVec2(mx.x - pad - balSz.x, mn.y + (h - balSz.y) * 0.5f),
|
||||
balance > 0 ? OnSurface() : OnSurfaceDisabled(), balBuf);
|
||||
|
||||
ImGui::Dummy(ImVec2(w, h));
|
||||
}
|
||||
|
||||
static void renderResult() {
|
||||
using namespace material;
|
||||
if (s_success) {
|
||||
ImGui::Spacing();
|
||||
Type().text(TypeStyle::H6, TR("transfer_sent"));
|
||||
ImGui::Spacing();
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("transfer_sent_desc"));
|
||||
ImGui::Spacing();
|
||||
{
|
||||
char buf[128];
|
||||
snprintf(buf, sizeof(buf), "Operation ID: %s",
|
||||
s_resultMsg.size() > 40
|
||||
? (s_resultMsg.substr(0, 20) + "..." + s_resultMsg.substr(s_resultMsg.size() - 10)).c_str()
|
||||
: s_resultMsg.c_str());
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf);
|
||||
}
|
||||
} else {
|
||||
ImGui::Spacing();
|
||||
Type().textColored(TypeStyle::H6, Error(), TR("transfer_failed"));
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped("%s", s_resultMsg.c_str());
|
||||
}
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::Spacing();
|
||||
float btnW = 120.0f;
|
||||
ImGui::SetCursorPosX((ImGui::GetContentRegionAvail().x - btnW) * 0.5f);
|
||||
if (TactileButton(TR("close"), ImVec2(btnW, 0))) {
|
||||
s_open = false;
|
||||
}
|
||||
}
|
||||
|
||||
static inline bool s_open = false;
|
||||
static inline App* s_app = nullptr;
|
||||
static inline TransferInfo s_info;
|
||||
static inline char s_amount[64] = {};
|
||||
static inline double s_fee = 0.0001;
|
||||
static inline bool s_sending = false;
|
||||
static inline bool s_success = false;
|
||||
static inline std::string s_resultMsg;
|
||||
};
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
@@ -5,6 +5,8 @@
|
||||
#include "balance_tab.h"
|
||||
#include "key_export_dialog.h"
|
||||
#include "qr_popup_dialog.h"
|
||||
#include "address_label_dialog.h"
|
||||
#include "address_transfer_dialog.h"
|
||||
#include "send_tab.h"
|
||||
#include "../../app.h"
|
||||
#include "../../config/settings.h"
|
||||
@@ -1360,50 +1362,70 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
Type().text(TypeStyle::H6, TR("your_addresses"));
|
||||
ImGui::Spacing();
|
||||
|
||||
// ---- Persistent state ----
|
||||
static char addr_search[128] = "";
|
||||
static bool s_hideZeroBalances = true;
|
||||
static bool s_showHidden = false;
|
||||
|
||||
struct AddrRow { const AddressInfo* info; bool isZ; bool hidden; bool favorite; };
|
||||
// Drag state
|
||||
static int s_dragIdx = -1; // row being dragged (-1 = none)
|
||||
static float s_dragOffsetY = 0.0f; // mouse offset from row top
|
||||
static float s_dragStartY = 0.0f; // mouse Y at drag start
|
||||
static bool s_dragActive = false; // drag distance threshold passed
|
||||
static int s_dropTargetIdx = -1; // row hovered during drag
|
||||
|
||||
// Copy feedback
|
||||
static int s_copiedRow = -1;
|
||||
static float s_copiedTime = 0.0f;
|
||||
|
||||
// ---- Build and filter address rows ----
|
||||
struct AddrRow {
|
||||
const AddressInfo* info;
|
||||
bool isZ, hidden, favorite;
|
||||
std::string label, icon;
|
||||
int sortOrder;
|
||||
};
|
||||
std::vector<AddrRow> rows;
|
||||
rows.reserve(state.z_addresses.size() + state.t_addresses.size());
|
||||
for (const auto& a : state.z_addresses) {
|
||||
std::string filter(addr_search);
|
||||
if (!containsIgnoreCase(a.address, filter) &&
|
||||
!containsIgnoreCase(a.label, filter)) continue;
|
||||
bool isHidden = app->isAddressHidden(a.address);
|
||||
if (isHidden && !s_showHidden) continue;
|
||||
bool isFav = app->isAddressFavorite(a.address);
|
||||
if (s_hideZeroBalances && a.balance < 1e-9 && !isHidden && !isFav) continue;
|
||||
rows.push_back({&a, true, isHidden, isFav});
|
||||
}
|
||||
for (const auto& a : state.t_addresses) {
|
||||
std::string filter(addr_search);
|
||||
if (!containsIgnoreCase(a.address, filter) &&
|
||||
!containsIgnoreCase(a.label, filter)) continue;
|
||||
bool isHidden = app->isAddressHidden(a.address);
|
||||
if (isHidden && !s_showHidden) continue;
|
||||
bool isFav = app->isAddressFavorite(a.address);
|
||||
if (s_hideZeroBalances && a.balance < 1e-9 && !isHidden && !isFav) continue;
|
||||
rows.push_back({&a, false, isHidden, isFav});
|
||||
}
|
||||
static int s_sortCol = 3;
|
||||
static bool s_sortAsc = false;
|
||||
|
||||
auto addRows = [&](const auto& addrs, bool isZ) {
|
||||
for (const auto& a : addrs) {
|
||||
std::string filter(addr_search);
|
||||
std::string addrLabel = app->getAddressLabel(a.address);
|
||||
if (!containsIgnoreCase(a.address, filter) &&
|
||||
!containsIgnoreCase(a.label, filter) &&
|
||||
!containsIgnoreCase(addrLabel, filter)) continue;
|
||||
bool isHidden = app->isAddressHidden(a.address);
|
||||
if (isHidden && !s_showHidden) continue;
|
||||
bool isFav = app->isAddressFavorite(a.address);
|
||||
if (s_hideZeroBalances && a.balance < 1e-9 && !isHidden && !isFav) continue;
|
||||
rows.push_back({&a, isZ, isHidden, isFav,
|
||||
addrLabel, app->getAddressIcon(a.address),
|
||||
app->getAddressSortOrder(a.address)});
|
||||
}
|
||||
};
|
||||
addRows(state.z_addresses, true);
|
||||
addRows(state.t_addresses, false);
|
||||
|
||||
// Sort: custom order (if any) → favorites → Z first → balance desc
|
||||
std::sort(rows.begin(), rows.end(),
|
||||
[](const AddrRow& a, const AddrRow& b) -> bool {
|
||||
bool aHasOrder = a.sortOrder >= 0;
|
||||
bool bHasOrder = b.sortOrder >= 0;
|
||||
if (aHasOrder && bHasOrder) return a.sortOrder < b.sortOrder;
|
||||
if (aHasOrder != bHasOrder) return aHasOrder > bHasOrder;
|
||||
if (a.favorite != b.favorite) return a.favorite > b.favorite;
|
||||
if (a.isZ != b.isZ) return a.isZ > b.isZ;
|
||||
if (s_sortAsc) return a.info->balance < b.info->balance;
|
||||
else return a.info->balance > b.info->balance;
|
||||
return a.info->balance > b.info->balance;
|
||||
});
|
||||
|
||||
// Search + create buttons row
|
||||
// ---- Toolbar: search + checkboxes + create buttons ----
|
||||
float avail = ImGui::GetContentRegionAvail().x;
|
||||
float schemaMaxW = (searchIn.maxWidth >= 0) ? searchIn.maxWidth : 250.0f;
|
||||
float schemaRatio = (searchIn.widthRatio >= 0) ? searchIn.widthRatio : 0.30f;
|
||||
float searchW = std::min(schemaMaxW * hs, avail * schemaRatio);
|
||||
ImGui::SetNextItemWidth(searchW);
|
||||
ImGui::InputTextWithHint("##AddrSearch", "Filter...", addr_search, sizeof(addr_search));
|
||||
ImGui::InputTextWithHint("##AddrSearch", TR("filter"), addr_search, sizeof(addr_search));
|
||||
ImGui::SameLine(0, Layout::spacingLg());
|
||||
ImGui::Checkbox(TrId("hide_zero_balances", "hide0_v2").c_str(), &s_hideZeroBalances);
|
||||
{
|
||||
@@ -1455,6 +1477,7 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
// ---- Glass panel container ----
|
||||
float addrListH = listH;
|
||||
if (addrListH < 40.0f * dp) addrListH = 40.0f * dp;
|
||||
|
||||
@@ -1477,6 +1500,7 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
// ---- Empty states ----
|
||||
if (!app->isConnected()) {
|
||||
ImGui::Dummy(ImVec2(0, 16 * dp));
|
||||
float cw = ImGui::GetContentRegionAvail().x;
|
||||
@@ -1487,23 +1511,17 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
float cw = ImGui::GetContentRegionAvail().x;
|
||||
float ch = ImGui::GetContentRegionAvail().y;
|
||||
if (ch < 60) ch = 60;
|
||||
if (addr_search[0]) {
|
||||
ImVec2 textSz = ImGui::CalcTextSize(TR("no_addresses_match"));
|
||||
ImGui::SetCursorPosX((cw - textSz.x) * 0.5f);
|
||||
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ch * 0.25f);
|
||||
ImGui::TextDisabled("%s", TR("no_addresses_match"));
|
||||
} else {
|
||||
const char* msg = "No addresses yet";
|
||||
ImVec2 msgSz = ImGui::CalcTextSize(msg);
|
||||
ImGui::SetCursorPosX((cw - msgSz.x) * 0.5f);
|
||||
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ch * 0.25f);
|
||||
ImGui::TextDisabled("%s", msg);
|
||||
}
|
||||
const char* emptyMsg = addr_search[0] ? TR("no_addresses_match") : TR("no_addresses_yet");
|
||||
ImVec2 msgSz = ImGui::CalcTextSize(emptyMsg);
|
||||
ImGui::SetCursorPosX((cw - msgSz.x) * 0.5f);
|
||||
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ch * 0.25f);
|
||||
ImGui::TextDisabled("%s", emptyMsg);
|
||||
} else {
|
||||
// ---- PASS 1: Compute row layout ----
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
ImFont* capFont = Type().caption();
|
||||
ImFont* body2 = Type().body2();
|
||||
float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg() + Layout::spacingMd();
|
||||
float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg() + Layout::spacingMd() + Layout::spacingMd();
|
||||
static int selected_row = -1;
|
||||
addrScrollY = ImGui::GetScrollY();
|
||||
addrScrollMaxY = ImGui::GetScrollMaxY();
|
||||
@@ -1515,181 +1533,345 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
S.drawElement("tabs.balance", "address-icon-size").size * hs);
|
||||
float innerW = ImGui::GetContentRegionAvail().x;
|
||||
|
||||
// Theme colors for buttons (resolved once)
|
||||
ImU32 favGoldFill = S.resolveColor("var(--favorite-fill)", IM_COL32(255, 200, 50, 40));
|
||||
ImU32 favGoldBorder = S.resolveColor("var(--favorite-border)", IM_COL32(255, 200, 50, 100));
|
||||
ImU32 favGoldIcon = S.resolveColor("var(--favorite-icon)", IM_COL32(255, 200, 50, 255));
|
||||
ImU32 favGoldOutline= S.resolveColor("var(--favorite-outline)", IM_COL32(255, 200, 50, 120));
|
||||
ImU32 btnFill = S.resolveColor("var(--row-button-fill)", IM_COL32(255, 255, 255, 12));
|
||||
ImU32 btnFillHov = S.resolveColor("var(--row-button-fill-hover)", IM_COL32(255, 255, 255, 25));
|
||||
ImU32 btnBorder = S.resolveColor("var(--row-button-border)", IM_COL32(255, 255, 255, 25));
|
||||
ImU32 btnBorderHov = S.resolveColor("var(--row-button-border-hover)", IM_COL32(255, 255, 255, 50));
|
||||
ImU32 rowHoverCol = S.resolveColor("var(--row-hover)", IM_COL32(255, 255, 255, 15));
|
||||
ImU32 rowSelectCol = S.resolveColor("var(--row-select)", IM_COL32(255, 255, 255, 20));
|
||||
ImU32 dividerCol = S.resolveColor("var(--sidebar-divider)", IM_COL32(255, 255, 255, 15));
|
||||
|
||||
// Compute Y positions for all rows (pass 1)
|
||||
std::vector<float> rowY(rows.size());
|
||||
float cursorStartY = ImGui::GetCursorScreenPos().y;
|
||||
for (int i = 0; i < (int)rows.size(); ++i)
|
||||
rowY[i] = cursorStartY + i * rowH;
|
||||
|
||||
// Drag logic — detect drag start, compute drag position
|
||||
ImVec2 mousePos = ImGui::GetMousePos();
|
||||
bool mouseDown = ImGui::IsMouseDown(ImGuiMouseButton_Left);
|
||||
bool mouseClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Left);
|
||||
|
||||
// Reset drop target each frame — it gets set only if mouse is over a row
|
||||
if (s_dragActive) s_dropTargetIdx = -1;
|
||||
|
||||
if (s_dragIdx >= 0 && !mouseDown) {
|
||||
// Mouse released — copy if it was a click (no drag activated)
|
||||
if (!s_dragActive && s_dragIdx >= 0 && s_dragIdx < (int)rows.size()) {
|
||||
const auto& clickRow = rows[s_dragIdx];
|
||||
ImGui::SetClipboardText(clickRow.info->address.c_str());
|
||||
selected_row = s_dragIdx;
|
||||
s_copiedRow = s_dragIdx;
|
||||
s_copiedTime = (float)ImGui::GetTime();
|
||||
}
|
||||
// Drop
|
||||
if (s_dragActive && s_dropTargetIdx >= 0 && s_dropTargetIdx != s_dragIdx) {
|
||||
const auto& srcRow = rows[s_dragIdx];
|
||||
const auto& dstRow = rows[s_dropTargetIdx];
|
||||
// Transfer: if dropped on another row and drag was active
|
||||
if (srcRow.info->balance > 1e-9) {
|
||||
AddressTransferDialog::TransferInfo ti;
|
||||
ti.fromAddr = srcRow.info->address;
|
||||
ti.toAddr = dstRow.info->address;
|
||||
ti.fromBalance = srcRow.info->balance;
|
||||
ti.toBalance = dstRow.info->balance;
|
||||
ti.fromIsZ = srcRow.isZ;
|
||||
ti.toIsZ = dstRow.isZ;
|
||||
AddressTransferDialog::show(app, ti);
|
||||
}
|
||||
} else if (s_dragActive && s_dropTargetIdx < 0) {
|
||||
// Reorder: dropped in gap — compute insert position from mouseY
|
||||
int insertIdx = 0;
|
||||
for (int i = 0; i < (int)rows.size(); ++i) {
|
||||
if (mousePos.y > rowY[i] + rowH * 0.5f) insertIdx = i + 1;
|
||||
}
|
||||
if (insertIdx != s_dragIdx && insertIdx != s_dragIdx + 1) {
|
||||
int targetIdx = (insertIdx > s_dragIdx) ? insertIdx - 1 : insertIdx;
|
||||
if (targetIdx >= 0 && targetIdx < (int)rows.size()) {
|
||||
app->swapAddressOrder(rows[s_dragIdx].info->address,
|
||||
rows[targetIdx].info->address);
|
||||
}
|
||||
}
|
||||
}
|
||||
s_dragIdx = -1;
|
||||
s_dragActive = false;
|
||||
s_dropTargetIdx = -1;
|
||||
}
|
||||
|
||||
// ---- PASS 2: Render rows ----
|
||||
for (int row_idx = 0; row_idx < (int)rows.size(); row_idx++) {
|
||||
const auto& row = rows[row_idx];
|
||||
const auto& addr = *row.info;
|
||||
ImVec2 rowPos = ImGui::GetCursorScreenPos();
|
||||
ImVec2 rowEnd(rowPos.x + innerW, rowPos.y + rowH);
|
||||
bool isDragged = (s_dragIdx == row_idx && s_dragActive);
|
||||
|
||||
// For dragged row, offset Y to follow mouse
|
||||
float drawY = rowY[row_idx];
|
||||
if (isDragged) {
|
||||
drawY = mousePos.y - s_dragOffsetY;
|
||||
}
|
||||
|
||||
ImVec2 rowPos(ImGui::GetCursorScreenPos().x, drawY);
|
||||
ImVec2 rowEnd(rowPos.x + innerW, drawY + rowH);
|
||||
ImU32 typeCol = row.isZ ? greenCol : goldCol;
|
||||
if (row.hidden) typeCol = OnSurfaceDisabled();
|
||||
|
||||
// Golden border for favorites
|
||||
if (row.favorite) {
|
||||
ImU32 favBorder = IM_COL32(255, 200, 50, 120);
|
||||
dl->AddRect(rowPos, rowEnd, favBorder, 4.0f * dp, 0, 1.5f * dp);
|
||||
// Dragged row: draw with semi-transparent elevation
|
||||
if (isDragged) {
|
||||
dl->AddRectFilled(ImVec2(rowPos.x - 2*dp, rowPos.y - 2*dp),
|
||||
ImVec2(rowEnd.x + 2*dp, rowEnd.y + 2*dp),
|
||||
IM_COL32(0, 0, 0, 30), 6.0f * dp);
|
||||
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(30, 30, 40, 120), 4.0f * dp);
|
||||
// Tooltip following cursor — show transfer intent if over a target row
|
||||
if (s_dropTargetIdx >= 0 && s_dropTargetIdx < (int)rows.size()) {
|
||||
const auto& target = rows[s_dropTargetIdx];
|
||||
ImGui::SetTooltip("%s\n%s\n\n%s %s",
|
||||
truncateAddress(addr.address, 32).c_str(),
|
||||
row.isZ ? TR("shielded") : TR("transparent"),
|
||||
TR("transfer_to"),
|
||||
truncateAddress(target.info->address, 32).c_str());
|
||||
} else {
|
||||
ImGui::SetTooltip("%s\n%s",
|
||||
truncateAddress(addr.address, 32).c_str(),
|
||||
row.isZ ? TR("shielded") : TR("transparent"));
|
||||
}
|
||||
}
|
||||
|
||||
if (selected_row == row_idx) {
|
||||
// Golden border for favorites — inset so it doesn't touch container
|
||||
if (row.favorite) {
|
||||
float inset = 2.0f * dp;
|
||||
dl->AddRect(ImVec2(rowPos.x + inset, rowPos.y + inset),
|
||||
ImVec2(rowEnd.x - inset, rowEnd.y - inset),
|
||||
favGoldOutline, 4.0f * dp, 0, 1.5f * dp);
|
||||
}
|
||||
|
||||
// Selection highlight
|
||||
if (selected_row == row_idx && !isDragged) {
|
||||
ImDrawFlags accentFlags = 0;
|
||||
float accentRound = 2.0f * dp;
|
||||
if (row_idx == 0) { accentFlags = ImDrawFlags_RoundCornersTopLeft; accentRound = glassRound; }
|
||||
if (row_idx == (int)rows.size() - 1) { accentFlags |= ImDrawFlags_RoundCornersBottomLeft; accentRound = glassRound; }
|
||||
dl->AddRectFilled(rowPos, ImVec2(rowPos.x + 3 * dp, rowEnd.y), typeCol, accentRound, accentFlags);
|
||||
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 20), 4.0f * dp);
|
||||
dl->AddRectFilled(rowPos, rowEnd, rowSelectCol, 4.0f * dp);
|
||||
}
|
||||
|
||||
bool hovered = material::IsRectHovered(rowPos, rowEnd);
|
||||
if (hovered && selected_row != row_idx) {
|
||||
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 15), 4.0f * dp);
|
||||
// Hover effects
|
||||
bool hovered = !isDragged && material::IsRectHovered(rowPos, rowEnd);
|
||||
|
||||
// Drop target highlight when dragging another row over this one
|
||||
if (s_dragActive && s_dragIdx != row_idx && material::IsRectHovered(rowPos, rowEnd)) {
|
||||
s_dropTargetIdx = row_idx;
|
||||
ImU32 dropCol = WithAlpha(Primary(), 40);
|
||||
dl->AddRectFilled(rowPos, rowEnd, dropCol, 4.0f * dp);
|
||||
dl->AddRect(rowPos, rowEnd, WithAlpha(Primary(), 140), 4.0f * dp, 0, 2.0f * dp);
|
||||
}
|
||||
|
||||
if (hovered && selected_row != row_idx && !s_dragActive) {
|
||||
dl->AddRectFilled(rowPos, rowEnd, rowHoverCol, 4.0f * dp);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
}
|
||||
|
||||
float cx = rowPos.x + rowPadLeft;
|
||||
float cy = rowPos.y + Layout::spacingMd();
|
||||
|
||||
// --- Button zone (right edge): [eye] [star] ---
|
||||
float btnH = rowH - Layout::spacingSm() * 2.0f;
|
||||
// ---- Button zone (right edge): [eye] [star] ----
|
||||
float btnH = rowH - Layout::spacingMd() * 2.0f;
|
||||
float btnW = btnH;
|
||||
float btnGap = Layout::spacingSm();
|
||||
float btnY = rowPos.y + (rowH - btnH) * 0.5f;
|
||||
float rightEdge = rowPos.x + innerW;
|
||||
float starX = rightEdge - btnW - rowPadRight;
|
||||
float starX = rightEdge - btnW - Layout::spacingXs();
|
||||
float eyeX = starX - btnGap - btnW;
|
||||
float btnRound = 6.0f * dp;
|
||||
bool btnClicked = false;
|
||||
|
||||
// Star button (always shown, rightmost)
|
||||
{
|
||||
ImVec2 bMin(starX, btnY), bMax(starX + btnW, btnY + btnH);
|
||||
bool bHov = ImGui::IsMouseHoveringRect(bMin, bMax);
|
||||
ImU32 starFill = row.favorite ? IM_COL32(255, 200, 50, 40) : IM_COL32(255, 255, 255, bHov ? 25 : 12);
|
||||
ImU32 starBorder = row.favorite ? IM_COL32(255, 200, 50, 100) : IM_COL32(255, 255, 255, bHov ? 50 : 25);
|
||||
dl->AddRectFilled(bMin, bMax, starFill, btnRound);
|
||||
dl->AddRect(bMin, bMax, starBorder, btnRound, 0, 1.0f * dp);
|
||||
ImFont* iconFont = material::Type().iconSmall();
|
||||
const char* starIcon = row.favorite ? ICON_MD_STAR : ICON_MD_STAR_BORDER;
|
||||
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, starIcon);
|
||||
ImU32 starCol = row.favorite ? IM_COL32(255, 200, 50, 255) : (bHov ? OnSurface() : OnSurfaceDisabled());
|
||||
dl->AddText(iconFont, iconFont->LegacySize,
|
||||
ImVec2(starX + (btnW - iSz.x) * 0.5f, btnY + (btnH - iSz.y) * 0.5f), starCol, starIcon);
|
||||
if (bHov && ImGui::IsMouseClicked(0)) {
|
||||
if (row.favorite) app->unfavoriteAddress(addr.address);
|
||||
else app->favoriteAddress(addr.address);
|
||||
btnClicked = true;
|
||||
if (!isDragged) {
|
||||
// Star button
|
||||
{
|
||||
ImVec2 bMin(starX, btnY), bMax(starX + btnW, btnY + btnH);
|
||||
bool bHov = ImGui::IsMouseHoveringRect(bMin, bMax);
|
||||
dl->AddRectFilled(bMin, bMax, row.favorite ? favGoldFill : (bHov ? btnFillHov : btnFill), btnRound);
|
||||
dl->AddRect(bMin, bMax, row.favorite ? favGoldBorder : (bHov ? btnBorderHov : btnBorder), btnRound, 0, 1.0f * dp);
|
||||
ImFont* iconFont = Type().iconSmall();
|
||||
const char* starIcon = row.favorite ? ICON_MD_STAR : ICON_MD_STAR_BORDER;
|
||||
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, starIcon);
|
||||
ImU32 starCol = row.favorite ? favGoldIcon : (bHov ? OnSurface() : OnSurfaceDisabled());
|
||||
dl->AddText(iconFont, iconFont->LegacySize,
|
||||
ImVec2(starX + (btnW - iSz.x) * 0.5f, btnY + (btnH - iSz.y) * 0.5f), starCol, starIcon);
|
||||
if (bHov && mouseClicked) {
|
||||
if (row.favorite) app->unfavoriteAddress(addr.address);
|
||||
else app->favoriteAddress(addr.address);
|
||||
btnClicked = true;
|
||||
}
|
||||
if (bHov) ImGui::SetTooltip("%s", row.favorite ? TR("remove_favorite") : TR("favorite_address"));
|
||||
}
|
||||
if (bHov) ImGui::SetTooltip("%s", row.favorite ? TR("remove_favorite") : TR("favorite_address"));
|
||||
}
|
||||
|
||||
// Eye button (zero balance or hidden)
|
||||
if (addr.balance < 1e-9 || row.hidden) {
|
||||
ImVec2 bMin(eyeX, btnY), bMax(eyeX + btnW, btnY + btnH);
|
||||
bool bHov = ImGui::IsMouseHoveringRect(bMin, bMax);
|
||||
ImU32 eyeFill = IM_COL32(255, 255, 255, bHov ? 25 : 12);
|
||||
ImU32 eyeBorder = IM_COL32(255, 255, 255, bHov ? 50 : 25);
|
||||
dl->AddRectFilled(bMin, bMax, eyeFill, btnRound);
|
||||
dl->AddRect(bMin, bMax, eyeBorder, btnRound, 0, 1.0f * dp);
|
||||
ImFont* iconFont = material::Type().iconSmall();
|
||||
const char* hideIcon = row.hidden ? ICON_MD_VISIBILITY : ICON_MD_VISIBILITY_OFF;
|
||||
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, hideIcon);
|
||||
ImU32 iconCol = bHov ? OnSurface() : OnSurfaceDisabled();
|
||||
dl->AddText(iconFont, iconFont->LegacySize,
|
||||
ImVec2(eyeX + (btnW - iSz.x) * 0.5f, btnY + (btnH - iSz.y) * 0.5f), iconCol, hideIcon);
|
||||
if (bHov && ImGui::IsMouseClicked(0)) {
|
||||
if (row.hidden) app->unhideAddress(addr.address);
|
||||
else app->hideAddress(addr.address);
|
||||
btnClicked = true;
|
||||
// Eye button (zero balance or hidden)
|
||||
bool showEye = true;
|
||||
// Always reserve space for both buttons so content doesn't shift
|
||||
float contentRight = eyeX - Layout::spacingSm();
|
||||
|
||||
if (showEye) {
|
||||
ImVec2 bMin(eyeX, btnY), bMax(eyeX + btnW, btnY + btnH);
|
||||
bool bHov = ImGui::IsMouseHoveringRect(bMin, bMax);
|
||||
dl->AddRectFilled(bMin, bMax, bHov ? btnFillHov : btnFill, btnRound);
|
||||
dl->AddRect(bMin, bMax, bHov ? btnBorderHov : btnBorder, btnRound, 0, 1.0f * dp);
|
||||
ImFont* iconFont = Type().iconSmall();
|
||||
const char* hideIcon = row.hidden ? ICON_MD_VISIBILITY : ICON_MD_VISIBILITY_OFF;
|
||||
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, hideIcon);
|
||||
ImU32 iconCol = bHov ? OnSurface() : OnSurfaceDisabled();
|
||||
dl->AddText(iconFont, iconFont->LegacySize,
|
||||
ImVec2(eyeX + (btnW - iSz.x) * 0.5f, btnY + (btnH - iSz.y) * 0.5f), iconCol, hideIcon);
|
||||
if (bHov && mouseClicked) {
|
||||
if (row.hidden) app->unhideAddress(addr.address);
|
||||
else app->hideAddress(addr.address);
|
||||
btnClicked = true;
|
||||
}
|
||||
if (bHov) ImGui::SetTooltip("%s", row.hidden ? TR("restore_address") : TR("hide_address"));
|
||||
}
|
||||
if (bHov) ImGui::SetTooltip("%s", row.hidden ? TR("restore_address") : TR("hide_address"));
|
||||
}
|
||||
|
||||
// Content zone ends before buttons
|
||||
float contentRight = eyeX - Layout::spacingSm();
|
||||
// ---- Type icon or custom icon ----
|
||||
float iconCx = cx + rowIconSz;
|
||||
float iconCy = cy + body2->LegacySize * 0.5f;
|
||||
{
|
||||
const char* customGlyph = row.icon.empty() ? nullptr : AddressLabelDialog::iconGlyphForName(row.icon);
|
||||
ImFont* iconFont = Type().iconSmall();
|
||||
const char* glyph = customGlyph ? customGlyph : (row.isZ ? ICON_MD_SHIELD : ICON_MD_CIRCLE);
|
||||
ImU32 icCol = customGlyph ? OnSurfaceMedium() : typeCol;
|
||||
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, glyph);
|
||||
dl->AddText(iconFont, iconFont->LegacySize,
|
||||
ImVec2(iconCx - iSz.x * 0.5f, iconCy - iSz.y * 0.5f), icCol, glyph);
|
||||
}
|
||||
|
||||
float iconCx = cx + rowIconSz;
|
||||
float iconCy = cy + body2->LegacySize * 0.5f;
|
||||
if (row.isZ) {
|
||||
ImFont* iconFont = material::Type().iconSmall();
|
||||
const char* shieldIcon = ICON_MD_SHIELD;
|
||||
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, shieldIcon);
|
||||
dl->AddText(iconFont, iconFont->LegacySize,
|
||||
ImVec2(iconCx - iSz.x * 0.5f, iconCy - iSz.y * 0.5f), typeCol, shieldIcon);
|
||||
} else {
|
||||
ImFont* iconFont = material::Type().iconSmall();
|
||||
const char* circIcon = ICON_MD_CIRCLE;
|
||||
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, circIcon);
|
||||
dl->AddText(iconFont, iconFont->LegacySize,
|
||||
ImVec2(iconCx - iSz.x * 0.5f, iconCy - iSz.y * 0.5f), typeCol, circIcon);
|
||||
}
|
||||
// ---- Type label (first line) ----
|
||||
float labelX = cx + rowIconSz * 2.0f + Layout::spacingMd();
|
||||
{
|
||||
const char* typeLabel = row.isZ ? TR("shielded") : TR("transparent");
|
||||
const char* hiddenTag = row.hidden ? TR("hidden_tag") : "";
|
||||
char typeBuf[64];
|
||||
snprintf(typeBuf, sizeof(typeBuf), "%s%s", typeLabel, hiddenTag);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), typeCol, typeBuf);
|
||||
|
||||
float labelX = cx + rowIconSz * 2.0f + Layout::spacingSm();
|
||||
const char* typeLabel = row.isZ ? "Shielded" : "Transparent";
|
||||
const char* hiddenTag = row.hidden ? " (hidden)" : "";
|
||||
char typeBuf[64];
|
||||
snprintf(typeBuf, sizeof(typeBuf), "%s%s", typeLabel, hiddenTag);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), typeCol, typeBuf);
|
||||
if (!addr.label.empty()) {
|
||||
float typeLabelW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, typeBuf).x;
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(labelX + typeLabelW + Layout::spacingLg(), cy),
|
||||
OnSurfaceMedium(), addr.label.c_str());
|
||||
}
|
||||
// User label next to type
|
||||
if (!row.label.empty()) {
|
||||
float typeLabelW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, typeBuf).x;
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(labelX + typeLabelW + Layout::spacingLg(), cy),
|
||||
OnSurfaceMedium(), row.label.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// Address (second line) — show full if it fits, otherwise truncate
|
||||
float addrAvailW = contentRight - labelX;
|
||||
ImVec2 fullAddrSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, addr.address.c_str());
|
||||
std::string display_addr;
|
||||
if (fullAddrSz.x <= addrAvailW) {
|
||||
display_addr = addr.address;
|
||||
} else {
|
||||
int addrTruncLen = (addrTable.columns.count("address") && addrTable.columns.at("address").truncate > 0)
|
||||
? addrTable.columns.at("address").truncate : 32;
|
||||
display_addr = truncateAddress(addr.address, addrTruncLen);
|
||||
}
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(labelX, cy + body2->LegacySize + Layout::spacingXs()),
|
||||
OnSurfaceMedium(), display_addr.c_str());
|
||||
// ---- Address (second line) ----
|
||||
{
|
||||
float addrAvailW = contentRight - labelX;
|
||||
ImVec2 fullAddrSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, addr.address.c_str());
|
||||
std::string display_addr;
|
||||
if (fullAddrSz.x <= addrAvailW) {
|
||||
display_addr = addr.address;
|
||||
} else {
|
||||
int addrTruncLen = (addrTable.columns.count("address") && addrTable.columns.at("address").truncate > 0)
|
||||
? addrTable.columns.at("address").truncate : 32;
|
||||
display_addr = truncateAddress(addr.address, addrTruncLen);
|
||||
}
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(labelX, cy + body2->LegacySize + Layout::spacingXs()),
|
||||
OnSurfaceMedium(), display_addr.c_str());
|
||||
}
|
||||
|
||||
// Balance (right-aligned within content zone)
|
||||
char balBuf[32];
|
||||
snprintf(balBuf, sizeof(balBuf), "%.8f", addr.balance);
|
||||
ImVec2 balSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, balBuf);
|
||||
float balX = contentRight - balSz.x;
|
||||
ImU32 balCol = addr.balance > 0.0
|
||||
? (row.isZ ? greenCol : OnSurface())
|
||||
: OnSurfaceDisabled();
|
||||
if (row.hidden) balCol = OnSurfaceDisabled();
|
||||
DrawTextShadow(dl, body2, body2->LegacySize, ImVec2(balX, cy), balCol, balBuf);
|
||||
// ---- Balance (right-aligned, first line) ----
|
||||
{
|
||||
char balBuf[32];
|
||||
snprintf(balBuf, sizeof(balBuf), "%.8f", addr.balance);
|
||||
ImVec2 balSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, balBuf);
|
||||
float balX = contentRight - balSz.x;
|
||||
ImU32 balCol = addr.balance > 0.0
|
||||
? (row.isZ ? greenCol : OnSurface())
|
||||
: OnSurfaceDisabled();
|
||||
if (row.hidden) balCol = OnSurfaceDisabled();
|
||||
DrawTextShadow(dl, body2, body2->LegacySize, ImVec2(balX, cy), balCol, balBuf);
|
||||
}
|
||||
|
||||
double priceUsd = state.market.price_usd;
|
||||
if (priceUsd > 0.0 && addr.balance > 0.0) {
|
||||
char usdBuf[32];
|
||||
double usdVal = addr.balance * priceUsd;
|
||||
if (usdVal >= 1.0) snprintf(usdBuf, sizeof(usdBuf), "$%.2f", usdVal);
|
||||
else if (usdVal >= 0.01) snprintf(usdBuf, sizeof(usdBuf), "$%.4f", usdVal);
|
||||
else snprintf(usdBuf, sizeof(usdBuf), "$%.6f", usdVal);
|
||||
ImVec2 usdSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, usdBuf);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(contentRight - usdSz.x,
|
||||
cy + body2->LegacySize + Layout::spacingXs()),
|
||||
OnSurfaceDisabled(), usdBuf);
|
||||
}
|
||||
// ---- USD value (second line, right-aligned) ----
|
||||
{
|
||||
double priceUsd = state.market.price_usd;
|
||||
if (priceUsd > 0.0 && addr.balance > 0.0) {
|
||||
char usdBuf[32];
|
||||
double usdVal = addr.balance * priceUsd;
|
||||
if (usdVal >= 1.0) snprintf(usdBuf, sizeof(usdBuf), "$%.2f", usdVal);
|
||||
else if (usdVal >= 0.01) snprintf(usdBuf, sizeof(usdBuf), "$%.4f", usdVal);
|
||||
else snprintf(usdBuf, sizeof(usdBuf), "$%.6f", usdVal);
|
||||
ImVec2 usdSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, usdBuf);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(contentRight - usdSz.x,
|
||||
cy + body2->LegacySize + Layout::spacingXs()),
|
||||
OnSurfaceDisabled(), usdBuf);
|
||||
}
|
||||
}
|
||||
|
||||
if (hovered && ImGui::IsMouseClicked(0) && !btnClicked) {
|
||||
ImGui::SetClipboardText(addr.address.c_str());
|
||||
selected_row = row_idx;
|
||||
}
|
||||
// ---- Copy feedback flash ----
|
||||
if (s_copiedRow == row_idx) {
|
||||
float elapsed = (float)ImGui::GetTime() - s_copiedTime;
|
||||
if (elapsed < 1.0f) {
|
||||
float alpha = 1.0f - elapsed;
|
||||
ImFont* capF = Type().caption();
|
||||
const char* copiedTxt = TR("copied");
|
||||
ImVec2 sz = capF->CalcTextSizeA(capF->LegacySize, 1000.0f, 0.0f, copiedTxt);
|
||||
ImVec4 fc = ImGui::ColorConvertU32ToFloat4(Success());
|
||||
fc.w = alpha;
|
||||
float tx = rowPos.x + (innerW - sz.x) * 0.5f;
|
||||
float ty = rowPos.y + (rowH - sz.y) * 0.5f;
|
||||
dl->AddText(capF, capF->LegacySize, ImVec2(tx, ty),
|
||||
ImGui::ColorConvertFloat4ToU32(fc), copiedTxt);
|
||||
} else {
|
||||
s_copiedRow = -1;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Click: begin drag tracking (copy deferred to mouse-up) ----
|
||||
if (hovered && mouseClicked && !btnClicked) {
|
||||
s_dragIdx = row_idx;
|
||||
s_dragOffsetY = mousePos.y - rowPos.y;
|
||||
s_dragStartY = mousePos.y;
|
||||
s_dragActive = false;
|
||||
}
|
||||
|
||||
// Activate drag after a small threshold
|
||||
if (s_dragIdx == row_idx && !s_dragActive && mouseDown) {
|
||||
if (std::abs(mousePos.y - s_dragStartY) > 6.0f * dp) {
|
||||
s_dragActive = true;
|
||||
s_dropTargetIdx = -1;
|
||||
}
|
||||
}
|
||||
} // end !isDragged
|
||||
|
||||
// Advance cursor for interaction area
|
||||
ImGui::PushID(row_idx);
|
||||
ImGui::SetCursorScreenPos(ImVec2(rowPos.x, rowY[row_idx]));
|
||||
ImGui::InvisibleButton("##addr", ImVec2(innerW, rowH));
|
||||
if (ImGui::IsItemHovered() && !btnClicked) ImGui::SetTooltip("%s", addr.address.c_str());
|
||||
if (ImGui::IsItemHovered() && !s_dragActive) {
|
||||
ImGui::SetTooltip("%s", addr.address.c_str());
|
||||
}
|
||||
|
||||
// Context menu
|
||||
const auto& acrTheme = GetCurrentAcrylicTheme();
|
||||
if (effects::ImGuiAcrylic::BeginAcrylicContextItem("AddressContext", 0, acrTheme.menu)) {
|
||||
if (ImGui::MenuItem(TR("copy_address"))) ImGui::SetClipboardText(addr.address.c_str());
|
||||
if (ImGui::MenuItem(TR("copy_address"))) {
|
||||
ImGui::SetClipboardText(addr.address.c_str());
|
||||
s_copiedRow = row_idx;
|
||||
s_copiedTime = (float)ImGui::GetTime();
|
||||
}
|
||||
if (ImGui::MenuItem(TR("send_from_this_address"))) {
|
||||
SetSendFromAddress(addr.address);
|
||||
app->setCurrentPage(NavPage::Send);
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem(TR("set_label"))) {
|
||||
AddressLabelDialog::show(app, addr.address, row.isZ);
|
||||
}
|
||||
if (ImGui::MenuItem(TR("export_private_key")))
|
||||
KeyExportDialog::show(addr.address, KeyExportDialog::KeyType::Private);
|
||||
if (row.isZ) {
|
||||
@@ -1697,7 +1879,7 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
KeyExportDialog::show(addr.address, KeyExportDialog::KeyType::Viewing);
|
||||
}
|
||||
if (ImGui::MenuItem(TR("show_qr_code")))
|
||||
QRPopupDialog::show(addr.address, row.isZ ? "Z-Address" : "T-Address");
|
||||
QRPopupDialog::show(addr.address, row.isZ ? TR("z_address") : TR("t_address"));
|
||||
ImGui::Separator();
|
||||
if (row.hidden) {
|
||||
if (ImGui::MenuItem(TR("restore_address")))
|
||||
@@ -1717,20 +1899,49 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
}
|
||||
ImGui::PopID();
|
||||
|
||||
// Separator between rows
|
||||
if (row_idx < (int)rows.size() - 1 && selected_row != row_idx) {
|
||||
ImVec2 divStart = ImGui::GetCursorScreenPos();
|
||||
float sepY = rowY[row_idx] + rowH;
|
||||
dl->AddLine(
|
||||
ImVec2(divStart.x + rowPadLeft + rowIconSz * 2.0f, divStart.y),
|
||||
ImVec2(divStart.x + innerW - Layout::spacingLg(), divStart.y),
|
||||
IM_COL32(255, 255, 255, 15));
|
||||
ImVec2(rowPos.x + rowPadLeft + rowIconSz * 2.0f, sepY),
|
||||
ImVec2(rowPos.x + innerW - Layout::spacingLg(), sepY),
|
||||
dividerCol);
|
||||
}
|
||||
}
|
||||
|
||||
// Advance cursor past all rows
|
||||
ImGui::SetCursorScreenPos(ImVec2(ImGui::GetCursorScreenPos().x,
|
||||
cursorStartY + (float)rows.size() * rowH));
|
||||
|
||||
// ---- Keyboard navigation ----
|
||||
if (!ImGui::GetIO().WantTextInput && !ImGui::GetIO().KeyCtrl && !s_dragActive) {
|
||||
bool nav = false;
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_UpArrow) || ImGui::IsKeyPressed(ImGuiKey_K)) {
|
||||
if (selected_row > 0) { selected_row--; nav = true; }
|
||||
else if (selected_row < 0 && !rows.empty()) { selected_row = 0; nav = true; }
|
||||
}
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_DownArrow) || ImGui::IsKeyPressed(ImGuiKey_J)) {
|
||||
if (selected_row < (int)rows.size() - 1) { selected_row++; nav = true; }
|
||||
else if (selected_row < 0 && !rows.empty()) { selected_row = 0; nav = true; }
|
||||
}
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_Enter) && selected_row >= 0 && selected_row < (int)rows.size()) {
|
||||
ImGui::SetClipboardText(rows[selected_row].info->address.c_str());
|
||||
s_copiedRow = selected_row;
|
||||
s_copiedTime = (float)ImGui::GetTime();
|
||||
}
|
||||
if (ImGui::IsKeyPressed(ImGuiKey_F2) && selected_row >= 0 && selected_row < (int)rows.size()) {
|
||||
const auto& r = rows[selected_row];
|
||||
AddressLabelDialog::show(app, r.info->address, r.isZ);
|
||||
}
|
||||
(void)nav;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
// Scroll fade
|
||||
{
|
||||
float fadeZone = std::min(
|
||||
(Type().body2()->LegacySize + Type().caption()->LegacySize +
|
||||
@@ -1740,11 +1951,12 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
listPanelMin.y, listPanelMax.y, fadeZone, addrScrollY, addrScrollMaxY);
|
||||
}
|
||||
|
||||
// Address count
|
||||
{
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
char countBuf[128];
|
||||
int totalAddrs = (int)(state.z_addresses.size() + state.t_addresses.size());
|
||||
snprintf(countBuf, sizeof(countBuf), "Showing %d of %d addresses",
|
||||
snprintf(countBuf, sizeof(countBuf), TR("showing_x_of_y"),
|
||||
(int)rows.size(), totalAddrs);
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), countBuf);
|
||||
}
|
||||
|
||||
@@ -550,9 +550,13 @@ void RenderPeersTab(App* app)
|
||||
float iconSz = iconFont->LegacySize;
|
||||
const char* label = isRefreshing ? TR("peers_refreshing") : TR("peers_refresh");
|
||||
ImVec2 lblSz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, label);
|
||||
// Use the wider label so button doesn't resize between states
|
||||
ImVec2 altSz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0,
|
||||
isRefreshing ? TR("peers_refresh") : TR("peers_refreshing"));
|
||||
float maxLblW = std::max(lblSz.x, altSz.x);
|
||||
float padH = Layout::spacingSm();
|
||||
float padV = Layout::spacingSm();
|
||||
float btnW = padH + iconSz + Layout::spacingXs() + lblSz.x + padH;
|
||||
float btnW = padH + iconSz + Layout::spacingXs() + maxLblW + padH;
|
||||
float btnH = std::max(iconSz, lblSz.y) + padV * 2;
|
||||
float btnX = ImGui::GetWindowPos().x + availWidth - btnW - Layout::spacingSm();
|
||||
float btnY = toggleY + (toggleH - btnH) * 0.5f;
|
||||
|
||||
@@ -401,6 +401,32 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["remove_favorite"] = "Remove Favorite";
|
||||
strings_["favorite_address"] = "Favorite Address";
|
||||
strings_["show_hidden"] = "Show Hidden (%d)";
|
||||
strings_["filter"] = "Filter...";
|
||||
strings_["no_addresses_yet"] = "No addresses yet";
|
||||
strings_["showing_x_of_y"] = "Showing %d of %d addresses";
|
||||
strings_["set_label"] = "Set Label...";
|
||||
strings_["copied"] = "Copied!";
|
||||
strings_["hidden_tag"] = " (hidden)";
|
||||
strings_["z_address"] = "Z-Address";
|
||||
strings_["t_address"] = "T-Address";
|
||||
strings_["shielded_address"] = "Shielded Address";
|
||||
strings_["transparent_address"] = "Transparent Address";
|
||||
strings_["label_placeholder"] = "e.g. Savings, Mining...";
|
||||
strings_["choose_icon"] = "Choose Icon";
|
||||
strings_["clear_icon"] = "Clear Icon";
|
||||
strings_["transfer_funds"] = "Transfer Funds";
|
||||
strings_["transfer_to"] = "Transfer to:";
|
||||
strings_["deshielding_warning"] = "Warning: This will de-shield funds from a private (Z) address to a transparent (T) address.";
|
||||
strings_["shielding_notice"] = "Note: This will shield funds from a transparent (T) address to a private (Z) address.";
|
||||
strings_["result_preview"] = "Result Preview";
|
||||
strings_["sender_balance"] = "Sender: %.8f \xe2\x86\x92 %.8f DRGX";
|
||||
strings_["recipient_balance"] = "Recipient: %.8f \xe2\x86\x92 %.8f DRGX";
|
||||
strings_["insufficient_funds"] = "Insufficient funds for this amount plus fee.";
|
||||
strings_["sends_full_balance_warning"] = "This sends the full balance. The sending address will have a zero balance.";
|
||||
strings_["confirm_transfer"] = "Confirm Transfer";
|
||||
strings_["transfer_sent"] = "Transfer Sent";
|
||||
strings_["transfer_sent_desc"] = "Your transfer has been submitted to the network.";
|
||||
strings_["transfer_failed"] = "Transfer Failed";
|
||||
|
||||
// Misc dialog strings
|
||||
strings_["shield_operation_id"] = "Operation ID: %s";
|
||||
|
||||
Reference in New Issue
Block a user