diff --git a/book/listings/Cargo.toml b/book/listings/Cargo.toml index 5ecab635a664..268a18761470 100644 --- a/book/listings/Cargo.toml +++ b/book/listings/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" [dependencies] gtk = { version = "*", package = "gtk4", features = ["v4_8"] } -adw = { version = ">= 0.3.1", package = "libadwaita", features = ["v1_2"] } +adw = { version = ">= 0.3.1", package = "libadwaita", features = ["v1_3"] } once_cell = "1.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/book/listings/todo/2/window/imp.rs b/book/listings/todo/2/window/imp.rs index 35b6ce73a16d..2fbc4b55a565 100644 --- a/book/listings/todo/2/window/imp.rs +++ b/book/listings/todo/2/window/imp.rs @@ -25,6 +25,7 @@ pub struct Window { } // ANCHOR_END: struct_default +// ANCHOR: object_subclass // The central trait for subclassing a GObject #[glib::object_subclass] impl ObjectSubclass for Window { @@ -35,12 +36,18 @@ impl ObjectSubclass for Window { fn class_init(klass: &mut Self::Class) { klass.bind_template(); + + // Create action to remove done tasks and add to action group "win" + klass.install_action("win.remove-done-tasks", None, |window, _, _| { + window.remove_done_tasks(); + }); } fn instance_init(obj: &InitializingObject) { obj.init_template(); } } +// ANCHOR_END: object_subclass // ANCHOR: object_impl // Trait shared by all GObjects diff --git a/book/listings/todo/2/window/mod.rs b/book/listings/todo/2/window/mod.rs index 610a67360350..ae4f227b57bf 100644 --- a/book/listings/todo/2/window/mod.rs +++ b/book/listings/todo/2/window/mod.rs @@ -222,27 +222,25 @@ impl Window { // Create action from key "filter" and add to action group "win" let action_filter = self.settings().create_action("filter"); self.add_action(&action_filter); - - // Create action to remove done tasks and add to action group "win" - let action_remove_done_tasks = gio::ActionEntry::builder("remove-done-tasks") - .activate(move |window: &Self, _, _| { - let tasks = window.tasks(); - let mut position = 0; - while let Some(item) = tasks.item(position) { - // Get `TaskObject` from `glib::Object` - let task_object = item - .downcast_ref::() - .expect("The object needs to be of type `TaskObject`."); - - if task_object.is_completed() { - tasks.remove(position); - } else { - position += 1; - } - } - }) - .build(); - self.add_action_entries([action_remove_done_tasks]); } // ANCHOR_END: setup_actions + + // ANCHOR: remove_done_tasks + fn remove_done_tasks(&self) { + let tasks = self.tasks(); + let mut position = 0; + while let Some(item) = tasks.item(position) { + // Get `TaskObject` from `glib::Object` + let task_object = item + .downcast_ref::() + .expect("The object needs to be of type `TaskObject`."); + + if task_object.is_completed() { + tasks.remove(position); + } else { + position += 1; + } + } + } + // ANCHOR_END: remove_done_tasks } diff --git a/book/listings/todo/3/window/imp.rs b/book/listings/todo/3/window/imp.rs index deef76cb0b56..27a690fa93f9 100644 --- a/book/listings/todo/3/window/imp.rs +++ b/book/listings/todo/3/window/imp.rs @@ -33,6 +33,11 @@ impl ObjectSubclass for Window { fn class_init(klass: &mut Self::Class) { klass.bind_template(); + + // Create action to remove done tasks and add to action group "win" + klass.install_action("win.remove-done-tasks", None, |window, _, _| { + window.remove_done_tasks(); + }); } fn instance_init(obj: &InitializingObject) { diff --git a/book/listings/todo/3/window/mod.rs b/book/listings/todo/3/window/mod.rs index ca1bec27eda2..2d34928c8bf9 100644 --- a/book/listings/todo/3/window/mod.rs +++ b/book/listings/todo/3/window/mod.rs @@ -211,26 +211,22 @@ impl Window { // Create action from key "filter" and add to action group "win" let action_filter = self.settings().create_action("filter"); self.add_action(&action_filter); + } + + fn remove_done_tasks(&self) { + let tasks = self.tasks(); + let mut position = 0; + while let Some(item) = tasks.item(position) { + // Get `TaskObject` from `glib::Object` + let task_object = item + .downcast_ref::() + .expect("The object needs to be of type `TaskObject`."); - // Create action to remove done tasks and add to action group "win" - let action_remove_done_tasks = gio::ActionEntry::builder("remove-done-tasks") - .activate(move |window: &Self, _, _| { - let tasks = window.tasks(); - let mut position = 0; - while let Some(item) = tasks.item(position) { - // Get `TaskObject` from `glib::Object` - let task_object = item - .downcast_ref::() - .expect("The object needs to be of type `TaskObject`."); - - if task_object.is_completed() { - tasks.remove(position); - } else { - position += 1; - } - } - }) - .build(); - self.add_action_entries([action_remove_done_tasks]); + if task_object.is_completed() { + tasks.remove(position); + } else { + position += 1; + } + } } } diff --git a/book/listings/todo/4/window/imp.rs b/book/listings/todo/4/window/imp.rs index 0bef45699675..72b1ac92038f 100644 --- a/book/listings/todo/4/window/imp.rs +++ b/book/listings/todo/4/window/imp.rs @@ -33,6 +33,11 @@ impl ObjectSubclass for Window { fn class_init(klass: &mut Self::Class) { klass.bind_template(); + + // Create action to remove done tasks and add to action group "win" + klass.install_action("win.remove-done-tasks", None, |window, _, _| { + window.remove_done_tasks(); + }); } fn instance_init(obj: &InitializingObject) { diff --git a/book/listings/todo/4/window/mod.rs b/book/listings/todo/4/window/mod.rs index 732da8353fcd..1c13830171fe 100644 --- a/book/listings/todo/4/window/mod.rs +++ b/book/listings/todo/4/window/mod.rs @@ -210,26 +210,22 @@ impl Window { // Create action from key "filter" and add to action group "win" let action_filter = self.settings().create_action("filter"); self.add_action(&action_filter); + } + + fn remove_done_tasks(&self) { + let tasks = self.tasks(); + let mut position = 0; + while let Some(item) = tasks.item(position) { + // Get `TaskObject` from `glib::Object` + let task_object = item + .downcast_ref::() + .expect("The object needs to be of type `TaskObject`."); - // Create action to remove done tasks and add to action group "win" - let action_remove_done_tasks = gio::ActionEntry::builder("remove-done-tasks") - .activate(move |window: &Self, _, _| { - let tasks = window.tasks(); - let mut position = 0; - while let Some(item) = tasks.item(position) { - // Get `TaskObject` from `glib::Object` - let task_object = item - .downcast_ref::() - .expect("The object needs to be of type `TaskObject`."); - - if task_object.is_completed() { - tasks.remove(position); - } else { - position += 1; - } - } - }) - .build(); - self.add_action_entries([action_remove_done_tasks]); + if task_object.is_completed() { + tasks.remove(position); + } else { + position += 1; + } + } } } diff --git a/book/listings/todo/5/window/imp.rs b/book/listings/todo/5/window/imp.rs index b73f1023f20b..01d43955844f 100644 --- a/book/listings/todo/5/window/imp.rs +++ b/book/listings/todo/5/window/imp.rs @@ -33,6 +33,11 @@ impl ObjectSubclass for Window { fn class_init(klass: &mut Self::Class) { klass.bind_template(); + + // Create action to remove done tasks and add to action group "win" + klass.install_action("win.remove-done-tasks", None, |window, _, _| { + window.remove_done_tasks(); + }); } fn instance_init(obj: &InitializingObject) { diff --git a/book/listings/todo/5/window/mod.rs b/book/listings/todo/5/window/mod.rs index d49a82e35306..b2db6b289763 100644 --- a/book/listings/todo/5/window/mod.rs +++ b/book/listings/todo/5/window/mod.rs @@ -211,26 +211,22 @@ impl Window { // Create action from key "filter" and add to action group "win" let action_filter = self.settings().create_action("filter"); self.add_action(&action_filter); + } + + fn remove_done_tasks(&self) { + let tasks = self.tasks(); + let mut position = 0; + while let Some(item) = tasks.item(position) { + // Get `TaskObject` from `glib::Object` + let task_object = item + .downcast_ref::() + .expect("The object needs to be of type `TaskObject`."); - // Create action to remove done tasks and add to action group "win" - let action_remove_done_tasks = gio::ActionEntry::builder("remove-done-tasks") - .activate(move |window: &Self, _, _| { - let tasks = window.tasks(); - let mut position = 0; - while let Some(item) = tasks.item(position) { - // Get `TaskObject` from `glib::Object` - let task_object = item - .downcast_ref::() - .expect("The object needs to be of type `TaskObject`."); - - if task_object.is_completed() { - tasks.remove(position); - } else { - position += 1; - } - } - }) - .build(); - self.add_action_entries([action_remove_done_tasks]); + if task_object.is_completed() { + tasks.remove(position); + } else { + position += 1; + } + } } } diff --git a/book/listings/todo/6/window/imp.rs b/book/listings/todo/6/window/imp.rs index d5e30f9f83b5..2e0e3a55795d 100644 --- a/book/listings/todo/6/window/imp.rs +++ b/book/listings/todo/6/window/imp.rs @@ -36,6 +36,11 @@ impl ObjectSubclass for Window { fn class_init(klass: &mut Self::Class) { klass.bind_template(); + + // Create action to remove done tasks and add to action group "win" + klass.install_action("win.remove-done-tasks", None, |window, _, _| { + window.remove_done_tasks(); + }); } fn instance_init(obj: &InitializingObject) { diff --git a/book/listings/todo/6/window/mod.rs b/book/listings/todo/6/window/mod.rs index c73fe63fb5aa..2349dd6b0390 100644 --- a/book/listings/todo/6/window/mod.rs +++ b/book/listings/todo/6/window/mod.rs @@ -211,26 +211,22 @@ impl Window { // Create action from key "filter" and add to action group "win" let action_filter = self.settings().create_action("filter"); self.add_action(&action_filter); + } - // Create action to remove done tasks and add to action group "win" - let action_remove_done_tasks = gio::ActionEntry::builder("remove-done-tasks") - .activate(move |window: &Self, _, _| { - let tasks = window.tasks(); - let mut position = 0; - while let Some(item) = tasks.item(position) { - // Get `TaskObject` from `glib::Object` - let task_object = item - .downcast_ref::() - .expect("The object needs to be of type `TaskObject`."); - - if task_object.is_completed() { - tasks.remove(position); - } else { - position += 1; - } - } - }) - .build(); - self.add_action_entries([action_remove_done_tasks]); + fn remove_done_tasks(&self) { + let tasks = self.tasks(); + let mut position = 0; + while let Some(item) = tasks.item(position) { + // Get `TaskObject` from `glib::Object` + let task_object = item + .downcast_ref::() + .expect("The object needs to be of type `TaskObject`."); + + if task_object.is_completed() { + tasks.remove(position); + } else { + position += 1; + } + } } } diff --git a/book/listings/todo/7/window/imp.rs b/book/listings/todo/7/window/imp.rs index 5e5ae543c75c..4f44bc11caec 100644 --- a/book/listings/todo/7/window/imp.rs +++ b/book/listings/todo/7/window/imp.rs @@ -37,6 +37,11 @@ impl ObjectSubclass for Window { fn class_init(klass: &mut Self::Class) { klass.bind_template(); + + // Create action to remove done tasks and add to action group "win" + klass.install_action("win.remove-done-tasks", None, |window, _, _| { + window.remove_done_tasks(); + }); } fn instance_init(obj: &InitializingObject) { diff --git a/book/listings/todo/7/window/mod.rs b/book/listings/todo/7/window/mod.rs index bf9194576f47..4ebf76cb1b88 100644 --- a/book/listings/todo/7/window/mod.rs +++ b/book/listings/todo/7/window/mod.rs @@ -214,26 +214,22 @@ impl Window { // Create action from key "filter" and add to action group "win" let action_filter = self.settings().create_action("filter"); self.add_action(&action_filter); + } - // Create action to remove done tasks and add to action group "win" - let action_remove_done_tasks = gio::ActionEntry::builder("remove-done-tasks") - .activate(move |window: &Self, _, _| { - let tasks = window.tasks(); - let mut position = 0; - while let Some(item) = tasks.item(position) { - // Get `TaskObject` from `glib::Object` - let task_object = item - .downcast_ref::() - .expect("The object needs to be of type `TaskObject`."); - - if task_object.is_completed() { - tasks.remove(position); - } else { - position += 1; - } - } - }) - .build(); - self.add_action_entries([action_remove_done_tasks]); + fn remove_done_tasks(&self) { + let tasks = self.tasks(); + let mut position = 0; + while let Some(item) = tasks.item(position) { + // Get `TaskObject` from `glib::Object` + let task_object = item + .downcast_ref::() + .expect("The object needs to be of type `TaskObject`."); + + if task_object.is_completed() { + tasks.remove(position); + } else { + position += 1; + } + } } } diff --git a/book/listings/todo/8/window/imp.rs b/book/listings/todo/8/window/imp.rs index 16bd2591b12a..55384f535068 100644 --- a/book/listings/todo/8/window/imp.rs +++ b/book/listings/todo/8/window/imp.rs @@ -41,6 +41,7 @@ pub struct Window { } // ANCHOR_END: struct +// ANCHOR: object_subclass // The central trait for subclassing a GObject #[glib::object_subclass] impl ObjectSubclass for Window { @@ -51,12 +52,27 @@ impl ObjectSubclass for Window { fn class_init(klass: &mut Self::Class) { klass.bind_template(); + + // Create action to remove done tasks and add to action group "win" + klass.install_action("win.remove-done-tasks", None, |window, _, _| { + window.remove_done_tasks(); + }); + + // Create async action to create new collection and add to action group "win" + klass.install_action_async( + "win.new-collection", + None, + |window, _, _| async move { + window.new_collection().await; + }, + ); } fn instance_init(obj: &InitializingObject) { obj.init_template(); } } +// ANCHOR_END: object_subclass // ANCHOR: object_impl // Trait shared by all GObjects diff --git a/book/listings/todo/8/window/mod.rs b/book/listings/todo/8/window/mod.rs index 944a541bf8f5..347731952a2f 100644 --- a/book/listings/todo/8/window/mod.rs +++ b/book/listings/todo/8/window/mod.rs @@ -361,40 +361,27 @@ impl Window { // Create action from key "filter" and add to action group "win" let action_filter = self.settings().create_action("filter"); self.add_action(&action_filter); + } - // Create action to remove done tasks and add to action group "win" - let action_remove_done_tasks = gio::ActionEntry::builder("remove-done-tasks") - .activate(move |window: &Self, _, _| { - let tasks = window.tasks(); - let mut position = 0; - while let Some(item) = tasks.item(position) { - // Get `TaskObject` from `glib::Object` - let task_object = item - .downcast_ref::() - .expect("The object needs to be of type `TaskObject`."); - - if task_object.is_completed() { - tasks.remove(position); - } else { - position += 1; - } - } - }) - .build(); + fn remove_done_tasks(&self) { + let tasks = self.tasks(); + let mut position = 0; + while let Some(item) = tasks.item(position) { + // Get `TaskObject` from `glib::Object` + let task_object = item + .downcast_ref::() + .expect("The object needs to be of type `TaskObject`."); - // ANCHOR: setup_actions - // Create action to create new collection and add to action group "win" - let action_new_collection = gio::ActionEntry::builder("new-collection") - .activate(move |window: &Self, _, _| { - window.new_collection(); - }) - .build(); - self.add_action_entries([action_remove_done_tasks, action_new_collection]); - // ANCHOR_END: setup_actions + if task_object.is_completed() { + tasks.remove(position); + } else { + position += 1; + } + } } // ANCHOR: new_collection - fn new_collection(&self) { + async fn new_collection(&self) { // Create entry let entry = Entry::builder() .placeholder_text("Name") @@ -434,34 +421,26 @@ impl Window { } })); - // Connect response to dialog - dialog.connect_response( - None, - clone!(@weak self as window, @weak entry => move |dialog, response| { - // Destroy dialog - dialog.destroy(); + let response = dialog.choose_future().await; - // Return if the user chose a response different than `create_response` - if response != create_response { - return; - } + // Return if the user chose `cancel_response` + if response == cancel_response { + return; + } - // Create a new list store - let tasks = gio::ListStore::new::(); + // Create a new list store + let tasks = gio::ListStore::new::(); - // Create a new collection object from the title the user provided - let title = entry.text().to_string(); - let collection = CollectionObject::new(&title, tasks); + // Create a new collection object from the title the user provided + let title = entry.text().to_string(); + let collection = CollectionObject::new(&title, tasks); - // Add new collection object and set current tasks - window.collections().append(&collection); - window.set_current_collection(collection); + // Add new collection object and set current tasks + self.collections().append(&collection); + self.set_current_collection(collection); - // Let the leaflet navigate to the next child - window.imp().leaflet.navigate(NavigationDirection::Forward); - }), - ); - dialog.present(); + // Let the leaflet navigate to the next child + self.imp().leaflet.navigate(NavigationDirection::Forward); } // ANCHOR_END: new_collection } diff --git a/book/src/libadwaita.md b/book/src/libadwaita.md index 3143893a6946..94c7818a8307 100644 --- a/book/src/libadwaita.md +++ b/book/src/libadwaita.md @@ -14,7 +14,7 @@ Libadwaita is a library augmenting GTK 4 which: In order to use the Rust bindings, add the [libadwaita crate](https://crates.io/crates/libadwaita) as dependency by executing: ``` -cargo add libadwaita --rename adw --features v1_2 +cargo add libadwaita --rename adw --features v1_3 ``` The versions of the `gtk4` and `libadwaita` crates need to be synced. diff --git a/book/src/todo_2.md b/book/src/todo_2.md index 89291f377535..d7a3a2cd0d18 100644 --- a/book/src/todo_2.md +++ b/book/src/todo_2.md @@ -107,7 +107,6 @@ Filename: listings/todo/2/window/mod.rs @@ -115,6 +114,27 @@ Filename: listings/todo/2/window/imp.rs + +```rust +{{#rustdoc_include ../listings/todo/2/window/imp.rs:object_subclass}} +``` + +This is the implementation of `remove_done_tasks`. +We iterate through the `gio::ListStore` and remove all completed task objects. + +Filename: listings/todo/2/window/mod.rs + +```rust +{{#rustdoc_include ../listings/todo/2/window/mod.rs:remove_done_tasks}} +``` + After activating the action "win.filter", the corresponding setting will be changed. So we need a method which translates this setting into a filter that the [`gtk::FilterListModel`](https://gtk-rs.org/gtk4-rs/stable/latest/docs/gtk4/struct.FilterListModel.html) understands. The possible states are "All", "Open" and "Done". diff --git a/book/src/todo_4.md b/book/src/todo_4.md index 7c129d1debcf..e9d44b228ddc 100644 --- a/book/src/todo_4.md +++ b/book/src/todo_4.md @@ -434,21 +434,23 @@ As soon as we start typing, the button becomes sensitive. When we remove all typed letters and the entry becomes empty again, the "Create" button becomes insensitive and the entry gets the "error" style. After clicking the "Create" button, a new collection is created, and we navigate to its task view. -To implement that behavior we will first add a "new-collection" action to the `setup_actions` method. +To implement that behavior we will first add a "new-collection" action to `class_init` method. This action will be activated by a click on the `+` button as well as on the button in the placeholder page. +We are using [`install_action_async`](https://gtk-rs.org/gtk4-rs/stable/latest/docs/gtk4/subclass/widget/trait.WidgetClassExt.html#method.install_action_async). +It is a convenient way to add asynchronous actions to subclassed widgets. -Filename: listings/todo/8/window/mod.rs +Filename: listings/todo/8/window/imp.rs ```rust,no_run,noplayground -{{#rustdoc_include ../listings/todo/8/window/mod.rs:setup_actions}} +{{#rustdoc_include ../listings/todo/8/window/imp.rs:object_subclass}} ``` -As soon as the "new-collection" action is activated, the `new_collection` method is called. +As soon as the "new-collection" action is activated, the `async` `new_collection` method is called. Here, we create the [`adw::MessageDialog`](https://world.pages.gitlab.gnome.org/Rust/libadwaita-rs/stable/latest/docs/libadwaita/struct.MessageDialog.html), set up the buttons as well as add the entry to it. We add a callback to the entry to ensure that when the content changes, an empty content sets `dialog_button` as insensitive and adds an "error" CSS class to the entry. -We also add a callback to the dialog itself. -If we click "Cancel", the dialog should just be destroyed without any further actions. -However, if we click "Create", we want a new collection to be created and set as current collection. +We then `await` on the user pressing a button on the dialog. +If they click "Cancel", we simply return. +However, if they click "Create", we want a new collection to be created and set as current collection. Afterwards we navigate forward on our leaflet, which means we navigate to the task view.