- Add refresh scheduler and network refresh service boundaries for typed refresh results, ordered RPC collectors, applicators, and price parsing. - Add daemon lifecycle and wallet security workflow helpers while preserving App-owned command RPC, decrypt, cancellation, and UI handoff behavior. - Split balance, console, mining, amount formatting, and async task logic into focused modules with expanded Phase 4 test coverage. - Fix market price loading by triggering price refresh immediately, avoiding queue-pressure drops, tracking loading/error state, and adding translations. - Polish send, explorer, peers, settings, theme/schema, and related tab UI. - Replace checked-in generated language headers with build-generated resources. - Document the cleanup audit, UI static-state guidance, and architecture updates.
255 lines
10 KiB
C++
255 lines
10 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
|
|
#pragma once
|
|
|
|
#include <string>
|
|
#include <vector>
|
|
#include <cctype>
|
|
#include <cstring>
|
|
#include <cstddef>
|
|
#include "../../app.h"
|
|
#include "../../util/i18n.h"
|
|
#include "../material/draw_helpers.h"
|
|
#include "../material/project_icons.h"
|
|
#include "../theme.h"
|
|
#include "imgui.h"
|
|
|
|
namespace dragonx {
|
|
namespace ui {
|
|
|
|
class AddressLabelDialog {
|
|
public:
|
|
static bool drawIconByName(ImDrawList* dl,
|
|
const std::string& name,
|
|
ImVec2 center,
|
|
float /*emSize*/,
|
|
ImU32 color,
|
|
ImFont* iconFont,
|
|
float iconFontSize) {
|
|
return material::project_icons::drawByName(dl, name, center, color, iconFont, iconFontSize);
|
|
}
|
|
|
|
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;
|
|
s_iconSearch[0] = '\0';
|
|
|
|
// 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 < material::project_icons::walletIconCount(); ++i) {
|
|
if (material::project_icons::walletIconName(i) == existingIcon) {
|
|
s_selectedIcon = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void render() {
|
|
if (!s_open || !s_app) return;
|
|
|
|
using namespace material;
|
|
|
|
constexpr float cardTopViewportRatio = 0.15f;
|
|
constexpr float cardBottomViewportRatio = 0.80f;
|
|
if (BeginOverlayDialog(TR("set_label"), &s_open, 660.0f, 0.92f, cardBottomViewportRatio)) {
|
|
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();
|
|
|
|
// Search bar
|
|
ImGui::SetNextItemWidth(-1);
|
|
ImGui::InputTextWithHint("##IconSearch", TR("search_icons"), s_iconSearch, sizeof(s_iconSearch));
|
|
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)));
|
|
|
|
const char* cancelLabel = TR("cancel");
|
|
const char* saveLabel = TR("save");
|
|
ImFont* buttonFont = Type().button();
|
|
float buttonFontSize = ScaledFontSize(buttonFont);
|
|
const ImGuiStyle& style = ImGui::GetStyle();
|
|
const float cardHeight = ImGui::GetMainViewport()->Size.y * (cardBottomViewportRatio - cardTopViewportRatio);
|
|
const float bottomPadding = style.WindowPadding.y;
|
|
const bool showClearIcon = (s_selectedIcon >= 0);
|
|
const float clearRowH = showClearIcon ? ImGui::GetTextLineHeight() : 0.0f;
|
|
const float buttonRowH = ImGui::GetFrameHeight();
|
|
const float separatorH = 1.0f;
|
|
const float preButtonReserve =
|
|
(showClearIcon ? (style.ItemSpacing.y + clearRowH) : 0.0f) +
|
|
style.ItemSpacing.y * 3.0f + separatorH;
|
|
const float buttonY = cardHeight - bottomPadding - buttonRowH;
|
|
|
|
// Build filtered index list
|
|
std::vector<int> visible;
|
|
visible.reserve(material::project_icons::walletIconCount());
|
|
{
|
|
// Simple case-insensitive substring match on icon name
|
|
std::string needle(s_iconSearch);
|
|
for (char& c : needle) c = (char)std::tolower((unsigned char)c);
|
|
for (int i = 0; i < material::project_icons::walletIconCount(); ++i) {
|
|
const char* iconName = material::project_icons::walletIconName(i);
|
|
if (needle.empty() || std::strstr(iconName, needle.c_str()) != nullptr)
|
|
visible.push_back(i);
|
|
}
|
|
}
|
|
|
|
// The grid owns all vertical space above the bottom action band.
|
|
const float gridStartY = ImGui::GetCursorPosY();
|
|
const float controlsTopY = std::max(gridStartY + cellSz * 2.0f, buttonY - preButtonReserve);
|
|
const float gridMaxH = std::max(cellSz * 2.0f, controlsTopY - gridStartY);
|
|
ImGui::PushStyleColor(ImGuiCol_ChildBg, IM_COL32(0, 0, 0, 0));
|
|
ImGui::BeginChild("##IconGrid", ImVec2(avail, gridMaxH), ImGuiChildFlags_None,
|
|
ImGuiWindowFlags_NoScrollbar);
|
|
|
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
|
|
if (visible.empty()) {
|
|
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_icons_found"));
|
|
}
|
|
|
|
int col = 0;
|
|
for (int vi = 0; vi < (int)visible.size(); ++vi) {
|
|
int i = visible[vi];
|
|
const char* iconName = material::project_icons::walletIconName(i);
|
|
if (col != 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
|
|
drawIconByName(dl,
|
|
iconName,
|
|
ImVec2(mn.x + cellSz * 0.5f, mn.y + cellSz * 0.5f),
|
|
iconFsz,
|
|
sel ? Primary() : (hov ? OnSurface() : OnSurfaceMedium()),
|
|
iconFont,
|
|
iconFsz);
|
|
|
|
ImGui::PushID(i);
|
|
ImGui::InvisibleButton("##icon", ImVec2(cellSz, cellSz));
|
|
if (ImGui::IsItemClicked()) s_selectedIcon = i;
|
|
if (hov) ImGui::SetTooltip("%s", iconName);
|
|
ImGui::PopID();
|
|
|
|
col = (col + 1) % cols;
|
|
}
|
|
|
|
ImGui::EndChild();
|
|
ImGui::PopStyleColor();
|
|
|
|
if (ImGui::GetCursorPosY() < controlsTopY) {
|
|
ImGui::SetCursorPosY(controlsTopY);
|
|
}
|
|
|
|
// "No icon" option
|
|
if (showClearIcon) {
|
|
ImGui::Spacing();
|
|
if (ImGui::SmallButton(TR("clear_icon"))) {
|
|
s_selectedIcon = -1;
|
|
}
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
if (ImGui::GetCursorPosY() < buttonY) {
|
|
ImGui::SetCursorPosY(buttonY);
|
|
}
|
|
|
|
// Buttons
|
|
float minBtnW = 120.0f * dp;
|
|
float buttonPadW = ImGui::GetStyle().FramePadding.x * 2.0f + 24.0f * dp;
|
|
float cancelW = std::max(minBtnW,
|
|
buttonFont->CalcTextSizeA(buttonFontSize, 1000.0f, 0.0f, cancelLabel).x + buttonPadW);
|
|
float saveW = std::max(minBtnW,
|
|
buttonFont->CalcTextSizeA(buttonFontSize, 1000.0f, 0.0f, saveLabel).x + buttonPadW);
|
|
float totalW = cancelW + saveW + Layout::spacingMd();
|
|
float rowStartX = ImGui::GetCursorPosX();
|
|
float contentW = ImGui::GetContentRegionAvail().x;
|
|
ImGui::SetCursorPosX(rowStartX + std::max(0.0f, (contentW - totalW) * 0.5f));
|
|
|
|
if (TactileButton(cancelLabel, ImVec2(cancelW, 0), buttonFont)) {
|
|
s_open = false;
|
|
}
|
|
ImGui::SameLine(0, Layout::spacingMd());
|
|
if (TactileButton(saveLabel, ImVec2(saveW, 0), buttonFont)) {
|
|
// Apply changes
|
|
s_app->setAddressLabel(s_address, s_label);
|
|
if (s_selectedIcon >= 0)
|
|
s_app->setAddressIcon(s_address, material::project_icons::walletIconName(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;
|
|
static inline char s_iconSearch[64] = {};
|
|
|
|
public:
|
|
// Expose for the address list to look up icon glyphs by name
|
|
static const char* iconGlyphForName(const std::string& name) {
|
|
return material::project_icons::glyphForName(name);
|
|
}
|
|
};
|
|
|
|
} // namespace ui
|
|
} // namespace dragonx
|