From 23c4832f9ab9d1975278c78c871b1ff35e40b901 Mon Sep 17 00:00:00 2001 From: yggverse Date: Tue, 10 Dec 2024 20:46:22 +0200 Subject: [PATCH] separate child components, use custom actions to activate extra features --- .../tab/item/page/content/status/download.rs | 159 +++++++----------- .../page/content/status/download/cancel.rs | 52 ++++++ .../page/content/status/download/choose.rs | 53 ++++++ .../item/page/content/status/download/open.rs | 52 ++++++ .../page/content/status/download/progress.rs | 25 +++ .../page/content/status/download/status.rs | 23 +++ 6 files changed, 266 insertions(+), 98 deletions(-) create mode 100644 src/app/browser/window/tab/item/page/content/status/download/cancel.rs create mode 100644 src/app/browser/window/tab/item/page/content/status/download/choose.rs create mode 100644 src/app/browser/window/tab/item/page/content/status/download/open.rs create mode 100644 src/app/browser/window/tab/item/page/content/status/download/progress.rs create mode 100644 src/app/browser/window/tab/item/page/content/status/download/status.rs diff --git a/src/app/browser/window/tab/item/page/content/status/download.rs b/src/app/browser/window/tab/item/page/content/status/download.rs index e4cfadd6..a05ea062 100644 --- a/src/app/browser/window/tab/item/page/content/status/download.rs +++ b/src/app/browser/window/tab/item/page/content/status/download.rs @@ -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") diff --git a/src/app/browser/window/tab/item/page/content/status/download/cancel.rs b/src/app/browser/window/tab/item/page/content/status/download/cancel.rs new file mode 100644 index 00000000..1308fffa --- /dev/null +++ b/src/app/browser/window/tab/item/page/content/status/download/cancel.rs @@ -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()) + }) + } +} diff --git a/src/app/browser/window/tab/item/page/content/status/download/choose.rs b/src/app/browser/window/tab/item/page/content/status/download/choose.rs new file mode 100644 index 00000000..d861f4b3 --- /dev/null +++ b/src/app/browser/window/tab/item/page/content/status/download/choose.rs @@ -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()) + }) + } +} diff --git a/src/app/browser/window/tab/item/page/content/status/download/open.rs b/src/app/browser/window/tab/item/page/content/status/download/open.rs new file mode 100644 index 00000000..237a95e8 --- /dev/null +++ b/src/app/browser/window/tab/item/page/content/status/download/open.rs @@ -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()) + }) + } +} diff --git a/src/app/browser/window/tab/item/page/content/status/download/progress.rs b/src/app/browser/window/tab/item/page/content/status/download/progress.rs new file mode 100644 index 00000000..044c7619 --- /dev/null +++ b/src/app/browser/window/tab/item/page/content/status/download/progress.rs @@ -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(), + } + } +} diff --git a/src/app/browser/window/tab/item/page/content/status/download/status.rs b/src/app/browser/window/tab/item/page/content/status/download/status.rs new file mode 100644 index 00000000..01638ec6 --- /dev/null +++ b/src/app/browser/window/tab/item/page/content/status/download/status.rs @@ -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(), + } + } +}