separate child components, use custom actions to activate extra features

This commit is contained in:
yggverse 2024-12-10 20:46:22 +02:00
parent 5c8aba30c5
commit 23c4832f9a
6 changed files with 266 additions and 98 deletions

View File

@ -1,22 +1,23 @@
mod cancel;
mod choose;
mod open;
mod progress;
mod status;
use cancel::Cancel;
use choose::Choose;
use open::Open;
use progress::Progress;
use status::Status;
use adw::StatusPage;
use gtk::{
gio::{Cancellable, File},
prelude::{BoxExt, ButtonExt, CancellableExt, WidgetExt},
Align,
Box,
Button,
FileDialog,
FileLauncher,
Label,
Orientation,
Spinner, // use adw::Spinner; @TODO adw 1.6 / ubuntu 24.10+
Window,
prelude::{BoxExt, CancellableExt, WidgetExt},
Box, FileDialog, FileLauncher, Label, Orientation, Window,
};
use std::rc::Rc;
const MARGIN: i32 = 16;
const SPINNER_SIZE: i32 = 32; // 16-64
/// Create new [StatusPage](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.StatusPage.html)
/// with progress indication and UI controls
/// * applies callback function once on destination [File](https://docs.gtk.org/gio/iface.File.html) selected
@ -26,151 +27,113 @@ pub fn new(
cancellable: &Cancellable,
on_choose: impl Fn(File, Label) + 'static,
) -> StatusPage {
// Init file chooser dialog
// Init components
let dialog = FileDialog::builder().initial_name(initial_filename).build();
// Init file launcher dialog
let file_launcher = FileLauncher::new(File::NONE);
// Init spinner component
let spinner = Spinner::builder()
.height_request(SPINNER_SIZE)
.visible(false)
.width_request(SPINNER_SIZE)
.build();
let progress = Rc::new(Progress::new());
let status = Rc::new(Status::new());
let cancel = Rc::new(Cancel::new());
let open = Rc::new(Open::new());
let choose = Rc::new(Choose::new());
// Init `status` feature
// * indicates current download state in text label
let status = Label::builder()
.label("Choose location to download")
.margin_top(MARGIN)
.build();
// Init `cancel` feature
// * applies shared `Cancellable`
let cancel = Button::builder()
.css_classes(["error"])
.halign(Align::Center)
.label("Cancel")
.margin_top(MARGIN)
.visible(false)
.build();
cancel.connect_clicked({
// Init events
cancel.on_activate({
let cancellable = cancellable.clone();
let spinner = spinner.clone();
let progress = progress.clone();
let status = status.clone();
move |this| {
move |_, button| {
// apply cancellable
cancellable.cancel();
// deactivate `spinner`
spinner.set_visible(false);
spinner.stop();
progress.spinner.set_visible(false);
progress.spinner.stop();
// update `status`
status.set_css_classes(&["warning"]);
status.set_label("Operation cancelled");
status.label.set_css_classes(&["warning"]);
status.label.set_label("Operation cancelled");
// hide self
this.set_visible(false);
button.set_visible(false);
}
});
// Init `open` feature
// * open selected file on download complete
let open = Button::builder()
.css_classes(["accent"])
.halign(Align::Center)
.label("Open")
.margin_top(MARGIN)
.visible(false)
.build();
open.connect_clicked({
open.on_activate({
let cancellable = cancellable.clone();
let file_launcher = file_launcher.clone();
let status = status.clone();
move |this| {
this.set_sensitive(false); // lock
move |_, button| {
button.set_sensitive(false); // lock
file_launcher.launch(Window::NONE, Some(&cancellable), {
let status = status.clone();
let this = this.clone();
let button = button.clone();
move |result| {
if let Err(ref e) = result {
status.set_css_classes(&["error"]);
status.set_label(e.message())
status.label.set_css_classes(&["error"]);
status.label.set_label(e.message())
}
this.set_sensitive(true); // unlock
button.set_sensitive(true); // unlock
}
})
}
});
// Init `choose` feature
// * select file destination for download
let choose = Button::builder()
.css_classes(["accent"])
.halign(Align::Center)
.label("Choose..")
.margin_top(MARGIN)
.build();
choose.connect_clicked({
choose.on_activate({
// init shared references
let cancellable = cancellable.clone();
let cancel = cancel.clone();
let dialog = dialog.clone();
let file_launcher = file_launcher.clone();
let spinner = spinner.clone();
let progress = progress.clone();
let status = status.clone();
let on_choose = Rc::new(on_choose);
move |this| {
move |_, button| {
// lock choose button to prevent double click
this.set_sensitive(false);
button.set_sensitive(false);
dialog.save(Window::NONE, Some(&cancellable), {
// delegate shared references
let cancel = cancel.clone();
let file_launcher = file_launcher.clone();
let spinner = spinner.clone();
let progress = progress.clone();
let status = status.clone();
let this = this.clone();
let button = button.clone();
let on_choose = on_choose.clone();
move |result| {
this.set_sensitive(true); // unlock
button.set_sensitive(true); // unlock
match result {
Ok(file) => {
// update destination file
file_launcher.set_file(Some(&file));
// update `status`
status.set_css_classes(&[]);
status.set_label("Loading...");
status.label.set_css_classes(&[]);
status.label.set_label("Loading...");
// show `cancel` button
cancel.set_visible(true);
cancel.button.set_visible(true);
// show `spinner`
spinner.set_visible(true);
spinner.start();
progress.spinner.set_visible(true);
progress.spinner.start();
// hide self
this.set_visible(false);
button.set_visible(false);
// callback
on_choose(file, status)
on_choose(file, status.label.clone())
}
Err(e) => {
// update destination file
file_launcher.set_file(File::NONE);
// update `spinner`
spinner.set_visible(false);
spinner.stop();
progress.spinner.set_visible(false);
progress.spinner.stop();
// update `status`
status.set_css_classes(&["warning"]);
status.set_label(e.message())
status.label.set_css_classes(&["warning"]);
status.label.set_label(e.message())
}
}
}
@ -178,16 +141,16 @@ pub fn new(
}
});
// Init main container
// Init `child` as the container for extra features
let child = Box::builder().orientation(Orientation::Vertical).build();
child.append(&spinner);
child.append(&status);
child.append(&cancel);
child.append(&choose);
child.append(&open);
child.append(&progress.spinner);
child.append(&status.label);
child.append(&cancel.button);
child.append(&choose.button);
child.append(&open.button);
// Done
// Init main widget
StatusPage::builder()
.child(&child)
.icon_name("document-save-symbolic")

View File

@ -0,0 +1,52 @@
use gtk::{
gio::SimpleAction,
glib::{uuid_string_random, SignalHandlerId},
prelude::ActionExt,
Align, Button,
};
// Defaults
const CSS_CLASSES: [&str; 1] = ["error"];
const LABEL: &str = "Cancel";
const MARGIN: i32 = 16;
/// Cancel download using shared [Cancellable](https://docs.gtk.org/gio/class.Cancellable.html)
pub struct Cancel {
pub action: SimpleAction,
pub button: Button,
}
impl Cancel {
// Constructors
/// Create new `Self`
pub fn new() -> Self {
let action = SimpleAction::new(&uuid_string_random(), None);
let button = Button::builder()
.action_name(action.name())
.css_classes(CSS_CLASSES)
.halign(Align::Center)
.label(LABEL)
.margin_top(MARGIN)
.visible(false)
.build();
Self { action, button }
}
// Actions
/// Formatted action connector for external implementation
pub fn on_activate(
&self,
callback: impl Fn(SimpleAction, Button) + 'static,
) -> SignalHandlerId {
self.action.connect_activate({
let action = self.action.clone();
let button = self.button.clone();
move |_, _| callback(action.clone(), button.clone())
})
}
}

View File

@ -0,0 +1,53 @@
use gtk::{
gio::SimpleAction,
glib::{uuid_string_random, SignalHandlerId},
prelude::ActionExt,
Align, Button,
};
// Defaults
const CSS_CLASSES: [&str; 1] = ["error"];
const LABEL: &str = "Choose";
const MARGIN: i32 = 16;
/// Choose destination [File](https://docs.gtk.org/gio/iface.File.html)
/// to write bytes on download
pub struct Choose {
pub action: SimpleAction,
pub button: Button,
}
impl Choose {
// Constructors
/// Create new `Self`
pub fn new() -> Self {
let action = SimpleAction::new(&uuid_string_random(), None);
let button = Button::builder()
.action_name(action.name())
.css_classes(CSS_CLASSES)
.halign(Align::Center)
.label(LABEL)
.margin_top(MARGIN)
.visible(false)
.build();
Self { action, button }
}
// Actions
/// Formatted action connector for external implementation
pub fn on_activate(
&self,
callback: impl Fn(SimpleAction, Button) + 'static,
) -> SignalHandlerId {
self.action.connect_activate({
let action = self.action.clone();
let button = self.button.clone();
move |_, _| callback(action.clone(), button.clone())
})
}
}

View File

@ -0,0 +1,52 @@
use gtk::{
gio::SimpleAction,
glib::{uuid_string_random, SignalHandlerId},
prelude::ActionExt,
Align, Button,
};
// Defaults
const CSS_CLASSES: [&str; 1] = ["error"];
const LABEL: &str = "Open";
const MARGIN: i32 = 16;
/// Open [File](https://docs.gtk.org/gio/iface.File.html) on download complete
pub struct Open {
pub action: SimpleAction,
pub button: Button,
}
impl Open {
// Constructors
/// Create new `Self`
pub fn new() -> Self {
let action = SimpleAction::new(&uuid_string_random(), None);
let button = Button::builder()
.action_name(action.name())
.css_classes(CSS_CLASSES)
.halign(Align::Center)
.label(LABEL)
.margin_top(MARGIN)
.visible(false)
.build();
Self { action, button }
}
// Actions
/// Formatted action connector for external implementation
pub fn on_activate(
&self,
callback: impl Fn(SimpleAction, Button) + 'static,
) -> SignalHandlerId {
self.action.connect_activate({
let action = self.action.clone();
let button = self.button.clone();
move |_, _| callback(action.clone(), button.clone())
})
}
}

View File

@ -0,0 +1,25 @@
use gtk::Spinner; // use adw::Spinner; @TODO adw 1.6 / ubuntu 24.10+
// Defaults
const SIZE: i32 = 32; // 16-64
/// Animate loading process by the [Spinner](https://docs.gtk.org/gtk4/class.Spinner.html)
pub struct Progress {
pub spinner: Spinner,
}
impl Progress {
// Constructors
/// Create new `Self`
pub fn new() -> Self {
Self {
spinner: Spinner::builder()
.height_request(SIZE)
.visible(false)
.width_request(SIZE)
.build(),
}
}
}

View File

@ -0,0 +1,23 @@
use gtk::Label;
// Defaults
const LABEL: &str = "Choose location to download";
const MARGIN: i32 = 16;
/// Indicate current download state as the text
/// [Label](https://docs.gtk.org/gtk4/class.Label.html)
pub struct Status {
pub label: Label,
}
impl Status {
// Constructors
/// Create new `Self`
pub fn new() -> Self {
Self {
label: Label::builder().label(LABEL).margin_top(MARGIN).build(),
}
}
}