linux: Show warning if file picker portal is missing (#14401)
This PR adds a warning when the file chooser couldn't be opened on Linux It's quite confusing when trying to open a file and apparently nothing happens: fixes https://github.com/zed-industries/zed/issues/11089, https://github.com/zed-industries/zed/issues/14328, https://github.com/zed-industries/zed/issues/13753#issuecomment-2225812703, https://github.com/zed-industries/zed/issues/13766, https://github.com/zed-industries/zed/issues/14384, https://github.com/zed-industries/zed/issues/14353, https://github.com/zed-industries/zed/issues/9209  Release Notes: - N/A
This commit is contained in:
parent
5d860e2286
commit
f3ddd18201
11 changed files with 248 additions and 75 deletions
|
@ -612,10 +612,11 @@ impl AppContext {
|
|||
/// Displays a platform modal for selecting paths.
|
||||
/// When one or more paths are selected, they'll be relayed asynchronously via the returned oneshot channel.
|
||||
/// If cancelled, a `None` will be relayed instead.
|
||||
/// May return an error on Linux if the file picker couldn't be opened.
|
||||
pub fn prompt_for_paths(
|
||||
&self,
|
||||
options: PathPromptOptions,
|
||||
) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
|
||||
) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
|
||||
self.platform.prompt_for_paths(options)
|
||||
}
|
||||
|
||||
|
@ -623,7 +624,11 @@ impl AppContext {
|
|||
/// The provided directory will be used to set the initial location.
|
||||
/// When a path is selected, it is relayed asynchronously via the returned oneshot channel.
|
||||
/// If cancelled, a `None` will be relayed instead.
|
||||
pub fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>> {
|
||||
/// May return an error on Linux if the file picker couldn't be opened.
|
||||
pub fn prompt_for_new_path(
|
||||
&self,
|
||||
directory: &Path,
|
||||
) -> oneshot::Receiver<Result<Option<PathBuf>>> {
|
||||
self.platform.prompt_for_new_path(directory)
|
||||
}
|
||||
|
||||
|
|
|
@ -137,8 +137,8 @@ pub(crate) trait Platform: 'static {
|
|||
fn prompt_for_paths(
|
||||
&self,
|
||||
options: PathPromptOptions,
|
||||
) -> oneshot::Receiver<Option<Vec<PathBuf>>>;
|
||||
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>>;
|
||||
) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>>;
|
||||
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Result<Option<PathBuf>>>;
|
||||
fn reveal_path(&self, path: &Path);
|
||||
|
||||
fn on_quit(&self, callback: Box<dyn FnMut()>);
|
||||
|
|
|
@ -21,6 +21,7 @@ use std::{
|
|||
use anyhow::anyhow;
|
||||
use ashpd::desktop::file_chooser::{OpenFileRequest, SaveFileRequest};
|
||||
use ashpd::desktop::open_uri::{OpenDirectoryRequest, OpenFileRequest as OpenUriRequest};
|
||||
use ashpd::desktop::ResponseError;
|
||||
use ashpd::{url, ActivationToken};
|
||||
use async_task::Runnable;
|
||||
use calloop::channel::Channel;
|
||||
|
@ -54,6 +55,9 @@ pub(crate) const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(400);
|
|||
pub(crate) const DOUBLE_CLICK_DISTANCE: Pixels = px(5.0);
|
||||
pub(crate) const KEYRING_LABEL: &str = "zed-github-account";
|
||||
|
||||
const FILE_PICKER_PORTAL_MISSING: &str =
|
||||
"Couldn't open file picker due to missing xdg-desktop-portal implementation.";
|
||||
|
||||
pub trait LinuxClient {
|
||||
fn compositor_name(&self) -> &'static str;
|
||||
fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R;
|
||||
|
@ -256,7 +260,7 @@ impl<P: LinuxClient + 'static> Platform for P {
|
|||
fn prompt_for_paths(
|
||||
&self,
|
||||
options: PathPromptOptions,
|
||||
) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
|
||||
) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
|
||||
let (done_tx, done_rx) = oneshot::channel();
|
||||
self.foreground_executor()
|
||||
.spawn(async move {
|
||||
|
@ -274,7 +278,7 @@ impl<P: LinuxClient + 'static> Platform for P {
|
|||
}
|
||||
};
|
||||
|
||||
let result = OpenFileRequest::default()
|
||||
let request = match OpenFileRequest::default()
|
||||
.modal(true)
|
||||
.title(title)
|
||||
.accept_label("Select")
|
||||
|
@ -282,49 +286,68 @@ impl<P: LinuxClient + 'static> Platform for P {
|
|||
.directory(options.directories)
|
||||
.send()
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|request| request.response().ok())
|
||||
.and_then(|response| {
|
||||
{
|
||||
Ok(request) => request,
|
||||
Err(err) => {
|
||||
let result = match err {
|
||||
ashpd::Error::PortalNotFound(_) => anyhow!(FILE_PICKER_PORTAL_MISSING),
|
||||
err => err.into(),
|
||||
};
|
||||
done_tx.send(Err(result));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let result = match request.response() {
|
||||
Ok(response) => Ok(Some(
|
||||
response
|
||||
.uris()
|
||||
.iter()
|
||||
.map(|uri| uri.to_file_path().ok())
|
||||
.collect()
|
||||
});
|
||||
|
||||
.filter_map(|uri| uri.to_file_path().ok())
|
||||
.collect::<Vec<_>>(),
|
||||
)),
|
||||
Err(ashpd::Error::Response(ResponseError::Cancelled)) => Ok(None),
|
||||
Err(e) => Err(e.into()),
|
||||
};
|
||||
done_tx.send(result);
|
||||
})
|
||||
.detach();
|
||||
done_rx
|
||||
}
|
||||
|
||||
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>> {
|
||||
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Result<Option<PathBuf>>> {
|
||||
let (done_tx, done_rx) = oneshot::channel();
|
||||
let directory = directory.to_owned();
|
||||
self.foreground_executor()
|
||||
.spawn(async move {
|
||||
let request = SaveFileRequest::default()
|
||||
let request = match SaveFileRequest::default()
|
||||
.modal(true)
|
||||
.title("Select new path")
|
||||
.accept_label("Accept")
|
||||
.current_folder(directory);
|
||||
|
||||
let result = if let Ok(request) = request {
|
||||
request
|
||||
.send()
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|request| request.response().ok())
|
||||
.and_then(|response| {
|
||||
response
|
||||
.uris()
|
||||
.first()
|
||||
.and_then(|uri| uri.to_file_path().ok())
|
||||
})
|
||||
} else {
|
||||
None
|
||||
.current_folder(directory)
|
||||
.expect("pathbuf should not be nul terminated")
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(request) => request,
|
||||
Err(err) => {
|
||||
let result = match err {
|
||||
ashpd::Error::PortalNotFound(_) => anyhow!(FILE_PICKER_PORTAL_MISSING),
|
||||
err => err.into(),
|
||||
};
|
||||
done_tx.send(Err(result));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let result = match request.response() {
|
||||
Ok(response) => Ok(response
|
||||
.uris()
|
||||
.first()
|
||||
.and_then(|uri| uri.to_file_path().ok())),
|
||||
Err(ashpd::Error::Response(ResponseError::Cancelled)) => Ok(None),
|
||||
Err(e) => Err(e.into()),
|
||||
};
|
||||
done_tx.send(result);
|
||||
})
|
||||
.detach();
|
||||
|
|
|
@ -602,7 +602,7 @@ impl Platform for MacPlatform {
|
|||
fn prompt_for_paths(
|
||||
&self,
|
||||
options: PathPromptOptions,
|
||||
) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
|
||||
) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
|
||||
let (done_tx, done_rx) = oneshot::channel();
|
||||
self.foreground_executor()
|
||||
.spawn(async move {
|
||||
|
@ -632,7 +632,7 @@ impl Platform for MacPlatform {
|
|||
};
|
||||
|
||||
if let Some(done_tx) = done_tx.take() {
|
||||
let _ = done_tx.send(result);
|
||||
let _ = done_tx.send(Ok(result));
|
||||
}
|
||||
});
|
||||
let block = block.copy();
|
||||
|
@ -643,7 +643,7 @@ impl Platform for MacPlatform {
|
|||
done_rx
|
||||
}
|
||||
|
||||
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>> {
|
||||
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Result<Option<PathBuf>>> {
|
||||
let directory = directory.to_owned();
|
||||
let (done_tx, done_rx) = oneshot::channel();
|
||||
self.foreground_executor()
|
||||
|
@ -665,7 +665,7 @@ impl Platform for MacPlatform {
|
|||
}
|
||||
|
||||
if let Some(done_tx) = done_tx.take() {
|
||||
let _ = done_tx.send(result);
|
||||
let _ = done_tx.send(Ok(result));
|
||||
}
|
||||
});
|
||||
let block = block.copy();
|
||||
|
|
|
@ -34,7 +34,7 @@ pub(crate) struct TestPlatform {
|
|||
#[derive(Default)]
|
||||
pub(crate) struct TestPrompts {
|
||||
multiple_choice: VecDeque<oneshot::Sender<usize>>,
|
||||
new_path: VecDeque<(PathBuf, oneshot::Sender<Option<PathBuf>>)>,
|
||||
new_path: VecDeque<(PathBuf, oneshot::Sender<Result<Option<PathBuf>>>)>,
|
||||
}
|
||||
|
||||
impl TestPlatform {
|
||||
|
@ -80,7 +80,7 @@ impl TestPlatform {
|
|||
.new_path
|
||||
.pop_front()
|
||||
.expect("no pending new path prompt");
|
||||
tx.send(select_path(&path)).ok();
|
||||
tx.send(Ok(select_path(&path))).ok();
|
||||
}
|
||||
|
||||
pub(crate) fn simulate_prompt_answer(&self, response_ix: usize) {
|
||||
|
@ -216,14 +216,14 @@ impl Platform for TestPlatform {
|
|||
fn prompt_for_paths(
|
||||
&self,
|
||||
_options: crate::PathPromptOptions,
|
||||
) -> oneshot::Receiver<Option<Vec<std::path::PathBuf>>> {
|
||||
) -> oneshot::Receiver<Result<Option<Vec<std::path::PathBuf>>>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn prompt_for_new_path(
|
||||
&self,
|
||||
directory: &std::path::Path,
|
||||
) -> oneshot::Receiver<Option<std::path::PathBuf>> {
|
||||
) -> oneshot::Receiver<Result<Option<std::path::PathBuf>>> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.prompts
|
||||
.borrow_mut()
|
||||
|
|
|
@ -335,7 +335,10 @@ impl Platform for WindowsPlatform {
|
|||
self.state.borrow_mut().callbacks.open_urls = Some(callback);
|
||||
}
|
||||
|
||||
fn prompt_for_paths(&self, options: PathPromptOptions) -> Receiver<Option<Vec<PathBuf>>> {
|
||||
fn prompt_for_paths(
|
||||
&self,
|
||||
options: PathPromptOptions,
|
||||
) -> Receiver<Result<Option<Vec<PathBuf>>>> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
self.foreground_executor()
|
||||
|
@ -374,7 +377,7 @@ impl Platform for WindowsPlatform {
|
|||
if hr.unwrap_err().code() == HRESULT(0x800704C7u32 as i32) {
|
||||
// user canceled error
|
||||
if let Some(tx) = tx.take() {
|
||||
tx.send(None).unwrap();
|
||||
tx.send(Ok(None)).unwrap();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
@ -393,10 +396,10 @@ impl Platform for WindowsPlatform {
|
|||
}
|
||||
|
||||
if let Some(tx) = tx.take() {
|
||||
if paths.len() == 0 {
|
||||
tx.send(None).unwrap();
|
||||
if paths.is_empty() {
|
||||
tx.send(Ok(None)).unwrap();
|
||||
} else {
|
||||
tx.send(Some(paths)).unwrap();
|
||||
tx.send(Ok(Some(paths))).unwrap();
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -405,27 +408,27 @@ impl Platform for WindowsPlatform {
|
|||
rx
|
||||
}
|
||||
|
||||
fn prompt_for_new_path(&self, directory: &Path) -> Receiver<Option<PathBuf>> {
|
||||
fn prompt_for_new_path(&self, directory: &Path) -> Receiver<Result<Option<PathBuf>>> {
|
||||
let directory = directory.to_owned();
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.foreground_executor()
|
||||
.spawn(async move {
|
||||
unsafe {
|
||||
let Ok(dialog) = show_savefile_dialog(directory) else {
|
||||
let _ = tx.send(None);
|
||||
let _ = tx.send(Ok(None));
|
||||
return;
|
||||
};
|
||||
let Ok(_) = dialog.Show(None) else {
|
||||
let _ = tx.send(None); // user cancel
|
||||
let _ = tx.send(Ok(None)); // user cancel
|
||||
return;
|
||||
};
|
||||
if let Ok(shell_item) = dialog.GetResult() {
|
||||
if let Ok(file) = shell_item.GetDisplayName(SIGDN_FILESYSPATH) {
|
||||
let _ = tx.send(Some(PathBuf::from(file.to_string().unwrap())));
|
||||
let _ = tx.send(Ok(Some(PathBuf::from(file.to_string().unwrap()))));
|
||||
return;
|
||||
}
|
||||
}
|
||||
let _ = tx.send(None);
|
||||
let _ = tx.send(Ok(None));
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue