Manipulating State of To-Do App

Filtering Tasks

Now it is time to continue working on our To-Do app. One nice feature to add would be filtering of the tasks. What a chance to use our newly gained knowledge of actions! Using actions, we can access the filter via the menu as well as via keyboard shortcuts. This is how we want this to work in the end:

Note that the screencast also shows a button with label "Clear" which will remove all done tasks. This will come in handy when we later make the app preserve the tasks between sessions.

Let's start by adding a menu and a header bar to window.ui. After reading the actions chapter the added code should feel familiar.

Filename: listings/todo/2/resources/window.ui

<?xml version="1.0" encoding="UTF-8"?> <interface> + <menu id="main-menu"> + <submenu> + <attribute name="label" translatable="yes">_Filter</attribute> + <item> + <attribute name="label" translatable="yes">_All</attribute> + <attribute name="action">win.filter</attribute> + <attribute name="target">All</attribute> + </item> + <item> + <attribute name="label" translatable="yes">_Open</attribute> + <attribute name="action">win.filter</attribute> + <attribute name="target">Open</attribute> + </item> + <item> + <attribute name="label" translatable="yes">_Done</attribute> + <attribute name="action">win.filter</attribute> + <attribute name="target">Done</attribute> + </item> + </submenu> + <item> + <attribute name="label" translatable="yes">_Remove Done Tasks</attribute> + <attribute name="action">win.remove-done-tasks</attribute> + </item> + <item> + <attribute name="label" translatable="yes">_Keyboard Shortcuts</attribute> + <attribute name="action">win.show-help-overlay</attribute> + </item> + </menu> <template class="TodoWindow" parent="GtkApplicationWindow"> <property name="width-request">360</property> <property name="title" translatable="yes">To-Do</property> + <child type="titlebar"> + <object class="GtkHeaderBar"> + <child type="end"> + <object class="GtkMenuButton" id="menu_button"> + <property name="icon-name">open-menu-symbolic</property> + <property name="menu-model">main-menu</property> + </object> + </child> + </object> + </child>

Then, we create a settings schema. Again, the "filter" setting correspond to the stateful actions called by the menu.

Filename: listings/todo/2/org.gtk_rs.Todo2.gschema.xml

<?xml version="1.0" encoding="utf-8"?> <schemalist> <schema id="org.gtk_rs.Todo2" path="/org/gtk_rs/Todo2/"> <key name="filter" type="s"> <choices> <choice value='All'/> <choice value='Open'/> <choice value='Done'/> </choices> <default>'All'</default> <summary>Filter of the tasks</summary> </key> </schema> </schemalist>

We install the schema as described in the settings chapter Then we add a reference to settings to imp::Window.

Filename: listings/todo/2/window/imp.rs

use std::cell::RefCell; use std::fs::File; use gio::Settings; use glib::subclass::InitializingObject; use gtk::prelude::*; use gtk::subclass::prelude::*; use gtk::{gio, glib, CompositeTemplate, Entry, ListView}; use std::cell::OnceCell; use crate::task_object::{TaskData, TaskObject}; use crate::utils::data_path; // Object holding the state #[derive(CompositeTemplate, Default)] #[template(resource = "/org/gtk_rs/Todo2/window.ui")] pub struct Window { #[template_child] pub entry: TemplateChild<Entry>, #[template_child] pub tasks_list: TemplateChild<ListView>, pub tasks: RefCell<Option<gio::ListStore>>, pub settings: OnceCell<Settings>, } // The central trait for subclassing a GObject #[glib::object_subclass] impl ObjectSubclass for Window { // `NAME` needs to match `class` attribute of template const NAME: &'static str = "TodoWindow"; type Type = super::Window; type ParentType = gtk::ApplicationWindow; 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<Self>) { obj.init_template(); } } // Trait shared by all GObjects impl ObjectImpl for Window { fn constructed(&self) { // Call "constructed" on parent self.parent_constructed(); // Setup let obj = self.obj(); obj.setup_settings(); obj.setup_tasks(); obj.restore_data(); obj.setup_callbacks(); obj.setup_factory(); obj.setup_actions(); } } // Trait shared by all widgets impl WidgetImpl for Window {} // Trait shared by all windows impl WindowImpl for Window { fn close_request(&self) -> glib::Propagation { // Store task data in vector let backup_data: Vec<TaskData> = self .obj() .tasks() .iter::<TaskObject>() .filter_map(Result::ok) .map(|task_object| task_object.task_data()) .collect(); // Save state to file let file = File::create(data_path()).expect("Could not create json file."); serde_json::to_writer(file, &backup_data) .expect("Could not write data to json file"); // Pass close request on to the parent self.parent_close_request() } } // Trait shared by all application windows impl ApplicationWindowImpl for Window {}

Again, we create functions to make it easier to access settings.

Filename: listings/todo/2/window/mod.rs

