refactor: combine open and save dialogs
This commit is contained in:
parent
2602e28d22
commit
a8ce524baa
12 changed files with 421 additions and 531 deletions
216
src/dialog/file_chooser/mod.rs
Normal file
216
src/dialog/file_chooser/mod.rs
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue