This commit is contained in:
parent
986f68f969
commit
986f977a01
|
|
@ -382,6 +382,8 @@ fn services(inbox: Inbox) -> Services {
|
|||
};
|
||||
reply.send(answer);
|
||||
}),
|
||||
// Browser folder handles cannot be represented as filesystem paths for engine commands.
|
||||
FileDialogRequest::Folder { .. } => reply.send(None),
|
||||
// No save dialog on the web: the suggested name becomes the download name.
|
||||
FileDialogRequest::Save { suggested } => {
|
||||
let name = std::path::Path::new(&suggested).file_name().map_or_else(|| suggested.clone(), |n| n.to_string_lossy().to_string());
|
||||
|
|
|
|||
|
|
@ -115,6 +115,13 @@ fn show_file_dialog(request: FileDialogRequest, parent: Option<&eframe::Frame>,
|
|||
Box::pin(async move { picked.await.map(|file| FileDialogAnswer::Paths(vec![path_of(&file)])) })
|
||||
}
|
||||
}
|
||||
FileDialogRequest::Folder { initial_dir } => {
|
||||
if let Some(dir) = initial_dir {
|
||||
dialog = dialog.set_directory(dir);
|
||||
}
|
||||
let picked = dialog.pick_folder();
|
||||
Box::pin(async move { picked.await.map(|folder| FileDialogAnswer::FolderTo(path_of(&folder))) })
|
||||
}
|
||||
FileDialogRequest::Save { suggested } => {
|
||||
for (name, exts) in save_filters(&suggested) {
|
||||
dialog = dialog.add_filter(name, &exts);
|
||||
|
|
|
|||
|
|
@ -21,12 +21,14 @@ use serde_json::{Value, json};
|
|||
use crate::PhotocraftApp;
|
||||
use crate::file_open::display_name;
|
||||
|
||||
/// What a file dialog asks the user for.
|
||||
/// What a file or folder dialog asks the user for.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum FileDialogRequest {
|
||||
/// Files to read: several for File › Open, one for a command that reads a file (Place,
|
||||
/// scripts, notes, presets). Starts in `initial_dir` (the last-used folder, UI-217-3).
|
||||
Open { multiple: bool, initial_dir: Option<String>, extensions: Option<Vec<String>> },
|
||||
/// Which folder to read from. Starts in `initial_dir` when available.
|
||||
Folder { initial_dir: Option<String> },
|
||||
/// Where to write, starting from `suggested` (a file name, or the document's own path).
|
||||
Save { suggested: String },
|
||||
}
|
||||
|
|
@ -42,6 +44,8 @@ pub enum FileDialogAnswer {
|
|||
ContentsMany(Vec<(String, Vec<u8>)>),
|
||||
/// The path to save to (desktop), or the download's name (web).
|
||||
SaveTo(String),
|
||||
/// The selected folder's path (desktop).
|
||||
FolderTo(String),
|
||||
/// No dialog could be shown (on Linux: no desktop portal and no zenity, #3354); the message
|
||||
/// says what to install. Reported as an error rather than taken for Cancel.
|
||||
Unavailable(String),
|
||||
|
|
@ -88,7 +92,7 @@ impl PhotocraftApp {
|
|||
}
|
||||
|
||||
/// Show a file dialog; `then` continues with the user's choice on a later frame (see the
|
||||
/// module docs). Returns at once with `{"fileDialog": "open" | "save"}`; `Err` without a
|
||||
/// module docs). Returns at once with `{"fileDialog": "open" | "folder" | "save"}`; `Err` without a
|
||||
/// dialog service ("cancelled", as before there were async dialogs) or while one is open.
|
||||
pub(crate) fn ask_file(
|
||||
&mut self,
|
||||
|
|
@ -103,6 +107,7 @@ impl PhotocraftApp {
|
|||
}
|
||||
let kind = match request {
|
||||
FileDialogRequest::Open { .. } => "open",
|
||||
FileDialogRequest::Folder { .. } => "folder",
|
||||
FileDialogRequest::Save { .. } => "save",
|
||||
};
|
||||
let then: Then = Box::new(move |app, answer| match answer {
|
||||
|
|
@ -114,6 +119,16 @@ impl PhotocraftApp {
|
|||
Ok(json!({ "fileDialog": kind }))
|
||||
}
|
||||
|
||||
/// Ask the user to choose a folder, starting in the folder of the most recent file when known.
|
||||
/// `then` receives the selected path when the dialog is answered.
|
||||
pub(crate) fn ask_folder(&mut self, then: impl FnOnce(&mut Self, String) -> Result<Value, String> + 'static) -> Result<Value, String> {
|
||||
let initial_dir = last_used_dir(&self.ui.recent_files);
|
||||
self.ask_file(FileDialogRequest::Folder { initial_dir }, move |app, answer| match answer {
|
||||
FileDialogAnswer::FolderTo(path) => then(app, path),
|
||||
_ => Err(UNEXPECTED.into()),
|
||||
})
|
||||
}
|
||||
|
||||
/// Ask where to save: `then` gets the chosen path, with a lowercase extension when
|
||||
/// Preferences ▸ File Handling › Lowercase Extension is on (its default).
|
||||
///
|
||||
|
|
@ -193,6 +208,7 @@ impl PhotocraftApp {
|
|||
}
|
||||
FileDialogAnswer::Contents(name, bytes) => app.open_contents(vec![(name, bytes)]),
|
||||
FileDialogAnswer::ContentsMany(files) => app.open_contents(files),
|
||||
FileDialogAnswer::FolderTo(_) => return Err(UNEXPECTED.into()),
|
||||
FileDialogAnswer::SaveTo(_) | FileDialogAnswer::Unavailable(_) => return Err(UNEXPECTED.into()),
|
||||
}
|
||||
Ok(Value::Null)
|
||||
|
|
@ -290,6 +306,7 @@ fn read_picked(answer: FileDialogAnswer) -> Result<(String, Vec<u8>), String> {
|
|||
let bytes = photocraft_format::read_file(std::path::Path::new(&path)).map_err(|e| format!("{}: {e}", display_name(&path)))?;
|
||||
Ok((path, bytes))
|
||||
}
|
||||
FileDialogAnswer::FolderTo(_) => Err(UNEXPECTED.into()),
|
||||
FileDialogAnswer::SaveTo(_) | FileDialogAnswer::Unavailable(_) => Err(UNEXPECTED.into()),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -175,6 +175,30 @@ fn open_starts_in_the_last_used_folder() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ask_folder_uses_the_async_dialog_and_returns_the_selected_path() {
|
||||
let (mut app, open, _) = app();
|
||||
let ctx = egui::Context::default();
|
||||
app.ui.recent_files = vec!["/pics/cat.psd".into()];
|
||||
let selected = Rc::new(RefCell::new(None));
|
||||
let result = selected.clone();
|
||||
|
||||
assert_eq!(
|
||||
app.ask_folder(move |_, path| {
|
||||
*result.borrow_mut() = Some(path);
|
||||
Ok(Value::Null)
|
||||
})
|
||||
.unwrap(),
|
||||
json!({"fileDialog": "folder"})
|
||||
);
|
||||
app.poll_file_dialog(&ctx, None);
|
||||
assert!(matches!(open.borrow().as_slice(), [(FileDialogRequest::Folder { initial_dir: Some(dir) }, _)] if dir == "/pics"));
|
||||
|
||||
answer(&open, Some(FileDialogAnswer::FolderTo("/pics/frames".into())));
|
||||
app.poll_file_dialog(&ctx, None);
|
||||
assert_eq!(selected.borrow().as_deref(), Some("/pics/frames"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filtered_picker_requests_only_the_given_extensions() {
|
||||
let (mut app, open, _) = app();
|
||||
|
|
|
|||
|
|
@ -292,6 +292,9 @@ pub(crate) fn invoke_unguarded(app: &mut PhotocraftApp, ctx: &egui::Context, id:
|
|||
app.open_dialog_file()
|
||||
}
|
||||
}
|
||||
"file.import.videoFramesToLayers" if params.get("path").and_then(Value::as_str).is_none() => {
|
||||
app.ask_folder(|app, path| app.run("file.import.videoFramesToLayers", json!({"path": path})).map_err(|e| e.to_string()))
|
||||
}
|
||||
"file.save" => {
|
||||
// Writes back only to a layered file; a flat one goes through Save As.
|
||||
let tga_bits = crate::targa_options_ui::bits_param(¶ms, id)?;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ fn harness() -> Option<(Harness, Writes)> {
|
|||
file_dialog: Some(Box::new(|request, _parent, reply| {
|
||||
reply.send(match request {
|
||||
FileDialogRequest::Save { suggested } => Some(FileDialogAnswer::SaveTo(suggested)),
|
||||
FileDialogRequest::Folder { .. } => None,
|
||||
FileDialogRequest::Open { .. } => None,
|
||||
})
|
||||
})),
|
||||
|
|
|
|||
Loading…
Reference in New Issue