mirror of
https://github.com/YGGverse/Yoda.git
synced 2025-01-15 01:00:02 +00:00
separate child components, use custom actions to activate extra features
This commit is contained in:
parent
5c8aba30c5
commit
23c4832f9a
@ -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")
|
||||
|
@ -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())
|
||||
})
|
||||
}
|
||||
}
|
@ -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())
|
||||
})
|
||||
}
|
||||
}
|
@ -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())
|
||||
})
|
||||
}
|
||||
}
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user