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();
|
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()
|
void App::renderImportKeyDialog()
|
||||||
{
|
{
|
||||||
namespace m = ui::material;
|
namespace m = ui::material;
|
||||||
@@ -2959,14 +2896,16 @@ void App::renderImportKeyDialog()
|
|||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Masked key field; the reveal (eye) toggle sits frameless flush at its right edge, with frameless
|
// Masked key field; the reveal (eye) toggle sits frameless flush at its right edge, with labelled
|
||||||
// Paste / Clear icon actions beneath — lighter and less cluttered than three boxed buttons.
|
// 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"));
|
ImGui::TextUnformatted(viewMode ? TR("import_viewkey_field") : TR("import_key_field"));
|
||||||
const float fieldH = ImGui::GetFrameHeight();
|
const float fieldH = ImGui::GetFrameHeight();
|
||||||
const ImVec2 iconSz(fieldH, fieldH);
|
const ImVec2 iconSz(fieldH, fieldH);
|
||||||
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - fieldH);
|
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - fieldH);
|
||||||
ImGuiInputTextFlags flags = import_key_reveal_ ? 0 : ImGuiInputTextFlags_Password;
|
ImGuiInputTextFlags flags = import_key_reveal_ ? 0 : ImGuiInputTextFlags_Password;
|
||||||
ImGui::InputText("##importkey", import_key_input_, sizeof(import_key_input_), flags);
|
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);
|
ImGui::SameLine(0, 0);
|
||||||
m::IconButtonStyle eyeSt;
|
m::IconButtonStyle eyeSt;
|
||||||
eyeSt.hoverBg = m::WithAlpha(m::OnSurface(), 30);
|
eyeSt.hoverBg = m::WithAlpha(m::OnSurface(), 30);
|
||||||
@@ -2976,13 +2915,9 @@ void App::renderImportKeyDialog()
|
|||||||
m::Type().iconSmall(), iconSz, eyeSt))
|
m::Type().iconSmall(), iconSz, eyeSt))
|
||||||
import_key_reveal_ = !import_key_reveal_;
|
import_key_reveal_ = !import_key_reveal_;
|
||||||
|
|
||||||
m::IconButtonStyle actSt;
|
// Paste / Clear — labelled buttons.
|
||||||
actSt.restBg = m::WithAlpha(m::OnSurface(), 14); // faint pill so it still reads as a button
|
const bool pasteClicked = m::TactileButton(TR("paste"));
|
||||||
actSt.hoverBg = m::WithAlpha(m::OnSurface(), 34);
|
const bool pasteHovered = ImGui::IsItemHovered();
|
||||||
// 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);
|
|
||||||
if (pasteClicked) {
|
if (pasteClicked) {
|
||||||
const char* clipboard = ImGui::GetClipboardText();
|
const char* clipboard = ImGui::GetClipboardText();
|
||||||
if (clipboard) {
|
if (clipboard) {
|
||||||
@@ -2992,12 +2927,34 @@ void App::renderImportKeyDialog()
|
|||||||
snprintf(import_key_input_, sizeof(import_key_input_), "%s", trimmed.c_str());
|
snprintf(import_key_input_, sizeof(import_key_input_), "%s", trimmed.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ImGui::SameLine(0, ui::Layout::spacingSm());
|
ImGui::SameLine();
|
||||||
actSt.tooltip = TR("clear");
|
if (m::TactileButton(TR("clear"))) sodium_memzero(import_key_input_, sizeof(import_key_input_));
|
||||||
if (m::IconButton("##clearkey", ICON_MD_CLEAR, m::Type().iconSmall(), iconSz, actSt))
|
|
||||||
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_);
|
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.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();
|
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 isSpend = services::WalletSecurityController::isRecognizedPrivateKey(keyTrim);
|
||||||
bool shielded = services::WalletSecurityController::classifyPrivateKey(keyTrim)
|
bool shielded = services::WalletSecurityController::classifyPrivateKey(keyTrim)
|
||||||
== services::WalletSecurityController::KeyKind::Shielded;
|
== services::WalletSecurityController::KeyKind::Shielded;
|
||||||
// Each button accepts only its own key type; a key valid for the OTHER button is flagged with a
|
// Each button accepts only its own key type; the Import/Sweep guard uses `recognized` (wrong-type
|
||||||
// redirect hint rather than lumped in with truly unrecognized input.
|
// redirect handling lives in the indicator's iWrongType path below).
|
||||||
bool recognized = viewMode ? isView : isSpend;
|
bool recognized = viewMode ? isView : isSpend;
|
||||||
bool wrongType = viewMode ? isSpend : isView;
|
|
||||||
|
|
||||||
if (!keyTrim.empty()) {
|
// Indicator key = the paste preview while hovering, else the field content.
|
||||||
if (recognized) {
|
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")
|
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");
|
: TR("import_key_type_tkey");
|
||||||
ImGui::PushFont(m::Type().iconSmall());
|
ImGui::PushFont(m::Type().iconSmall());
|
||||||
ImGui::TextColored(m::SuccessVec4(), ICON_MD_CHECK_CIRCLE);
|
ImGui::TextColored(m::SuccessVec4(), ICON_MD_CHECK_CIRCLE);
|
||||||
@@ -3021,11 +2986,11 @@ void App::renderImportKeyDialog()
|
|||||||
ImGui::SameLine(0, 4.0f * dp);
|
ImGui::SameLine(0, 4.0f * dp);
|
||||||
ImGui::TextColored(m::SuccessVec4(), "%s", label);
|
ImGui::TextColored(m::SuccessVec4(), "%s", label);
|
||||||
} else {
|
} else {
|
||||||
const char* msg = wrongType
|
const char* msg = iWrongType
|
||||||
? (viewMode ? TR("import_viewkey_wrong_type") : TR("import_key_wrong_type"))
|
? (viewMode ? TR("import_viewkey_wrong_type") : TR("import_key_wrong_type"))
|
||||||
: TR("import_key_type_unknown");
|
: TR("import_key_type_unknown");
|
||||||
ImGui::PushFont(m::Type().iconSmall());
|
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::PopFont();
|
||||||
ImGui::SameLine(0, 4.0f * dp);
|
ImGui::SameLine(0, 4.0f * dp);
|
||||||
ImGui::PushTextWrapPos(0.0f);
|
ImGui::PushTextWrapPos(0.0f);
|
||||||
|
|||||||
Reference in New Issue
Block a user