feat(settings): add hover tooltips to the settings controls that lacked them

A per-card audit found 38 interactive controls with no tooltip. Add them, with
new tt_* i18n keys (English source; per-language JSONs fall back to English
until translated):
- Chat & Contacts (8): emoji/bubble style, accent, density, text size, poll
  rate, timestamps, enter-sends — the segmented/beginRow helpers added none.
- Node & Security (26): the whole lite wallet lifecycle / backup-keys /
  encryption panel, plus the full-node RPC-details toggle and daemon Refresh.
- Debug Options (4): the screenshot-sweep / full-UI-sweep / open-folder /
  seed-demo-chat buttons.

Theme, Wallet, Explorer, About were already fully covered.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-21 21:31:15 -05:00
parent 541a9e1fd5
commit 7cf0bb8bc7
3 changed files with 81 additions and 2 deletions

View File

@@ -1646,6 +1646,7 @@ void RenderSettingsPage(App* app) {
if (material::TactileButton(TrId("lite_wallet_request", "LiteLifecycleToggle").c_str(), ImVec2(liteInputW, 0))) {
s_settingsState.lite_lifecycle_expanded = !s_settingsState.lite_lifecycle_expanded;
}
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_lifecycle_toggle"));
if (s_settingsState.lite_lifecycle_expanded) {
const char* lifecycleLabels[] = {TR("lite_op_create"), TR("lite_op_open"), TR("lite_op_restore")};
@@ -1667,6 +1668,7 @@ void RenderSettingsPage(App* app) {
}
ImGui::EndCombo();
}
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_lifecycle_op"));
if (s_settingsState.lite_lifecycle_operation == 1 ||
s_settingsState.lite_lifecycle_operation == 2) {
@@ -1677,6 +1679,7 @@ void RenderSettingsPage(App* app) {
ImGui::SetNextItemWidth(liteInputW);
ImGui::InputText("##LiteWalletPath", s_settingsState.lite_wallet_path,
sizeof(s_settingsState.lite_wallet_path));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_wallet_path"));
}
if (s_settingsState.lite_lifecycle_operation == 2) {
@@ -1688,6 +1691,7 @@ void RenderSettingsPage(App* app) {
ImGui::InputText("##LiteRestoreSeed", s_settingsState.lite_restore_seed,
sizeof(s_settingsState.lite_restore_seed),
ImGuiInputTextFlags_Password);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_restore_seed"));
// Live 24-word count so the user knows the seed is complete before restoring.
{
const int wc = liteSeedWordCount(s_settingsState.lite_restore_seed);
@@ -1703,6 +1707,7 @@ void RenderSettingsPage(App* app) {
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
ImGui::SetNextItemWidth(std::min(160.0f, liteInputW));
ImGui::InputInt("##LiteRestoreBirthday", &s_settingsState.lite_restore_birthday);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_restore_birthday"));
if (s_settingsState.lite_restore_birthday < 0) s_settingsState.lite_restore_birthday = 0;
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
@@ -1714,11 +1719,13 @@ void RenderSettingsPage(App* app) {
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
ImGui::SetNextItemWidth(std::min(160.0f, liteInputW));
ImGui::InputInt("##LiteRestoreAccount", &s_settingsState.lite_restore_account);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_restore_account"));
if (s_settingsState.lite_restore_account < 0) s_settingsState.lite_restore_account = 0;
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::Checkbox(TrId("lite_overwrite", "LiteRestoreOverwrite").c_str(),
&s_settingsState.lite_restore_overwrite);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_restore_overwrite"));
}
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
@@ -1730,6 +1737,7 @@ void RenderSettingsPage(App* app) {
s_settingsState.lite_lifecycle_passphrase,
sizeof(s_settingsState.lite_lifecycle_passphrase),
ImGuiInputTextFlags_Password);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_lifecycle_pass"));
// Poll a completed async create/open/restore (driven by the App-owned
// controller; finalized on the main thread by App::update's
@@ -1760,6 +1768,7 @@ void RenderSettingsPage(App* app) {
if (TactileButton(TrId("lite_validate", "LiteLifecycleValidate").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
evaluateLiteLifecycleRequestFromPageState(app);
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_lite_lifecycle_run"));
if (liteLifecycleBusy) ImGui::EndDisabled();
if (!s_settingsState.lite_lifecycle_status.empty()) {
@@ -1806,6 +1815,7 @@ void RenderSettingsPage(App* app) {
}
wallet::secureWipeLiteSecret(r.seedPhrase); // wipe the result copy
}
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_show_seed"));
ImGui::SameLine(0, Layout::spacingSm());
if (material::ActionButton("##LiteExportKeys", TR("lite_show_private_keys"), ICON_MD_KEY, material::ActionTier::Secondary)) {
auto r = app->liteWallet()->exportPrivateKeys();
@@ -1821,6 +1831,7 @@ void RenderSettingsPage(App* app) {
}
wallet::secureWipeLiteSecret(r.privateKeysJson);
}
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_show_keys"));
// Revealed secret: shown read-only (no extra copies), with copy + wipe.
if (!s_settingsState.lite_export_secret.empty()) {
@@ -1843,6 +1854,7 @@ void RenderSettingsPage(App* app) {
if (TactileButton(TrId("lite_copy", "LiteExportCopy").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
app->copySecretToClipboard(s_settingsState.lite_export_secret);
}
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_copy"));
ImGui::SameLine(0, Layout::spacingSm());
// Save the seed (+ birthday) to an owner-only (0600) file in the config dir.
if (s_settingsState.lite_export_is_seed &&
@@ -1857,12 +1869,16 @@ void RenderSettingsPage(App* app) {
? (std::string(TR("lite_saved_to")) + path)
: (std::string(TR("lite_could_not_write")) + path);
}
if (s_settingsState.lite_export_is_seed) ImGui::SameLine(0, Layout::spacingSm());
if (s_settingsState.lite_export_is_seed) {
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_save_seed_file"));
ImGui::SameLine(0, Layout::spacingSm());
}
if (TactileButton(TrId("lite_hide_wipe", "LiteExportHide").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret);
s_settingsState.lite_export_label.clear();
s_settingsState.lite_export_is_seed = false;
}
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_hide_wipe"));
}
// Import a spending/viewing key (history appears after the next sync).
@@ -1874,6 +1890,7 @@ void RenderSettingsPage(App* app) {
ImGui::InputText("##LiteImportKey", s_settingsState.lite_import_key,
sizeof(s_settingsState.lite_import_key),
ImGuiInputTextFlags_Password);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_import_key"));
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
if (TactileButton(TrId("lite_import", "LiteImportKeyBtn").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
std::string liteKey(s_settingsState.lite_import_key);
@@ -1888,6 +1905,7 @@ void RenderSettingsPage(App* app) {
r.ok ? TR("lite_key_imported") : r.error;
}
}
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_import_key_btn"));
if (!s_settingsState.lite_backup_status.empty()) {
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
@@ -1909,12 +1927,14 @@ void RenderSettingsPage(App* app) {
ImGui::SetNextItemWidth(liteInputW);
ImGui::InputText("##LiteEncryptPass", s_settingsState.lite_enc_pass,
sizeof(s_settingsState.lite_enc_pass), ImGuiInputTextFlags_Password);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_encrypt_pass"));
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
if (TactileButton(TrId("lite_encrypt_wallet", "LiteEncrypt").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
const auto r = app->liteWallet()->encryptWallet(s_settingsState.lite_enc_pass);
sodium_memzero(s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass));
s_settingsState.lite_encryption_status = r.ok ? TR("lite_wallet_encrypted") : r.error;
}
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_encrypt"));
} else {
if (wstate.isLocked()) {
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
@@ -1924,12 +1944,14 @@ void RenderSettingsPage(App* app) {
ImGui::SetNextItemWidth(liteInputW);
ImGui::InputText("##LiteUnlockPass", s_settingsState.lite_enc_pass,
sizeof(s_settingsState.lite_enc_pass), ImGuiInputTextFlags_Password);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_unlock_pass"));
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
if (TactileButton(TrId("lite_unlock", "LiteUnlock").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
const bool ok = app->liteWallet()->unlockWallet(s_settingsState.lite_enc_pass);
sodium_memzero(s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass));
s_settingsState.lite_encryption_status = ok ? TR("lite_wallet_unlocked") : TR("lite_unlock_failed");
}
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_unlock"));
} else {
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
if (TactileButton(TrId("lite_lock_now", "LiteLock").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
@@ -1937,6 +1959,7 @@ void RenderSettingsPage(App* app) {
s_settingsState.lite_encryption_status =
app->lockLiteWallet() ? TR("lite_wallet_locked") : TR("lite_lock_failed");
}
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_lock"));
}
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
@@ -1945,12 +1968,14 @@ void RenderSettingsPage(App* app) {
ImGui::SetNextItemWidth(liteInputW);
ImGui::InputText("##LiteDecryptPass", s_settingsState.lite_dec_pass,
sizeof(s_settingsState.lite_dec_pass), ImGuiInputTextFlags_Password);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_decrypt_pass"));
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
if (TactileButton(TrId("lite_remove_encryption", "LiteDecrypt").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
const auto r = app->liteWallet()->decryptWallet(s_settingsState.lite_dec_pass);
sodium_memzero(s_settingsState.lite_dec_pass, sizeof(s_settingsState.lite_dec_pass));
s_settingsState.lite_encryption_status = r.ok ? TR("lite_encryption_removed") : r.error;
}
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_remove_encrypt"));
}
if (!s_settingsState.lite_encryption_status.empty()) {
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
@@ -2096,7 +2121,7 @@ void RenderSettingsPage(App* app) {
ImVec2 hdrPos = ImGui::GetCursorScreenPos();
if (ImGui::Button("##RpcToggle", ImVec2(contentW, ImGui::GetFrameHeight())))
s_settingsState.rpc_expanded = !s_settingsState.rpc_expanded;
if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsItemHovered()) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); material::Tooltip("%s", TR("tt_rpc_toggle")); }
float textY = hdrPos.y + (ImGui::GetFrameHeight() - rpcHdrFont->LegacySize) * 0.5f;
dl->AddText(rpcHdrFont, rpcHdrFont->LegacySize, ImVec2(hdrPos.x, textY), OnSurfaceMedium(), TR("rpc_connection"));
ImFont* rpcIconFont = Type().iconSmall();
@@ -2288,6 +2313,7 @@ void RenderSettingsPage(App* app) {
uf.next(material::ActionButtonWidth(TR("refresh"), ICON_MD_REFRESH));
if (material::ActionButton("##drefresh", TR("refresh"), ICON_MD_REFRESH, AT::Secondary))
s_settingsState.daemon_info_loaded = false;
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_daemon_refresh"));
ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon() || !bun.available);
uf.next(material::ActionButtonWidth(TR("daemon_install_bundled"), ICON_MD_DOWNLOAD));
if (material::ActionButton("##dinst", TR("daemon_install_bundled"), ICON_MD_DOWNLOAD, AT::Secondary))
@@ -2619,18 +2645,22 @@ void RenderSettingsPage(App* app) {
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
if (TactileButton(TR("screenshot_sweep"), ImVec2(0, 0), S.resolveFont("button")))
app->startScreenshotSweep();
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_screenshot_sweep"));
ImGui::SameLine();
// Full UI sweep: also captures every modal / dialog / flow / state (demo data, offline).
if (TactileButton(TR("screenshot_sweep_full"), ImVec2(0, 0), S.resolveFont("button")))
app->startFullUiSweep();
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_screenshot_sweep_full"));
ImGui::SameLine();
if (TactileButton(TR("screenshot_open_dir"), ImVec2(0, 0), S.resolveFont("button")))
util::Platform::openFolder(app->screenshotDir());
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_screenshot_open_dir"));
if (chat::hushChatFeatureEnabledAtBuild()) {
// Populate the Chat tab with demo conversations so the sweep captures its real UI.
ImGui::SameLine();
if (TactileButton("Seed demo chat", ImVec2(0, 0), S.resolveFont("button")))
app->seedChatDemoData();
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_seed_demo_chat"));
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::Separator();