mod imp; use std::fs::File; use gio::Settings; use glib::{clone, Object}; use gtk::subclass::prelude::*; use gtk::{ gio, glib, Application, CustomFilter, FilterListModel, NoSelection, SignalListItemFactory, }; use gtk::{prelude::*, ListItem}; use crate::task_object::{TaskData, TaskObject}; use crate::task_row::TaskRow; use crate::utils::data_path; use crate::APP_ID; glib::wrapper! { pub struct Window(ObjectSubclass<imp::Window>) @extends gtk::ApplicationWindow, gtk::Window, gtk::Widget, @implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager; } impl Window { pub fn new(app: &Application) -> Self { // Create new window Object::builder().property("application", app).build() } fn setup_settings(&self) { let settings = Settings::new(APP_ID); self.imp() .settings .set(settings) .expect("`settings` should not be set before calling `setup_settings`."); } fn settings(&self) -> &Settings { self.imp() .settings .get() .expect("`settings` should be set in `setup_settings`.") } fn tasks(&self) -> gio::ListStore { // Get state self.imp() .tasks .borrow() .clone() .expect("Could not get current tasks.") } fn filter(&self) -> Option<CustomFilter> { // Get filter_state from settings let filter_state: String = self.settings().get("filter"); // Create custom filters let filter_open = CustomFilter::new(|obj| { // Get `TaskObject` from `glib::Object` let task_object = obj .downcast_ref::<TaskObject>() .expect("The object needs to be of type `TaskObject`."); // Only allow completed tasks !task_object.is_completed() }); let filter_done = CustomFilter::new(|obj| { // Get `TaskObject` from `glib::Object` let task_object = obj .downcast_ref::<TaskObject>() .expect("The object needs to be of type `TaskObject`."); // Only allow done tasks task_object.is_completed() }); // Return the correct filter match filter_state.as_str() { "All" => None, "Open" => Some(filter_open), "Done" => Some(filter_done), _ => unreachable!(), } } fn setup_tasks(&self) { // Create new model let model = gio::ListStore::new::<TaskObject>(); // Get state and set model self.imp().tasks.replace(Some(model)); // Wrap model with filter and selection and pass it to the list view let filter_model = FilterListModel::new(Some(self.tasks()), self.filter()); let selection_model = NoSelection::new(Some(filter_model.clone())); self.imp().tasks_list.set_model(Some(&selection_model)); // Filter model whenever the value of the key "filter" changes self.settings().connect_changed( Some("filter"), clone!( #[weak(rename_to = window)] self, #[weak] filter_model, move |_, _| { filter_model.set_filter(window.filter().as_ref()); } ), ); } fn restore_data(&self) { if let Ok(file) = File::open(data_path()) { // Deserialize data from file to vector let backup_data: Vec<TaskData> = serde_json::from_reader(file).expect( "It should be possible to read `backup_data` from the json file.", ); // Convert `Vec<TaskData>` to `Vec<TaskObject>` let task_objects: Vec<TaskObject> = backup_data .into_iter() .map(TaskObject::from_task_data) .collect(); // Insert restored objects into model self.tasks().extend_from_slice(&task_objects); } } fn setup_callbacks(&self) { // Setup callback for activation of the entry self.imp().entry.connect_activate(clone!( #[weak(rename_to = window)] self, move |_| { window.new_task(); } )); // Setup callback for clicking (and the releasing) the icon of the entry self.imp().entry.connect_icon_release(clone!( #[weak(rename_to = window)] self, move |_, _| { window.new_task(); } )); } fn new_task(&self) { // Get content from entry and clear it let buffer = self.imp().entry.buffer(); let content = buffer.text().to_string(); if content.is_empty() { return; } buffer.set_text(""); // Add new task to model let task = TaskObject::new(false, content); self.tasks().append(&task); } fn setup_factory(&self) { // Create a new factory let factory = SignalListItemFactory::new(); // Create an empty `TaskRow` during setup factory.connect_setup(move |_, list_item| { // Create `TaskRow` let task_row = TaskRow::new(); list_item .downcast_ref::<ListItem>() .expect("Needs to be ListItem") .set_child(Some(&task_row)); }); // Tell factory how to bind `TaskRow` to a `TaskObject` factory.connect_bind(move |_, list_item| { // Get `TaskObject` from `ListItem` let task_object = list_item .downcast_ref::<ListItem>() .expect("Needs to be ListItem") .item() .and_downcast::<TaskObject>() .expect("The item has to be an `TaskObject`."); // Get `TaskRow` from `ListItem` let task_row = list_item .downcast_ref::<ListItem>() .expect("Needs to be ListItem") .child() .and_downcast::<TaskRow>() .expect("The child has to be a `TaskRow`."); task_row.bind(&task_object); }); // Tell factory how to unbind `TaskRow` from `TaskObject` factory.connect_unbind(move |_, list_item| { // Get `TaskRow` from `ListItem` let task_row = list_item .downcast_ref::<ListItem>() .expect("Needs to be ListItem") .child() .and_downcast::<TaskRow>() .expect("The child has to be a `TaskRow`."); task_row.unbind(); }); // Set the factory of the list view self.imp().tasks_list.set_factory(Some(&factory)); } fn setup_actions(&self) { // 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::<TaskObject>() .expect("The object needs to be of type `TaskObject`."); if task_object.is_completed() { tasks.remove(position); } else { position += 1; } } } }

We also add the methods is_completed, task_data and from_task_data to TaskObject. We will make use of them in the following snippets.

Filename: listings/todo/2/task_object/mod.rs

mod imp; use glib::Object; use gtk::glib; use gtk::subclass::prelude::*; use serde::{Deserialize, Serialize}; glib::wrapper! { pub struct TaskObject(ObjectSubclass<imp::TaskObject>); } impl TaskObject { pub fn new(completed: bool, content: String) -> Self { Object::builder() .property("completed", completed) .property("content", content) .build() } pub fn is_completed(&self) -> bool { self.imp().data.borrow().completed } pub fn task_data(&self) -> TaskData { self.imp().data.borrow().clone() } pub fn from_task_data(task_data: TaskData) -> Self { Self::new(task_data.completed, task_data.content) } } #[derive(Default, Clone, Serialize, Deserialize)] pub struct TaskData { pub completed: bool, pub content: String, }

Similar to the previous chapter, we let settings create the action. Then we add the newly created action "filter" to our window.

Filename: listings/todo/2/window/mod.rs

mod imp; use std::fs::File; use gio::Settings; use glib::{clone, Object}; use gtk::subclass::prelude::*; use gtk::{ gio, glib, Application, CustomFilter, FilterListModel, NoSelection, SignalListItemFactory, }; use gtk::{prelude::*, ListItem}; use crate::task_object::{TaskData, TaskObject}; use crate::task_row::TaskRow; use crate::utils::data_path; use crate::APP_ID; glib::wrapper! { pub struct Window(ObjectSubclass<imp::Window>) @extends gtk::ApplicationWindow, gtk::Window, gtk::Widget, @implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager; } impl Window { pub fn new(app: &Application) -> Self { // Create new window Object::builder().property("application", app).build() } fn setup_settings(&self) { let settings = Settings::new(APP_ID); self.imp() .settings .set(settings) .expect("`settings` should not be set before calling `setup_settings`."); } fn settings(&self) -> &Settings { self.imp() .settings .get() .expect("`settings` should be set in `setup_settings`.") } fn tasks(&self) -> gio::ListStore { // Get state self.imp() .tasks .borrow() .clone() .expect("Could not get current tasks.") } fn filter(&self) -> Option<CustomFilter> { // Get filter_state from settings let filter_state: String = self.settings().get("filter"); // Create custom filters let filter_open = CustomFilter::new(|obj| { // Get `TaskObject` from `glib::Object` let task_object = obj .downcast_ref::<TaskObject>() .expect("The object needs to be of type `TaskObject`."); // Only allow completed tasks !task_object.is_completed() }); let filter_done = CustomFilter::new(|obj| { // Get `TaskObject` from `glib::Object` let task_object = obj .downcast_ref::<TaskObject>() .expect("The object needs to be of type `TaskObject`."); // Only allow done tasks task_object.is_completed() }); // Return the correct filter match filter_state.as_str() { "All" => None, "Open" => Some(filter_open), "Done" => Some(filter_done), _ => unreachable!(), } } fn setup_tasks(&self) { // Create new model let model = gio::ListStore::new::<TaskObject>(); // Get state and set model self.imp().tasks.replace(Some(model)); // Wrap model with filter and selection and pass it to the list view let filter_model = FilterListModel::new(Some(self.tasks()), self.filter()); let selection_model = NoSelection::new(Some(filter_model.clone())); self.imp().tasks_list.set_model(Some(&selection_model)); // Filter model whenever the value of the key "filter" changes self.settings().connect_changed( Some("filter"), clone!( #[weak(rename_to = window)] self, #[weak] filter_model, move |_, _| { filter_model.set_filter(window.filter().as_ref()); } ), ); } fn restore_data(&self) { if let Ok(file) = File::open(data_path()) { // Deserialize data from file to vector let backup_data: Vec<TaskData> = serde_json::from_reader(file).expect( "It should be possible to read `backup_data` from the json file.", ); // Convert `Vec<TaskData>` to `Vec<TaskObject>` let task_objects: Vec<TaskObject> = backup_data .into_iter() .map(TaskObject::from_task_data) .collect(); // Insert restored objects into model self.tasks().extend_from_slice(&task_objects); } } fn setup_callbacks(&self) { // Setup callback for activation of the entry self.imp().entry.connect_activate(clone!( #[weak(rename_to = window)] self, move |_| { window.new_task(); } )); // Setup callback for clicking (and the releasing) the icon of the entry self.imp().entry.connect_icon_release(clone!( #[weak(rename_to = window)] self, move |_, _| { window.new_task(); } )); } fn new_task(&self) { // Get content from entry and clear it let buffer = self.imp().entry.buffer(); let content = buffer.text().to_string(); if content.is_empty() { return; } buffer.set_text(""); // Add new task to model let task = TaskObject::new(false, content); self.tasks().append(&task); } fn setup_factory(&self) { // Create a new factory let factory = SignalListItemFactory::new(); // Create an empty `TaskRow` during setup factory.connect_setup(move |_, list_item| { // Create `TaskRow` let task_row = TaskRow::new(); list_item .downcast_ref::<ListItem>() .expect("Needs to be ListItem") .set_child(Some(&task_row)); }); // Tell factory how to bind `TaskRow` to a `TaskObject` factory.connect_bind(move |_, list_item| { // Get `TaskObject` from `ListItem` let task_object = list_item .downcast_ref::<ListItem>() .expect("Needs to be ListItem") .item() .and_downcast::<TaskObject>() .expect("The item has to be an `TaskObject`."); // Get `TaskRow` from `ListItem` let task_row = list_item .downcast_ref::<ListItem>() .expect("Needs to be ListItem") .child() .and_downcast::<TaskRow>() .expect("The child has to be a `TaskRow`."); task_row.bind(&task_object); }); // Tell factory how to unbind `TaskRow` from `TaskObject` factory.connect_unbind(move |_, list_item| { // Get `TaskRow` from `ListItem` let task_row = list_item .downcast_ref::<ListItem>() .expect("Needs to be ListItem") .child() .and_downcast::<TaskRow>() .expect("The child has to be a `TaskRow`."); task_row.unbind(); }); // Set the factory of the list view self.imp().tasks_list.set_factory(Some(&factory)); } fn setup_actions(&self) { // 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::<TaskObject>() .expect("The object needs to be of type `TaskObject`."); if task_object.is_completed() { tasks.remove(position); } else { position += 1; } } } }

We also add an action which allows us to remove done tasks. This time we use another method called install_action. This method has a couple of limitation. It can only be used when subclassing widgets, and it doesn't support stateful actions. On the flipside, its usage is concise and it has a corresponding sister-method install_action_async which we will use in one of the future chapters.

Filename: listings/todo/2/window/imp.rs

use std::cell::RefCell; use std::fs::File; use gio::Settings; use glib::subclass::InitializingObject; use gtk::prelude::*; use gtk::subclass::prelude::*; use gtk::{gio, glib, CompositeTemplate, Entry, ListView}; use std::cell::OnceCell; use crate::task_object::{TaskData, TaskObject}; use crate::utils::data_path; // Object holding the state #[derive(CompositeTemplate, Default)] #[template(resource = "/org/gtk_rs/Todo2/window.ui")] pub struct Window { #[template_child] pub entry: TemplateChild<Entry>, #[template_child] pub tasks_list: TemplateChild<ListView>, pub tasks: RefCell<Option<gio::ListStore>>, pub settings: OnceCell<Settings>, } // The central trait for subclassing a GObject #[glib::object_subclass] impl ObjectSubclass for Window { // `NAME` needs to match `class` attribute of template const NAME: &'static str = "TodoWindow"; type Type = super::Window; type ParentType = gtk::ApplicationWindow; 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<Self>) { obj.init_template(); } } // Trait shared by all GObjects impl ObjectImpl for Window { fn constructed(&self) { // Call "constructed" on parent self.parent_constructed(); // Setup let obj = self.obj(); obj.setup_settings(); obj.setup_tasks(); obj.restore_data(); obj.setup_callbacks(); obj.setup_factory(); obj.setup_actions(); } } // Trait shared by all widgets impl WidgetImpl for Window {} // Trait shared by all windows impl WindowImpl for Window { fn close_request(&self) -> glib::Propagation { // Store task data in vector let backup_data: Vec<TaskData> = self .obj() .tasks() .iter::<TaskObject>() .filter_map(Result::ok) .map(|task_object| task_object.task_data()) .collect(); // Save state to file let file = File::create(data_path()).expect("Could not create json file."); serde_json::to_writer(file, &backup_data) .expect("Could not write data to json file"); // Pass close request on to the parent self.parent_close_request() } } // Trait shared by all application windows impl ApplicationWindowImpl for Window {}

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

mod imp; use std::fs::File; use gio::Settings; use glib::{clone, Object}; use gtk::subclass::prelude::*; use gtk::{ gio, glib, Application, CustomFilter, FilterListModel, NoSelection, SignalListItemFactory, }; use gtk::{prelude::*, ListItem}; use crate::task_object::{TaskData, TaskObject}; use crate::task_row::TaskRow; use crate::utils::data_path; use crate::APP_ID; glib::wrapper! { pub struct Window(ObjectSubclass<imp::Window>) @extends gtk::ApplicationWindow, gtk::Window, gtk::Widget, @implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager; } impl Window { pub fn new(app: &Application) -> Self { // Create new window Object::builder().property("application", app).build() } fn setup_settings(&self) { let settings = Settings::new(APP_ID); self.imp() .settings .set(settings) .expect("`settings` should not be set before calling `setup_settings`."); } fn settings(&self) -> &Settings { self.imp() .settings .get() .expect("`settings` should be set in `setup_settings`.") } fn tasks(&self) -> gio::ListStore { // Get state self.imp() .tasks .borrow() .clone() .expect("Could not get current tasks.") } fn filter(&self) -> Option<CustomFilter> { // Get filter_state from settings let filter_state: String = self.settings().get("filter"); // Create custom filters let filter_open = CustomFilter::new(|obj| { // Get `TaskObject` from `glib::Object` let task_object = obj .downcast_ref::<TaskObject>() .expect("The object needs to be of type `TaskObject`."); // Only allow completed tasks !task_object.is_completed() }); let filter_done = CustomFilter::new(|obj| { // Get `TaskObject` from `glib::Object` let task_object = obj .downcast_ref::<TaskObject>() .expect("The object needs to be of type `TaskObject`."); // Only allow done tasks task_object.is_completed() }); // Return the correct filter match filter_state.as_str() { "All" => None, "Open" => Some(filter_open), "Done" => Some(filter_done), _ => unreachable!(), } } fn setup_tasks(&self) { // Create new model let model = gio::ListStore::new::<TaskObject>(); // Get state and set model self.imp().tasks.replace(Some(model)); // Wrap model with filter and selection and pass it to the list view let filter_model = FilterListModel::new(Some(self.tasks()), self.filter()); let selection_model = NoSelection::new(Some(filter_model.clone())); self.imp().tasks_list.set_model(Some(&selection_model)); // Filter model whenever the value of the key "filter" changes self.settings().connect_changed( Some("filter"), clone!( #[weak(rename_to = window)] self, #[weak] filter_model, move |_, _| { filter_model.set_filter(window.filter().as_ref()); } ), ); } fn restore_data(&self) { if let Ok(file) = File::open(data_path()) { // Deserialize data from file to vector let backup_data: Vec<TaskData> = serde_json::from_reader(file).expect( "It should be possible to read `backup_data` from the json file.", ); // Convert `Vec<TaskData>` to `Vec<TaskObject>` let task_objects: Vec<TaskObject> = backup_data .into_iter() .map(TaskObject::from_task_data) .collect(); // Insert restored objects into model self.tasks().extend_from_slice(&task_objects); } } fn setup_callbacks(&self) { // Setup callback for activation of the entry self.imp().entry.connect_activate(clone!( #[weak(rename_to = window)] self, move |_| { window.new_task(); } )); // Setup callback for clicking (and the releasing) the icon of the entry self.imp().entry.connect_icon_release(clone!( #[weak(rename_to = window)] self, move |_, _| { window.new_task(); } )); } fn new_task(&self) { // Get content from entry and clear it let buffer = self.imp().entry.buffer(); let content = buffer.text().to_string(); if content.is_empty() { return; } buffer.set_text(""); // Add new task to model let task = TaskObject::new(false, content); self.tasks().append(&task); } fn setup_factory(&self) { // Create a new factory let factory = SignalListItemFactory::new(); // Create an empty `TaskRow` during setup factory.connect_setup(move |_, list_item| { // Create `TaskRow` let task_row = TaskRow::new(); list_item .downcast_ref::<ListItem>() .expect("Needs to be ListItem") .set_child(Some(&task_row)); }); // Tell factory how to bind `TaskRow` to a `TaskObject` factory.connect_bind(move |_, list_item| { // Get `TaskObject` from `ListItem` let task_object = list_item .downcast_ref::<ListItem>() .expect("Needs to be ListItem") .item() .and_downcast::<TaskObject>() .expect("The item has to be an `TaskObject`."); // Get `TaskRow` from `ListItem` let task_row = list_item .downcast_ref::<ListItem>() .expect("Needs to be ListItem") .child() .and_downcast::<TaskRow>() .expect("The child has to be a `TaskRow`."); task_row.bind(&task_object); }); // Tell factory how to unbind `TaskRow` from `TaskObject` factory.connect_unbind(move |_, list_item| { // Get `TaskRow` from `ListItem` let task_row = list_item .downcast_ref::<ListItem>() .expect("Needs to be ListItem") .child() .and_downcast::<TaskRow>() .expect("The child has to be a `TaskRow`."); task_row.unbind(); }); // Set the factory of the list view self.imp().tasks_list.set_factory(Some(&factory)); } fn setup_actions(&self) { // 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::<TaskObject>() .expect("The object needs to be of type `TaskObject`."); if task_object.is_completed() { tasks.remove(position); } else { position += 1; } } } }

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 understands. The possible states are "All", "Open" and "Done". We return Some(filter) for "Open" and "Done". If the state is "All" nothing has to be filtered out, so we return None.

Filename: listings/todo/2/window/mod.rs

mod imp; use std::fs::File; use gio::Settings; use glib::{clone, Object}; use gtk::subclass::prelude::*; use gtk::{ gio, glib, Application, CustomFilter, FilterListModel, NoSelection, SignalListItemFactory, }; use gtk::{prelude::*, ListItem}; use crate::task_object::{TaskData, TaskObject}; use crate::task_row::TaskRow; use crate::utils::data_path; use crate::APP_ID; glib::wrapper! { pub struct Window(ObjectSubclass<imp::Window>) @extends gtk::ApplicationWindow, gtk::Window, gtk::Widget, @implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager; } impl Window { pub fn new(app: &Application) -> Self { // Create new window Object::builder().property("application", app).build() } fn setup_settings(&self) { let settings = Settings::new(APP_ID); self.imp() .settings .set(settings) .expect("`settings` should not be set before calling `setup_settings`."); } fn settings(&self) -> &Settings { self.imp() .settings .get() .expect("`settings` should be set in `setup_settings`.") } fn tasks(&self) -> gio::ListStore { // Get state self.imp() .tasks .borrow() .clone() .expect("Could not get current tasks.") } fn filter(&self) -> Option<CustomFilter> { // Get filter_state from settings let filter_state: String = self.settings().get("filter"); // Create custom filters let filter_open = CustomFilter::new(|obj| { // Get `TaskObject` from `glib::Object` let task_object = obj .downcast_ref::<TaskObject>() .expect("The object needs to be of type `TaskObject`."); // Only allow completed tasks !task_object.is_completed() }); let filter_done = CustomFilter::new(|obj| { // Get `TaskObject` from `glib::Object` let task_object = obj .downcast_ref::<TaskObject>() .expect("The object needs to be of type `TaskObject`."); // Only allow done tasks task_object.is_completed() }); // Return the correct filter match filter_state.as_str() { "All" => None, "Open" => Some(filter_open), "Done" => Some(filter_done), _ => unreachable!(), } } fn setup_tasks(&self) { // Create new model let model = gio::ListStore::new::<TaskObject>(); // Get state and set model self.imp().tasks.replace(Some(model)); // Wrap model with filter and selection and pass it to the list view let filter_model = FilterListModel::new(Some(self.tasks()), self.filter()); let selection_model = NoSelection::new(Some(filter_model.clone())); self.imp().tasks_list.set_model(Some(&selection_model)); // Filter model whenever the value of the key "filter" changes self.settings().connect_changed( Some("filter"), clone!( #[weak(rename_to = window)] self, #[weak] filter_model, move |_, _| { filter_model.set_filter(window.filter().as_ref()); } ), ); } fn restore_data(&self) { if let Ok(file) = File::open(data_path()) { // Deserialize data from file to vector let backup_data: Vec<TaskData> = serde_json::from_reader(file).expect( "It should be possible to read `backup_data` from the json file.", ); // Convert `Vec<TaskData>` to `Vec<TaskObject>` let task_objects: Vec<TaskObject> = backup_data .into_iter() .map(TaskObject::from_task_data) .collect(); // Insert restored objects into model self.tasks().extend_from_slice(&task_objects); } } fn setup_callbacks(&self) { // Setup callback for activation of the entry self.imp().entry.connect_activate(clone!( #[weak(rename_to = window)] self, move |_| { window.new_task(); } )); // Setup callback for clicking (and the releasing) the icon of the entry self.imp().entry.connect_icon_release(clone!( #[weak(rename_to = window)] self, move |_, _| { window.new_task(); } )); } fn new_task(&self) { // Get content from entry and clear it let buffer = self.imp().entry.buffer(); let content = buffer.text().to_string(); if content.is_empty() { return; } buffer.set_text(""); // Add new task to model let task = TaskObject::new(false, content); self.tasks().append(&task); } fn setup_factory(&self) { // Create a new factory let factory = SignalListItemFactory::new(); // Create an empty `TaskRow` during setup factory.connect_setup(move |_, list_item| { // Create `TaskRow` let task_row = TaskRow::new(); list_item .downcast_ref::<ListItem>() .expect("Needs to be ListItem") .set_child(Some(&task_row)); }); // Tell factory how to bind `TaskRow` to a `TaskObject` factory.connect_bind(move |_, list_item| { // Get `TaskObject` from `ListItem` let task_object = list_item .downcast_ref::<ListItem>() .expect("Needs to be ListItem") .item() .and_downcast::<TaskObject>() .expect("The item has to be an `TaskObject`."); // Get `TaskRow` from `ListItem` let task_row = list_item .downcast_ref::<ListItem>() .expect("Needs to be ListItem") .child() .and_downcast::<TaskRow>() .expect("The child has to be a `TaskRow`."); task_row.bind(&task_object); }); // Tell factory how to unbind `TaskRow` from `TaskObject` factory.connect_unbind(move |_, list_item| { // Get `TaskRow` from `ListItem` let task_row = list_item .downcast_ref::<ListItem>() .expect("Needs to be ListItem") .child() .and_downcast::<TaskRow>() .expect("The child has to be a `TaskRow`."); task_row.unbind(); }); // Set the factory of the list view self.imp().tasks_list.set_factory(Some(&factory)); } fn setup_actions(&self) { // 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::<TaskObject>() .expect("The object needs to be of type `TaskObject`."); if task_object.is_completed() { tasks.remove(position); } else { position += 1; } } } }

Now, we can set up the model. We initialize filter_model with the state from the settings by calling the method filter. Whenever the state of the key "filter" changes, we call the method filter again to get the updated filter_model.

Filename: listings/todo/2/window/mod.rs

mod imp; use std::fs::File; use gio::Settings; use glib::{clone, Object}; use gtk::subclass::prelude::*; use gtk::{ gio, glib, Application, CustomFilter, FilterListModel, NoSelection, SignalListItemFactory, }; use gtk::{prelude::*, ListItem}; use crate::task_object::{TaskData, TaskObject}; use crate::task_row::TaskRow; use crate::utils::data_path; use crate::APP_ID; glib::wrapper! { pub struct Window(ObjectSubclass<imp::Window>) @extends gtk::ApplicationWindow, gtk::Window, gtk::Widget, @implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager; } impl Window { pub fn new(app: &Application) -> Self { // Create new window Object::builder().property("application", app).build() } fn setup_settings(&self) { let settings = Settings::new(APP_ID); self.imp() .settings .set(settings) .expect("`settings` should not be set before calling `setup_settings`."); } fn settings(&self) -> &Settings { self.imp() .settings .get() .expect("`settings` should be set in `setup_settings`.") } fn tasks(&self) -> gio::ListStore { // Get state self.imp() .tasks .borrow() .clone() .expect("Could not get current tasks.") } fn filter(&self) -> Option<CustomFilter> { // Get filter_state from settings let filter_state: String = self.settings().get("filter"); // Create custom filters let filter_open = CustomFilter::new(|obj| { // Get `TaskObject` from `glib::Object` let task_object = obj .downcast_ref::<TaskObject>() .expect("The object needs to be of type `TaskObject`."); // Only allow completed tasks !task_object.is_completed() }); let filter_done = CustomFilter::new(|obj| { // Get `TaskObject` from `glib::Object` let task_object = obj .downcast_ref::<TaskObject>() .expect("The object needs to be of type `TaskObject`."); // Only allow done tasks task_object.is_completed() }); // Return the correct filter match filter_state.as_str() { "All" => None, "Open" => Some(filter_open), "Done" => Some(filter_done), _ => unreachable!(), } } fn setup_tasks(&self) { // Create new model let model = gio::ListStore::new::<TaskObject>(); // Get state and set model self.imp().tasks.replace(Some(model)); // Wrap model with filter and selection and pass it to the list view let filter_model = FilterListModel::new(Some(self.tasks()), self.filter()); let selection_model = NoSelection::new(Some(filter_model.clone())); self.imp().tasks_list.set_model(Some(&selection_model)); // Filter model whenever the value of the key "filter" changes self.settings().connect_changed( Some("filter"), clone!( #[weak(rename_to = window)] self, #[weak] filter_model, move |_, _| { filter_model.set_filter(window.filter().as_ref()); } ), ); } fn restore_data(&self) { if let Ok(file) = File::open(data_path()) { // Deserialize data from file to vector let backup_data: Vec<TaskData> = serde_json::from_reader(file).expect( "It should be possible to read `backup_data` from the json file.", ); // Convert `Vec<TaskData>` to `Vec<TaskObject>` let task_objects: Vec<TaskObject> = backup_data .into_iter() .map(TaskObject::from_task_data) .collect(); // Insert restored objects into model self.tasks().extend_from_slice(&task_objects); } } fn setup_callbacks(&self) { // Setup callback for activation of the entry self.imp().entry.connect_activate(clone!( #[weak(rename_to = window)] self, move |_| { window.new_task(); } )); // Setup callback for clicking (and the releasing) the icon of the entry self.imp().entry.connect_icon_release(clone!( #[weak(rename_to = window)] self, move |_, _| { window.new_task(); } )); } fn new_task(&self) { // Get content from entry and clear it let buffer = self.imp().entry.buffer(); let content = buffer.text().to_string(); if content.is_empty() { return; } buffer.set_text(""); // Add new task to model let task = TaskObject::new(false, content); self.tasks().append(&task); } fn setup_factory(&self) { // Create a new factory let factory = SignalListItemFactory::new(); // Create an empty `TaskRow` during setup factory.connect_setup(move |_, list_item| { // Create `TaskRow` let task_row = TaskRow::new(); list_item .downcast_ref::<ListItem>() .expect("Needs to be ListItem") .set_child(Some(&task_row)); }); // Tell factory how to bind `TaskRow` to a `TaskObject` factory.connect_bind(move |_, list_item| { // Get `TaskObject` from `ListItem` let task_object = list_item .downcast_ref::<ListItem>() .expect("Needs to be ListItem") .item() .and_downcast::<TaskObject>() .expect("The item has to be an `TaskObject`."); // Get `TaskRow` from `ListItem` let task_row = list_item .downcast_ref::<ListItem>() .expect("Needs to be ListItem") .child() .and_downcast::<TaskRow>() .expect("The child has to be a `TaskRow`."); task_row.bind(&task_object); }); // Tell factory how to unbind `TaskRow` from `TaskObject` factory.connect_unbind(move |_, list_item| { // Get `TaskRow` from `ListItem` let task_row = list_item .downcast_ref::<ListItem>() .expect("Needs to be ListItem") .child() .and_downcast::<TaskRow>() .expect("The child has to be a `TaskRow`."); task_row.unbind(); }); // Set the factory of the list view self.imp().tasks_list.set_factory(Some(&factory)); } fn setup_actions(&self) { // 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::<TaskObject>() .expect("The object needs to be of type `TaskObject`."); if task_object.is_completed() { tasks.remove(position); } else { position += 1; } } } }

Then, we bind the shortcuts to their actions with set_accels_for_action. Here as well, a detailed action name is used. Since this has to be done at the application level, setup_shortcuts takes a gtk::Application as parameter.

Filename: listings/todo/2/main.rs

mod task_object; mod task_row; mod utils; mod window; use gtk::prelude::*; use gtk::{gio, glib, Application}; use window::Window; const APP_ID: &str = "org.gtk_rs.Todo2"; fn main() -> glib::ExitCode { // Register and include resources gio::resources_register_include!("todo_2.gresource") .expect("Failed to register resources."); // Create a new application let app = Application::builder().application_id(APP_ID).build(); // Connect to signals app.connect_startup(setup_shortcuts); app.connect_activate(build_ui); // Run the application app.run() } fn setup_shortcuts(app: &Application) { app.set_accels_for_action("win.filter('All')", &["<Ctrl>a"]); app.set_accels_for_action("win.filter('Open')", &["<Ctrl>o"]); app.set_accels_for_action("win.filter('Done')", &["<Ctrl>d"]); } fn build_ui(app: &Application) { // Create a new custom window and present it let window = Window::new(app); window.present(); }

Now that we created all these nice shortcuts we will want our users to find them. We do that by creating a shortcut window. Again we use an ui file to describe it, but here we don't want to use it as template for our custom widget. Instead we instantiate a widget of the existing class gtk::ShortcutsWindow with it.

Filename: listings/todo/2/resources/shortcuts.ui

<?xml version="1.0" encoding="UTF-8"?> <interface> <object class="GtkShortcutsWindow" id="help_overlay"> <property name="modal">True</property> <child> <object class="GtkShortcutsSection"> <property name="section-name">shortcuts</property> <property name="max-height">10</property> <child> <object class="GtkShortcutsGroup"> <property name="title" translatable="yes" context="shortcut window">General</property> <child> <object class="GtkShortcutsShortcut"> <property name="title" translatable="yes" context="shortcut window">Show shortcuts</property> <property name="action-name">win.show-help-overlay</property> </object> </child> <child> <object class="GtkShortcutsShortcut"> <property name="title" translatable="yes" context="shortcut window">Filter to show all tasks</property> <property name="action-name">win.filter('All')</property> </object> </child> <child> <object class="GtkShortcutsShortcut"> <property name="title" translatable="yes" context="shortcut window">Filter to show only open tasks</property> <property name="action-name">win.filter('Open')</property> </object> </child> <child> <object class="GtkShortcutsShortcut"> <property name="title" translatable="yes" context="shortcut window">Filter to show only completed tasks</property> <property name="action-name">win.filter('Done')</property> </object> </child> </object> </child> </object> </child> </object> </interface>

The entries can be organized with gtk::ShortcutsSection and gtk::ShortcutsGroup. If we specify the action name, we also don't have to repeat the keyboard accelerator. gtk::ShortcutsShortcut looks it up on its own.

Note the way we set action-name for ShortcutsShortcut. Instead of using a separate property for the target, it takes a detailed action name. Detailed names look like this: action_group.action_name(target). Formatting of the target depends on its type and is documented here. In particular, strings have to be enclosed single quotes as you can see in this example.

Finally, we have to add the shortcuts.ui to our resources. Note that we give it the alias gtk/help-overlay.ui. We do that to take advantage of a convenience functionality documented here. It will look for a resource at gtk/help-overlay.ui which defines a ShortcutsWindow with id help_overlay. If it can find one it will create a action win.show-help-overlay which will show the window and associate the shortcut Ctrl + ? with this action.

Filename: listings/todo/2/resources/resources.gresource.xml

<?xml version="1.0" encoding="UTF-8"?> <gresources> <gresource prefix="/org/gtk_rs/Todo2/"> <file compressed="true" preprocess="xml-stripblanks" alias="gtk/help-overlay.ui">shortcuts.ui</file> <file compressed="true" preprocess="xml-stripblanks">task_row.ui</file> <file compressed="true" preprocess="xml-stripblanks">window.ui</file> </gresource> </gresources>
The shortcut window

Saving and Restoring Tasks

Since we use Settings, our filter state will persist between sessions. However, the tasks themselves will not. Let us implement that. We could store our tasks in Settings, but it would be inconvenient. When it comes to serializing and deserializing nothing beats the crate serde. Combined with serde_json we can save our tasks as serialized json files.

First, we extend our Cargo.toml with the serde and serde_json crate.

cargo add serde --features derive cargo add serde_json

Filename: listings/Cargo.toml

[dependencies] serde = { version = "1.0", features = ["derive"] } serde_json = "1.0"

Serde is a framework for serializing and deserializing Rust data structures. The derive feature allows us to make our structures (de-)serializable with a single line of code. We also use the rc feature so that Serde can deal with std::rc::Rc objects. This is why we stored the data of TodoObject in a distinct TodoData structure. Doing so allows us to derive Serialize and Deserialize for TodoData.

Filename: listings/todo/2/task_object/mod.rs

mod imp; use glib::Object; use gtk::glib; use gtk::subclass::prelude::*; use serde::{Deserialize, Serialize}; glib::wrapper! { pub struct TaskObject(ObjectSubclass<imp::TaskObject>); } impl TaskObject { pub fn new(completed: bool, content: String) -> Self { Object::builder() .property("completed", completed) .property("content", content) .build() } pub fn is_completed(&self) -> bool { self.imp().data.borrow().completed } pub fn task_data(&self) -> TaskData { self.imp().data.borrow().clone() } pub fn from_task_data(task_data: TaskData) -> Self { Self::new(task_data.completed, task_data.content) } } #[derive(Default, Clone, Serialize, Deserialize)] pub struct TaskData { pub completed: bool, pub content: String, }

We plan to store our data as a file, so we create a utility function to provide a suitable file path for us. We use glib::user_config_dir to get the path to the config directory and create a new subdirectory for our app. Then we return the file path.

Filename: listings/todo/2/utils.rs

use std::path::PathBuf; use gtk::glib; use crate::APP_ID; pub fn data_path() -> PathBuf { let mut path = glib::user_data_dir(); path.push(APP_ID); std::fs::create_dir_all(&path).expect("Could not create directory."); path.push("data.json"); path }

We override the close_request virtual function to save the tasks when the window is closed. To do so, we first iterate through all entries and store them in a Vec. Then we serialize the Vec and store the data as a json file.

Filename: listings/todo/2/window/imp.rs

use std::cell::RefCell; use std::fs::File; use gio::Settings; use glib::subclass::InitializingObject; use gtk::prelude::*; use gtk::subclass::prelude::*; use gtk::{gio, glib, CompositeTemplate, Entry, ListView}; use std::cell::OnceCell; use crate::task_object::{TaskData, TaskObject}; use crate::utils::data_path; // Object holding the state #[derive(CompositeTemplate, Default)] #[template(resource = "/org/gtk_rs/Todo2/window.ui")] pub struct Window { #[template_child] pub entry: TemplateChild<Entry>, #[template_child] pub tasks_list: TemplateChild<ListView>, pub tasks: RefCell<Option<gio::ListStore>>, pub settings: OnceCell<Settings>, } // The central trait for subclassing a GObject #[glib::object_subclass] impl ObjectSubclass for Window { // `NAME` needs to match `class` attribute of template const NAME: &'static str = "TodoWindow"; type Type = super::Window; type ParentType = gtk::ApplicationWindow; 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<Self>) { obj.init_template(); } } // Trait shared by all GObjects impl ObjectImpl for Window { fn constructed(&self) { // Call "constructed" on parent self.parent_constructed(); // Setup let obj = self.obj(); obj.setup_settings(); obj.setup_tasks(); obj.restore_data(); obj.setup_callbacks(); obj.setup_factory(); obj.setup_actions(); } } // Trait shared by all widgets impl WidgetImpl for Window {} // Trait shared by all windows impl WindowImpl for Window { fn close_request(&self) -> glib::Propagation { // Store task data in vector let backup_data: Vec<TaskData> = self .obj() .tasks() .iter::<TaskObject>() .filter_map(Result::ok) .map(|task_object| task_object.task_data()) .collect(); // Save state to file let file = File::create(data_path()).expect("Could not create json file."); serde_json::to_writer(file, &backup_data) .expect("Could not write data to json file"); // Pass close request on to the parent self.parent_close_request() } } // Trait shared by all application windows impl ApplicationWindowImpl for Window {}

Let's it have a look into what a Vec<TaskData> will be serialized. Note that serde_json::to_writer saves the data into a more concise, but also less readable way. To create the equivalent but nicely formatted json below you can just replace to_writer with serde_json::to_writer_pretty.

Filename: data.json

[ { "completed": true, "content": "Task Number Two" }, { "completed": false, "content": "Task Number Five" }, { "completed": true, "content": "Task Number Six" }, { "completed": false, "content": "Task Number Seven" }, { "completed": false, "content": "Task Number Eight" } ]

When we start the app, we will want to restore the saved data. Let us add a restore_data method for that. We make sure to handle the case where there is no data file there yet. It might be the first time that we started the app and therefore there is no former session to restore.

Filename: listings/todo/2/window/mod.rs

mod imp; use std::fs::File; use gio::Settings; use glib::{clone, Object}; use gtk::subclass::prelude::*; use gtk::{ gio, glib, Application, CustomFilter, FilterListModel, NoSelection, SignalListItemFactory, }; use gtk::{prelude::*, ListItem}; use crate::task_object::{TaskData, TaskObject}; use crate::task_row::TaskRow; use crate::utils::data_path; use crate::APP_ID; glib::wrapper! { pub struct Window(ObjectSubclass<imp::Window>) @extends gtk::ApplicationWindow, gtk::Window, gtk::Widget, @implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager; } impl Window { pub fn new(app: &Application) -> Self { // Create new window Object::builder().property("application", app).build() } fn setup_settings(&self) { let settings = Settings::new(APP_ID); self.imp() .settings .set(settings) .expect("`settings` should not be set before calling `setup_settings`."); } fn settings(&self) -> &Settings { self.imp() .settings .get() .expect("`settings` should be set in `setup_settings`.") } fn tasks(&self) -> gio::ListStore { // Get state self.imp() .tasks .borrow() .clone() .expect("Could not get current tasks.") } fn filter(&self) -> Option<CustomFilter> { // Get filter_state from settings let filter_state: String = self.settings().get("filter"); // Create custom filters let filter_open = CustomFilter::new(|obj| { // Get `TaskObject` from `glib::Object` let task_object = obj .downcast_ref::<TaskObject>() .expect("The object needs to be of type `TaskObject`."); // Only allow completed tasks !task_object.is_completed() }); let filter_done = CustomFilter::new(|obj| { // Get `TaskObject` from `glib::Object` let task_object = obj .downcast_ref::<TaskObject>() .expect("The object needs to be of type `TaskObject`."); // Only allow done tasks task_object.is_completed() }); // Return the correct filter match filter_state.as_str() { "All" => None, "Open" => Some(filter_open), "Done" => Some(filter_done), _ => unreachable!(), } } fn setup_tasks(&self) { // Create new model let model = gio::ListStore::new::<TaskObject>(); // Get state and set model self.imp().tasks.replace(Some(model)); // Wrap model with filter and selection and pass it to the list view let filter_model = FilterListModel::new(Some(self.tasks()), self.filter()); let selection_model = NoSelection::new(Some(filter_model.clone())); self.imp().tasks_list.set_model(Some(&selection_model)); // Filter model whenever the value of the key "filter" changes self.settings().connect_changed( Some("filter"), clone!( #[weak(rename_to = window)] self, #[weak] filter_model, move |_, _| { filter_model.set_filter(window.filter().as_ref()); } ), ); } fn restore_data(&self) { if let Ok(file) = File::open(data_path()) { // Deserialize data from file to vector let backup_data: Vec<TaskData> = serde_json::from_reader(file).expect( "It should be possible to read `backup_data` from the json file.", ); // Convert `Vec<TaskData>` to `Vec<TaskObject>` let task_objects: Vec<TaskObject> = backup_data .into_iter() .map(TaskObject::from_task_data) .collect(); // Insert restored objects into model self.tasks().extend_from_slice(&task_objects); } } fn setup_callbacks(&self) { // Setup callback for activation of the entry self.imp().entry.connect_activate(clone!( #[weak(rename_to = window)] self, move |_| { window.new_task(); } )); // Setup callback for clicking (and the releasing) the icon of the entry self.imp().entry.connect_icon_release(clone!( #[weak(rename_to = window)] self, move |_, _| { window.new_task(); } )); } fn new_task(&self) { // Get content from entry and clear it let buffer = self.imp().entry.buffer(); let content = buffer.text().to_string(); if content.is_empty() { return; } buffer.set_text(""); // Add new task to model let task = TaskObject::new(false, content); self.tasks().append(&task); } fn setup_factory(&self) { // Create a new factory let factory = SignalListItemFactory::new(); // Create an empty `TaskRow` during setup factory.connect_setup(move |_, list_item| { // Create `TaskRow` let task_row = TaskRow::new(); list_item .downcast_ref::<ListItem>() .expect("Needs to be ListItem") .set_child(Some(&task_row)); }); // Tell factory how to bind `TaskRow` to a `TaskObject` factory.connect_bind(move |_, list_item| { // Get `TaskObject` from `ListItem` let task_object = list_item .downcast_ref::<ListItem>() .expect("Needs to be ListItem") .item() .and_downcast::<TaskObject>() .expect("The item has to be an `TaskObject`."); // Get `TaskRow` from `ListItem` let task_row = list_item .downcast_ref::<ListItem>() .expect("Needs to be ListItem") .child() .and_downcast::<TaskRow>() .expect("The child has to be a `TaskRow`."); task_row.bind(&task_object); }); // Tell factory how to unbind `TaskRow` from `TaskObject` factory.connect_unbind(move |_, list_item| { // Get `TaskRow` from `ListItem` let task_row = list_item .downcast_ref::<ListItem>() .expect("Needs to be ListItem") .child() .and_downcast::<TaskRow>() .expect("The child has to be a `TaskRow`."); task_row.unbind(); }); // Set the factory of the list view self.imp().tasks_list.set_factory(Some(&factory)); } fn setup_actions(&self) { // 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::<TaskObject>() .expect("The object needs to be of type `TaskObject`."); if task_object.is_completed() { tasks.remove(position); } else { position += 1; } } } }

Finally, we make sure that everything is set up in constructed.

Filename: listings/todo/2/window/imp.rs

use std::cell::RefCell; use std::fs::File; use gio::Settings; use glib::subclass::InitializingObject; use gtk::prelude::*; use gtk::subclass::prelude::*; use gtk::{gio, glib, CompositeTemplate, Entry, ListView}; use std::cell::OnceCell; use crate::task_object::{TaskData, TaskObject}; use crate::utils::data_path; // Object holding the state #[derive(CompositeTemplate, Default)] #[template(resource = "/org/gtk_rs/Todo2/window.ui")] pub struct Window { #[template_child] pub entry: TemplateChild<Entry>, #[template_child] pub tasks_list: TemplateChild<ListView>, pub tasks: RefCell<Option<gio::ListStore>>, pub settings: OnceCell<Settings>, } // The central trait for subclassing a GObject #[glib::object_subclass] impl ObjectSubclass for Window { // `NAME` needs to match `class` attribute of template const NAME: &'static str = "TodoWindow"; type Type = super::Window; type ParentType = gtk::ApplicationWindow; 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<Self>) { obj.init_template(); } } // Trait shared by all GObjects impl ObjectImpl for Window { fn constructed(&self) { // Call "constructed" on parent self.parent_constructed(); // Setup let obj = self.obj(); obj.setup_settings(); obj.setup_tasks(); obj.restore_data(); obj.setup_callbacks(); obj.setup_factory(); obj.setup_actions(); } } // Trait shared by all widgets impl WidgetImpl for Window {} // Trait shared by all windows impl WindowImpl for Window { fn close_request(&self) -> glib::Propagation { // Store task data in vector let backup_data: Vec<TaskData> = self .obj() .tasks() .iter::<TaskObject>() .filter_map(Result::ok) .map(|task_object| task_object.task_data()) .collect(); // Save state to file let file = File::create(data_path()).expect("Could not create json file."); serde_json::to_writer(file, &backup_data) .expect("Could not write data to json file"); // Pass close request on to the parent self.parent_close_request() } } // Trait shared by all application windows impl ApplicationWindowImpl for Window {}

Our To-Do app suddenly became much more useful. Not only can we filter tasks, we also retain our tasks between sessions.