Skip to content

Commit

Permalink
feat: Add support for selecting multiple folders, fixes #68 (#73)
Browse files Browse the repository at this point in the history
* feat: add support for multiple dirs on Linux etc

* feat: add support for multiple directories on MacOS

* feat: add support for multiple directories on Windows

* feat: add support for multiple directories for XDG on linux
  • Loading branch information
betamos authored Jun 12, 2022
1 parent 9d0f347 commit ac1217a
Show file tree
Hide file tree
Showing 9 changed files with 202 additions and 1 deletion.
2 changes: 2 additions & 0 deletions src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ pub trait FileSaveDialogImpl {
/// Dialog used to pick folder
pub trait FolderPickerDialogImpl {
fn pick_folder(self) -> Option<PathBuf>;
fn pick_folders(self) -> Option<Vec<PathBuf>>;
}

pub trait MessageDialogImpl {
Expand All @@ -75,6 +76,7 @@ pub trait AsyncFilePickerDialogImpl {
/// Dialog used to pick folder
pub trait AsyncFolderPickerDialogImpl {
fn pick_folder_async(self) -> DialogFutureType<Option<FileHandle>>;
fn pick_folders_async(self) -> DialogFutureType<Option<Vec<FileHandle>>>;
}

/// Dialog used to pick folder
Expand Down
36 changes: 36 additions & 0 deletions src/backend/gtk3/file_dialog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,22 @@ impl FolderPickerDialogImpl for FileDialog {
}
})
}

fn pick_folders(self) -> Option<Vec<PathBuf>> {
GTK_MUTEX.run_locked(|| {
if !gtk_init_check() {
return None;
};

let dialog = GtkFileDialog::build_pick_folders(&self);

if dialog.run() == gtk_sys::GTK_RESPONSE_ACCEPT {
Some(dialog.get_results())
} else {
None
}
})
}
}

use crate::backend::AsyncFolderPickerDialogImpl;
Expand All @@ -124,6 +140,26 @@ impl AsyncFolderPickerDialogImpl for FileDialog {

Box::pin(future)
}

fn pick_folders_async(self) -> DialogFutureType<Option<Vec<FileHandle>>> {
let builder = move || GtkFileDialog::build_pick_folders(&self);

let future = GtkDialogFuture::new(builder, |dialog, res_id| {
if res_id == gtk_sys::GTK_RESPONSE_ACCEPT {
Some(
dialog
.get_results()
.into_iter()
.map(FileHandle::wrap)
.collect(),
)
} else {
None
}
});

Box::pin(future)
}
}

//
Expand Down
22 changes: 22 additions & 0 deletions src/backend/gtk3/file_dialog/dialog_ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,28 @@ impl GtkFileDialog {
dialog
}

pub fn build_pick_folders(opt: &FileDialog) -> Self {
let dialog = GtkFileDialog::new(
opt.title.as_deref().unwrap_or("Select Folder"),
GtkFileChooserAction::SelectFolder,
"Cancel",
"Select",
);
unsafe { gtk_sys::gtk_file_chooser_set_select_multiple(dialog.ptr as _, 1) };
dialog.set_path(opt.starting_directory.as_deref());

if let (Some(mut path), Some(file_name)) =
(opt.starting_directory.to_owned(), opt.file_name.as_deref())
{
path.push(file_name);
dialog.set_file_name(path.deref().to_str());
} else {
dialog.set_file_name(opt.file_name.as_deref());
}

dialog
}

pub fn build_pick_files(opt: &FileDialog) -> Self {
let mut dialog = GtkFileDialog::new(
opt.title.as_deref().unwrap_or("Open File"),
Expand Down
37 changes: 37 additions & 0 deletions src/backend/macos/file_dialog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,19 @@ impl FolderPickerDialogImpl for FileDialog {
})
})
}

fn pick_folders(self) -> Option<Vec<PathBuf>> {
objc::rc::autoreleasepool(move || {
run_on_main(move || {
let panel = Panel::build_pick_folders(&self);
if panel.run_modal() == 1 {
Some(panel.get_results())
} else {
None
}
})
})
}
}

use crate::backend::AsyncFolderPickerDialogImpl;
Expand All @@ -130,6 +143,30 @@ impl AsyncFolderPickerDialogImpl for FileDialog {

Box::pin(future)
}

fn pick_folders_async(self) -> DialogFutureType<Option<Vec<FileHandle>>> {
let win = self.parent.as_ref().map(NSWindow::from_raw_window_handle);

let future = ModalFuture::new(
win,
move || Panel::build_pick_folders(&self),
|panel, res_id| {
if res_id == 1 {
Some(
panel
.get_results()
.into_iter()
.map(FileHandle::wrap)
.collect(),
)
} else {
None
}
},
);

Box::pin(future)
}
}

//
Expand Down
22 changes: 22 additions & 0 deletions src/backend/macos/file_dialog/panel_ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,28 @@ impl Panel {
panel
}

pub fn build_pick_folders(opt: &FileDialog) -> Self {
let panel = Panel::open_panel();

if let Some(path) = &opt.starting_directory {
panel.set_path(path, opt.file_name.as_deref());
}

if let Some(title) = &opt.title {
panel.set_title(title);
}

if let Some(parent) = &opt.parent {
panel.set_parent(parent);
}

panel.set_can_choose_directories(YES);
panel.set_can_choose_files(NO);
panel.set_allows_multiple_selection(YES);

panel
}

