Skip to content

Implemented multiple selection for gtk backend #2081

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Dec 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions druid-shell/src/backend/gtk/dialog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ pub(crate) fn get_file_dialog_path(
window: &Window,
ty: FileDialogType,
options: FileDialogOptions,
) -> Result<OsString, Error> {
) -> Result<Vec<OsString>, Error> {
// TODO: support message localization

let (title, action) = match (ty, options.select_directories) {
Expand Down Expand Up @@ -98,9 +98,17 @@ pub(crate) fn get_file_dialog_path(
let result = dialog.run();

let result = match result {
ResponseType::Accept => match dialog.filename() {
Some(path) => Ok(path.into_os_string()),
None => Err(anyhow!("No path received for filename")),
ResponseType::Accept => match dialog.filenames() {
filenames if filenames.is_empty() => Err(anyhow!("No path received for filename")),
// If we receive more than one file, but `multi_selection` is false, return an error.
filenames if filenames.len() > 1 && !options.multi_selection => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One last corner case that I noticed because of the unwrap below: if they pass FileDialogType::Save and options.multi_selection = true (which they shouldn't but I don't think we prevent it), then we correctly handle that combination on line 62 (by ignoring multi selection) but we don't check here that we only got one file back. Ok, maybe that's a bit paranoid, but I don't think it's any more paranoid than the existing checks that we have here...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand what you mean:

if they pass FileDialogType::Save and options.multi_selection = true, then we correctly handle that combination on line 62 but we don't check here that we only got one file back

We do check that the we only got one file back (filenames.is_empty and filenames.len() > 1), and return an Err if that is not true.

Do you mean that since we ignored the option in the case of a Save dialog, we should not return an error, but the whole filenames vec (essentially avoid this check)?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I meant was: in the case of a Save dialog, we should return an error if there's more than one filename, even if options.multi_selection is true. (This is what the unwrap at line 945 assumes)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, ok, so instead of checking for && !options.multi_selection we should do:

            filenames if filenames.len() > 1 && action == FileChooserAction::Save => {

to uphold what we write later in the other comment, right?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but you will need to check both the action and options.multi_selection because we still need to handle the case of open dialogs that expect a single file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, fixing it now

Err(anyhow!("More than one path received for single selection"))
}
// If we receive more than one file with a save action, return an error.
filenames if filenames.len() > 1 && action == FileChooserAction::Save => {
Err(anyhow!("More than one path received for save action"))
}
filenames => Ok(filenames.into_iter().map(|p| p.into_os_string()).collect()),
},
ResponseType::Cancel => Err(anyhow!("Dialog was deleted")),
_ => {
Expand Down
33 changes: 24 additions & 9 deletions druid-shell/src/backend/gtk/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -907,17 +907,30 @@ impl WindowState {
for op in queue {
match op {
DeferredOp::Open(options, token) => {
let file_info = dialog::get_file_dialog_path(
// Keep the value of this option for later
let multi_selection = options.multi_selection;
let file_infos = match dialog::get_file_dialog_path(
self.window.upcast_ref(),
FileDialogType::Open,
options,
)
.ok()
.map(|s| FileInfo {
path: s.into(),
format: None,
});
self.with_handler(|h| h.open_file(token, file_info));
) {
Ok(infos) => infos
.iter()
.map(|path| FileInfo {
path: path.into(),
format: None,
})
.collect(),
Err(err) => {
tracing::error!("Error trying to open file: {}", err);
vec![]
}
};
if multi_selection {
self.with_handler(|h| h.open_files(token, file_infos));
} else {
self.with_handler(|h| h.open_file(token, file_infos.first().cloned()));
}
}
DeferredOp::SaveAs(options, token) => {
let file_info = dialog::get_file_dialog_path(
Expand All @@ -927,7 +940,9 @@ impl WindowState {
)
.ok()
.map(|s| FileInfo {
path: s.into(),
// `get_file_dialog_path` guarantees that save dialogs
// only return on path
path: s.first().unwrap().into(),
format: None,
});
self.with_handler(|h| h.save_as(token, file_info));
Expand Down
7 changes: 7 additions & 0 deletions druid-shell/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,13 @@ pub trait WinHandler {
#[allow(unused_variables)]
fn open_file(&mut self, token: FileDialogToken, file: Option<FileInfo>) {}

/// Called when an "Open" dialog with multiple selection is closed.
///
/// `token` is the value returned by [`WindowHandle::open_file`]. `files` contains the information
/// of the chosen paths, or `None` if the save dialog was cancelled.
#[allow(unused_variables)]
fn open_files(&mut self, token: FileDialogToken, files: Vec<FileInfo>) {}

/// Called on a key down event.
///
/// Return `true` if the event is handled.
Expand Down
5 changes: 5 additions & 0 deletions druid/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,11 @@ pub mod sys {
/// [`FileInfo`]: ../struct.FileInfo.html
pub const OPEN_FILE: Selector<FileInfo> = Selector::new("druid-builtin.open-file-path");

/// Open a path, must be handled by the application.
///
/// [`FileInfo`]: ../struct.FileInfo.html
pub const OPEN_FILES: Selector<Vec<FileInfo>> = Selector::new("druid-builtin.open-files-path");

/// When submitted by the application, the system will show the 'save as' panel,
/// and if a path is selected the system will issue a [`SAVE_FILE`] command
/// with the selected path as the payload.
Expand Down
9 changes: 9 additions & 0 deletions druid/src/dialog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ use crate::{FileInfo, FileSpec, Selector};
pub struct FileDialogOptions {
pub(crate) opt: ShellOptions,
pub(crate) accept_cmd: Option<Selector<FileInfo>>,
pub(crate) accept_multiple_cmd: Option<Selector<Vec<FileInfo>>>,
pub(crate) cancel_cmd: Option<Selector<()>>,
}

Expand Down Expand Up @@ -238,6 +239,14 @@ impl FileDialogOptions {
self
}

/// Sets a custom command to use when the file dialog succeeds with multi selection.
///
/// This only works for "open" dialogs configured for multiselection.
pub fn accept_multiple_command(mut self, cmd: Selector<Vec<FileInfo>>) -> Self {
self.accept_multiple_cmd = Some(cmd);
self
}

/// Sets a custom command to use when the file dialog is cancelled.
///
/// By default, an "open" dialog sends the [`OPEN_PANEL_CANCELLED`] command when it is cancelled, and a "save"
Expand Down
39 changes: 39 additions & 0 deletions druid/src/win_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ struct DialogInfo {
id: WindowId,
/// The command to send if the dialog is accepted.
accept_cmd: Selector<FileInfo>,
/// The command to send if the dialog is accepted with multiple files.
accept_multiple_cmd: Selector<Vec<FileInfo>>,
/// The command to send if the dialog is cancelled.
cancel_cmd: Selector<()>,
}
Expand Down Expand Up @@ -713,6 +715,9 @@ impl<T: Data> AppState<T> {
.map(|w| w.handle.clone());

let accept_cmd = options.accept_cmd.unwrap_or(crate::commands::OPEN_FILE);
let accept_multiple_cmd = options
.accept_multiple_cmd
.unwrap_or(crate::commands::OPEN_FILES);
let cancel_cmd = options
.cancel_cmd
.unwrap_or(crate::commands::OPEN_PANEL_CANCELLED);
Expand All @@ -723,6 +728,7 @@ impl<T: Data> AppState<T> {
DialogInfo {
id: window_id,
accept_cmd,
accept_multiple_cmd,
cancel_cmd,
},
);
Expand All @@ -738,6 +744,9 @@ impl<T: Data> AppState<T> {
.get_mut(window_id)
.map(|w| w.handle.clone());
let accept_cmd = options.accept_cmd.unwrap_or(crate::commands::SAVE_FILE_AS);
let accept_multiple_cmd = options
.accept_multiple_cmd
.unwrap_or(crate::commands::OPEN_FILES);
let cancel_cmd = options
.cancel_cmd
.unwrap_or(crate::commands::SAVE_PANEL_CANCELLED);
Expand All @@ -748,11 +757,36 @@ impl<T: Data> AppState<T> {
DialogInfo {
id: window_id,
accept_cmd,
accept_multiple_cmd,
cancel_cmd,
},
);
}
}
fn handle_dialog_multiple_response(
&mut self,
token: FileDialogToken,
file_info: Vec<FileInfo>,
) {
let mut inner = self.inner.borrow_mut();
if let Some(dialog_info) = inner.file_dialogs.remove(&token) {
let cmd = if !file_info.is_empty() {
dialog_info
.accept_multiple_cmd
.with(file_info)
.to(dialog_info.id)
} else {
dialog_info.cancel_cmd.to(dialog_info.id)
};
inner.append_command(cmd);
} else {
tracing::error!("unknown dialog token");
}

std::mem::drop(inner);
self.process_commands();
self.inner.borrow_mut().do_update();
}

fn handle_dialog_response(&mut self, token: FileDialogToken, file_info: Option<FileInfo>) {
let mut inner = self.inner.borrow_mut();
Expand Down Expand Up @@ -933,6 +967,11 @@ impl<T: Data> WinHandler for DruidHandler<T> {
self.app_state.handle_dialog_response(token, file_info);
}

fn open_files(&mut self, token: FileDialogToken, file_info: Vec<FileInfo>) {
self.app_state
.handle_dialog_multiple_response(token, file_info);
}

fn mouse_down(&mut self, event: &MouseEvent) {
// TODO: double-click detection (or is this done in druid-shell?)
let event = Event::MouseDown(event.clone().into());
Expand Down