From 326fb4ba1fb6f0c1efee5a909e9919f7c39fb132 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Tue, 27 Feb 2024 13:03:39 -0700 Subject: [PATCH] Implement new file/folder --- Cargo.lock | 40 +++++----- i18n/en/cosmic_files.ftl | 19 ++++- src/app.rs | 155 ++++++++++++++++++++++++++++++++++++--- src/dialog.rs | 8 +- 4 files changed, 188 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 69cf747..1b81827 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,9 +110,9 @@ checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" [[package]] name = "ahash" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d713b3834d76b85304d4d525563c1276e2e30dc97cc67bfb4585a4a29fc2c89f" +checksum = "8b79b82693f705137f8fb9b37871d99e4f9a7df12b917eed79c3d3954830a60b" dependencies = [ "cfg-if 1.0.0", "getrandom", @@ -205,9 +205,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.12" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b09b5178381e0874812a9b157f7fe84982617e48f71f4e3235482775e5b540" +checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" dependencies = [ "anstyle", "anstyle-parse", @@ -1121,7 +1121,7 @@ dependencies = [ [[package]] name = "cosmic-config" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#a593f866efb25d149065b11714bb954c26d34cec" +source = "git+https://github.com/pop-os/libcosmic.git#7c3145828e780c6f6e9487f3f30486e5c44b4b2e" dependencies = [ "atomicwrites", "cosmic-config-derive", @@ -1138,7 +1138,7 @@ dependencies = [ [[package]] name = "cosmic-config-derive" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#a593f866efb25d149065b11714bb954c26d34cec" +source = "git+https://github.com/pop-os/libcosmic.git#7c3145828e780c6f6e9487f3f30486e5c44b4b2e" dependencies = [ "quote", "syn 1.0.109", @@ -1198,7 +1198,7 @@ dependencies = [ [[package]] name = "cosmic-theme" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#a593f866efb25d149065b11714bb954c26d34cec" +source = "git+https://github.com/pop-os/libcosmic.git#7c3145828e780c6f6e9487f3f30486e5c44b4b2e" dependencies = [ "almost", "cosmic-config", @@ -2580,7 +2580,7 @@ dependencies = [ [[package]] name = "iced" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#a593f866efb25d149065b11714bb954c26d34cec" +source = "git+https://github.com/pop-os/libcosmic.git#7c3145828e780c6f6e9487f3f30486e5c44b4b2e" dependencies = [ "iced_accessibility", "iced_core", @@ -2595,7 +2595,7 @@ dependencies = [ [[package]] name = "iced_accessibility" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#a593f866efb25d149065b11714bb954c26d34cec" +source = "git+https://github.com/pop-os/libcosmic.git#7c3145828e780c6f6e9487f3f30486e5c44b4b2e" dependencies = [ "accesskit", "accesskit_winit", @@ -2604,7 +2604,7 @@ dependencies = [ [[package]] name = "iced_core" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#a593f866efb25d149065b11714bb954c26d34cec" +source = "git+https://github.com/pop-os/libcosmic.git#7c3145828e780c6f6e9487f3f30486e5c44b4b2e" dependencies = [ "bitflags 1.3.2", "log", @@ -2621,7 +2621,7 @@ dependencies = [ [[package]] name = "iced_futures" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#a593f866efb25d149065b11714bb954c26d34cec" +source = "git+https://github.com/pop-os/libcosmic.git#7c3145828e780c6f6e9487f3f30486e5c44b4b2e" dependencies = [ "futures", "iced_core", @@ -2634,7 +2634,7 @@ dependencies = [ [[package]] name = "iced_graphics" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#a593f866efb25d149065b11714bb954c26d34cec" +source = "git+https://github.com/pop-os/libcosmic.git#7c3145828e780c6f6e9487f3f30486e5c44b4b2e" dependencies = [ "bitflags 1.3.2", "bytemuck", @@ -2658,7 +2658,7 @@ dependencies = [ [[package]] name = "iced_renderer" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#a593f866efb25d149065b11714bb954c26d34cec" +source = "git+https://github.com/pop-os/libcosmic.git#7c3145828e780c6f6e9487f3f30486e5c44b4b2e" dependencies = [ "iced_graphics", "iced_tiny_skia", @@ -2670,7 +2670,7 @@ dependencies = [ [[package]] name = "iced_runtime" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#a593f866efb25d149065b11714bb954c26d34cec" +source = "git+https://github.com/pop-os/libcosmic.git#7c3145828e780c6f6e9487f3f30486e5c44b4b2e" dependencies = [ "iced_core", "iced_futures", @@ -2680,7 +2680,7 @@ dependencies = [ [[package]] name = "iced_style" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#a593f866efb25d149065b11714bb954c26d34cec" +source = "git+https://github.com/pop-os/libcosmic.git#7c3145828e780c6f6e9487f3f30486e5c44b4b2e" dependencies = [ "iced_core", "once_cell", @@ -2690,7 +2690,7 @@ dependencies = [ [[package]] name = "iced_tiny_skia" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#a593f866efb25d149065b11714bb954c26d34cec" +source = "git+https://github.com/pop-os/libcosmic.git#7c3145828e780c6f6e9487f3f30486e5c44b4b2e" dependencies = [ "bytemuck", "cosmic-text", @@ -2707,7 +2707,7 @@ dependencies = [ [[package]] name = "iced_wgpu" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#a593f866efb25d149065b11714bb954c26d34cec" +source = "git+https://github.com/pop-os/libcosmic.git#7c3145828e780c6f6e9487f3f30486e5c44b4b2e" dependencies = [ "bitflags 1.3.2", "bytemuck", @@ -2726,7 +2726,7 @@ dependencies = [ [[package]] name = "iced_widget" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#a593f866efb25d149065b11714bb954c26d34cec" +source = "git+https://github.com/pop-os/libcosmic.git#7c3145828e780c6f6e9487f3f30486e5c44b4b2e" dependencies = [ "iced_renderer", "iced_runtime", @@ -2740,7 +2740,7 @@ dependencies = [ [[package]] name = "iced_winit" version = "0.12.0" -source = "git+https://github.com/pop-os/libcosmic.git#a593f866efb25d149065b11714bb954c26d34cec" +source = "git+https://github.com/pop-os/libcosmic.git#7c3145828e780c6f6e9487f3f30486e5c44b4b2e" dependencies = [ "iced_graphics", "iced_runtime", @@ -3059,7 +3059,7 @@ checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libcosmic" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic.git#a593f866efb25d149065b11714bb954c26d34cec" +source = "git+https://github.com/pop-os/libcosmic.git#7c3145828e780c6f6e9487f3f30486e5c44b4b2e" dependencies = [ "apply", "ashpd", diff --git a/i18n/en/cosmic_files.ftl b/i18n/en/cosmic_files.ftl index 14a7a1b..a20a3df 100644 --- a/i18n/en/cosmic_files.ftl +++ b/i18n/en/cosmic_files.ftl @@ -4,18 +4,31 @@ filesystem = Filesystem home = Home trash = Trash -# Dialog +# New File/Folder Dialog +create-new-file = Create new file +create-new-folder = Create new folder +file-name = File name +folder-name = Folder name +file-already-exists = A file with that name already exists. +folder-already-exists = A folder with that name already exists. +name-hidden = Names starting with "." will be hidden. +name-invalid = Name cannot be "{$filename}". +name-no-slashes = Name cannot contain slashes. + +# Open/Save Dialog cancel = Cancel open = Open open-file = Open file open-folder = Open folder open-multiple-files = Open multiple files open-multiple-folders = Open multiple folders +save = Save +save-file = Save file + +# Replace Dialog replace = Replace replace-title = {$filename} already exists in this location. replace-warning = Do you want to replace it with the one you are saving? Replacing it will overwrite its content. -save = Save -save-file = Save file # List view name = Name diff --git a/src/app.rs b/src/app.rs index 0422267..da4b3b0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -75,8 +75,8 @@ impl Action { Action::HistoryPrevious => Message::TabMessage(None, tab::Message::GoPrevious), Action::LocationUp => Message::TabMessage(None, tab::Message::LocationUp), Action::MoveToTrash => Message::MoveToTrash(entity_opt), - Action::NewFile => Message::NewFile(entity_opt), - Action::NewFolder => Message::NewFolder(entity_opt), + Action::NewFile => Message::NewItem(entity_opt, false), + Action::NewFolder => Message::NewItem(entity_opt, true), Action::Paste => Message::Paste(entity_opt), Action::Properties => Message::ToggleContextPage(ContextPage::Properties), Action::RestoreFromTrash => Message::RestoreFromTrash(entity_opt), @@ -106,11 +106,13 @@ pub enum Message { Config(Config), Copy(Option), Cut(Option), + DialogCancel, + DialogComplete, + DialogPage(DialogPage), Key(Modifiers, Key), Modifiers(Modifiers), MoveToTrash(Option), - NewFile(Option), - NewFolder(Option), + NewItem(Option, bool), NotifyEvent(notify::Event), NotifyWatcher(WatcherWrapper), Paste(Option), @@ -150,6 +152,15 @@ impl ContextPage { } } +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum DialogPage { + NewItem { + parent: PathBuf, + name: String, + dir: bool, + }, +} + #[derive(Debug)] pub struct WatcherWrapper { watcher_opt: Option, @@ -176,6 +187,8 @@ pub struct App { config: Config, app_themes: Vec, context_page: ContextPage, + dialog_page: Option, + dialog_text_input: widget::Id, key_binds: HashMap, modifiers: Modifiers, pending_operation_id: u64, @@ -500,6 +513,8 @@ impl Application for App { config: flags.config, app_themes, context_page: ContextPage::Settings, + dialog_page: None, + dialog_text_input: widget::Id::unique(), key_binds: key_binds(), modifiers: Modifiers::empty(), pending_operation_id: 0, @@ -566,6 +581,11 @@ impl Application for App { fn on_escape(&mut self) -> Command { let entity = self.tab_model.active(); + // Close dialog if open + if self.dialog_page.take().is_some() { + return Command::none(); + } + // Close menus and context panes in order per message // Why: It'd be weird to close everything all at once // Usually, the Escape key (for example) closes menus and panes one by one instead @@ -627,6 +647,32 @@ impl Application for App { Message::Cut(_entity_opt) => { log::warn!("TODO: CUT"); } + Message::DialogCancel => { + self.dialog_page = None; + } + Message::DialogComplete => { + if let Some(dialog_page) = self.dialog_page.take() { + match dialog_page { + DialogPage::NewItem { parent, name, dir } => { + let path = parent.join(name); + match if dir { + fs::create_dir(&path) + } else { + fs::File::create(&path).map(|_| ()) + } { + Ok(()) => {} + Err(err) => { + //TODO: dialog + log::warn!("failed to create {:?}: {}", path, err); + } + } + } + } + } + } + Message::DialogPage(dialog_page) => { + self.dialog_page = Some(dialog_page); + } Message::Key(modifiers, key) => { let entity = self.tab_model.active(); for (key_bind, action) in self.key_binds.iter() { @@ -654,11 +700,20 @@ impl Application for App { self.operation(Operation::Delete { paths }); } } - Message::NewFile(_entity_opt) => { - log::warn!("TODO: NEW FILE"); - } - Message::NewFolder(_entity_opt) => { - log::warn!("TODO: NEW FOLDER"); + Message::NewItem(entity_opt, dir) => { + if self.dialog_page.is_none() { + let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); + if let Some(tab) = self.tab_model.data_mut::(entity) { + if let Location::Path(path) = &tab.location { + self.dialog_page = Some(DialogPage::NewItem { + parent: path.clone(), + name: String::new(), + dir, + }); + return widget::text_input::focus(self.dialog_text_input.clone()); + } + } + } } Message::NotifyEvent(event) => { log::debug!("{:?}", event); @@ -931,6 +986,88 @@ impl Application for App { }) } + fn dialog(&self) -> Option> { + let dialog_page = match &self.dialog_page { + Some(some) => some, + None => return None, + }; + + let cosmic_theme::Spacing { space_xxs, .. } = self.core().system_theme().cosmic().spacing; + + let dialog = match dialog_page { + DialogPage::NewItem { parent, name, dir } => { + let mut dialog = widget::dialog(if *dir { + fl!("create-new-folder") + } else { + fl!("create-new-file") + }); + + let complete_maybe = if name.is_empty() { + None + } else if name == "." || name == ".." { + dialog = dialog.tertiary_action(widget::text::body(fl!( + "name-invalid", + filename = name.as_str() + ))); + None + } else if name.contains('/') { + dialog = dialog.tertiary_action(widget::text::body(fl!("name-no-slashes"))); + None + } else { + let path = parent.join(name); + if path.exists() { + if path.is_dir() { + dialog = dialog + .tertiary_action(widget::text::body(fl!("folder-already-exists"))); + } else { + dialog = dialog + .tertiary_action(widget::text::body(fl!("file-already-exists"))); + } + None + } else { + if name.starts_with('.') { + dialog = dialog.tertiary_action(widget::text::body(fl!("name-hidden"))); + } + Some(Message::DialogComplete) + } + }; + + dialog + .primary_action( + widget::button::suggested(fl!("save")) + .on_press_maybe(complete_maybe.clone()), + ) + .secondary_action( + widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), + ) + .control( + widget::column::with_children(vec![ + widget::text::body(if *dir { + fl!("folder-name") + } else { + fl!("file-name") + }) + .into(), + widget::text_input("", name.as_str()) + .id(self.dialog_text_input.clone()) + .on_input(move |name| { + Message::DialogPage(DialogPage::NewItem { + parent: parent.clone(), + name, + dir: *dir, + }) + }) + .on_submit_maybe(complete_maybe) + .into(), + ]) + .spacing(space_xxs), + ) + } + }; + + Some(dialog.into()) + } + fn header_start(&self) -> Vec> { vec![menu::menu_bar(&self.key_binds).into()] } diff --git a/src/dialog.rs b/src/dialog.rs index 051a046..805bef2 100644 --- a/src/dialog.rs +++ b/src/dialog.rs @@ -372,8 +372,12 @@ impl Application for App { widget::dialog(fl!("replace-title", filename = filename.as_str())) .icon(widget::icon::from_name("dialog-question").size(64)) .body(fl!("replace-warning")) - .primary_action(fl!("replace"), Message::Save(true)) - .secondary_action(fl!("cancel"), Message::Cancel) + .primary_action( + widget::button::suggested(fl!("replace")).on_press(Message::Save(true)), + ) + .secondary_action( + widget::button::standard(fl!("cancel")).on_press(Message::Cancel), + ) .into(), ); }