From 0b471341124ccfca142a527a46a85587ee2d01dc Mon Sep 17 00:00:00 2001 From: DanS Date: Mon, 13 Jul 2026 17:43:17 -0500 Subject: [PATCH] feat(settings): labelled Paste/Clear + in-field clipboard preview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restore text labels on the import dialog's Paste and Clear buttons (the reveal eye stays a compact icon flush in the field), and replace the paste-hover tooltip with an in-field preview like the send tab: while Paste is hovered and the Key field is empty, the trimmed clipboard is drawn as a transparent ghost overlay inside the field — masked with '*' unless the key is revealed — so the user can verify it before pasting. The type indicator now reflects the hovered clipboard (indKey/iRecognized/…), while the Import/Sweep button guard and the scan panel keep using the actual field content only, so a hover can never enable or trigger the action. Removes the now-unused RenderPasteHoverPreview tooltip helper and the dead wrongType local. Reviewed via an adversarial pass (overlay masking/security, the indicator-vs-action classification split, button regressions); the one finding (dead wrongType) is fixed. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app.cpp | 135 +++++++++++++++++++--------------------------------- 1 file changed, 50 insertions(+), 85 deletions(-) diff --git a/src/app.cpp b/src/app.cpp index fb34df0..67fb4c7 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -2835,69 +2835,6 @@ static void RenderScanHeightControl(char* buf, size_t bufSize, int tipHeight, bo ImGui::EndDisabled(); } -// Hover preview for the Paste button: peek at the clipboard, classify it in the dialog's current -// key-type mode, and show a validity line + a preview masked with '*' unless the key is currently -// revealed — so the user can verify the clipboard holds a valid key before pasting it. -static void RenderPasteHoverPreview(bool viewMode, bool reveal, float dp) -{ - namespace m = ui::material; - const char* clipRaw = ImGui::GetClipboardText(); - std::string key(clipRaw ? clipRaw : ""); - while (!key.empty() && (key.front()==' '||key.front()=='\t'||key.front()=='\n'||key.front()=='\r')) key.erase(key.begin()); - while (!key.empty() && (key.back()==' '||key.back()=='\t'||key.back()=='\n'||key.back()=='\r')) key.pop_back(); - - ImGui::BeginTooltip(); - ImGui::TextUnformatted(TR("paste")); - - if (key.empty()) { - ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(m::OnSurfaceDisabled()), "%s", TR("paste_clip_empty")); - ImGui::EndTooltip(); - return; - } - - const bool isView = services::WalletSecurityController::isViewingKey(key); - const bool isSpend = services::WalletSecurityController::isRecognizedPrivateKey(key); - const bool shielded = services::WalletSecurityController::classifyPrivateKey(key) - == services::WalletSecurityController::KeyKind::Shielded; - const bool recognized = viewMode ? isView : isSpend; // valid for THIS dialog's mode - const bool wrongType = viewMode ? isSpend : isView; // a key, but for the other button - - // valid / wrong-type / not-a-key. The field indicator softens the last case to amber+HELP, but this - // is a verify-before-paste check, so a truly-unrecognized clipboard gets a deliberately stronger - // red ✗ "don't paste" signal (amber ⚠ stays reserved for a real key aimed at the other button). - const ImVec4 amber(0.8f, 0.6f, 0.0f, 1.0f); // matches the field indicator's wrong-type amber - const ImVec4 red = m::ErrorVec4(); - ImGui::PushFont(m::Type().iconSmall()); - if (recognized) ImGui::TextColored(m::SuccessVec4(), ICON_MD_CHECK_CIRCLE); - else if (wrongType) ImGui::TextColored(amber, ICON_MD_WARNING); - else ImGui::TextColored(red, ICON_MD_CANCEL); - ImGui::PopFont(); - ImGui::SameLine(0, 4.0f * dp); - if (recognized) { - const char* label = viewMode ? TR("import_key_type_zview") - : shielded ? TR("import_key_type_zspend") - : TR("import_key_type_tkey"); - ImGui::TextColored(m::SuccessVec4(), "%s", label); - } else if (wrongType) { - ImGui::TextColored(amber, "%s", viewMode ? TR("import_viewkey_wrong_type") : TR("import_key_wrong_type")); - } else { - ImGui::TextColored(red, "%s", TR("import_key_type_unknown")); - } - - // Preview line — masked with '*' unless the key is currently revealed (then a truncated peek). - std::string preview; - if (reveal) { - preview = key.size() > 22 ? key.substr(0, 14) + "..." + key.substr(key.size() - 6) : key; - } else { - const size_t n = std::min(key.size(), (size_t)32); - preview.assign(n, '*'); - if (key.size() > n) preview += "..."; - } - ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(m::OnSurfaceMedium()), "%s", preview.c_str()); - - ImGui::EndTooltip(); -} - void App::renderImportKeyDialog() { namespace m = ui::material; @@ -2959,14 +2896,16 @@ void App::renderImportKeyDialog() ImGui::Spacing(); } - // Masked key field; the reveal (eye) toggle sits frameless flush at its right edge, with frameless - // Paste / Clear icon actions beneath — lighter and less cluttered than three boxed buttons. + // Masked key field; the reveal (eye) toggle sits frameless flush at its right edge, with labelled + // Paste / Clear buttons beneath. Hovering Paste previews the clipboard INSIDE the field (masked + // with '*' unless the key is revealed) — like the send tab — so the user can verify before pasting. ImGui::TextUnformatted(viewMode ? TR("import_viewkey_field") : TR("import_key_field")); const float fieldH = ImGui::GetFrameHeight(); const ImVec2 iconSz(fieldH, fieldH); ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - fieldH); ImGuiInputTextFlags flags = import_key_reveal_ ? 0 : ImGuiInputTextFlags_Password; ImGui::InputText("##importkey", import_key_input_, sizeof(import_key_input_), flags); + const ImVec2 fieldMin = ImGui::GetItemRectMin(); // anchor for the paste-preview overlay ImGui::SameLine(0, 0); m::IconButtonStyle eyeSt; eyeSt.hoverBg = m::WithAlpha(m::OnSurface(), 30); @@ -2976,13 +2915,9 @@ void App::renderImportKeyDialog() m::Type().iconSmall(), iconSz, eyeSt)) import_key_reveal_ = !import_key_reveal_; - m::IconButtonStyle actSt; - actSt.restBg = m::WithAlpha(m::OnSurface(), 14); // faint pill so it still reads as a button - actSt.hoverBg = m::WithAlpha(m::OnSurface(), 34); - // Paste has no static tooltip — hovering shows a live clipboard preview that classifies the key - // (masked with '*' when the key isn't revealed) so the user can verify it before pasting. - const bool pasteClicked = m::IconButton("##pastekey", ICON_MD_CONTENT_PASTE, m::Type().iconSmall(), iconSz, actSt); - if (ImGui::IsItemHovered()) RenderPasteHoverPreview(viewMode, import_key_reveal_, dp); + // Paste / Clear — labelled buttons. + const bool pasteClicked = m::TactileButton(TR("paste")); + const bool pasteHovered = ImGui::IsItemHovered(); if (pasteClicked) { const char* clipboard = ImGui::GetClipboardText(); if (clipboard) { @@ -2992,12 +2927,34 @@ void App::renderImportKeyDialog() snprintf(import_key_input_, sizeof(import_key_input_), "%s", trimmed.c_str()); } } - ImGui::SameLine(0, ui::Layout::spacingSm()); - actSt.tooltip = TR("clear"); - if (m::IconButton("##clearkey", ICON_MD_CLEAR, m::Type().iconSmall(), iconSz, actSt)) - sodium_memzero(import_key_input_, sizeof(import_key_input_)); + ImGui::SameLine(); + if (m::TactileButton(TR("clear"))) sodium_memzero(import_key_input_, sizeof(import_key_input_)); - // Trimmed view + classification. The indicator and the Import guard share isRecognizedImportKey. + // While Paste is hovered and the field is empty, preview the trimmed clipboard inside the field, + // masked with '*' unless the key is revealed. pastePreview also drives the type indicator (below) + // so the user sees how the clipboard would classify before pasting — but NOT the action button. + std::string pastePreview; + if (pasteHovered && import_key_input_[0] == '\0') { + const char* clip = ImGui::GetClipboardText(); + if (clip && clip[0]) { + std::string t(clip); + while (!t.empty() && (t.front()==' '||t.front()=='\t'||t.front()=='\n'||t.front()=='\r')) t.erase(t.begin()); + while (!t.empty() && (t.back()==' '||t.back()=='\t'||t.back()=='\n'||t.back()=='\r')) t.pop_back(); + if (!t.empty()) { + pastePreview = t; + std::string shown = import_key_reveal_ ? t : std::string(std::min(t.size(), (size_t)56), '*'); + if (shown.size() > 48) shown.resize(48); // fits the field before the flush eye button + const ImVec2 tp(fieldMin.x + ImGui::GetStyle().FramePadding.x, + fieldMin.y + ImGui::GetStyle().FramePadding.y); + ImVec4 pc = ImGui::GetStyleColorVec4(ImGuiCol_Text); + pc.w = 0.45f; // transparent ghost, like the send tab + ImGui::GetWindowDrawList()->AddText(tp, ImGui::ColorConvertFloat4ToU32(pc), shown.c_str()); + } + } + } + + // Trimmed field content drives the Import/Sweep guard + scan panel; the paste preview (only while + // hovering an empty field) drives just the type indicator, so a hover can never enable the button. std::string keyTrim(import_key_input_); while (!keyTrim.empty() && (keyTrim.front()==' '||keyTrim.front()=='\t'||keyTrim.front()=='\n'||keyTrim.front()=='\r')) keyTrim.erase(keyTrim.begin()); while (!keyTrim.empty() && (keyTrim.back()==' '||keyTrim.back()=='\t'||keyTrim.back()=='\n'||keyTrim.back()=='\r')) keyTrim.pop_back(); @@ -3005,15 +2962,23 @@ void App::renderImportKeyDialog() bool isSpend = services::WalletSecurityController::isRecognizedPrivateKey(keyTrim); bool shielded = services::WalletSecurityController::classifyPrivateKey(keyTrim) == services::WalletSecurityController::KeyKind::Shielded; - // Each button accepts only its own key type; a key valid for the OTHER button is flagged with a - // redirect hint rather than lumped in with truly unrecognized input. + // Each button accepts only its own key type; the Import/Sweep guard uses `recognized` (wrong-type + // redirect handling lives in the indicator's iWrongType path below). bool recognized = viewMode ? isView : isSpend; - bool wrongType = viewMode ? isSpend : isView; - if (!keyTrim.empty()) { - if (recognized) { + // Indicator key = the paste preview while hovering, else the field content. + const std::string& indKey = pastePreview.empty() ? keyTrim : pastePreview; + const bool iShielded = services::WalletSecurityController::classifyPrivateKey(indKey) + == services::WalletSecurityController::KeyKind::Shielded; + const bool iRecognized = viewMode ? services::WalletSecurityController::isViewingKey(indKey) + : services::WalletSecurityController::isRecognizedPrivateKey(indKey); + const bool iWrongType = viewMode ? services::WalletSecurityController::isRecognizedPrivateKey(indKey) + : services::WalletSecurityController::isViewingKey(indKey); + + if (!indKey.empty()) { + if (iRecognized) { const char* label = viewMode ? TR("import_key_type_zview") - : shielded ? TR("import_key_type_zspend") + : iShielded ? TR("import_key_type_zspend") : TR("import_key_type_tkey"); ImGui::PushFont(m::Type().iconSmall()); ImGui::TextColored(m::SuccessVec4(), ICON_MD_CHECK_CIRCLE); @@ -3021,11 +2986,11 @@ void App::renderImportKeyDialog() ImGui::SameLine(0, 4.0f * dp); ImGui::TextColored(m::SuccessVec4(), "%s", label); } else { - const char* msg = wrongType + const char* msg = iWrongType ? (viewMode ? TR("import_viewkey_wrong_type") : TR("import_key_wrong_type")) : TR("import_key_type_unknown"); ImGui::PushFont(m::Type().iconSmall()); - ImGui::TextColored(ImVec4(0.8f, 0.6f, 0.0f, 1.0f), wrongType ? ICON_MD_WARNING : ICON_MD_HELP); + ImGui::TextColored(ImVec4(0.8f, 0.6f, 0.0f, 1.0f), iWrongType ? ICON_MD_WARNING : ICON_MD_HELP); ImGui::PopFont(); ImGui::SameLine(0, 4.0f * dp); ImGui::PushTextWrapPos(0.0f);