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