From b7b32bfde8204b8aa02e986db579b8420a51844c Mon Sep 17 00:00:00 2001 From: DanS Date: Mon, 13 Jul 2026 17:52:11 -0500 Subject: [PATCH] fix(settings): stop paste-preview hover flicker from layout shift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/app.cpp | 50 ++++++++++++++++++++++++-------------------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/src/app.cpp b/src/app.cpp index 67fb4c7..2340587 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -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);