diff --git a/res/lang/de.json b/res/lang/de.json index 98dfd6c..e22a357 100644 --- a/res/lang/de.json +++ b/res/lang/de.json @@ -785,6 +785,7 @@ "output_filename": "Ausgabedateiname:", "overview": "Übersicht", "paste": "Einfügen", + "paste_clip_empty": "Zwischenablage ist leer", "paste_from_clipboard": "Aus Zwischenablage einfügen", "pay_from": "Zahlen von", "payment_request": "ZAHLUNGSANFRAGE", diff --git a/res/lang/es.json b/res/lang/es.json index 1ab4be4..5f8d835 100644 --- a/res/lang/es.json +++ b/res/lang/es.json @@ -785,6 +785,7 @@ "output_filename": "Nombre del archivo:", "overview": "Resumen", "paste": "Pegar", + "paste_clip_empty": "El portapapeles está vacío", "paste_from_clipboard": "Pegar del Portapapeles", "pay_from": "Pagar desde", "payment_request": "SOLICITUD DE PAGO", diff --git a/res/lang/fr.json b/res/lang/fr.json index 2a12a21..9bc3523 100644 --- a/res/lang/fr.json +++ b/res/lang/fr.json @@ -785,6 +785,7 @@ "output_filename": "Nom du fichier de sortie :", "overview": "Aperçu", "paste": "Coller", + "paste_clip_empty": "Le presse-papiers est vide", "paste_from_clipboard": "Coller depuis le presse-papiers", "pay_from": "Payer depuis", "payment_request": "DEMANDE DE PAIEMENT", diff --git a/res/lang/ja.json b/res/lang/ja.json index dca7c60..fb13e75 100644 --- a/res/lang/ja.json +++ b/res/lang/ja.json @@ -785,6 +785,7 @@ "output_filename": "出力ファイル名:", "overview": "概要", "paste": "貼り付け", + "paste_clip_empty": "クリップボードは空です", "paste_from_clipboard": "クリップボードから貼り付け", "pay_from": "支払い元", "payment_request": "支払い請求", diff --git a/res/lang/ko.json b/res/lang/ko.json index d1d1fee..6634750 100644 --- a/res/lang/ko.json +++ b/res/lang/ko.json @@ -785,6 +785,7 @@ "output_filename": "출력 파일명:", "overview": "개요", "paste": "붙여넣기", + "paste_clip_empty": "클립보드가 비어 있습니다", "paste_from_clipboard": "클립보드에서 붙여넣기", "pay_from": "보낼 곳", "payment_request": "결제 요청", diff --git a/res/lang/pt.json b/res/lang/pt.json index d66e818..93522f7 100644 --- a/res/lang/pt.json +++ b/res/lang/pt.json @@ -785,6 +785,7 @@ "output_filename": "Nome do arquivo de saída:", "overview": "Resumo", "paste": "Colar", + "paste_clip_empty": "A área de transferência está vazia", "paste_from_clipboard": "Colar da Área de Transferência", "pay_from": "Pagar de", "payment_request": "SOLICITAÇÃO DE PAGAMENTO", diff --git a/res/lang/ru.json b/res/lang/ru.json index b7fcab2..234d4b8 100644 --- a/res/lang/ru.json +++ b/res/lang/ru.json @@ -785,6 +785,7 @@ "output_filename": "Имя выходного файла:", "overview": "Обзор", "paste": "Вставить", + "paste_clip_empty": "Буфер обмена пуст", "paste_from_clipboard": "Вставить из буфера обмена", "pay_from": "Оплатить с", "payment_request": "ЗАПРОС НА ОПЛАТУ", diff --git a/res/lang/zh.json b/res/lang/zh.json index 44fac30..8a465d8 100644 --- a/res/lang/zh.json +++ b/res/lang/zh.json @@ -785,6 +785,7 @@ "output_filename": "输出文件名:", "overview": "概览", "paste": "粘贴", + "paste_clip_empty": "剪贴板为空", "paste_from_clipboard": "从剪贴板粘贴", "pay_from": "付款来源", "payment_request": "付款请求", diff --git a/src/app.cpp b/src/app.cpp index 1cde3c2..34125c8 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -2761,12 +2761,13 @@ static std::string formatIntWithCommas(long long v) return s; } -// A send-tab-style slider (mirrors RenderAmountBar in send_tab.cpp: rounded track, accent fill, glass -// thumb) for the optional rescan start height, stacked above a full-width numeric field for exact -// entry. Both share `buf` — the height as a decimal string — so it's the single source of truth. -// `tipHeight` is the chain tip = the slider's max; when <= 0 (height unknown) the slider is omitted -// and only the field shows. -static void RenderScanHeightControl(char* buf, size_t bufSize, int tipHeight, bool enabled, float dp) +// A send-tab-style slider (mirrors RenderAmountBar in send_tab.cpp: rounded track, accent fill, +// centered % label, glass thumb) for the optional rescan start height, stacked above a full-width +// numeric field for exact entry. Both share `buf` — the height as a decimal string — so it's the +// single source of truth. `tipHeight` is the chain tip = the slider's max; when <= 0 (height unknown) +// the slider is omitted and only the field shows. `availW` is the interior width (bar + field span it). +static void RenderScanHeightControl(char* buf, size_t bufSize, int tipHeight, bool enabled, + float dp, float availW) { namespace m = ui::material; int cur = buf[0] ? std::atoi(buf) : 0; @@ -2775,8 +2776,8 @@ static void RenderScanHeightControl(char* buf, size_t bufSize, int tipHeight, bo ImGui::BeginDisabled(!enabled); if (tipHeight > 0) { - const float barW = ImGui::GetContentRegionAvail().x; // slider spans the full row, stacked above the field - const float barH = ImGui::GetFrameHeight(); // match the numeric field's height (already dp-scaled) + const float barW = availW; // slider spans the panel interior, stacked above the field + const float barH = ImGui::GetFrameHeight(); // match the numeric field's height (already dp-scaled) const float barRound = barH * 0.5f; const float thumbR = barH * 0.5f; @@ -2804,7 +2805,18 @@ static void RenderScanHeightControl(char* buf, size_t bufSize, int tipHeight, bo dl->AddRectFilled(barMin, ImVec2(fillRight, barMax.y), m::WithAlpha(m::Primary(), enabled ? 150 : 55), barRound); - // Glass thumb. + // Percentage-of-chain label centered on the bar (mirrors the send-tab amount bar). + { + ImFont* cap = m::Type().caption(); + char pctBuf[16]; + snprintf(pctBuf, sizeof(pctBuf), "%.0f%%", pct * 100.0f); + const ImVec2 tsz = cap->CalcTextSizeA(cap->LegacySize, 10000.0f, 0.0f, pctBuf); + dl->AddText(cap, cap->LegacySize, + ImVec2(barMin.x + (barW - tsz.x) * 0.5f, barMin.y + (barH - tsz.y) * 0.5f), + IM_COL32(255, 255, 255, enabled ? 190 : 80), pctBuf); + } + + // Glass thumb (drawn last so it sits above the fill + label). const float thumbY = barMin.y + barH * 0.5f; dl->AddCircleFilled(ImVec2(thumbCenterX, thumbY), thumbR, IM_COL32(255, 255, 255, enabled ? 205 : 90), 24); dl->AddCircle(ImVec2(thumbCenterX, thumbY), thumbR, IM_COL32(255, 255, 255, enabled ? 120 : 50), 24, 1.4f * dp); @@ -2816,13 +2828,76 @@ static void RenderScanHeightControl(char* buf, size_t bufSize, int tipHeight, bo } } - // Exact numeric entry — full-width row beneath the slider. - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + // Exact numeric entry — full-width beneath the slider. + ImGui::SetNextItemWidth(availW); ImGui::InputText("##importscanheight", buf, bufSize, ImGuiInputTextFlags_CharsDecimal); 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; @@ -2869,19 +2944,31 @@ void App::renderImportKeyDialog() ImGui::Spacing(); } - // Masked key field with a reveal (eye) toggle + Paste/Clear. + // 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. ImGui::TextUnformatted(viewMode ? TR("import_viewkey_field") : TR("import_key_field")); - float eyeW = ImGui::GetFrameHeight(); - ImGui::SetNextItemWidth(-(eyeW + ui::Layout::spacingSm())); + 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); - ImGui::SameLine(0, ui::Layout::spacingSm()); - if (m::TactileButton(import_key_reveal_ ? ICON_MD_VISIBILITY_OFF : ICON_MD_VISIBILITY, - ImVec2(eyeW, eyeW), m::Type().iconSmall())) + ImGui::SameLine(0, 0); + m::IconButtonStyle eyeSt; + eyeSt.hoverBg = m::WithAlpha(m::OnSurface(), 30); + eyeSt.bgRounding = 4.0f * dp; // match the input frame's corners (it sits flush at the field's edge) + eyeSt.tooltip = TR("import_key_reveal_tip"); + if (m::IconButton("##revealkey", import_key_reveal_ ? ICON_MD_VISIBILITY_OFF : ICON_MD_VISIBILITY, + m::Type().iconSmall(), iconSz, eyeSt)) import_key_reveal_ = !import_key_reveal_; - if (ImGui::IsItemHovered()) m::Tooltip("%s", TR("import_key_reveal_tip")); - if (m::TactileButton(TR("paste"))) { + 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); + if (pasteClicked) { const char* clipboard = ImGui::GetClipboardText(); if (clipboard) { std::string trimmed(clipboard); @@ -2890,8 +2977,10 @@ void App::renderImportKeyDialog() snprintf(import_key_input_, sizeof(import_key_input_), "%s", trimmed.c_str()); } } - ImGui::SameLine(); - if (m::TactileButton(TR("clear"))) sodium_memzero(import_key_input_, sizeof(import_key_input_)); + 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_)); // Trimmed view + classification. The indicator and the Import guard share isRecognizedImportKey. std::string keyTrim(import_key_input_); @@ -2931,36 +3020,63 @@ void App::renderImportKeyDialog() } ImGui::Spacing(); - // Optional scan-from height. z_importkey / z_importviewingkey accept a start block, so a shielded - // import of a recent key needn't rescan the whole chain. A send-tab-style slider (bounded by the - // chain tip) plus a numeric field set it. Transparent WIF (importprivkey) has no start-height - // parameter, so the control is disabled + noted once a transparent key is recognized. + // Optional scan-from-height, grouped in a subtle glass panel so it reads as one distinct, + // clearly-optional block. z_importkey / z_importviewingkey accept a start block, so a shielded + // import of a recent key needn't rescan the whole chain. Transparent WIF (importprivkey) has no + // start-height parameter, so the control is disabled + noted once a transparent key is recognized. + ImGui::Dummy(ImVec2(0, 2.0f * dp)); { const bool transparentKey = !viewMode && isSpend && !shielded; // recognized transparent WIF if (transparentKey && import_key_scan_height_[0]) sodium_memzero(import_key_scan_height_, sizeof(import_key_scan_height_)); // no stale height const int tip = state_.sync.blocks; // chain tip = slider max; 0 when unknown - // Label, with the current chain height right-aligned when known. + // Render the content on an upper draw channel, then paint the glass panel behind it once the + // block's height is known (immediate-mode auto-sized panel via a draw-list splitter). + ImDrawList* dl = ImGui::GetWindowDrawList(); + ImDrawListSplitter splitter; + splitter.Split(dl, 2); + splitter.SetCurrentChannel(dl, 1); + + const float padX = 12.0f * dp, padY = 10.0f * dp; + const float panelW = ImGui::GetContentRegionAvail().x; + const ImVec2 panelMin = ImGui::GetCursorScreenPos(); + + ImGui::Dummy(ImVec2(0, padY)); + ImGui::Indent(padX); + const float availW = ImGui::GetContentRegionAvail().x - padX; // interior width (both side paddings) + + // Label + current chain height, right-aligned within the interior. const float rowStartX = ImGui::GetCursorPosX(); - const float rowW = ImGui::GetContentRegionAvail().x; ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(m::OnSurfaceMedium()), "%s", TR("import_scan_label")); if (tip > 0) { char tipBuf[64]; snprintf(tipBuf, sizeof(tipBuf), "%s %s", TR("import_scan_tip"), formatIntWithCommas(tip).c_str()); - const float targetX = rowStartX + rowW - ImGui::CalcTextSize(tipBuf).x; + const float targetX = rowStartX + availW - ImGui::CalcTextSize(tipBuf).x; ImGui::SameLine(); if (targetX > ImGui::GetCursorPosX()) ImGui::SetCursorPosX(targetX); ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(m::OnSurfaceDisabled()), "%s", tipBuf); } RenderScanHeightControl(import_key_scan_height_, sizeof(import_key_scan_height_), - tip, !transparentKey, dp); + tip, !transparentKey, dp, availW); ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(m::OnSurfaceDisabled()), "%s", transparentKey ? TR("import_scan_transparent") : TR("import_scan_hint")); - ImGui::Spacing(); + + ImGui::Unindent(padX); + ImGui::Dummy(ImVec2(0, padY)); + + const ImVec2 panelMax(panelMin.x + panelW, ImGui::GetCursorScreenPos().y); + splitter.SetCurrentChannel(dl, 0); + m::GlassPanelSpec spec; + spec.rounding = 8.0f * dp; + spec.fillAlpha = 20; + spec.borderAlpha = 30; + m::DrawGlassPanel(dl, panelMin, panelMax, spec); + splitter.Merge(dl); } + ImGui::Dummy(ImVec2(0, 2.0f * dp)); // Progress / result / error. if (import_in_progress_) { diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 572c2fa..398de9a 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -1370,6 +1370,7 @@ void I18n::loadBuiltinEnglish() strings_["import_scan_hint"] = "0 = rescan from the start"; strings_["import_scan_transparent"] = "Transparent keys always rescan fully"; strings_["import_scan_tip"] = "current height"; + strings_["paste_clip_empty"] = "Clipboard is empty"; // --- Key Export Dialog --- strings_["key_export_fetching"] = "Fetching key from wallet...";