Merge pull request #6879 from dotCooCoo/fix/palette-opens-menu-dialog

Palette: a pick opens the dialog its menu item opens
This commit is contained in:
BFlat
2026-11-12 23:10:36 +07:00
committed by GitHub
33 changed files with 328 additions and 27 deletions
+1
View File
@@ -4249,6 +4249,7 @@ axis Both И двете
@msg colorTable.{what}: `{h}` is not a colour such as "#ff8800" colorTable.{what}: `{h}` не е цвят като "#ff8800"
@msg colorTable.{what}: at most {MAX_TABLE} colours colorTable.{what}: най-много {MAX_TABLE} цвята
@msg command `{0}` is not available right now: {1} команда `{0}` не е налична в момента: {1}
@msg command `{id}` is not available right now команда `{id}` не е налична в момента
@msg compound paths can only contain paths съставните пътища могат да съдържат само пътища
@msg compressed (RLE) bitmaps can't be read yet компресираните (RLE) битови карти все още не могат да бъдат четени
@msg damaged повреден
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4639,6 +4639,7 @@ imageTrace Outline Obris
@msg colorTable.{what}: `{h}` is not a colour such as "#ff8800" colorTable.{what}: `{h}` nije boja poput "#ff8800"
@msg colorTable.{what}: at most {MAX_TABLE} colours colorTable.{what}: najviše {MAX_TABLE} boja
@msg command `{0}` is not available right now: {1} naredba `{0}` trenutno nije dostupna: {1}
@msg command `{id}` is not available right now naredba `{id}` trenutno nije dostupna
@msg compound paths can only contain paths složene putanje mogu sadržavati samo putanje
@msg compressed (RLE) bitmaps can't be read yet komprimirane (RLE) bitmape se još ne mogu čitati
@msg damaged oštećeno
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -5314,6 +5314,7 @@ digits Malayalam Malaiàlam
@msg column must be true or a number from 1 column ha de ser true o un nombre a partir d’1
@msg Combine Slices: the slices have no bounds Combina els talls: els talls no tenen límits
@msg command `{0}` is not available right now: {1} l’ordre `{0}` no està disponible ara mateix: {1}
@msg command `{id}` is not available right now l’ordre `{id}` no està disponible ara mateix
@msg compound paths can only contain paths els traçats compostos només poden contenir traçats
@msg compressed (RLE) bitmaps can't be read yet encara no es poden llegir els mapes de bits comprimits (RLE)
@msg control must be fillStroke, coloring, drawMode or screenMode control ha de ser fillStroke, coloring, drawMode o screenMode
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4720,6 +4720,7 @@ imageTrace Outline Obrys
@msg colorTable.{what}: `{h}` is not a colour such as "#ff8800" colorTable.{what}: `{h}` není barva jako "#ff8800"
@msg colorTable.{what}: at most {MAX_TABLE} colours colorTable.{what}: nejvýše {MAX_TABLE} barev
@msg command `{0}` is not available right now: {1} příkaz `{0}` nyní není k dispozici: {1}
@msg command `{id}` is not available right now příkaz `{id}` nyní není k dispozici
@msg compound paths can only contain paths složené cesty mohou obsahovat jen cesty
@msg compressed (RLE) bitmaps can't be read yet komprimované bitmapy (RLE) zatím nelze načíst
@msg damaged poškozeno
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4361,6 +4361,7 @@ imageTrace Outline Kontur
@msg colorTable.{what}: `{h}` is not a colour such as "#ff8800" colorTable.{what}: `{h}` er ikke en farve som "#ff8800"
@msg colorTable.{what}: at most {MAX_TABLE} colours colorTable.{what}: højst {MAX_TABLE} farver
@msg command `{0}` is not available right now: {1} kommandoen `{0}` er ikke tilgængelig lige nu: {1}
@msg command `{id}` is not available right now kommandoen `{id}` er ikke tilgængelig lige nu
@msg compound paths can only contain paths sammensatte kurver kan kun indeholde kurver
@msg compressed (RLE) bitmaps can't be read yet komprimerede (RLE) bitmaps kan endnu ikke læses
@msg damaged beskadiget
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4335,6 +4335,7 @@ theme Appearance Erscheinungsbild
@msg colorTable.{what}: `{h}` is not a colour such as "#ff8800" colorTable.{what}: `{h}` ist keine Farbe wie "#ff8800"
@msg colorTable.{what}: at most {MAX_TABLE} colours colorTable.{what}: höchstens {MAX_TABLE} Farben
@msg command `{0}` is not available right now: {1} der Befehl `{0}` ist derzeit nicht verfügbar: {1}
@msg command `{id}` is not available right now der Befehl `{id}` ist derzeit nicht verfügbar
@msg compound paths can only contain paths zusammengesetzte Pfade können nur Pfade enthalten
@msg compressed (RLE) bitmaps can't be read yet komprimierte Bitmaps (RLE) können noch nicht gelesen werden
@msg damaged beschädigt
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4364,6 +4364,7 @@ digits Malayalam Μαλαγιάλαμ
@msg colorTable.{what}: `{h}` is not a colour such as "#ff8800" colorTable.{what}: το `{h}` δεν είναι έγκυρο χρώμα, όπως το "#ff8800"
@msg colorTable.{what}: at most {MAX_TABLE} colours colorTable.{what}: επιτρέπονται το πολύ {MAX_TABLE} χρώματα
@msg command `{0}` is not available right now: {1} η εντολή `{0}` δεν είναι διαθέσιμη αυτήν τη στιγμή: {1}
@msg command `{id}` is not available right now η εντολή `{id}` δεν είναι διαθέσιμη αυτήν τη στιγμή
@msg compound paths can only contain paths οι σύνθετες διαδρομές μπορούν να περιέχουν μόνο διαδρομές
@msg compressed (RLE) bitmaps can't be read yet τα συμπιεσμένα (RLE) bitmap δεν μπορούν ακόμη να διαβαστούν
@msg damaged κατεστραμμένο
Can't render this file because it is too large.
+1
View File
@@ -4411,6 +4411,7 @@ axis Both Ambos
@msg colorTable.{what}: `{h}` is not a colour such as "#ff8800" colorTable.{what}: `{h}` no es un color como "#ff8800"
@msg colorTable.{what}: at most {MAX_TABLE} colours colorTable.{what}: como máximo {MAX_TABLE} colores
@msg command `{0}` is not available right now: {1} el comando `{0}` no está disponible ahora: {1}
@msg command `{id}` is not available right now el comando `{id}` no está disponible ahora
@msg compound paths can only contain paths los trazados compuestos solo pueden contener trazados
@msg compressed (RLE) bitmaps can't be read yet todavía no se pueden leer mapas de bits comprimidos (RLE)
@msg damaged dañado
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4626,6 +4626,7 @@ imageTrace Outline Ääriviiva
@msg colorTable.{what}: `{h}` is not a colour such as "#ff8800" colorTable.{what}: `{h}` ei ole väri, kuten "#ff8800"
@msg colorTable.{what}: at most {MAX_TABLE} colours colorTable.{what}: enintään {MAX_TABLE} väriä
@msg command `{0}` is not available right now: {1} komento `{0}` ei ole nyt käytettävissä: {1}
@msg command `{id}` is not available right now komento `{id}` ei ole nyt käytettävissä
@msg compound paths can only contain paths yhdistelmäpolut voivat sisältää vain polkuja
@msg compressed (RLE) bitmaps can't be read yet pakattuja (RLE) bittikarttoja ei voi vielä lukea
@msg damaged vaurioitunut
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4400,6 +4400,7 @@ axis Both Les deux
@msg colorTable.{what}: `{h}` is not a colour such as "#ff8800" colorTable.{what} : `{h}` n’est pas une couleur comme "#ff8800"
@msg colorTable.{what}: at most {MAX_TABLE} colours colorTable.{what} : {MAX_TABLE} couleurs au maximum
@msg command `{0}` is not available right now: {1} la commande `{0}` n’est pas disponible pour le moment : {1}
@msg command `{id}` is not available right now la commande `{id}` n’est pas disponible pour le moment
@msg compound paths can only contain paths les tracés transparents ne peuvent contenir que des tracés
@msg compressed (RLE) bitmaps can't be read yet les images bitmap compressées (RLE) ne peuvent pas encore être lues
@msg damaged endommagé
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -6311,6 +6311,7 @@ antiAlias Sharp शार्प
@msg columns {first} and {column} both name the variable `{name}` कॉलम {first} और {column} दोनों नाम चर `{name}`
@msg Combine Slices: the slices have no bounds स्लाइस को मिलाएं: स्लाइस में कोई सीमा नहीं है
@msg command `{0}` is not available right now: {1} आदेश `{0}` अभी उपलब्ध नहीं है: {1}
@msg command `{id}` is not available right now आदेश `{id}` अभी उपलब्ध नहीं है
@msg command must be a shape.* command command: आदेश एक आकार होना चाहिए।* आदेश
@msg commands[{i}]: missing `command` आदेश [{i}]: लापता `command`
@msg composer must be "singleLine" or "everyLine" composer: संगीतकार "singleLine" या "everyLine" होना चाहिए।
Can't render this file because it is too large.
+1
View File
@@ -4962,6 +4962,7 @@ imageTrace Outline Körvonal
@msg column must be true or a number from 1 a column értéke true vagy 1-től kezdődő szám lehet
@msg Combine Slices: the slices have no bounds Szeletek egyesítése: a szeleteknek nincsenek határai
@msg command `{0}` is not available right now: {1} a parancs jelenleg nem érhető el: `{0}`: {1}
@msg command `{id}` is not available right now a parancs jelenleg nem érhető el: `{id}`
@msg compound paths can only contain paths összetett útvonal csak útvonalakat tartalmazhat
@msg compressed (RLE) bitmaps can't be read yet a tömörített (RLE) bitképek még nem olvashatók
@msg Convert to Artboards: selection has no area Átalakítás rajztáblákká: a kijelölésnek nincs területe
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4361,6 +4361,7 @@ imageTrace Outline Kontur
@msg colorTable.{what}: `{h}` is not a colour such as "#ff8800" colorTable.{what}: `{h}` bukan warna seperti "#ff8800"
@msg colorTable.{what}: at most {MAX_TABLE} colours colorTable.{what}: maksimal {MAX_TABLE} warna
@msg command `{0}` is not available right now: {1} perintah `{0}` tidak tersedia saat ini: {1}
@msg command `{id}` is not available right now perintah `{id}` tidak tersedia saat ini
@msg compound paths can only contain paths jalur majemuk hanya dapat berisi jalur
@msg compressed (RLE) bitmaps can't be read yet bitmap terkompresi (RLE) belum dapat dibaca
@msg damaged rusak
Can't render this file because it contains an unexpected character in line 2390 and column 5.
+1
View File
@@ -4462,6 +4462,7 @@ axis Both Entrambi
@msg colorTable.{what}: `{h}` is not a colour such as "#ff8800" colorTable.{what}: `{h}` non è un colore come "#ff8800"
@msg colorTable.{what}: at most {MAX_TABLE} colours colorTable.{what}: al massimo {MAX_TABLE} colori
@msg command `{0}` is not available right now: {1} il comando `{0}` non è disponibile in questo momento: {1}
@msg command `{id}` is not available right now il comando `{id}` non è disponibile in questo momento
@msg compound paths can only contain paths i tracciati composti possono contenere solo tracciati
@msg compressed (RLE) bitmaps can't be read yet le bitmap compresse (RLE) non possono ancora essere lette
@msg damaged danneggiato
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4594,6 +4594,7 @@ swatch Bright Violet ビビッドバイオレット
@msg nothing to distort 変形するものがありません
@msg unknown option `{path}.{k}` `{path}.{k}` は不明なオプションです
@msg command `{0}` is not available right now: {1} コマンド `{0}` は現在使用できません: {1}
@msg command `{id}` is not available right now コマンド `{id}` は現在使用できません
@msg internal error in `{cmd}`: {msg} (the document was left as it was before; please report this bug) `{cmd}` で内部エラーが発生しました: {msg} (ドキュメントは変更されていません。このバグを報告してください)
@msg invalid parameters for `{cmd}`: {msg} `{cmd}` のパラメーターが正しくありません: {msg}
@msg no active document アクティブなドキュメントがありません
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4180,6 +4180,7 @@ font-technology Variable ცვლადი
@msg colorTable.{what}: `{h}` is not a colour such as "#ff8800" colorTable.{what}: `{h}` ფერი არ არის, მაგალითად "#ff8800"
@msg colorTable.{what}: at most {MAX_TABLE} colours colorTable.{what}: არაუმეტეს {MAX_TABLE} ფერი
@msg command `{0}` is not available right now: {1} ბრძანება `{0}` ახლა ხელმისაწვდომი არ არის: {1}
@msg command `{id}` is not available right now ბრძანება `{id}` ახლა ხელმისაწვდომი არ არის
@msg compound paths can only contain paths შედგენილი ტრაექტორია მხოლოდ ტრაექტორიებს შეიძლება შეიცავდეს
@msg compressed (RLE) bitmaps can't be read yet შეკუმშული (RLE) ბიტმაპების წაკითხვა ჯერ არ ხერხდება
@msg damaged დაზიანებულია
Can't render this file because it is too large.
+1
View File
@@ -4521,6 +4521,7 @@ theme Appearance 외관
@msg nothing to distort 왜곡할 항목이 없습니다
@msg unknown option `{path}.{k}` 알 수 없는 옵션 `{path}.{k}`
@msg command `{0}` is not available right now: {1} 명령 `{0}`은(는) 지금 사용할 수 없습니다: {1}
@msg command `{id}` is not available right now 명령 `{id}`은(는) 지금 사용할 수 없습니다
@msg internal error in `{cmd}`: {msg} (the document was left as it was before; please report this bug) `{cmd}`에서 내부 오류가 발생했습니다: {msg}(문서는 이전 상태로 유지되었습니다. 이 버그를 신고해 주세요)
@msg invalid parameters for `{cmd}`: {msg} `{cmd}`의 매개변수가 올바르지 않습니다: {msg}
@msg no active document 활성 문서가 없습니다
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4375,6 +4375,7 @@ imageTrace Outline Contour
@msg colorTable.{what}: `{h}` is not a colour such as "#ff8800" colorTable.{what}: `{h}` is geen kleur zoals "#ff8800"
@msg colorTable.{what}: at most {MAX_TABLE} colours colorTable.{what}: maximaal {MAX_TABLE} kleuren
@msg command `{0}` is not available right now: {1} opdracht `{0}` is nu niet beschikbaar: {1}
@msg command `{id}` is not available right now opdracht `{id}` is nu niet beschikbaar
@msg compound paths can only contain paths samengestelde paden kunnen alleen paden bevatten
@msg compressed (RLE) bitmaps can't be read yet gecomprimeerde (RLE) bitmaps kunnen nog niet gelezen worden
@msg damaged beschadigd
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4543,6 +4543,7 @@ imageTrace Outline Kontur
@msg colorTable.{what}: `{h}` is not a colour such as "#ff8800" colorTable.{what}: `{h}` nie jest kolorem w rodzaju "#ff8800"
@msg colorTable.{what}: at most {MAX_TABLE} colours colorTable.{what}: maksymalnie {MAX_TABLE} kolorów
@msg command `{0}` is not available right now: {1} polecenie `{0}` jest teraz niedostępne: {1}
@msg command `{id}` is not available right now polecenie `{id}` jest teraz niedostępne
@msg compound paths can only contain paths ścieżki złożone mogą zawierać tylko ścieżki
@msg compressed (RLE) bitmaps can't be read yet skompresowanych bitmap (RLE) nie można jeszcze odczytać
@msg damaged uszkodzony
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3949,6 +3949,7 @@ axis Both Ambos
@msg colorTable.{what}: `{h}` is not a colour such as "#ff8800" colorTable.{what}: `{h}` não é uma cor como "#ff8800"
@msg colorTable.{what}: at most {MAX_TABLE} colours colorTable.{what}: no máximo {MAX_TABLE} cores
@msg command `{0}` is not available right now: {1} o comando `{0}` não está disponível agora: {1}
@msg command `{id}` is not available right now o comando `{id}` não está disponível agora
@msg compound paths can only contain paths traçados compostos só podem conter traçados
@msg compressed (RLE) bitmaps can't be read yet bitmaps compactados (RLE) ainda não podem ser lidos
@msg damaged danificado
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -4448,6 +4448,7 @@ axis Both Оба
@msg colorTable.{what}: `{h}` is not a colour such as "#ff8800" colorTable.{what}: `{h}` не является цветом, например "#ff8800"
@msg colorTable.{what}: at most {MAX_TABLE} colours colorTable.{what}: не более {MAX_TABLE} цветов
@msg command `{0}` is not available right now: {1} команда `{0}` сейчас недоступна: {1}
@msg command `{id}` is not available right now команда `{id}` сейчас недоступна
@msg compound paths can only contain paths составные контуры могут содержать только контуры
@msg compressed (RLE) bitmaps can't be read yet сжатые (RLE) растровые изображения пока не читаются
@msg damaged повреждено
Can't render this file because it is too large.
+1
View File
@@ -4312,6 +4312,7 @@ axis Both Båda
@msg colorTable.{what}: `{h}` is not a colour such as "#ff8800" colorTable.{what}: `{h}` är inte en färg som "#ff8800"
@msg colorTable.{what}: at most {MAX_TABLE} colours colorTable.{what}: högst {MAX_TABLE} färger
@msg command `{0}` is not available right now: {1} kommandot `{0}` är inte tillgängligt just nu: {1}
@msg command `{id}` is not available right now kommandot `{id}` är inte tillgängligt just nu
@msg compound paths can only contain paths sammansatta banor kan bara innehålla banor
@msg compressed (RLE) bitmaps can't be read yet komprimerade (RLE) bitmappar kan inte läsas än
@msg damaged skadad
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -4176,6 +4176,7 @@ axis Both Her İkisi
@msg colorTable.{what}: `{h}` is not a colour such as "#ff8800" colorTable.{what}: `{h}`, "#ff8800" gibi bir renk değil
@msg colorTable.{what}: at most {MAX_TABLE} colours colorTable.{what}: en fazla {MAX_TABLE} renginde
@msg command `{0}` is not available right now: {1} `{0}` komutu şu anda kullanılamıyor: {1}
@msg command `{id}` is not available right now `{id}` komutu şu anda kullanılamıyor
@msg compound paths can only contain paths bileşik yollar yalnızca yollar içerebilir
@msg compressed (RLE) bitmaps can't be read yet sıkıştırılmış (RLE) bitmapler henüz okunamıyor
@msg damaged hasarlı
Can't render this file because it contains an unexpected character in line 5 and column 25.
+1
View File
@@ -4446,6 +4446,7 @@ axis Both Обидві
@msg colorTable.{what}: `{h}` is not a colour such as "#ff8800" colorTable.{what}: `{h}` не є кольором, наприклад "#ff8800"
@msg colorTable.{what}: at most {MAX_TABLE} colours colorTable.{what}: не більше {MAX_TABLE} кольорів
@msg command `{0}` is not available right now: {1} команда `{0}` зараз недоступна: {1}
@msg command `{id}` is not available right now команда `{id}` зараз недоступна
@msg compound paths can only contain paths складені контури можуть містити лише контури
@msg compressed (RLE) bitmaps can't be read yet стислі (RLE) растрові зображення поки не читаються
@msg damaged пошкоджено
Can't render this file because it contains an unexpected character in line 182 and column 5.
+1
View File
@@ -4619,6 +4619,7 @@ theme Appearance Giao diện
@msg column must be true or a number from 1 column phải là true hoặc số từ 1
@msg Combine Slices: the slices have no bounds Kết hợp lát cắt: các lát cắt không có giới hạn
@msg command `{0}` is not available right now: {1} lệnh `{0}` hiện không khả dụng: {1}
@msg command `{id}` is not available right now lệnh `{id}` hiện không khả dụng
@msg compound paths can only contain paths đường dẫn phức chỉ có thể chứa đường dẫn
@msg compressed (RLE) bitmaps can't be read yet bitmap nén (RLE) chưa thể đọc được
@msg Convert to Artboards: selection has no area Chuyển thành bảng vẽ: vùng chọn không có diện tích
Can't render this file because it contains an unexpected character in line 204 and column 5.
+1
View File
@@ -4298,6 +4298,7 @@ imageTrace Outline 轮廓
@msg nothing to distort 没有可扭曲的内容
@msg unknown option `{path}.{k}` 未知选项 `{path}.{k}`
@msg command `{0}` is not available right now: {1} 命令 `{0}` 当前不可用:{1}
@msg command `{id}` is not available right now 命令 `{id}` 当前不可用
@msg internal error in `{cmd}`: {msg} (the document was left as it was before; please report this bug) `{cmd}` 中出现内部错误:{msg} (文档保持原样;请报告此错误)
@msg invalid parameters for `{cmd}`: {msg} `{cmd}` 的参数无效:{msg}
@msg no active document 没有活动文档
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -2080,6 +2080,7 @@ file-filter Templates 範本
@msg nothing to distort 沒有可扭曲的內容
@msg unknown option `{path}.{k}` 未知的選項 `{path}.{k}`
@msg command `{0}` is not available right now: {1} 指令 `{0}` 目前無法使用:{1}
@msg command `{id}` is not available right now 指令 `{id}` 目前無法使用
@msg internal error in `{cmd}`: {msg} (the document was left as it was before; please report this bug) `{cmd}` 發生內部錯誤:{msg} (文件維持原狀;請回報此錯誤)
@msg invalid parameters for `{cmd}`: {msg} `{cmd}` 的參數無效:{msg}
@msg no active document 沒有作用中的文件
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1 -3
View File
@@ -71,10 +71,8 @@ pub(crate) use invoke::graph_dialog;
#[cfg(test)]
pub(crate) use invoke::with_held_keys;
pub use invoke::{click_target, invoke, invoke_from_system_menu};
#[cfg(test)]
pub(crate) use labels::lists_plugin;
#[cfg(test)]
use labels::shows_a_name;
pub(crate) use labels::shows_a_name;
pub use labels::{CONTEXT_LABELS, DYNAMIC_LABELS, display_label, dynamic_label, item_shortcut, menu_strings};
pub use run::{check_params, run_ui_command};
pub(crate) use run::{normalize_panel, opt_bool, opt_index, opt_number};
+1 -1
View File
@@ -125,7 +125,7 @@ fn command_label(app: &VectorcraftApp, id: &str, label: &str) -> Option<String>
/// label: a recent file, a saved view, a font, a user library or preset, a custom workspace, a
/// plug-in, an SVG filter, a script of the Scripts folder? Such names are shown as they are, never
/// translated (a workspace the user calls "Layers" stays "Layers").
pub(super) fn shows_a_name(id: &str, label: &str) -> bool {
pub(crate) fn shows_a_name(id: &str, label: &str) -> bool {
const SLOTS: [&str; 8] = [
"file.openRecent",
"view.goto",
+158 -9
View File
@@ -91,7 +91,10 @@ fn build(lang: crate::i18n::Lang) -> Vec<Entry> {
}
/// Runs the palette result `id`: selects a tool, or invokes a command with the params its entry
/// stands for.
/// stands for. A command whose menu bar item gives params runs what a click on that item runs
/// ([`crate::shortcut_editor::menu_target_now`]), so Clean Up… and Find and Replace… open their
/// dialogs. While that item is dimmed, no dialog opens and the status line says why the command
/// can't run.
pub(crate) fn run_pick(app: &mut VectorcraftApp, id: &str) {
if let Some(tool) = id.strip_prefix("tool:") {
app.select_tool(tool);
@@ -99,7 +102,11 @@ pub(crate) fn run_pick(app: &mut VectorcraftApp, id: &str) {
let command = crate::keymap::command_of(id);
menus::invoke(app, &command.id, command.params);
} else {
menus::invoke(app, id, json!({}));
match crate::shortcut_editor::menu_target_now(app, id) {
Some(Ok((id, p))) => menus::invoke(app, &id, p),
Some(Err(why)) => app.status(why),
None => menus::invoke(app, id, json!({})),
}
}
}
@@ -217,7 +224,7 @@ mod tests {
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default());
let before = app.session.prefs.use_precise_cursors;
let first = matches.first().expect("query has a result");
menus::invoke(&mut app, &first.id, json!({}));
run_pick(&mut app, &first.id);
assert_ne!(app.session.prefs.use_precise_cursors, before, "first result toggles the preference");
}
@@ -238,8 +245,7 @@ mod tests {
app.run("select.set", json!({"ids": [a, b]})).unwrap();
app.run("select.key", json!({"id": b})).unwrap();
assert!(app.session.active().unwrap().selection.key.is_some());
// Run a pick the way `show` runs it.
menus::invoke(&mut app, ids[0], json!({}));
run_pick(&mut app, ids[0]);
let st = app.session.active().unwrap();
assert_eq!((st.selection.len(), st.selection.key), (2, None), "both objects stay selected, without a key");
}
@@ -255,20 +261,163 @@ mod tests {
app.run("file.new", json!({"width": 200, "height": 200})).unwrap();
app.ui.status.clear();
assert!(!crate::panel_docking::showing(&app.ui, crate::panels::asset_export::ID), "{id}");
// Run a pick the way `show` runs it.
menus::invoke(&mut app, id, json!({}));
run_pick(&mut app, id);
assert!(crate::panel_docking::showing(&app.ui, crate::panels::asset_export::ID), "{id}: {}", app.ui.status);
assert!(!app.ui.status.contains("invalid parameters"), "{id}: {}", app.ui.status);
// A second pick leaves the open panel open, docked or floating.
menus::invoke(&mut app, id, json!({}));
run_pick(&mut app, id);
assert!(crate::panel_docking::showing(&app.ui, crate::panels::asset_export::ID), "{id}: picked twice");
app.run("window.panel.float", json!({"panel": crate::panels::asset_export::ID})).unwrap();
assert!(crate::panel_docking::showing(&app.ui, crate::panels::asset_export::ID), "{id}: floating");
menus::invoke(&mut app, id, json!({}));
run_pick(&mut app, id);
assert!(crate::panel_docking::showing(&app.ui, crate::panels::asset_export::ID), "{id}: floating, picked again");
}
}
/// The open dialog's kind and fields, the active document's undo depth and the status line.
type Outcome = (Option<(String, serde_json::Map<String, serde_json::Value>)>, Option<usize>, String);
/// The [`Outcome`] `app` shows.
fn outcome(app: &VectorcraftApp) -> Outcome {
(
app.ui.dialog.as_ref().map(|d| (d.kind.clone(), d.fields.clone())),
app.session.active().map(|st| st.history.undo.len()),
app.ui.status.clone(),
)
}
/// A pick of a "…" command whose menu bar item gives params opens that item's dialog, and Smooth
/// opens its slider; nothing changes until the dialog is confirmed.
#[test]
fn a_pick_opens_the_dialog_its_menu_item_opens() {
let expected = [
("object.path.cleanUp", "command"),
("object.mesh.create", "command"),
("type.smartPunctuation", "command"),
("edit.findReplace", "command"),
("object.path.smooth", crate::dialogs::smooth_slider::KIND),
];
for (id, kind) in expected {
assert!(items().iter().any(|(_, item, _)| item == id), "{id} is in the palette");
let mut app = VectorcraftApp::new(vectorcraft_testkit::fixtures::Fixture::Single.session(), crate::Services::default());
app.ui.status.clear();
let before = app.session.active().unwrap().history.undo.len();
run_pick(&mut app, id);
let d = app.ui.dialog.as_ref().unwrap_or_else(|| panic!("{id} opens its dialog: {}", app.ui.status));
assert_eq!(d.kind, kind, "{id}");
if kind == "command" {
assert_eq!(d.fields["__command"], json!(id), "{id}: {:?}", d.fields);
}
assert_eq!(app.session.active().unwrap().history.undo.len(), before, "{id} changes nothing yet");
assert!(app.ui.status.is_empty(), "{id}: {}", app.ui.status);
}
}
/// Every palette command whose menu bar items all give it the same params does what a click on
/// such an item does, as the menu row shows it ([`menus::entry`]): with no document open, in an
/// empty document and with one or several objects selected, the pick opens the same dialog with
/// the same fields and leaves the same undo steps and status. While the row is dimmed, the pick
/// opens no dialog, changes nothing and says why the command can't run.
#[test]
fn every_pick_runs_what_its_menu_item_runs() {
use vectorcraft_testkit::fixtures::Fixture;
fn walk<'a>(items: &'a [menus::Item], out: &mut Vec<&'a menus::Item>) {
for it in items {
match it {
menus::Item::Sub(_, children) => walk(children, out),
// Items that list an installed plug-in or show a name that is user data come and
// go as tests beside this one install plug-ins or add toolbars, and no shortcut or
// pick reads them.
menus::Item::Cmd(label, id, p) if !menus::lists_plugin(id, p) && !menus::shows_a_name(id, label) => out.push(it),
_ => {}
}
}
}
let tree = menus::menu_tree();
let mut rows = vec![];
for (_, items) in &tree {
walk(items, &mut rows);
}
let mut checked: Vec<(String, &menus::Item)> = vec![];
for (_, id, _) in items() {
let of_id: Vec<&menus::Item> = rows.iter().copied().filter(|it| matches!(it, menus::Item::Cmd(_, c, _) if *c == id)).collect();
let params: Vec<&serde_json::Value> =
of_id.iter().filter_map(|it| if let menus::Item::Cmd(_, _, p) = it { Some(p) } else { None }).collect();
let Some(first) = params.first() else { continue };
if first.as_object().is_none_or(serde_json::Map::is_empty) || params.iter().any(|p| p != first) {
continue;
}
checked.push((id, of_id[0]));
}
assert!(checked.iter().any(|(id, _)| id == "edit.findReplace"), "{:?}", checked.iter().map(|c| &c.0).collect::<Vec<_>>());
let state = |n: usize| match n.checked_sub(1) {
None => vectorcraft_engine::Session::new(),
Some(k) => Fixture::ALL[k].session(),
};
let (mut compared, mut dimmed) = (0, 0);
for (id, item) in &checked {
for n in 0..=Fixture::ALL.len() {
let mut picked = VectorcraftApp::new(state(n), crate::Services::default());
let mut clicked = VectorcraftApp::new(state(n), crate::Services::default());
picked.ui.status.clear();
clicked.ui.status.clear();
// A row the menu leaves out in this state has no click to compare with.
let Some(menus::Entry::Item(row)) = menus::entry(&clicked, item) else { continue };
let (target, enabled) = (row.target().expect("the item is implemented"), row.enabled);
let before = outcome(&picked);
run_pick(&mut picked, id);
if enabled {
menus::invoke(&mut clicked, &target.0, target.1);
assert_eq!(outcome(&picked), outcome(&clicked), "{id} [state {n}]");
compared += 1;
} else {
let after = outcome(&picked);
assert_eq!((after.0, after.1), (None, before.1), "{id} [state {n}] is dimmed");
assert!(after.2.contains("not available"), "{id} [state {n}]: {}", after.2);
dimmed += 1;
}
}
}
assert!(compared > 0 && dimmed > 0, "{compared} compared, {dimmed} dimmed");
}
/// A pick of a "…" command whose menu bar item is dimmed (Remove Background and Object Mosaic
/// with no image selected, Find and Replace with no document open) opens no dialog, changes
/// nothing and reports why the command can't run.
#[test]
fn a_pick_of_a_dimmed_command_says_why_it_cannot_run() {
let cases = [
("addon.image.removeBackground", vectorcraft_testkit::fixtures::Fixture::Multi.session()),
("object.createObjectMosaic", vectorcraft_testkit::fixtures::Fixture::Multi.session()),
("edit.findReplace", vectorcraft_engine::Session::new()),
];
for (id, session) in cases {
assert!(crate::shortcut_editor::menu_target(id).is_some(), "{id} has a dialog");
let mut app = VectorcraftApp::new(session, crate::Services::default());
assert!(!menus::enabled(&app, id), "{id}");
app.ui.status.clear();
let before = app.session.active().map(|st| st.history.undo.len());
run_pick(&mut app, id);
assert!(app.ui.dialog.is_none(), "{id}");
assert_eq!(app.session.active().map(|st| st.history.undo.len()), before, "{id}");
assert!(app.ui.status.contains("not available"), "{id}: {}", app.ui.status);
}
}
/// A pick of a command that opens no dialog runs it at once.
#[test]
fn a_pick_of_a_command_without_a_dialog_runs_at_once() {
for id in ["object.group", "paint.swap"] {
assert!(items().iter().any(|(_, item, _)| item == id), "{id} is in the palette");
assert_eq!(crate::shortcut_editor::menu_target(id), None, "{id}");
let mut app = VectorcraftApp::new(vectorcraft_testkit::fixtures::Fixture::Multi.session(), crate::Services::default());
let before = app.session.active().unwrap().history.undo.len();
run_pick(&mut app, id);
assert!(app.ui.dialog.is_none(), "{id}");
assert_eq!(app.session.active().unwrap().history.undo.len(), before + 1, "{id}: {}", app.ui.status);
}
}
/// #1757: Up and Down move the highlighted result, wrapping at the ends; a new query starts
/// from the first again.
#[test]
+77 -9
View File
@@ -653,23 +653,31 @@ const NEEDS_A_PARAM: [&str; 20] = [
/// The (label, params) of each menu bar item, by command id.
fn menu_bar_items() -> &'static HashMap<&'static str, Vec<(&'static str, Value)>> {
static ITEMS: OnceLock<HashMap<&'static str, Vec<(&'static str, Value)>>> = OnceLock::new();
ITEMS.get_or_init(|| bar_items(&menus::menu_tree()))
}
/// The (label, params) of each item of the menus `tree`, by command id. Items that list an
/// installed plug-in ([`menus::lists_plugin`]) or show a name that is user data
/// ([`menus::shows_a_name`]: a custom toolbar, a script, a recent file, a saved selection) are left
/// out: [`menu_bar_items`] is built once, and such items come and go after that.
fn bar_items(tree: &[(&'static str, Vec<menus::Item>)]) -> HashMap<&'static str, Vec<(&'static str, Value)>> {
fn walk(items: &[menus::Item], out: &mut HashMap<&'static str, Vec<(&'static str, Value)>>) {
for it in items {
match it {
menus::Item::Cmd(label, id, p) => out.entry(*id).or_default().push((*label, p.clone())),
menus::Item::Cmd(label, id, p) if !menus::lists_plugin(id, p) && !menus::shows_a_name(id, label) => {
out.entry(*id).or_default().push((*label, p.clone()))
}
menus::Item::Sub(_, children) => walk(children, out),
_ => {}
}
}
}
static ITEMS: OnceLock<HashMap<&'static str, Vec<(&'static str, Value)>>> = OnceLock::new();
ITEMS.get_or_init(|| {
let mut out = HashMap::new();
for (_, items) in menus::menu_tree() {
walk(&items, &mut out);
}
out
})
let mut out = HashMap::new();
for (_, items) in tree {
walk(items, &mut out);
}
out
}
fn is_bare(p: &Value) -> bool {
@@ -686,6 +694,25 @@ pub(crate) fn menu_target(id: &str) -> Option<(String, Value)> {
items.iter().all(|(_, q)| q == p).then(|| menus::click_target(label, id, p))
}
/// What a command search pick, or a shortcut without params, of command `id` runs now: `Ok` with
/// what a click on its menu bar item runs ([`menu_target`]), or `None` when no such item gives it
/// params. While that item is dimmed ([`menus::enabled`]), no dialog opens for it: `Err` holds the
/// status line ([`dimmed`]), as for Remove Background… with no image selected.
pub(crate) fn menu_target_now(app: &VectorcraftApp, id: &str) -> Option<Result<(String, Value), String>> {
let target = menu_target(id)?;
Some(if menus::enabled(app, id) { Ok(target) } else { dimmed(app, id) })
}
/// What [`menu_target_now`] gives for command `id` while its menu bar item is dimmed: the status
/// line, with the engine's reason when it gives one. The app's own rules ([`menus::enabled`]) dim
/// UI commands, and some commands the engine would run, without giving a reason.
fn dimmed(app: &VectorcraftApp, id: &str) -> Result<(String, Value), String> {
let Some(why) = vectorcraft_engine::find_command(id).and_then(|c| (c.enabled)(&app.session).err()) else {
return Err(format!("command `{id}` is not available right now"));
};
Err(vectorcraft_engine::EngineError::Disabled(id.to_string(), why).to_string())
}
/// Is `key` a command with no default shortcut that a key press can't run: no item of the menu
/// bar runs it without params or with one set of params ([`menu_target`]), it opens no dialog when
/// run without them (as the Variables panel's commands do), and it is a query, whose answer a key
@@ -2270,6 +2297,47 @@ mod tests {
assert_eq!(menu_target("edit.undo"), None, "runs without params");
}
/// The menu bar items a shortcut or a command search pick reads leave out items that list an
/// installed plug-in (a File › Open With importer, an Object › Plug-ins filter) or show a name
/// that is user data (a custom toolbar, a script of the Scripts folder), which come and go after
/// the items are read; the other items stay.
#[test]
fn menu_bar_items_leave_out_installed_plug_ins_and_user_data() {
let tree = vec![(
"File",
vec![
menus::Item::Sub("Open With", vec![menus::Item::Cmd("Spins…", "file.openWith", json!({"importer": "plugin.test.spins"}))]),
menus::Item::Cmd("Filter…", "plugin.dialog", json!({"id": "test.filter"})),
menus::Item::Cmd("Mine", "window.toolbar.custom", json!({"name": "Mine"})),
menus::Item::Cmd("tidy.js", crate::scripts_menu::COMMAND, json!({"path": "/scripts/tidy.js"})),
menus::Item::Cmd("Clean Up…", "object.path.cleanUp", json!({"strayPoints": true})),
],
)];
let items = bar_items(&tree);
assert!(!items.contains_key("file.openWith"), "{items:?}");
assert!(!items.contains_key("plugin.dialog"), "{items:?}");
assert!(!items.contains_key("window.toolbar.custom"), "{items:?}");
assert!(!items.contains_key(crate::scripts_menu::COMMAND), "{items:?}");
assert_eq!(items.get("object.path.cleanUp"), Some(&vec![("Clean Up…", json!({"strayPoints": true}))]));
}
/// While a menu item is dimmed, a pick or a shortcut gets a status line and no target: with the
/// engine's reason when it gives one, and without one when the app's own rules dim the item (an
/// Appearance command with nothing targeted, a UI command).
#[test]
fn a_dimmed_item_gives_a_status_line_with_or_without_a_reason() {
use vectorcraft_testkit::fixtures::Fixture;
let none = VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default());
assert!(!menus::enabled(&none, "edit.findReplace"));
assert_eq!(dimmed(&none, "edit.findReplace"), Err("command `edit.findReplace` is not available right now: no document open".into()));
let empty = VectorcraftApp::new(Fixture::Empty.session(), crate::Services::default());
for id in ["appearance.addFill", "ui.cropImage"] {
assert!(!menus::enabled(&empty, id), "{id}");
assert!(vectorcraft_engine::find_command(id).is_none_or(|c| (c.enabled)(&empty.session).is_ok()), "{id}: the engine gives no reason");
assert_eq!(dimmed(&empty, id), Err(format!("command `{id}` is not available right now")));
}
}
/// Every command the editor lists, run as a key press runs it on a document with one or with
/// several objects selected, reports no missing param.
#[test]
+9 -5
View File
@@ -985,9 +985,10 @@ fn dispatch(app: &mut VectorcraftApp, ctx: &egui::Context, accept: impl FnMut(&H
}
/// What a shortcut without params runs when its command's menu bar item gives params
/// ([`crate::shortcut_editor::menu_target`]): what a click on that item runs.
fn menu_target(c: &CommandRef) -> Option<(String, serde_json::Value)> {
c.params.as_object().is_none_or(serde_json::Map::is_empty).then(|| crate::shortcut_editor::menu_target(&c.id)).flatten()
/// ([`crate::shortcut_editor::menu_target_now`]): what a click on that item runs, or while the item
/// is dimmed, why the command can't run.
fn menu_target(app: &VectorcraftApp, c: &CommandRef) -> Option<Result<(String, serde_json::Value), String>> {
c.params.as_object().is_none_or(serde_json::Map::is_empty).then(|| crate::shortcut_editor::menu_target_now(app, &c.id)).flatten()
}
/// Params used when a shortcut invokes its command. The named panel function keys retain their
@@ -1030,8 +1031,11 @@ fn run_hit(app: &mut VectorcraftApp, ctx: &egui::Context, c: &CommandRef) {
// every artboard instead of the art.
crate::panels::artboards::select_all(app);
} else {
let (id, p) = menu_target(c).unwrap_or_else(|| (c.id.clone(), shortcut_params(c)));
crate::menus::invoke(app, &id, p);
match menu_target(app, c) {
Some(Ok((id, p))) => crate::menus::invoke(app, &id, p),
Some(Err(why)) => app.status(why),
None => crate::menus::invoke(app, &c.id, shortcut_params(c)),
}
}
}
+55
View File
@@ -412,6 +412,38 @@ fn a_single_key_shortcut_without_a_dialog_runs_at_once() {
assert_eq!((open_dialog(&app), undo_len(&app)), (None, before + 1), "Shift+X swaps fill and stroke: {}", app.ui.status);
}
/// A key or a chord of a command whose menu item is dimmed opens no dialog: Remove Background… and
/// Object Mosaic… with a rectangle selected change nothing and say why they can't run, and so does
/// Find and Replace… with no document open.
#[test]
fn a_shortcut_of_a_dimmed_command_opens_no_dialog_and_says_why() {
let chord = vec![
egui::Event::ModifiersChanged(cmd() | Modifiers::ALT | Modifiers::SHIFT),
egui::Event::Key { key: Key::F9, physical_key: None, pressed: true, repeat: false, modifiers: cmd() | Modifiers::ALT | Modifiers::SHIFT },
];
for id in ["addon.image.removeBackground", "object.createObjectMosaic", "edit.findReplace"] {
assert!(crate::shortcut_editor::menu_target(id).is_some(), "{id} has a dialog");
for (shortcut, events) in [("F", press_f()), ("Cmd+Alt+Shift+F9", chord.clone())] {
let mut app = if id == "edit.findReplace" {
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
crate::keymap::set_override(&mut app.ui.shortcut_overrides, "view.screenMode", "");
crate::keymap::set_override(&mut app.ui.shortcut_overrides, id, shortcut);
app.ui.status.clear();
app
} else {
app_with_shortcut(id, shortcut)
};
assert!(!crate::menus::enabled(&app, id), "{id}");
let before = app.session.active().map(|st| st.history.undo.len());
frame(&mut app, events);
crate::shortcut_editor::sync(&crate::state::UiState::default());
let after = app.session.active().map(|st| st.history.undo.len());
assert_eq!((open_dialog(&app), after), (None, before), "{id} on {shortcut}: {}", app.ui.status);
assert!(app.ui.status.contains("not available"), "{id} on {shortcut}: {}", app.ui.status);
}
}
}
/// With no document open, a command's key without a modifier reports what the same command
/// reports on a chord.
#[test]
@@ -1616,6 +1648,29 @@ fn the_command_search_that_opens_while_enter_is_down_waits_for_a_new_press() {
assert!(!app.ui.palette_open, "a new press runs the highlighted result");
}
/// Enter on a command search result whose menu item opens a dialog (Create Gradient Mesh…) opens
/// that dialog: the press and its repeats do not confirm it, and a new press does.
#[test]
fn enter_on_a_command_search_result_opens_its_dialog_and_a_new_press_confirms_it() {
let mut app = app_with_a_selected_rectangle();
let held = HeldKey::new(Key::Enter, Modifiers::NONE);
app.select_tool("selection");
app.ui.palette_open = true;
app.ui.palette_query = "object.mesh.create".into();
let before = undo_len(&app);
for repeat in 0..4 {
held.press(&mut app);
let command = app.ui.dialog.as_ref().and_then(|d| d.fields.get("__command").cloned());
assert_eq!(command, Some(json!("object.mesh.create")), "repeat {repeat}: its dialog stays open: {}", app.ui.status);
assert_eq!(undo_len(&app), before, "repeat {repeat}: nothing changed");
}
assert!(!app.ui.palette_open);
held.release(&mut app);
held.press(&mut app);
assert_eq!(open_dialog(&app), None, "a new press confirms");
assert_eq!(undo_len(&app), before + 1, "the mesh is one undo step: {}", app.ui.status);
}
/// A field that rejects its text on Enter keeps the dialog open, while the key is held too.
#[test]
fn holding_enter_on_a_rejected_value_leaves_the_dialog_open() {