mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 20:27:05 -06:00
Merge pull request #4424 from el-j/fix/window-menu-order-4143
Fix #4143: Window menu lists panel items alphabetically by localized name
This commit is contained in:
@@ -664,17 +664,22 @@ mod tests;
|
||||
|
||||
/// `name` lowercased with the Latin accents that occur in language names folded away, so a
|
||||
/// byte-wise sort reads alphabetically. Other scripts are left as they are.
|
||||
fn name_sort_key(name: &str) -> String {
|
||||
pub fn name_sort_key(name: &str) -> String {
|
||||
name.chars()
|
||||
.flat_map(char::to_lowercase)
|
||||
.map(|c| match c {
|
||||
'à'..='å' | 'ā' | 'ă' | 'ą' => 'a',
|
||||
'ç' | 'ć' | 'č' => 'c',
|
||||
'ď' | 'đ' => 'd',
|
||||
'è'..='ë' | 'ē' | 'ě' | 'ę' | 'ế' | 'ệ' => 'e',
|
||||
'ì'..='ï' | 'ī' => 'i',
|
||||
'ñ' | 'ń' | 'ň' => 'n',
|
||||
'ğ' | 'ģ' => 'g',
|
||||
'ì'..='ï' | 'ī' | 'ı' => 'i',
|
||||
'ł' | 'ļ' => 'l',
|
||||
'ñ' | 'ń' | 'ň' | 'ņ' => 'n',
|
||||
'ò'..='ö' | 'ø' | 'ő' => 'o',
|
||||
'ś' | 'š' => 's',
|
||||
'ř' => 'r',
|
||||
'ś' | 'š' | 'ş' | 'ș' => 's',
|
||||
'ť' | 'ț' | 'ţ' => 't',
|
||||
'ù'..='ü' | 'ū' | 'ů' | 'ű' => 'u',
|
||||
'ý' | 'ÿ' => 'y',
|
||||
'ź' | 'ż' | 'ž' => 'z',
|
||||
@@ -696,5 +701,6 @@ mod picker_order_tests {
|
||||
assert!(keys.windows(2).all(|w| w[0] <= w[1]), "{keys:?}");
|
||||
assert_eq!(name_sort_key("Čeština"), "cestina");
|
||||
assert_eq!(name_sort_key("Türkçe"), "turkce");
|
||||
assert_eq!(name_sort_key("Łącza"), "lacza");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -768,3 +768,81 @@ fn new_window_is_disabled_until_it_opens_a_window() {
|
||||
let e = app.run("window.newWindow", json!({})).unwrap_err();
|
||||
assert!(!e.contains("M11.5"), "a user-facing message, not a milestone: {e}");
|
||||
}
|
||||
|
||||
/// #4143: Window menu panels and panel submenus are listed in alphabetical order according to the
|
||||
/// active interface language (e.g. French), rather than staying frozen in English alphabetical order.
|
||||
#[test]
|
||||
fn window_menu_panels_are_sorted_alphabetically_in_active_language() {
|
||||
use crate::i18n::{Lang, current, name_sort_key, set_current, tr, tr_id};
|
||||
|
||||
let prev = current();
|
||||
|
||||
// 1. In English: panels between separator 2 and 3 are alphabetical.
|
||||
set_current(Lang::EN);
|
||||
let tree = menu_tree();
|
||||
let (_, window) = tree.iter().find(|(t, _)| *t == "Window").unwrap();
|
||||
|
||||
let seps: Vec<usize> = window.iter().enumerate().filter(|(_, it)| matches!(it, Item::Sep)).map(|(i, _)| i).collect();
|
||||
assert!(seps.len() >= 3, "Window menu should have at least 3 separators");
|
||||
let panels = &window[seps[1] + 1..seps[2]];
|
||||
|
||||
let item_display = |it: &Item, lang: Lang| -> String {
|
||||
match it {
|
||||
Item::Cmd(l, id, _) => tr_id(lang, id, l).to_string(),
|
||||
Item::Sub(l, _) | Item::Todo(l, _) | Item::Header(l) => tr(lang, l).to_string(),
|
||||
Item::Sep => String::new(),
|
||||
}
|
||||
};
|
||||
|
||||
let en_keys: Vec<String> = panels.iter().map(|it| name_sort_key(&item_display(it, Lang::EN))).collect();
|
||||
assert!(en_keys.windows(2).all(|w| w[0] <= w[1]), "English panels must be alphabetical: {en_keys:?}");
|
||||
|
||||
// 2. In French: panels are sorted alphabetically by their French names.
|
||||
let fr = Lang::from_code("fr").unwrap();
|
||||
set_current(fr);
|
||||
let tree_fr = menu_tree();
|
||||
let (_, window_fr) = tree_fr.iter().find(|(t, _)| *t == "Window").unwrap();
|
||||
let seps_fr: Vec<usize> = window_fr.iter().enumerate().filter(|(_, it)| matches!(it, Item::Sep)).map(|(i, _)| i).collect();
|
||||
let panels_fr = &window_fr[seps_fr[1] + 1..seps_fr[2]];
|
||||
|
||||
let fr_labels: Vec<String> = panels_fr.iter().map(|it| item_display(it, fr)).collect();
|
||||
let fr_keys: Vec<String> = fr_labels.iter().map(|l| name_sort_key(l)).collect();
|
||||
assert!(fr_keys.windows(2).all(|w| w[0] <= w[1]), "French panels must be alphabetical: {fr_labels:?}");
|
||||
|
||||
// Specific French placements:
|
||||
// Calques (Layers) is under C, Plans de travail (Artboards) is under P, Nuancier (Swatches) is under N.
|
||||
let pos = |name: &str| fr_labels.iter().position(|l| l == name).unwrap_or_else(|| panic!("missing {name} in {fr_labels:?}"));
|
||||
assert!(pos("Aspect") < pos("Calques"), "Aspect before Calques");
|
||||
assert!(pos("Calques") < pos("Dégradé"), "Calques before Dégradé");
|
||||
assert!(pos("Nuancier") < pos("Plans de travail"), "Nuancier before Plans de travail");
|
||||
assert!(pos("Plans de travail") < pos("Texte"), "Plans de travail before Texte");
|
||||
|
||||
// 3. Type submenu is also alphabetical in French.
|
||||
let type_sub = panels_fr
|
||||
.iter()
|
||||
.find_map(|it| match it {
|
||||
Item::Sub(l, items) if *l == "Type" => Some(items),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap();
|
||||
let type_labels: Vec<String> = type_sub.iter().map(|it| item_display(it, fr)).collect();
|
||||
let type_keys: Vec<String> = type_labels.iter().map(|l| name_sort_key(l)).collect();
|
||||
assert!(type_keys.windows(2).all(|w| w[0] <= w[1]), "Type submenu must be alphabetical: {type_labels:?}");
|
||||
assert_eq!(type_labels, vec!["Caractère", "Glyphes", "OpenType", "Paragraphe", "Styles de caractère", "Styles de paragraphe", "Tabulations",]);
|
||||
|
||||
// 4. In Polish: Łącza (Links) folds to L and sorts before Warstwy and Zmienne.
|
||||
let pl = Lang::from_code("pl").unwrap();
|
||||
set_current(pl);
|
||||
let tree_pl = menu_tree();
|
||||
let (_, window_pl) = tree_pl.iter().find(|(t, _)| *t == "Window").unwrap();
|
||||
let seps_pl: Vec<usize> = window_pl.iter().enumerate().filter(|(_, it)| matches!(it, Item::Sep)).map(|(i, _)| i).collect();
|
||||
let panels_pl = &window_pl[seps_pl[1] + 1..seps_pl[2]];
|
||||
let pl_labels: Vec<String> = panels_pl.iter().map(|it| item_display(it, pl)).collect();
|
||||
let pl_keys: Vec<String> = pl_labels.iter().map(|l| name_sort_key(l)).collect();
|
||||
assert!(pl_keys.windows(2).all(|w| w[0] <= w[1]), "Polish panels must be alphabetical: {pl_labels:?}");
|
||||
let pos_pl = |name: &str| pl_labels.iter().position(|l| l == name).unwrap_or_else(|| panic!("missing {name} in {pl_labels:?}"));
|
||||
assert!(pos_pl("Łącza") < pos_pl("Warstwy"), "Łącza before Warstwy");
|
||||
assert!(pos_pl("Warstwy") < pos_pl("Zmienne"), "Warstwy before Zmienne");
|
||||
|
||||
set_current(prev);
|
||||
}
|
||||
|
||||
@@ -597,9 +597,8 @@ pub fn menu_tree_named(english_names: bool) -> Vec<(&'static str, Vec<Item>)> {
|
||||
v.extend(SAVED_VIEW_IDS.iter().map(|id| c("Saved View", id)));
|
||||
v
|
||||
}),
|
||||
(
|
||||
"Window",
|
||||
vec![
|
||||
("Window", {
|
||||
let mut window_items = vec![
|
||||
c("New Window", "window.newWindow"),
|
||||
sub("Arrange", vec![todo("Cascade"), todo("Tile"), todo("Float in Window"), todo("Consolidate All Windows")]),
|
||||
sub("Workspace", crate::workspaces::menu_items()),
|
||||
@@ -609,6 +608,9 @@ pub fn menu_tree_named(english_names: bool) -> Vec<(&'static str, Vec<Item>)> {
|
||||
c("Tools", "window.toolbar"),
|
||||
sub("Toolbars", vec![c("Advanced", "window.toolbarAdvanced"), c("Double Column", "window.toolbarColumns")]),
|
||||
Sep,
|
||||
];
|
||||
let lang = crate::i18n::current();
|
||||
let mut panels = vec![
|
||||
panel("Actions", "actions"),
|
||||
panel("Align", "align"),
|
||||
panel("Appearance", "appearance"),
|
||||
@@ -643,9 +645,8 @@ pub fn menu_tree_named(english_names: bool) -> Vec<(&'static str, Vec<Item>)> {
|
||||
panel("Symbols", "symbols"),
|
||||
panel("Transform", "transform"),
|
||||
panel("Transparency", "transparency"),
|
||||
sub(
|
||||
"Type",
|
||||
vec![
|
||||
sub("Type", {
|
||||
let mut type_items = vec![
|
||||
panel("Character", "character"),
|
||||
panel("Character Styles", "charStyles"),
|
||||
panel("Glyphs", "glyphs"),
|
||||
@@ -653,16 +654,25 @@ pub fn menu_tree_named(english_names: bool) -> Vec<(&'static str, Vec<Item>)> {
|
||||
panel("Paragraph", "paragraph"),
|
||||
panel("Paragraph Styles", "paraStyles"),
|
||||
panel("Tabs", "tabs"),
|
||||
],
|
||||
),
|
||||
];
|
||||
sort_by_localized_label(&mut type_items, lang);
|
||||
type_items
|
||||
}),
|
||||
panel("Variables", crate::panels::variables::ID),
|
||||
Sep,
|
||||
];
|
||||
sort_by_localized_label(&mut panels, lang);
|
||||
window_items.extend(panels);
|
||||
window_items.push(Sep);
|
||||
let mut lib_items = vec![
|
||||
sub("Brush Libraries", crate::panels::brushes::window_menu()),
|
||||
sub("Graphic Style Libraries", crate::panels::graphic_styles::window_menu()),
|
||||
sub("Swatch Libraries", crate::panels::swatches::window_menu()),
|
||||
sub("Symbol Libraries", crate::panels::symbols::window_menu()),
|
||||
],
|
||||
),
|
||||
];
|
||||
sort_by_localized_label(&mut lib_items, lang);
|
||||
window_items.extend(lib_items);
|
||||
window_items
|
||||
}),
|
||||
(
|
||||
"Help",
|
||||
vec![
|
||||
@@ -709,3 +719,22 @@ pub(super) fn arrange_items() -> Vec<Item> {
|
||||
c("Send to Current Layer", "object.arrange.sendToCurrentLayer"),
|
||||
]
|
||||
}
|
||||
|
||||
/// Sort menu items alphabetically by their display label in `lang`, folding Latin accents so
|
||||
/// panels and submenus appear in the expected alphabetical order in every interface language (#4143).
|
||||
fn sort_by_localized_label(items: &mut [Item], lang: crate::i18n::Lang) {
|
||||
items.sort_by_cached_key(|it| {
|
||||
let label = match it {
|
||||
Item::Cmd(l, id, _) => crate::i18n::tr_id(lang, id, l),
|
||||
Item::Sub(l, _) | Item::Todo(l, _) | Item::Header(l) => crate::i18n::tr(lang, l),
|
||||
Item::Sep => "",
|
||||
};
|
||||
(
|
||||
crate::i18n::name_sort_key(label),
|
||||
match it {
|
||||
Item::Cmd(l, ..) | Item::Sub(l, ..) | Item::Todo(l, ..) | Item::Header(l) => *l,
|
||||
Item::Sep => "",
|
||||
},
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user