diff --git a/crates/engine/src/prefs.rs b/crates/engine/src/prefs.rs index b163d40d88..295870b3ca 100644 --- a/crates/engine/src/prefs.rs +++ b/crates/engine/src/prefs.rs @@ -954,6 +954,20 @@ pub struct MenuCustomization { pub colors: BTreeMap, } +impl MenuCustomization { + /// False for the items Edit › Menus can't hide (#5096): the way back into this dialog + /// (Edit › Menus, Edit › Keyboard Shortcuts), Preferences, Save, Save As and Exit. + pub fn hideable(id: &str) -> bool { + !matches!(id, "edit.menus" | "edit.keyboardShortcuts" | "file.save" | "file.saveAs" | "file.exit") && !id.starts_with("edit.preferences") + } + + /// Whether the menus leave `id` out. Items that can't be hidden stay shown even when an + /// older or hand-edited preferences file lists them. + pub fn is_hidden(&self, id: &str) -> bool { + Self::hideable(id) && self.hidden.iter().any(|h| h == id) + } +} + /// Edit › Toolbar: hidden tools and a custom order (tool names as in `ui.set {tool}`). #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] #[serde(default, rename_all = "camelCase")] @@ -1898,6 +1912,9 @@ fn menus(s: &mut Session, p: &Value) -> Result { } } let (hide, show) = (ids("hide"), ids("show")); + if let Some(id) = hide.iter().find(|id| !MenuCustomization::hideable(id)) { + return Err(bad("edit.menus", format!("`{id}` can't be hidden"))); + } s.edit_prefs(|pr| { if p.get("reset").and_then(Value::as_bool) == Some(true) { pr.menus = MenuCustomization::default(); diff --git a/crates/engine/src/prefs/tests.rs b/crates/engine/src/prefs/tests.rs index 6b56cbc7ba..7f5c863e7f 100644 --- a/crates/engine/src/prefs/tests.rs +++ b/crates/engine/src/prefs/tests.rs @@ -397,6 +397,19 @@ fn menus_and_toolbar_customisation_persist() { assert_eq!(back.toolbar.hidden, vec!["Sponge".to_string()]); } +/// #5096: Edit › Menus can't hide the way back into itself, Preferences, Save or Exit. +#[test] +fn menus_refuse_to_hide_essential_items() { + let mut s = session(); + for id in ["edit.menus", "edit.keyboardShortcuts", "edit.preferences.general", "file.save", "file.saveAs", "file.exit"] { + assert!(s.execute("edit.menus", json!({"hide": ["edit.fade", id]})).is_err(), "{id}"); + assert!(s.prefs().menus.hidden.is_empty(), "{id}: a refused call changes nothing"); + } + // A preferences file that lists them anyway still shows them. + let menus = MenuCustomization { hidden: vec!["file.exit".into(), "edit.fade".into()], ..Default::default() }; + assert!(!menus.is_hidden("file.exit") && menus.is_hidden("edit.fade")); +} + #[test] fn preference_sections_are_commands() { let mut s = Session::new(); diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 403994ac10..d25d6a41de 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -1111,7 +1111,7 @@ pub fn menu_items(app: &PhotocraftApp) -> Vec { } } if !prefs.menus.hidden.is_empty() { - items.retain(|i| !prefs.menus.hidden.contains(&i.id)); + items.retain(|i| !prefs.menus.is_hidden(&i.id)); } if prefs.interface.show_menu_colors { for it in &mut items { diff --git a/crates/ui-egui/src/prefs_ui.rs b/crates/ui-egui/src/prefs_ui.rs index db908ae23f..497a7dba62 100644 --- a/crates/ui-egui/src/prefs_ui.rs +++ b/crates/ui-egui/src/prefs_ui.rs @@ -1753,8 +1753,10 @@ fn shortcuts_body(app: &mut PhotocraftApp, ui: &mut egui::Ui, f: &mut Map