-
Notifications
You must be signed in to change notification settings - Fork 49
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: combine open and save dialogs
- Loading branch information
Showing
12 changed files
with
421 additions
and
531 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,216 @@ | ||
// Copyright 2023 System76 <info@system76.com> | ||
// SPDX-License-Identifier: MPL-2.0 | ||
|
||
//! Dialogs for opening and save files. | ||
|
||
pub mod open; | ||
pub mod save; | ||
|
||
pub use ashpd::desktop::file_chooser::{Choice, FileFilter, SelectedFiles}; | ||
use iced::futures::{channel, SinkExt, StreamExt}; | ||
use iced::{Command, Subscription}; | ||
use std::sync::atomic::{AtomicBool, Ordering}; | ||
use thiserror::Error; | ||
|
||
/// Prevents duplicate file chooser dialog requests. | ||
static OPENED: AtomicBool = AtomicBool::new(false); | ||
|
||
/// Whether a file chooser dialog is currently active. | ||
fn dialog_active() -> bool { | ||
OPENED.load(Ordering::Relaxed) | ||
} | ||
|
||
/// Sets the existence of a file chooser dialog. | ||
fn dialog_active_set(value: bool) { | ||
OPENED.store(value, Ordering::SeqCst); | ||
} | ||
|
||
/// Creates an [`open::Dialog`] if no other file chooser exists. | ||
pub fn open_file() -> Option<open::Dialog> { | ||
if dialog_active() { | ||
None | ||
} else { | ||
Some(open::Dialog::new()) | ||
} | ||
} | ||
|
||
/// Creates a [`save::Dialog`] if no other file chooser exists. | ||
pub fn save_file() -> Option<save::Dialog> { | ||
if dialog_active() { | ||
None | ||
} else { | ||
Some(save::Dialog::new()) | ||
} | ||
} | ||
|
||
/// Creates a subscription for file chooser events. | ||
pub fn subscription<M: Send + 'static>(handle: fn(Message) -> M) -> Subscription<M> { | ||
let type_id = std::any::TypeId::of::<Handler<M>>(); | ||
|
||
iced::subscription::channel(type_id, 1, move |output| async move { | ||
let mut state = Handler { | ||
active: None, | ||
handle, | ||
output, | ||
}; | ||
|
||
loop { | ||
let (sender, mut receiver) = channel::mpsc::channel(1); | ||
|
||
state.emit(Message::Init(Sender(sender))).await; | ||
|
||
while let Some(request) = receiver.next().await { | ||
match request { | ||
Request::Close => state.close().await, | ||
|
||
Request::Open(dialog) => { | ||
state.open(dialog).await; | ||
dialog_active_set(false); | ||
} | ||
|
||
Request::Save(dialog) => { | ||
state.save(dialog).await; | ||
dialog_active_set(false); | ||
} | ||
|
||
Request::Response => state.response().await, | ||
} | ||
} | ||
} | ||
}) | ||
} | ||
|
||
/// Errors that my occur when interacting with the file chooser subscription | ||
#[derive(Debug, Error)] | ||
pub enum Error { | ||
#[error("dialog close failed")] | ||
Close(#[source] ashpd::Error), | ||
#[error("dialog open failed")] | ||
Open(#[source] ashpd::Error), | ||
#[error("dialog response failed")] | ||
Response(#[source] ashpd::Error), | ||
} | ||
|
||
/// Requests for the file chooser subscription | ||
enum Request { | ||
Close, | ||
Open(open::Dialog), | ||
Save(save::Dialog), | ||
Response, | ||
} | ||
|
||
/// Messages from the file chooser subscription. | ||
pub enum Message { | ||
Closed, | ||
Err(Error), | ||
Init(Sender), | ||
Opened, | ||
Selected(SelectedFiles), | ||
} | ||
|
||
/// Sends requests to the file chooser subscription. | ||
#[derive(Clone, Debug)] | ||
pub struct Sender(channel::mpsc::Sender<Request>); | ||
|
||
impl Sender { | ||
/// Creates a [`Command`] that closes a file chooser dialog. | ||
pub fn close(&mut self) -> Command<()> { | ||
let mut sender = self.0.clone(); | ||
|
||
crate::command::future(async move { | ||
let _res = sender.send(Request::Close).await; | ||
() | ||
}) | ||
} | ||
|
||
/// Creates a [`Command`] that opens the file chooser. | ||
pub fn open(&mut self, dialog: open::Dialog) -> Command<()> { | ||
dialog_active_set(true); | ||
let mut sender = self.0.clone(); | ||
|
||
crate::command::future(async move { | ||
let _res = sender.send(Request::Open(dialog)).await; | ||
() | ||
}) | ||
} | ||
|
||
/// Creates a [`Command`] that requests the response from a file chooser dialog. | ||
pub fn response(&mut self) -> Command<()> { | ||
let mut sender = self.0.clone(); | ||
|
||
crate::command::future(async move { | ||
let _res = sender.send(Request::Response).await; | ||
() | ||
}) | ||
} | ||
|
||
/// Creates a [`Command`] that opens a new save file dialog. | ||
pub fn save(&mut self, dialog: save::Dialog) -> Command<()> { | ||
dialog_active_set(true); | ||
let mut sender = self.0.clone(); | ||
|
||
crate::command::future(async move { | ||
let _res = sender.send(Request::Save(dialog)).await; | ||
() | ||
}) | ||
} | ||
} | ||
|
||
struct Handler<M> { | ||
active: Option<ashpd::desktop::Request<SelectedFiles>>, | ||
handle: fn(Message) -> M, | ||
output: channel::mpsc::Sender<M>, | ||
} | ||
|
||
impl<M> Handler<M> { | ||
/// Emits close request if there is an active dialog request. | ||
async fn close(&mut self) { | ||
if let Some(request) = self.active.take() { | ||
if let Err(why) = request.close().await { | ||
self.emit(Message::Err(Error::Close(why))).await; | ||
} | ||
} | ||
} | ||
|
||
async fn emit(&mut self, response: Message) { | ||
let _res = self.output.send((self.handle)(response)).await; | ||
} | ||
|
||
/// Creates a new dialog, and closes any prior active dialogs. | ||
async fn open(&mut self, dialog: open::Dialog) { | ||
let response = match open::create(dialog).await { | ||
Ok(request) => { | ||
self.active = Some(request); | ||
Message::Opened | ||
} | ||
Err(why) => Message::Err(Error::Open(why)), | ||
}; | ||
|
||
self.emit(response).await; | ||
} | ||
|
||
/// Collects selected files from the active dialog. | ||
async fn response(&mut self) { | ||
if let Some(request) = self.active.as_ref() { | ||
let response = match request.response() { | ||
Ok(selected) => Message::Selected(selected), | ||
Err(why) => Message::Err(Error::Response(why)), | ||
}; | ||
|
||
self.emit(response).await; | ||
} | ||
} | ||
|
||
/// Creates a new dialog, and closes any prior active dialogs. | ||
async fn save(&mut self, dialog: save::Dialog) { | ||
let response = match save::create(dialog).await { | ||
Ok(request) => { | ||
self.active = Some(request); | ||
Message::Opened | ||
} | ||
Err(why) => Message::Err(Error::Open(why)), | ||
}; | ||
|
||
self.emit(response).await; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
// Copyright 2023 System76 <info@system76.com> | ||
// SPDX-License-Identifier: MPL-2.0 | ||
|
||
//! Request to open files and/or directories. | ||
//! | ||
//! Check out the [open-dialog](https://github.com/pop-os/libcosmic/tree/master/examples/open-dialog) | ||
//! example in our repository. | ||
|
||
use derive_setters::Setters; | ||
use iced::Command; | ||
|
||
/// A builder for an open file dialog, passed as a request by a [`Sender`] | ||
#[derive(Setters)] | ||
#[must_use] | ||
pub struct Dialog { | ||
/// The label for the dialog's window title. | ||
title: String, | ||
|
||
/// The label for the accept button. Mnemonic underlines are allowed. | ||
#[setters(strip_option)] | ||
accept_label: Option<String>, | ||
|
||
/// Whether to select for folders instead of files. Default is to select files. | ||
include_directories: bool, | ||
|
||
/// Modal dialogs require user input before continuing the program. | ||
modal: bool, | ||
|
||
/// Whether to allow selection of multiple files. Default is no. | ||
multiple_files: bool, | ||
|
||
/// Adds a list of choices. | ||
choices: Vec<super::Choice>, | ||
|
||
/// Specifies the default file filter. | ||
#[setters(into)] | ||
current_filter: Option<super::FileFilter>, | ||
|
||
/// A collection of file filters. | ||
filters: Vec<super::FileFilter>, | ||
} | ||
|
||
impl Dialog { | ||
pub(super) const fn new() -> Self { | ||
Self { | ||
title: String::new(), | ||
accept_label: None, | ||
include_directories: false, | ||
modal: true, | ||
multiple_files: false, | ||
current_filter: None, | ||
choices: Vec::new(), | ||
filters: Vec::new(), | ||
} | ||
} | ||
|
||
/// Creates a [`Command`] which opens the dialog. | ||
pub fn create(self, sender: &mut super::Sender) -> Command<()> { | ||
sender.open(self) | ||
} | ||
|
||
/// Adds a choice. | ||
pub fn choice(mut self, choice: impl Into<super::Choice>) -> Self { | ||
self.choices.push(choice.into()); | ||
self | ||
} | ||
|
||
/// Adds a files filter. | ||
pub fn filter(mut self, filter: impl Into<super::FileFilter>) -> Self { | ||
self.filters.push(filter.into()); | ||
self | ||
} | ||
} | ||
|
||
/// Creates a new file dialog, and begins to await its responses. | ||
pub(super) async fn create( | ||
dialog: Dialog, | ||
) -> ashpd::Result<ashpd::desktop::Request<super::SelectedFiles>> { | ||
ashpd::desktop::file_chooser::OpenFileRequest::default() | ||
.title(Some(dialog.title.as_str())) | ||
.accept_label(dialog.accept_label.as_deref()) | ||
.directory(dialog.include_directories) | ||
.modal(dialog.modal) | ||
.multiple(dialog.multiple_files) | ||
.choices(dialog.choices) | ||
.filters(dialog.filters) | ||
.current_filter(dialog.current_filter) | ||
.send() | ||
.await | ||
} |
Oops, something went wrong.