pub fn build_pick_files(opt: &FileDialog) -> Self {
let panel = Panel::open_panel();

Expand Down
16 changes: 16 additions & 0 deletions src/backend/win_cid/file_dialog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,17 @@ impl FolderPickerDialogImpl for FileDialog {

run(self).ok()
}

fn pick_folders(self) -> Option<Vec<PathBuf>> {
fn run(opt: FileDialog) -> Result<Vec<PathBuf>> {
init_com(|| {
let dialog = IDialog::build_pick_folders(&opt)?;
dialog.show()?;
dialog.get_results()
})?
}
run(self).ok()
}
}

use crate::backend::AsyncFolderPickerDialogImpl;
Expand All @@ -81,6 +92,11 @@ impl AsyncFolderPickerDialogImpl for FileDialog {
let ret = single_return_future(move || IDialog::build_pick_folder(&self));
Box::pin(ret)
}

fn pick_folders_async(self) -> DialogFutureType<Option<Vec<FileHandle>>> {
let ret = multiple_return_future(move || IDialog::build_pick_folders(&self));
Box::pin(ret)
}
}

//
Expand Down
16 changes: 15 additions & 1 deletion src/backend/win_cid/file_dialog/dialog_ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use windows::Win32::{
UI::Shell::{
Common::COMDLG_FILTERSPEC, FileOpenDialog, FileSaveDialog, IFileDialog, IFileOpenDialog,
IFileSaveDialog, IShellItem, SHCreateItemFromParsingName, FOS_ALLOWMULTISELECT,
FOS_PICKFOLDERS, SIGDN_FILESYSPATH,
FOS_PICKFOLDERS, SIGDN_FILESYSPATH, FILEOPENDIALOGOPTIONS,
},
};

Expand Down Expand Up @@ -257,6 +257,20 @@ impl IDialog {
Ok(dialog)
}

pub fn build_pick_folders(opt: &FileDialog) -> Result<Self> {
let dialog = IDialog::new_open_dialog(opt)?;

dialog.set_path(&opt.starting_directory)?;
dialog.set_title(&opt.title)?;
let opts = FILEOPENDIALOGOPTIONS(FOS_PICKFOLDERS.0 | FOS_ALLOWMULTISELECT.0);

unsafe {
dialog.0.as_dialog().SetOptions(opts)?;
}

Ok(dialog)
}

pub fn build_pick_files(opt: &FileDialog) -> Result<Self> {
let dialog = IDialog::new_open_dialog(opt)?;

Expand Down
39 changes: 39 additions & 0 deletions src/backend/xdg_desktop_portal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,11 @@ impl FolderPickerDialogImpl for FileDialog {
fn pick_folder(self) -> Option<PathBuf> {
block_on(self.pick_folder_async()).map(PathBuf::from)
}

fn pick_folders(self) -> Option<Vec<PathBuf>> {
block_on(self.pick_folders_async())
.map(|vec_file_handle| vec_file_handle.iter().map(PathBuf::from).collect())
}
}

use crate::backend::AsyncFolderPickerDialogImpl;
Expand All @@ -174,6 +179,40 @@ impl AsyncFolderPickerDialogImpl for FileDialog {
uri_to_pathbuf(&selected_files.unwrap().uris()[0]).map(FileHandle::from)
})
}

fn pick_folders_async(self) -> DialogFutureType<Option<Vec<FileHandle>>> {
Box::pin(async {
let proxy = file_chooser_proxy().await?;
let mut options = OpenFileOptions::default()
.accept_label("Pick folders")
.multiple(true)
.directory(true);
options = add_filters_to_open_file_options(self.filters, options);
let selected_files = proxy
.open_file(
&WindowIdentifier::default(),
&self
.title
.unwrap_or_else(|| "Pick one or more folders".to_string()),
options,
)
.await;
if selected_files.is_err() {
return None;
}
let selected_files = selected_files
.unwrap()
.uris()
.iter()
.filter_map(|string| uri_to_pathbuf(string))
.map(FileHandle::from)
.collect::<Vec<FileHandle>>();
if selected_files.is_empty() {
return None;
}
Some(selected_files)
})
}
}

//
Expand Down
13 changes: 13 additions & 0 deletions src/file_dialog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ impl FileDialog {
FolderPickerDialogImpl::pick_folder(self)
}

/// Pick multiple folders
pub fn pick_folders(self) -> Option<Vec<PathBuf>> {
FolderPickerDialogImpl::pick_folders(self)
}

/// Opens save file dialog
///
/// #### Platform specific notes regarding save dialog filters:
Expand Down Expand Up @@ -214,6 +219,14 @@ impl AsyncFileDialog {
AsyncFolderPickerDialogImpl::pick_folder_async(self.file_dialog)
}

#[cfg(not(target_arch = "wasm32"))]
/// Pick multiple folders
///
/// Does not exist in `WASM32`
pub fn pick_folders(self) -> impl Future<Output = Option<Vec<FileHandle>>> {
AsyncFolderPickerDialogImpl::pick_folders_async(self.file_dialog)
}

#[cfg(not(target_arch = "wasm32"))]
/// Opens save file dialog
///
Expand Down

0 comments on commit ac1217a

Please sign in to comment.