diff --git a/src/app/mod.rs b/src/app/mod.rs index 6ee56bbe4..9269c3b3d 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -2044,6 +2044,8 @@ pub enum Message { UcsIconAtOriginChanged(bool), /// Toggle selection cycling from Options; the status-bar pill toggles the same flag. SelectionCyclingChanged(bool), + /// Reveal one of the application's own folders in the system file manager. + OpenFolder(String), /// Change isolines per surface in the current drawing (ISOLINES). IsolinesChanged(i16), /// Toggle silhouette edges in the current drawing (DISPSILH). diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 3e88cb454..3b4844fe5 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -6363,6 +6363,8 @@ impl OpenCADStudio { Task::none() } + Message::OpenFolder(path) => crate::sys::open_url(&path, None), + Message::PickDragRectToggled(rectangle) => { self.pick_drag_rect = rectangle; self.persist_settings_if_changed(); diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index e9b5278f6..0f59969ed 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -230,6 +230,17 @@ impl OpenCADStudio { .map_or(1, |h| h.show_solid_history), } }, + crate::ui::window::options::Folders { + config: crate::config::config_dir() + .map(|p| p.display().to_string()), + plot_styles: crate::io::plot_style::plot_styles_dir() + .ok() + .map(|p| p.display().to_string()), + plugins: crate::plugin::external::plugins_dir() + .map(|p| p.display().to_string()), + autosave: crate::config::config_dir() + .map(|_| std::env::temp_dir().display().to_string()), + }, self.double_click_block_refedit, self.double_click_block_attedit, self.cursor_type, diff --git a/src/ui/window/options.rs b/src/ui/window/options.rs index a367f24be..0923ebe21 100644 --- a/src/ui/window/options.rs +++ b/src/ui/window/options.rs @@ -25,6 +25,7 @@ const DIALOG_HEIGHT: f32 = 620.0; pub enum OptionsTab { #[default] General, + Files, OpenAndSave, Display, Drafting, @@ -74,6 +75,18 @@ pub struct AppPrefs { pub ucs_icon_at_origin: bool, } +/// The fixed locations the Files page lists. +/// +/// `None` where the platform could not give one — the row then says so and +/// its button is disabled rather than opening nothing. +#[derive(Clone, Default)] +pub struct Folders { + pub config: Option, + pub plot_styles: Option, + pub plugins: Option, + pub autosave: Option, +} + /// Values read from the current drawing's header rather than from preferences. /// /// These are saved in the DWG, so they follow the drawing rather than the @@ -142,6 +155,7 @@ pub fn view_window<'a>( prefs: AppPrefs, snap_angle_input: &'a str, drawing_prefs: DrawingPrefs, + folders: Folders, double_click_block_refedit: bool, double_click_block_attedit: bool, cursor_type: CursorType, @@ -259,6 +273,38 @@ pub fn view_window<'a>( ] .spacing(12) .align_y(iced::Center), + Space::new().height(24), + text(crate::t!("Applications")).size(15), + Space::new().height(10), + row![ + text(crate::t!("Installed plugins and their sources")).size(12).width(Fill), + button(text(crate::t!("Plugins…")).size(11)) + .on_press(Message::PluginManagerOpen) + .padding([4, 10]) + .style(button::secondary), + ] + .spacing(10) + .align_y(iced::Center), + Space::new().height(10), + row![ + text(crate::t!("Keyboard shortcuts")).size(12).width(Fill), + button(text(crate::t!("Keyboard Shortcuts…")).size(11)) + .on_press(Message::ShortcutsPanelOpen) + .padding([4, 10]) + .style(button::secondary), + ] + .spacing(10) + .align_y(iced::Center), + Space::new().height(10), + row![ + text(crate::t!("Command aliases")).size(12).width(Fill), + button(text(crate::t!("Command Aliases…")).size(11)) + .on_press(Message::AliasEditorOpen) + .padding([4, 10]) + .style(button::secondary), + ] + .spacing(10) + .align_y(iced::Center), ] .spacing(0) .width(sizing.width); @@ -1464,10 +1510,57 @@ pub fn view_window<'a>( let modeling = modeling.spacing(0).width(sizing.width); + + // The application has no support-file search path, so this page does not + // pretend to offer one. It shows where things actually live and opens the + // folder — which is the question people are really asking when they go + // looking for a Files page. + let folder_row = |label: std::borrow::Cow<'a, str>, path: Option| { + let shown = path.clone().unwrap_or_else(|| crate::t!("Not available").into_owned()); + let mut open = button(text(crate::t!("Open folder")).size(11)) + .padding([4, 10]) + .style(button::secondary); + if let Some(path) = path { + open = open.on_press(Message::OpenFolder(path)); + } + row![ + column![ + text(label).size(12), + text(shown).size(11), + ] + .spacing(2) + .width(Fill), + open, + ] + .spacing(10) + .align_y(iced::Center) + }; + + let files = column![ + text(crate::t!("Files")).size(15), + Space::new().height(6), + text(crate::t!( + "Where the application keeps its own files. These locations are fixed." + )) + .size(11) + .width(sizing.width), + Space::new().height(16), + folder_row(crate::t!("Configuration"), folders.config.clone()), + Space::new().height(12), + folder_row(crate::t!("Plot styles"), folders.plot_styles.clone()), + Space::new().height(12), + folder_row(crate::t!("Plugins"), folders.plugins.clone()), + Space::new().height(12), + folder_row(crate::t!("Autosave files"), folders.autosave.clone()), + ] + .spacing(0) + .width(sizing.width); + let content: Element<'a, Message> = match active_tab { OptionsTab::General => general.into(), OptionsTab::Display => display_element.into(), OptionsTab::Selection => selection.into(), + OptionsTab::Files => files.into(), OptionsTab::OpenAndSave => open_and_save.into(), OptionsTab::Drafting => drafting.into(), OptionsTab::Modeling => modeling.into(), @@ -1488,6 +1581,7 @@ pub fn view_window<'a>( }; let tabs = column![ tab_button(crate::t!("General"), OptionsTab::General), + tab_button(crate::t!("Files"), OptionsTab::Files), tab_button(crate::t!("Open and Save"), OptionsTab::OpenAndSave), tab_button(crate::t!("Display"), OptionsTab::Display), tab_button(crate::t!("Drafting"), OptionsTab::Drafting),