diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 131c6f8b6..b9d4502f2 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -2687,6 +2687,8 @@ digits Malayalam Малаялам Reset OpenType Features Нулиране на OpenType характеристиките Reset Panel Нулиране на панела Reset Preferences Нулиране на настройките + Search Preferences Търсене в настройките + No settings found Няма намерени настройки Reset Profiles Нулиране на профилите Reset Rotate View Нулиране на изгледа на завъртане Rotate View to Selection Завъртане на изгледа спрямо селекцията diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index f78642c10..037d060fa 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -3634,6 +3634,8 @@ theme Appearance Izgled Real-time Drawing and Editing Crtanje i uređivanje u stvarnom vremenu Render Threads (-1 = Automatic) Niti iscrtavanja (-1 = automatski) Reset Preferences Vrati postavke na početne + Search Preferences Pretraga postavki + No settings found Nema pronađenih postavki Scale Corners Skaliraj uglove Scale Cursor Proportional to UI Skaliraj kursor proporcionalno interfejsu Scale Strokes & Effects Skaliraj poteze i efekte diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index b85cafc2c..99e6000e6 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -3401,6 +3401,8 @@ digits Malayalam Malaiàlam Reset Painting Restableix Pintura Reset Panel Restableix el panell Reset Preferences Restableix les preferències + Search Preferences Cerca a les preferències + No settings found No s'ha trobat cap paràmetre Reset Printing and Proofing Restableix impressió i prova Reset Profiles Restableix els perfils Reset Rotate View Restableix la rotació de la vista diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 55876eaa3..6e4469004 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -3675,6 +3675,8 @@ theme Appearance Motiv Real-time Drawing and Editing Kreslení a úpravy v reálném čase Render Threads (-1 = Automatic) Vlákna vykreslování (-1 = automaticky) Reset Preferences Obnovit předvolby + Search Preferences Hledat v předvolbách + No settings found Nenalezena žádná nastavení Scale Corners Měnit velikost rohů Scale Cursor Proportional to UI Měnit velikost kurzoru úměrně rozhraní Scale Strokes & Effects Měnit velikost tahů a efektů diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 654354f44..890f8a5ed 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -3403,6 +3403,8 @@ theme Appearance Udseende Real-time Drawing and Editing Tegning og redigering i realtid Render Threads (-1 = Automatic) Renderingstråde (-1 = automatisk) Reset Preferences Nulstil indstillinger + Search Preferences Søg i præferencer + No settings found Ingen indstillinger fundet Scale Corners Skalér hjørner Scale Cursor Proportional to UI Skalér markør proportionalt med brugergrænsefladen Scale Strokes & Effects Skalér konturer & effekter diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 931623a77..06ca85316 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -2714,6 +2714,8 @@ digits Malayalam Malayalam Reset OpenType Features OpenType-Funktionen zurücksetzen Reset Panel Bedienfeld zurücksetzen Reset Preferences Voreinstellungen zurücksetzen + Search Preferences Voreinstellungen durchsuchen + No settings found Keine Einstellungen gefunden Reset Profiles Profile zurücksetzen Rotate View Ansicht drehen Reset Rotate View Ansichtsdrehung zurücksetzen diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index bfb378a32..d9270ece7 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -1872,6 +1872,8 @@ font-technology Variable Μεταβλητές Reselect Επαναφορά προηγούμενης επιλογής Reset Επαναφορά Reset Preferences Επαναφορά προτιμήσεων + Search Preferences Αναζήτηση στις προτιμήσεις + No settings found Δεν βρέθηκαν ρυθμίσεις Reset to Default Επαναφορά προεπιλογής Resolution: Ανάλυση: Restore Επαναφορά diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index daf3d6e84..9c8127b90 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -2757,6 +2757,8 @@ digits Malayalam Malayálam Reset OpenType Features Restablecer funciones OpenType Reset Panel Restablecer panel Reset Preferences Restablecer preferencias + Search Preferences Buscar en preferencias + No settings found No se encontraron ajustes Reset Profiles Restablecer perfiles Reset Rotate View Restablecer rotación de vista Rotate View to Selection Rotar vista según la selección diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index e4b8ff4ba..33814aed3 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -3599,6 +3599,8 @@ theme Appearance Ulkoasu Real-time Drawing and Editing Reaaliaikainen piirto ja muokkaus Render Threads (-1 = Automatic) Renderöintisäikeet (-1 = automaattinen) Reset Preferences Palauta asetukset + Search Preferences Hae asetuksista + No settings found Asetuksia ei löytynyt Scale Corners Skaalaa kulmat Scale Cursor Proportional to UI Skaalaa kohdistin suhteessa käyttöliittymään Scale Strokes & Effects Skaalaa viivat ja tehosteet diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 9613bdea3..903e2bf69 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -2749,6 +2749,8 @@ digits Malayalam Malayalam Reset OpenType Features Réinitialiser les fonctions OpenType Reset Panel Réinitialiser le panneau Reset Preferences Réinitialiser les préférences + Search Preferences Rechercher dans les préférences + No settings found Aucun réglage trouvé Reset Profiles Réinitialiser les profils Reset Rotate View Réinitialiser la rotation de l’affichage Rotate View to Selection Faire pivoter l’affichage selon la sélection diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index 71f483518..03192a67f 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -3717,6 +3717,8 @@ Reset Painting पेंटिंग रीसेट करें Reset Panel पैनल रीसेट करें Reset Preferences प्रेफ़रेंसेज़ रीसेट करें + Search Preferences प्रेफ़रेंसेज़ में खोजें + No settings found कोई सेटिंग नहीं मिली Reset Printing and Proofing मुद्रण और प्रूफिंग रीसेट करें Reset Profiles प्रोफ़ाइल रीसेट करें Reset Rotate View व्यू घुमाएँ रीसेट करें diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 94ab1f8c1..5215573f1 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -3295,6 +3295,8 @@ mark-style Japanese Japán Reset Painting Painting visszaállítása Reset Panel Panel visszaállítása Reset Preferences Beállítások visszaállítása + Search Preferences Keresés a beállításokban + No settings found Nincs ilyen beállítás Reset Printing and Proofing Printing and Proofing visszaállítása Reset Profiles Profilok visszaállítása Reset Rotate View Nézetforgatás visszaállítása diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 3ed6d7b14..5e92053de 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -3403,6 +3403,8 @@ theme Appearance Tampilan Real-time Drawing and Editing Penggambaran dan pengeditan waktu nyata Render Threads (-1 = Automatic) Utas render (-1 = Otomatis) Reset Preferences Atur ulang preferensi + Search Preferences Cari preferensi + No settings found Tidak ada pengaturan yang ditemukan Scale Corners Skalakan sudut Scale Cursor Proportional to UI Skalakan kursor proporsional dengan UI Scale Strokes & Effects Skalakan guratan & efek diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index c0bb946a0..b89eff01d 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -2790,6 +2790,8 @@ digits Malayalam Malayalam Reset OpenType Features Reimposta funzioni OpenType Reset Panel Reimposta pannello Reset Preferences Reimposta preferenze + Search Preferences Cerca nelle preferenze + No settings found Nessuna impostazione trovata Reset Profiles Reimposta profili Rotate View Ruota vista Reset Rotate View Reimposta rotazione vista diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index aedfbaa52..6867dd107 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -2768,6 +2768,8 @@ mark-style Japanese 日本式 Reset OpenType Features OpenType 機能をリセット Reset Panel パネルをリセット Reset Preferences 環境設定をリセット + Search Preferences 環境設定を検索 + No settings found 設定が見つかりません Reset Profiles プロファイルをリセット Reset the rows to the artwork's colors 行をアートワークのカラーにリセット Reset to Default 初期設定に戻す diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 38f897c2e..ae7d1337a 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -3220,6 +3220,8 @@ theme Appearance იერსახე Real-time Drawing and Editing რეალურ დროში ხატვა და რედაქტირება Render Threads (-1 = Automatic) ნაკადები (-1 = ავტომატური) Reset Preferences პარამეტრების ჩამოყრა + Search Preferences პარამეტრებში ძიება + No settings found პარამეტრები ვერ მოიძებნა Scale Corners კუთხეების მასშტაბირება Scale Cursor Proportional to UI კურსორის მასშტაბირება ინტერფეისის პროპორციულად Scale Strokes & Effects კონტურებისა და ეფექტების მასშტაბირება diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index c30d06632..68431ff6e 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -2757,6 +2757,8 @@ mark-style Japanese 일본식 Reset OpenType Features OpenType 기능 재설정 Reset Panel 패널 재설정 Reset Preferences 환경 설정 재설정 + Search Preferences 환경 설정 검색 + No settings found 설정을 찾을 수 없음 Reset Profiles 프로필 재설정 Reset the rows to the artwork's colors 행을 아트웍 색상으로 재설정 Reset to Default 기본값으로 재설정 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index b82336ceb..4245811a1 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -3417,6 +3417,8 @@ theme Appearance Vormgeving Real-time Drawing and Editing Tekenen en bewerken in realtime Render Threads (-1 = Automatic) Renderthreads (-1 = automatisch) Reset Preferences Voorkeuren herstellen + Search Preferences Voorkeuren doorzoeken + No settings found Geen instellingen gevonden Scale Corners Hoeken schalen Scale Cursor Proportional to UI Cursor proportioneel schalen met UI Scale Strokes & Effects Streken & effecten schalen diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 66c0e8bd9..6c19b14eb 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -3538,6 +3538,8 @@ theme Appearance Wygląd Real-time Drawing and Editing Rysowanie i edycja w czasie rzeczywistym Render Threads (-1 = Automatic) Wątki renderowania (-1 = automatycznie) Reset Preferences Resetuj preferencje + Search Preferences Szukaj w preferencjach + No settings found Nie znaleziono ustawień Scale Corners Skaluj narożniki Scale Cursor Proportional to UI Skaluj kursor proporcjonalnie do interfejsu Scale Strokes & Effects Skaluj obrysy i efekty diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 8d762e6a7..5ade07a3f 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -2300,6 +2300,8 @@ digits Malayalam Malaiala Reset OpenType Features Redefinir Recursos OpenType Reset Panel Redefinir Painel Reset Preferences Redefinir Preferências + Search Preferences Pesquisar nas Preferências + No settings found Nenhuma configuração encontrada Reset Profiles Redefinir Perfis Rotate View Girar Visualização Reset Rotate View Redefinir Visualização Girada diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index e92a1f735..7c971694e 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -2785,6 +2785,8 @@ digits Malayalam Малаялам Reset OpenType Features Сбросить функции OpenType Reset Panel Сбросить панель Reset Preferences Сбросить настройки + Search Preferences Поиск в настройках + No settings found Настройки не найдены Reset Profiles Сбросить профили Rotate View Повернуть вид Reset Rotate View Сбросить поворот вида diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index ec3c3212c..d921d2a1f 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -2693,6 +2693,8 @@ digits Malayalam Malayalam Reset OpenType Features Återställ OpenType-funktioner Reset Panel Återställ panel Reset Preferences Återställ inställningar + Search Preferences Sök i inställningar + No settings found Inga inställningar hittades Reset Profiles Återställ profiler Reset Rotate View Återställ Rotera vy Rotate View to Selection Rotera vyn efter markeringen diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 013843f20..d8d40856a 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -2562,6 +2562,8 @@ digits Malayalam Malayalam Reset OpenType Features OpenType Özelliklerini Sıfırla Reset Panel Paneli Sıfırla Reset Preferences Tercihleri Sıfırla + Search Preferences Tercihlerde Ara + No settings found Ayar bulunamadı Reset Profiles Profilleri Sıfırla Reset Rotate View Döndürme Görünümünü Sıfırla Rotate View to Selection Görünümü Seçime Döndür diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 1d94e3e72..cd7cb9465 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -2782,6 +2782,8 @@ digits Malayalam Малаялам Reset OpenType Features Скинути функції OpenType Reset Panel Скинути панель Reset Preferences Скинути налаштування + Search Preferences Пошук у налаштуваннях + No settings found Налаштування не знайдено Reset Profiles Скинути профілі Rotate View Повернути вид Reset Rotate View Скинути поворот виду diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index b5b998423..c09bcda63 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -3059,6 +3059,8 @@ mark-style Japanese Kiểu Nhật Reset OpenType Features Đặt lại tính năng OpenType Reset Panel Đặt lại bảng Reset Preferences Đặt lại tùy chọn + Search Preferences Tìm trong tùy chọn + No settings found Không tìm thấy cài đặt Reset Profiles Đặt lại hồ sơ màu Rotate View Xoay chế độ xem Reset Rotate View Đặt lại xoay chế độ xem diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 5f5ef8a0e..288ad797d 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -2661,6 +2661,8 @@ digits Malayalam 马拉雅拉姆数字 Reset OpenType Features 重置 OpenType 特性 Reset Panel 重置面板 Reset Preferences 重置首选项 + Search Preferences 搜索首选项 + No settings found 未找到设置 Reset Profiles 重置配置文件 Reset Rotate View 重置旋转视图 Rotate View to Selection 按所选对象旋转视图 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 1759c5b49..6cb5bdf07 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -3654,6 +3654,8 @@ digits Malayalam 馬來亞拉姆數字 Reset OpenType Features 重設 OpenType 功能 Reset Panel 重設面板 Reset Preferences 重設偏好設定 + Search Preferences 搜尋偏好設定 + No settings found 找不到設定 Reset Profiles 重設描述檔 Rotate View 旋轉檢視 Reset Rotate View 重設旋轉檢視 diff --git a/crates/ui-egui/src/prefs_dialog.rs b/crates/ui-egui/src/prefs_dialog.rs index 20b9b2e82..c3532da83 100644 --- a/crates/ui-egui/src/prefs_dialog.rs +++ b/crates/ui-egui/src/prefs_dialog.rs @@ -32,9 +32,158 @@ pub fn open(app: &mut VectorcraftApp, category: Option<&str>) { fields.insert("__smartGuides".into(), json!(app.ui.view.smart_guides)); fields.insert("__snapToGrid".into(), json!(app.ui.view.snap_to_grid)); fields.insert("__taskBar".into(), json!(app.ui.task_bar)); + fields.insert("__search".into(), json!("")); + fields.insert("__focusSearch".into(), json!(true)); app.ui.dialog = Some(Dialog { kind: "preferences".into(), fields }); } +/// A setting the dialog shows, by its key, category, section ("" for none) and label, in English. +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) struct Setting { + pub key: &'static str, + pub category: &'static str, + pub section: &'static str, + pub label: &'static str, +} + +/// Every setting the dialog shows on this platform: the UI-only fields, then the engine's +/// preferences, each category's in the order the dialog lists them. +fn settings() -> impl Iterator { + let ui_fields = UI_FIELDS.iter().map(|&(key, category, label)| Setting { key, category, section: "", label }); + let shown = |key: &str| match key { + "systemTitleBar" => cfg!(all(not(target_arch = "wasm32"), not(target_os = "macos"))), + "linuxDisplayServer" => cfg!(target_os = "linux"), + _ => true, + }; + let prefs = + PREF_SPECS.iter().filter(move |s| shown(s.key)).map(|s| Setting { key: s.key, category: s.category, section: s.section, label: s.label }); + ui_fields.chain(prefs) +} + +/// The settings whose label, section or category contains `query`, ignoring case, in the language +/// the dialog is drawn in or in English, grouped by category in the dialog's order. An empty +/// `query` finds none. +pub(crate) fn search(query: &str) -> Vec { + let q = query.trim().to_lowercase(); + if q.is_empty() { + return vec![]; + } + let hit = |s: &str| !s.is_empty() && (tl!(s).to_lowercase().contains(&q) || s.to_lowercase().contains(&q)); + let mut found: Vec = settings().filter(|s| hit(s.label) || hit(s.section) || hit(s.category)).collect(); + found.sort_by_key(|s| PREF_CATEGORIES.iter().position(|c| *c == s.category).unwrap_or(usize::MAX)); + found +} + +/// Goes to the setting `s` a search found: its category is shown, the list scrolls to it and its +/// row is highlighted. The search field is cleared. +fn go_to(d: &mut Dialog, s: &Setting) { + d.fields.insert("__category".into(), json!(s.category)); + d.fields.insert("__found".into(), json!(s.key)); + d.fields.insert("__scrollTo".into(), json!(true)); + d.fields.insert("__search".into(), json!("")); +} + +/// The search field's memory key ([`widgets::search_field`]). +fn search_id() -> egui::Id { + egui::Id::unique("prefs-search") +} + +/// The search field's text edit, which takes the keyboard focus. +pub(crate) fn search_edit_id() -> egui::Id { + search_id().with("edit") +} + +/// Whether the search field had the keyboard focus when the dialog was last drawn. +fn search_focused_id() -> egui::Id { + egui::Id::unique("prefs-search-focused") +} + +/// Escape while the Preferences dialog's search field has text and the keyboard focus: the field is +/// cleared and keeps the focus, and the Escape goes no further. → Whether it did; otherwise Escape +/// does what it does in any dialog. +pub(crate) fn clear_search_on_escape(app: &mut VectorcraftApp, ctx: &egui::Context) -> bool { + let Some(d) = app.ui.dialog.as_mut().filter(|d| d.kind == "preferences") else { return false }; + // egui takes the focus from the field when the frame with Escape begins: the field's focus is + // the one recorded when the dialog was last drawn. + if d.str("__search").is_empty() || !ctx.data(|m| m.get_temp::(search_focused_id()).unwrap_or(false)) { + return false; + } + d.fields.insert("__search".into(), json!("")); + d.fields.insert("__focusSearch".into(), json!(true)); + ctx.input_mut(|i| i.events.retain(|e| !matches!(e, egui::Event::Key { key: egui::Key::Escape, pressed: true, .. }))); + true +} + +/// The search field above the category list. It takes the keyboard focus when the dialog opens and +/// on Cmd/Ctrl+F. → Its text. +fn search_box(ui: &mut egui::Ui, d: &mut Dialog) -> String { + let refocus = ui.input_mut(|i| i.consume_key(egui::Modifiers::COMMAND, egui::Key::F)); + if refocus { + d.fields.insert("__focusSearch".into(), json!(true)); + } + ui.data_mut(|m| m.insert_temp(search_id(), d.str("__search"))); + let query = widgets::search_field(ui, search_id(), tl!("Search Preferences")); + if d.bool("__focusSearch") { + // Not on the window's measuring frame, nor in the frame of the click that opened the + // dialog: egui takes the focus from a field such a click missed. + if ui.is_sizing_pass() || ui.input(|i| i.pointer.any_click()) { + ui.ctx().request_repaint(); + } else { + widgets::focus_field(ui, search_edit_id(), &query); + d.fields.remove("__focusSearch"); + } + } + let focused = ui.memory(|m| m.has_focus(search_edit_id())); + ui.data_mut(|m| m.insert_temp(search_focused_id(), focused)); + d.fields.insert("__search".into(), json!(query)); + query +} + +/// The settings `query` finds, in place of the category list: choosing one goes to it. +fn search_results(ui: &mut egui::Ui, d: &mut Dialog, query: &str) { + let t = Tokens::get(ui.ctx()); + let found = search(query); + if found.is_empty() { + ui.label(egui::RichText::new(tl!("No settings found")).color(t.text_dim).size(12.0)); + return; + } + egui::ScrollArea::vertical().id_salt("prefs-search-results").max_height(380.0).auto_shrink([false, true]).show(ui, |ui| { + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate); + for s in &found { + let text = egui::RichText::new(tl!(s.label)).size(12.5).color(t.text); + let place = if s.section.is_empty() { tl!(s.category).to_string() } else { format!("{} › {}", tl!(s.category), tl!(s.section)) }; + let b = egui::Button::selectable(false, text).min_size(egui::vec2(180.0, 22.0)); + if ui.add(b).on_hover_text(&place).clicked() { + go_to(d, s); + } + ui.label(egui::RichText::new(place).color(t.text_dim).size(11.0)); + ui.add_space(3.0); + } + }); +} + +/// Before a setting's row: where to paint the highlight behind it, when it is the setting a search +/// went to ([`go_to`]). +fn found_mark(ui: &mut egui::Ui, d: &Dialog, key: &str) -> Option<(egui::layers::ShapeIdx, f32)> { + (d.str("__found") == key).then(|| (ui.painter().add(egui::Shape::Noop), ui.cursor().top())) +} + +/// After the row [`found_mark`] marked: highlights it, and scrolls the list to it once. +fn found_paint(ui: &mut egui::Ui, d: &mut Dialog, mark: Option<(egui::layers::ShapeIdx, f32)>) { + let Some((idx, top)) = mark else { return }; + let bottom = ui.min_rect().bottom(); + if bottom <= top { + return; + } + let rect = egui::Rect::from_x_y_ranges(ui.max_rect().x_range(), top..=bottom).expand2(egui::vec2(0.0, 2.0)); + let t = Tokens::get(ui.ctx()); + ui.painter().set(idx, egui::epaint::RectShape::filled(rect, 3.0, t.accent.gamma_multiply(0.22))); + if d.bool("__scrollTo") && !ui.is_sizing_pass() { + ui.scroll_to_rect(rect, Some(egui::Align::Center)); + d.fields.remove("__scrollTo"); + } +} + /// OK: validate and apply every preference in the dialog (nothing changes on error). pub fn confirm(app: &mut VectorcraftApp) -> Result { let Some(d) = app.ui.dialog.clone() else { return Err("no dialog open".into()) }; @@ -189,20 +338,32 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { crate::dialogs::modal::heading(ui, tl!("Preferences")); ui.add_space(12.0); ui.horizontal_top(|ui| { - // Category list. - egui::Frame::NONE.fill(t.panel_darker).corner_radius(egui::CornerRadius::same(4)).inner_margin(egui::Margin::same(6)).show(ui, |ui| { - ui.set_width(196.0); - ui.set_min_height(430.0); - ui.vertical(|ui| { - ui.spacing_mut().item_spacing.y = 1.0; - for c in PREF_CATEGORIES { - let sel = *c == cat; - let text = egui::RichText::new(tl!(*c)).size(12.5).color(if sel { t.text_strong } else { t.text }); - let b = egui::Button::selectable(sel, text).min_size(egui::vec2(184.0, 24.0)); - if ui.add(b).clicked() { - d.fields.insert("__category".into(), json!(c)); + ui.vertical(|ui| { + ui.set_width(208.0); + // Search, above the category list; while it has text, the settings it finds show + // in place of the list. + let query = search_box(ui, &mut d); + ui.add_space(6.0); + // Category list. + let list = egui::Frame::NONE.fill(t.panel_darker).corner_radius(egui::CornerRadius::same(4)).inner_margin(egui::Margin::same(6)); + list.show(ui, |ui| { + ui.set_width(196.0); + ui.set_min_height(396.0); + ui.vertical(|ui| { + ui.spacing_mut().item_spacing.y = 1.0; + if !query.trim().is_empty() { + search_results(ui, &mut d, &query); + return; } - } + for c in PREF_CATEGORIES { + let sel = *c == cat; + let text = egui::RichText::new(tl!(*c)).size(12.5).color(if sel { t.text_strong } else { t.text }); + let b = egui::Button::selectable(sel, text).min_size(egui::vec2(184.0, 24.0)); + if ui.add(b).clicked() { + d.fields.insert("__category".into(), json!(c)); + } + } + }); }); }); ui.add_space(14.0); @@ -248,6 +409,12 @@ pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { d.fields.insert(k, v); } } + // The setting a search went to stays highlighted until another category is shown. + let found = d.str("__found"); + if !found.is_empty() && settings().find(|s| s.key == found).is_none_or(|s| s.category != d.str("__category")) { + d.fields.remove("__found"); + d.fields.remove("__scrollTo"); + } app.ui.dialog = Some(d); if cancel { app.ui.dialog = None; @@ -263,11 +430,15 @@ fn category_fields(ui: &mut egui::Ui, app: &mut VectorcraftApp, d: &mut Dialog, let specs: Vec<_> = PREF_SPECS.iter().filter(|s| s.category == cat).collect(); for (key, c, label) in UI_FIELDS { if *c == cat && cat != "Smart Guides" { + let mark = found_mark(ui, d, key); bool_row(ui, d, key, label); + found_paint(ui, d, mark); } } if cat == "Smart Guides" { + let mark = found_mark(ui, d, "__smartGuides"); bool_row(ui, d, "__smartGuides", UI_FIELDS[0].2); + found_paint(ui, d, mark); ui.add_space(4.0); } for sp in specs { @@ -283,6 +454,7 @@ fn category_fields(ui: &mut egui::Ui, app: &mut VectorcraftApp, d: &mut Dialog, } first = false; let v = d.fields.get(sp.key).cloned().unwrap_or(Value::Null); + let mark = found_mark(ui, d, sp.key); match sp.kind { // Units › Numbers Without Units Are Points only tells points from picas: dimmed // unless a unit is Picas. @@ -409,6 +581,7 @@ fn category_fields(ui: &mut egui::Ui, app: &mut VectorcraftApp, d: &mut Dialog, }); } } + found_paint(ui, d, mark); } } diff --git a/crates/ui-egui/src/prefs_dialog/tests.rs b/crates/ui-egui/src/prefs_dialog/tests.rs index 8d0285254..a43ec6677 100644 --- a/crates/ui-egui/src/prefs_dialog/tests.rs +++ b/crates/ui-egui/src/prefs_dialog/tests.rs @@ -324,6 +324,157 @@ fn the_category_shown_is_highlighted() { assert_eq!(filled, vec![3], "{rows:?}"); } +/// One frame of the Preferences dialog with `events`, keys first as the app takes them. +fn search_frame(a: &mut VectorcraftApp, ctx: &egui::Context, events: Vec) -> egui::FullOutput { + let mut out = ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| { + crate::shortcuts::handle(a, ui.ctx()); + show(a, ui.ctx()); + }); + out.textures_delta.clear(); + out +} + +/// A key pressed and let go in one frame. +fn key_press(key: egui::Key, modifiers: egui::Modifiers) -> Vec { + [true, false].map(|pressed| egui::Event::Key { key, physical_key: None, pressed, repeat: false, modifiers }).to_vec() +} + +fn search_text(a: &VectorcraftApp) -> String { + a.ui.dialog.as_ref().map(|d| d.str("__search")).unwrap_or_default() +} + +/// Preferences' search finds a setting by its label, ignoring case; an empty search finds nothing. +#[test] +fn a_search_finds_a_setting_by_its_label() { + let keys = |q: &str| search(q).iter().map(|s| s.key).collect::>(); + assert!(keys("tool tips").contains(&"showToolTips"), "{:?}", keys("tool tips")); + assert!(keys("SHOW TOOL TIPS").contains(&"showToolTips")); + assert!(keys("snap to grid").contains(&"__snapToGrid")); + let found = search("gridline every"); + assert_eq!(found.iter().map(|s| (s.key, s.category)).collect::>(), [("gridlineEvery", "Guides & Grid")]); + assert!(search("").is_empty() && search(" ").is_empty()); + assert!(search("no such setting at all").is_empty()); +} + +/// A translated label matches while the dialog is drawn in that language. +#[test] +fn a_translated_label_matches_in_the_interface_language() { + let de = crate::i18n::Lang::from_code("de").expect("de registered"); + let label = crate::i18n::tr(de, "Show Tool Tips"); + assert_ne!(label, "Show Tool Tips", "German has a row for the label"); + crate::i18n::set_current(de); + let found: Vec<_> = search(&label.to_uppercase()).iter().map(|s| s.key).collect(); + crate::i18n::set_current(crate::i18n::Lang::EN); + assert!(found.contains(&"showToolTips"), "{label}: {found:?}"); +} + +/// An empty search field shows the category list; a search that finds nothing says so. +#[test] +fn an_empty_search_lists_the_categories_and_no_match_says_so() { + let mut a = app(); + open(&mut a, Some("General")); + let text = crate::tests_labels::painted_text(&mut a, |a, ui| show(a, ui.ctx())); + assert!(PREF_CATEGORIES.iter().all(|c| text.contains(c)), "{text}"); + assert!(!text.contains("No settings found"), "{text}"); + a.ui.dialog.as_mut().unwrap().fields.insert("__search".into(), json!("no such setting at all")); + let text = crate::tests_labels::painted_text(&mut a, |a, ui| show(a, ui.ctx())); + assert!(text.contains("No settings found"), "{text}"); + assert!(!text.contains("Plug-ins & Scratch Disks"), "the list is hidden: {text}"); + a.ui.dialog.as_mut().unwrap().fields.insert("__search".into(), json!(" ")); + let text = crate::tests_labels::painted_text(&mut a, |a, ui| show(a, ui.ctx())); + assert!(PREF_CATEGORIES.iter().all(|c| text.contains(c)), "{text}"); +} + +/// Choosing a setting the search found shows its category, clears the search and highlights the +/// setting's row. +#[test] +fn choosing_a_found_setting_goes_to_its_category() { + let mut a = app(); + open(&mut a, Some("General")); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + a.ui.dialog.as_mut().unwrap().fields.insert("__search".into(), json!("gridline")); + search_frame(&mut a, &ctx, vec![]); + let out = search_frame(&mut a, &ctx, vec![]); + let mut at = None; + fn find(s: &egui::Shape, label: &str, at: &mut Option) { + match s { + egui::Shape::Text(t) if t.galley.text() == label => *at = Some(t.pos + t.galley.rect.center().to_vec2()), + egui::Shape::Vec(v) => v.iter().for_each(|s| find(s, label, at)), + _ => {} + } + } + out.shapes.iter().for_each(|c| find(&c.shape, "Gridline every", &mut at)); + let at = at.expect("the found setting is listed"); + let button = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }; + search_frame(&mut a, &ctx, vec![egui::Event::PointerMoved(at), button(true)]); + search_frame(&mut a, &ctx, vec![button(false)]); + let d = a.ui.dialog.as_ref().unwrap(); + assert_eq!((d.str("__category").as_str(), d.str("__found").as_str(), d.str("__search").as_str()), ("Guides & Grid", "gridlineEvery", "")); + search_frame(&mut a, &ctx, vec![]); + let out = search_frame(&mut a, &ctx, vec![]); + fn rects(s: &egui::Shape, out: &mut Vec<(egui::Rect, egui::Color32)>) { + match s { + egui::Shape::Rect(r) => out.push((r.rect, r.fill)), + egui::Shape::Vec(v) => v.iter().for_each(|s| rects(s, out)), + _ => {} + } + } + let mut shown = vec![]; + out.shapes.iter().for_each(|c| rects(&c.shape, &mut shown)); + let mut label = None; + out.shapes.iter().for_each(|c| find(&c.shape, "Gridline every:", &mut label)); + let label = label.expect("the setting's row is shown"); + // A tinted band across the fields column, behind the setting's label. + let band = shown.iter().any(|(r, fill)| r.width() >= 500.0 && r.height() < 40.0 && fill.a() > 0 && r.contains(label)); + assert!(band, "the setting's row is highlighted: {shown:?}"); + assert!(!a.ui.dialog.as_ref().unwrap().bool("__scrollTo"), "the list scrolled to it"); + // Another category: the highlight goes. + a.ui.dialog.as_mut().unwrap().fields.insert("__category".into(), json!("Units")); + search_frame(&mut a, &ctx, vec![]); + assert_eq!(a.ui.dialog.as_ref().unwrap().str("__found"), ""); +} + +/// The search field has the keyboard focus when the dialog opens, and Cmd/Ctrl+F gives it back. +#[test] +fn the_search_field_takes_the_focus_on_open_and_on_cmd_f() { + let mut a = app(); + open(&mut a, None); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let focused = |ctx: &egui::Context| ctx.memory(|m| m.has_focus(search_edit_id())); + search_frame(&mut a, &ctx, vec![]); + search_frame(&mut a, &ctx, vec![]); + assert!(focused(&ctx), "focused on open"); + ctx.memory_mut(|m| m.surrender_focus(search_edit_id())); + search_frame(&mut a, &ctx, vec![]); + assert!(!focused(&ctx)); + search_frame(&mut a, &ctx, key_press(egui::Key::F, egui::Modifiers::COMMAND)); + assert!(focused(&ctx), "focused on Cmd/Ctrl+F"); + assert!(a.ui.dialog.is_some()); +} + +/// Escape in the search field with text clears it and keeps the dialog open with the field +/// focused; Escape in the empty field closes the dialog. +#[test] +fn escape_clears_the_search_then_closes_the_dialog() { + let mut a = app(); + open(&mut a, None); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + search_frame(&mut a, &ctx, vec![]); + search_frame(&mut a, &ctx, vec![]); + search_frame(&mut a, &ctx, vec![egui::Event::Text("grid".into())]); + assert_eq!(search_text(&a), "grid"); + search_frame(&mut a, &ctx, key_press(egui::Key::Escape, egui::Modifiers::NONE)); + assert!(a.ui.dialog.is_some(), "the first Escape leaves the dialog open"); + assert_eq!(search_text(&a), ""); + search_frame(&mut a, &ctx, vec![]); + assert!(ctx.memory(|m| m.has_focus(search_edit_id())), "the field keeps the focus"); + search_frame(&mut a, &ctx, key_press(egui::Key::Escape, egui::Modifiers::NONE)); + assert!(a.ui.dialog.is_none(), "Escape in the empty field closes the dialog"); +} + /// Units › Numbers Without Units Are Points (#394): the preference reaches the fields when it /// changes, and its checkbox is dimmed unless a unit is Picas. #[test] diff --git a/crates/ui-egui/src/shortcuts.rs b/crates/ui-egui/src/shortcuts.rs index f35944a6e..406141b41 100644 --- a/crates/ui-egui/src/shortcuts.rs +++ b/crates/ui-egui/src/shortcuts.rs @@ -211,7 +211,8 @@ pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context) { // Escape closes a dropdown list or popover open in the dialog (each closes itself on it) and // leaves the dialog open. let popup = app.ui.dialog.as_ref().is_some_and(|d| egui::Popup::is_any_open(ctx) || crate::dialogs::brush_options::tips_open(d)); - if !popup && pressed_anew(ctx, Key::Escape) { + // Escape in Preferences' search field with text clears the field first. + if !popup && pressed_anew(ctx, Key::Escape) && !crate::prefs_dialog::clear_search_on_escape(app, ctx) { crate::dialogs::cancel(app); app.ui.palette_open = false; } diff --git a/docs/mcp.md b/docs/mcp.md index 0ecab3111..7b46e8acf 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -3872,6 +3872,12 @@ first; no spaces), stored back as the object name. `typeSizeIncrement`, `baselineShiftIncrement`) with `measure: "general"|"type"`; they are kept in points and also take a string with a unit. Unit names: `Points`, `Picas`, `Inches`, `Millimeters`, `Centimeters`, `Pixels`, `Feet & Inches`, `Meters`, `Yards`, `Feet` (the preferences use `points`, `picas`, … `feetInches`, `meters`, `yards`, `feet`). +`edit.preferences {category?}` opens the Preferences dialog (`preferences`) with its search field focused. Its +`__search` field (`ui.dialog.set`) holds the search text: while it has text, the category list is replaced by the +settings whose label, section or category contains it, ignoring case, in the interface language or in English, or by +"No settings found". Choosing a setting sets `__category` to its category, sets `__found` to its key (the row is +highlighted until another category is shown) and clears `__search`. Cmd/Ctrl+F focuses the field again; Escape in the +field with text clears it, and Escape in the empty field closes the dialog. ```json {"name":"run_command","arguments":{"command":"prefs.set","params":{"key":"unitsGeneral","value":"millimeters"}}} diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 963f978db..6a75a918b 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -599,6 +599,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Dragging a document tab out and a document window back (G7):** a document tab dragged more than 24 pt above or below the tab strip floats its document in a window under the pointer, and a document window dragged onto the tab strip goes back under the tabs ([public source](https://helpx.adobe.com/illustrator/using/customizing-workspace.html)). A window inside the app's window is dragged by its title bar; an OS window by the document's tab at its top, and the window follows the pointer. The 24 pt distance and the tab at the top of an OS window are VectorCraft's own. Tests: `crates/ui-egui/src/doc_windows/tests.rs`. - **Reordering document tabs (G7, #5663):** a document tab dragged along the tab strip moves its document among the others ([public source](https://helpx.adobe.com/illustrator/using/customizing-workspace.html)); a line marks the gap it goes in. The current document stays current, each document keeps its own zoom, centre and artboard, the views Window › New Window opened move with their document, and Window › Next/Previous Document follow the new order. `document.move` (`{from?, to}`) does the same from MCP, the control channel and recordings. `crates/ui-egui/src/tests/tab_reorder.rs`, `crates/engine/src/tests/document_move.rs`. - **Preferences › User Interface › Open Documents As Tabs (G7, #394):** checked by default, documents open under tabs in one document window ([public source](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/workspace-overview.html)). With the box cleared, each document opened or created afterwards opens in a floating window of its own, at the next cascade place, as Float in Window floats it; documents already open stay where they are, and checking the box again leaves floating documents in their windows. Tests: `crates/ui-egui/src/doc_windows/tests.rs`. +- **Preferences search:** a Search Preferences field above the category list has the keyboard focus when the dialog opens, and Cmd/Ctrl+F gives it back; typing lists the settings it finds, and choosing one shows its category, as the [help page](https://helpx.adobe.com/illustrator/desktop/get-started/preferences-and-settings/set-app-preferences.html) describes. VectorCraft's own rules: the field matches a setting's label, section or category, ignoring case, in the interface language or in English; the list replaces the categories while the field has text and shows every match, or "No settings found"; the chosen setting's row is highlighted and scrolled into view; Escape in the field with text clears it, and Escape in the empty field closes the dialog. The dialog's `__search` field (`ui.dialog.set`) holds the text. Tests: `crates/ui-egui/src/prefs_dialog/tests.rs`. - **Window › New Window (G7, #4116):** New Window (`window.newWindow`) opens another view of the current document ([public source](https://helpx.adobe.com/illustrator/using/viewing-artwork.html) describes showing one document in several windows at once), with its own zoom, center, rotation, artboard and Outline or Preview, as a tab of its own; an edit in one view shows in every view. Tile, Cascade, Float in Window, Float All in Windows and Consolidate All Windows arrange each view as they arrange a document, and Next and Previous Document step through the views. Closing a view leaves the document open; closing its last view closes the document, asking to save it first. The view's number after the name in its tab ("Name:2", while the document has more than one view) is VectorCraft's own. Tests: `crates/ui-egui/src/doc_windows/tests.rs`. - **Document Setup modal handoff:** Document Setup → Edit Artboards applies valid settings, closes the dialog and activates the Artboard tool. Pointer-click tests cover successful handoff, invalid settings, OK, Cancel and Undo in `dialogs/document_setup.rs`.