feat(settings): labelled Paste/Clear + in-field clipboard preview
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) <noreply@anthropic.com>
This commit is contained in:
135
src/app.cpp
135
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);
|
||||
|
||||
Reference in New Issue
Block a user