Files
ObsidianDragon/src/ui/windows/qr_popup_dialog.cpp
T
DanSandClaude Opus 4.8 a99f71a0cd fix(ux): land quick-win UX audit fixes across settings, exports, toasts, nav
Batch of safe, isolated fixes from the 10-surface UX audit:

- settings: Auto-shield / Save-z-transactions toggles now persist on change
  (they discarded CB()'s return and relied on a Save button that only renders
  on the About tab, so the choice was silently lost on relaunch).
- exports: show the resolved Platform::getConfigDir() instead of a hardcoded
  ~/.config path (wrong on Windows/macOS) in both the all-keys and CSV
  dialogs; all-keys also reveals the file path on a partial export.
- notifications: error/warning toasts last 9s/7s (were 4s/3.5s) so failures
  are readable.
- qr popup / tx-details: copy-to-clipboard now confirms with a toast.
- about: Escape-to-close + readable heading contrast via OnSurfaceMedium().
- sidebar: suppress single-key nav (J/K/arrows/[) while an overlay dialog or
  ImGui popup is open, so it no longer switches the tab behind a modal.
- i18n: strip the stale hardcoded path out of file_save_location in i18n.cpp
  and all eight res/lang/*.json locales (additive, one key each).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-17 16:27:58 -05:00

161 lines
5.8 KiB
C++

// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "qr_popup_dialog.h"
#include "../../app.h"
#include "../../util/i18n.h"
#include "../widgets/qr_code.h"
#include "../widgets/copy_field.h"
#include "../schema/ui_schema.h"
#include "../material/draw_helpers.h"
#include "../layout.h"
#include "../theme.h"
#include "../notifications.h"
#include "imgui.h"
namespace dragonx {
namespace ui {
// Static member initialization
bool QRPopupDialog::s_open = false;
std::string QRPopupDialog::s_address;
std::string QRPopupDialog::s_label;
uintptr_t QRPopupDialog::s_qr_texture = 0;
int QRPopupDialog::s_qr_width = 0;
int QRPopupDialog::s_qr_height = 0;
void QRPopupDialog::show(const std::string& address, const std::string& label)
{
// Cleanup previous texture
if (s_qr_texture != 0) {
FreeQRTexture(s_qr_texture);
s_qr_texture = 0;
}
s_open = true;
s_address = address;
s_label = label;
// Generate new QR texture
s_qr_texture = GenerateQRTexture(address.c_str(), &s_qr_width, &s_qr_height);
}
bool QRPopupDialog::isOpen()
{
return s_open;
}
void QRPopupDialog::close()
{
s_open = false;
if (s_qr_texture != 0) {
FreeQRTexture(s_qr_texture);
s_qr_texture = 0;
}
}
void QRPopupDialog::render(App* app)
{
(void)app; // Unused for now
if (!s_open) return;
auto& S = schema::UI();
auto qr = S.drawElement("dialogs.qr-popup", "qr-code");
auto actionBtn = S.button("dialogs.qr-popup", "action-button");
// Size the card to the address's own natural (chunked) box width — the widest content element — so
// AddressCopyField fills the card under its label instead of floating centered in an over-wide card
// (matches the key-export dialog). Adapts to address type + font scale; bounded to 85% of the window.
const float dp = Layout::dpiScale();
const std::string chunkedAddr = widgets::ChunkString(s_address, 4);
const float addrPadX = ImGui::GetStyle().FramePadding.x + 4.0f; // matches AddressCopyField
const float addrBoxW = ImGui::CalcTextSize(chunkedAddr.c_str()).x + addrPadX * 2.0f + 2.0f;
// The BlurFloat overlay card insets content by 28px per side (draw_helpers BeginOverlayDialog); add
// that (+ a little slack) so the address box fits on one line under its label instead of wrapping.
const float wantW = addrBoxW + 28.0f * 2.0f + 12.0f;
const float maxW = 0.85f * ImGui::GetMainViewport()->Size.x;
const float cardW = (wantW < maxW ? wantW : maxW) / dp;
material::OverlayDialogSpec ov;
ov.title = TR("qr_title"); ov.p_open = &s_open;
ov.style = material::OverlayStyle::BlurFloat;
ov.cardWidth = cardW; ov.idSuffix = "qrpopup";
if (material::BeginOverlayDialog(ov)) {
// Label if present
if (!s_label.empty()) {
ImGui::TextWrapped("%s", s_label.c_str());
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
}
// Center the QR code (responsive — larger, to suit the wider modal).
float window_width = ImGui::GetWindowWidth();
float qr_size = (qr.size > 0 ? (float)qr.size : 280.0f) * dp; // schema/fallback are logical px
const float responsive = window_width * 0.5f; // window_width is already dp-scaled
if (responsive > qr_size) qr_size = responsive;
if (qr_size > 420.0f * dp) qr_size = 420.0f * dp;
float padding = (window_width - qr_size) / 2.0f;
if (padding < 0.0f) padding = 0.0f;
ImGui::SetCursorPosX(padding);
// Render QR code
if (s_qr_texture != 0) {
ImGui::Image((ImTextureID)s_qr_texture, ImVec2(qr_size, qr_size));
} else {
// Fallback: show error
ImGui::BeginChild("QRPlaceholder", ImVec2(qr_size, qr_size), true);
ImGui::TextWrapped("%s", TR("qr_failed"));
ImGui::EndChild();
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Address — click-to-copy field (same formatting as the key-export dialog).
ImGui::Text("%s", TR("address_label"));
widgets::AddressCopyField("##QRAddress", s_address);
ImGui::Spacing();
// Footer — a primary "Copy address" + "Close" pair, placed via the shared design-system helper.
// Size both buttons to the wider label (so "Copy address" never clips) and hand that width to
// DialogActionFooter, which centers the pair.
ImFont* btnFont = S.resolveFont(actionBtn.font);
ImGui::PushFont(btnFont);
const float btnPad = ImGui::GetStyle().FramePadding.x * 2.0f + 24.0f;
float w_copy = ImGui::CalcTextSize(TR("copy_address")).x + btnPad;
float w_close = ImGui::CalcTextSize(TR("close")).x + btnPad;
ImGui::PopFont();
if (w_copy < actionBtn.width) w_copy = actionBtn.width;
if (w_close < actionBtn.width) w_close = actionBtn.width;
const float btnW = (w_copy > w_close ? w_copy : w_close);
bool doCopy = false, doClose = false;
material::DialogActionFooter(TR("copy_address"), true, TR("close"), doCopy, doClose, btnW);
if (doCopy) {
ImGui::SetClipboardText(s_address.c_str());
// Confirm the copy — a silent copy here risks a stale clipboard being pasted into an
// exchange withdrawal. Matches the Receive tab's feedback.
Notifications::instance().info(TR("address_copied"));
}
if (doClose) {
close();
}
material::EndOverlayDialog();
}
// Handle window close button
if (!s_open && s_qr_texture != 0) {
FreeQRTexture(s_qr_texture);
s_qr_texture = 0;
}
}
} // namespace ui
} // namespace dragonx