fix(settings): stop paste-preview hover flicker from layout shift

Hovering Paste reflected the clipboard's classification in the type-indicator
ROW, which appeared/disappeared with the hover. That grew/shrank the centered
dialog, moving the Paste button out from under the cursor → hover ended → row
vanished → dialog shrank back → cursor over the button again → rapid flicker.

Keep the in-field preview + verification but make them pure draw-list (no layout
change): the ghost overlay is now coloured by how the clipboard would classify
(green ok / amber wrong-type / red unrecognized), and the type indicator reverts
to the actual field content only. The dialog height is now identical whether or
not Paste is hovered, so there's no shift and no flicker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-13 17:52:11 -05:00
parent 0b47134112
commit b7b32bfde8

View File

@@ -2930,10 +2930,11 @@ void App::renderImportKeyDialog()
ImGui::SameLine();
if (m::TactileButton(TR("clear"))) sodium_memzero(import_key_input_, sizeof(import_key_input_));
// 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;
// While Paste is hovered and the field is empty, preview the trimmed clipboard INSIDE the field
// masked with '*' unless the key is revealed — coloured by how it would classify (green ok / amber
// wrong-type / red unrecognized). Drawn via the draw list only: it verifies the key WITHOUT adding
// a layout row, since a hover-toggled row would grow the centered dialog and shift the button out
// from under the cursor (hover flicker).
if (pasteHovered && import_key_input_[0] == '\0') {
const char* clip = ImGui::GetClipboardText();
if (clip && clip[0]) {
@@ -2941,20 +2942,25 @@ void App::renderImportKeyDialog()
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;
const bool pView = services::WalletSecurityController::isViewingKey(t);
const bool pSpend = services::WalletSecurityController::isRecognizedPrivateKey(t);
const bool pOk = viewMode ? pView : pSpend;
const bool pWrong = viewMode ? pSpend : pView;
ImVec4 col = pOk ? m::SuccessVec4()
: pWrong ? ImVec4(0.8f, 0.6f, 0.0f, 1.0f)
: m::ErrorVec4();
col.w = 0.6f; // ghost (uncommitted) preview
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());
ImGui::GetWindowDrawList()->AddText(tp, ImGui::ColorConvertFloat4ToU32(col), 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.
// Trimmed field content drives the type indicator, the Import/Sweep guard, and the scan panel — NOT
// the hover preview, so hovering can neither shift the layout nor enable the action 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();
@@ -2962,23 +2968,15 @@ 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; the Import/Sweep guard uses `recognized` (wrong-type
// redirect handling lives in the indicator's iWrongType path below).
// 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.
bool recognized = viewMode ? isView : isSpend;
bool wrongType = viewMode ? isSpend : isView;
// 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) {
if (!keyTrim.empty()) {
if (recognized) {
const char* label = viewMode ? TR("import_key_type_zview")
: iShielded ? TR("import_key_type_zspend")
: shielded ? TR("import_key_type_zspend")
: TR("import_key_type_tkey");
ImGui::PushFont(m::Type().iconSmall());
ImGui::TextColored(m::SuccessVec4(), ICON_MD_CHECK_CIRCLE);
@@ -2986,11 +2984,11 @@ void App::renderImportKeyDialog()
ImGui::SameLine(0, 4.0f * dp);
ImGui::TextColored(m::SuccessVec4(), "%s", label);
} else {
const char* msg = iWrongType
const char* msg = wrongType
? (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), iWrongType ? ICON_MD_WARNING : ICON_MD_HELP);
ImGui::TextColored(ImVec4(0.8f, 0.6f, 0.0f, 1.0f), wrongType ? ICON_MD_WARNING : ICON_MD_HELP);
ImGui::PopFont();
ImGui::SameLine(0, 4.0f * dp);
ImGui::PushTextWrapPos(0.0f);