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 us start by adding a menu and a header bar to window.ui. The code should feel familiar to the one in the former chapter.

Filename: listings/todo_app/2/window/window.ui

 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
+  <menu id="main-menu">
+    <submenu>
+      <attribute name="label" translatable="yes">_Filtering</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">_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="start">
+          <object class="GtkButton" id="clear_button">
+            <property name="label">Clear</property>
+          </object>
+        </child>
+        <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>
     <child>
       <object class="GtkBox">
         <property name="orientation">vertical</property>

We also need to add settings and a reference to clear_button to imp::Window. Since gio::Settings does not implement Default, we stop deriving Default for imp::Window and implement it manually.

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

use std::fs::File;

use gio::Settings;
use glib::signal::Inhibit;
use glib::subclass::InitializingObject;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib};
use gtk::{Button, CompositeTemplate, Entry, ListView, MenuButton};
use once_cell::sync::OnceCell;

use crate::todo_object::TodoObject;
use crate::utils::data_path;

// Object holding the state
#[derive(CompositeTemplate)]
#[template(file = "window.ui")]
pub struct Window {
    #[template_child]
    pub entry: TemplateChild<Entry>,
    #[template_child]
    pub menu_button: TemplateChild<MenuButton>,
    #[template_child]
    pub list_view: TemplateChild<ListView>,
    #[template_child]
    pub clear_button: TemplateChild<Button>,
    pub model: OnceCell<gio::ListStore>,
    pub settings: Settings,
}

impl Default for Window {
    fn default() -> Self {
        Window {
            entry: TemplateChild::default(),
            menu_button: TemplateChild::default(),
            list_view: TemplateChild::default(),
            clear_button: TemplateChild::default(),
            model: OnceCell::default(),
            settings: Settings::new("org.gtk-rs.Todo"),
        }
    }
}

// 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) {
        Self::bind_template(klass);
    }

    fn instance_init(obj: &InitializingObject<Self>) {
        obj.init_template();
    }
}

// Trait shared by all GObjects
impl ObjectImpl for Window {
    fn constructed(&self, obj: &Self::Type) {
        // Call "constructed" on parent
        self.parent_constructed(obj);

        // Setup
        obj.setup_model();
        obj.restore_data();
        obj.setup_callbacks();
        obj.setup_factory();
        obj.setup_shortcut_window();
        obj.setup_filter_action();
    }
}

// Trait shared by all widgets
impl WidgetImpl for Window {}

// Trait shared by all windows
impl WindowImpl for Window {
    fn close_request(&self, window: &Self::Type) -> Inhibit {
        // Store todo data in vector
        let mut backup_data = Vec::new();
        let mut position = 0;
        while let Some(item) = window.model().item(position) {
            // Get `TodoObject` from `glib::Object`
            let todo_data = item
                .downcast_ref::<TodoObject>()
                .expect("The object needs to be of type `TodoObject`.")
                .todo_data();
            // Add todo data to vector and increase position
            backup_data.push(todo_data);
            position += 1;
        }

        // Save state in file
        let file = File::create(data_path()).expect("Could not create json file.");
        serde_json::to_writer_pretty(file, &backup_data)
            .expect("Could not write data to json file");

        // Pass close request on to the parent
        self.parent_close_request(window)
    }
}

// Trait shared by all application
impl ApplicationWindowImpl for Window {}

We also add the getter methods is_completed and todo_data to TodoObject. We will make use of them in the following snippets.

Filename: listings/todo_app/2/todo_object/mod.rs

mod imp;

use glib::Object;
use gtk::glib;
use gtk::subclass::prelude::*;
use serde::{Deserialize, Serialize};

glib::wrapper! {
    pub struct TodoObject(ObjectSubclass<imp::TodoObject>);
}

impl TodoObject {
    pub fn new(completed: bool, content: String) -> Self {
        Object::new(&[("completed", &completed), ("content", &content)])
            .expect("Failed to create `TodoObject`.")
    }

    pub fn is_completed(&self) -> bool {
        let imp = imp::TodoObject::from_instance(self);
        imp.data.borrow().completed
    }

    pub fn todo_data(&self) -> TodoData {
        let imp = imp::TodoObject::from_instance(self);
        imp.data.borrow().clone()
    }
}

#[derive(Default, Serialize, Deserialize, Clone)]
pub struct TodoData {
    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_app/2/window/mod.rs

mod imp;

use std::fs::File;

use crate::todo_object::{TodoData, TodoObject};
use crate::todo_row::TodoRow;
use crate::utils::data_path;
use glib::{clone, Object};
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib};
use gtk::{Application, CustomFilter, FilterListModel, NoSelection, SignalListItemFactory};
use log::info;

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::new(&[("application", app)]).expect("Failed to create `Window`.")
    }

    fn model(&self) -> &gio::ListStore {
        // Get state
        let imp = imp::Window::from_instance(self);
        imp.model.get().expect("Could not get model")
    }

    fn filter(&self) -> Option<CustomFilter> {
        // Get state
        let imp = imp::Window::from_instance(self);

        // Get filter_state from settings
        let filter_state: String = imp.settings.get("filter");

        // Create custom filters
        let filter_open = CustomFilter::new(|obj| {
            // Get `TodoObject` from `glib::Object`
            let todo_object = obj
                .downcast_ref::<TodoObject>()
                .expect("The object needs to be of type `TodoObject`.");

            // Only allow completed tasks
            !todo_object.is_completed()
        });
        let filter_done = CustomFilter::new(|obj| {
            // Get `TodoObject` from `glib::Object`
            let todo_object = obj
                .downcast_ref::<TodoObject>()
                .expect("The object needs to be of type `TodoObject`.");

            // Only allow done tasks
            todo_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_model(&self) {
        // Create new model
        let model = gio::ListStore::new(TodoObject::static_type());

        // Get state and set model
        let imp = imp::Window::from_instance(self);
        imp.model.set(model).expect("Could not set model");

        // Wrap model with filter and selection and pass it to the list view
        let filter_model = FilterListModel::new(Some(self.model()), self.filter().as_ref());
        let selection_model = NoSelection::new(Some(&filter_model));
        imp.list_view.set_model(Some(&selection_model));

        // Filter model whenever the value of the key "filter" changes
        imp.settings.connect_changed(
            Some("filter"),
            clone!(@weak self as window, @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<TodoData> =
                serde_json::from_reader(file).expect("Could not get backup data from json file.");

            // Convert `Vec<TodoData>` to `Vec<Object>`
            let todo_objects: Vec<Object> = backup_data
                .into_iter()
                .map(|todo_data| TodoObject::new(todo_data.completed, todo_data.content))
                .map(|todo_object| todo_object.upcast())
                .collect();

            // Insert restored objects into model
            self.model().splice(0, 0, &todo_objects);
        } else {
            info!("Backup file does not exist yet {:?}", data_path());
        }
    }

    fn setup_callbacks(&self) {
        // Get state
        let imp = imp::Window::from_instance(self);
        let model = self.model();

        // Setup callback so that activation
        // creates a new todo object and clears the entry
        imp.entry
            .connect_activate(clone!(@weak model => move |entry| {
                let buffer = entry.buffer();
                let content = buffer.text();
                let todo_object = TodoObject::new(false, content);
                model.append(&todo_object);
                buffer.set_text("");
            }));

        // Setup callback so that click on the clear_button
        // removes all done tasks
        imp.clear_button
            .connect_clicked(clone!(@weak model => move |_| {
                let mut position = 0;
                while let Some(item) = model.item(position) {
                    // Get `TodoObject` from `glib::Object`
                    let todo_object = item
                        .downcast_ref::<TodoObject>()
                        .expect("The object needs to be of type `TodoObject`.");

                    if todo_object.is_completed() {
                        model.remove(position);
                    } else {
                        position += 1;
                    }
                }
            }));
    }

    fn setup_factory(&self) {
        // Create a new factory
        let factory = SignalListItemFactory::new();

        // Create an empty `TodoRow` during setup
        factory.connect_setup(move |_, list_item| {
            // Create `TodoRow`
            let todo_row = TodoRow::new();
            list_item.set_child(Some(&todo_row));
        });

        // Tell factory how to bind `TodoRow` to a `TodoObject`
        factory.connect_bind(move |_, list_item| {
            // Get `TodoObject` from `ListItem`
            let todo_object = list_item
                .item()
                .expect("The item has to exist.")
                .downcast::<TodoObject>()
                .expect("The item has to be an `TodoObject`.");

            // Get `TodoRow` from `ListItem`
            let todo_row = list_item
                .child()
                .expect("The child has to exist.")
                .downcast::<TodoRow>()
                .expect("The child has to be a `TodoRow`.");

            todo_row.bind(&todo_object);
        });

        // Tell factory how to unbind `TodoRow` from `TodoObject`
        factory.connect_unbind(move |_, list_item| {
            // Get `TodoRow` from `ListItem`
            let todo_row = list_item
                .child()
                .expect("The child has to exist.")
                .downcast::<TodoRow>()
                .expect("The child has to be a `TodoRow`.");

            todo_row.unbind();
        });

        // Set the factory of the list view
        let imp = imp::Window::from_instance(self);
        imp.list_view.set_factory(Some(&factory));
    }

    fn setup_shortcut_window(&self) {
        // Get `ShortcutsWindow` via `gtk::Builder`
        let builder = gtk::Builder::from_string(include_str!("shortcuts.ui"));
        let shortcuts = builder
            .object("shortcuts")
            .expect("Could not get object `shortcuts` from builder.");

        // After calling this method,
        // calling the action "win.show-help-overlay" will show the shortcut window
        self.set_help_overlay(Some(&shortcuts));
    }

    fn setup_filter_action(&self) {
        // Get state
        let imp = imp::Window::from_instance(self);

        // Create action from key "filter"
        let filter_action = imp.settings.create_action("filter");

        // Add action "filter" to action group "win"
        self.add_action(&filter_action);
    }
}

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_app/2/window/mod.rs

mod imp;

use std::fs::File;

use crate::todo_object::{TodoData, TodoObject};
use crate::todo_row::TodoRow;
use crate::utils::data_path;
use glib::{clone, Object};
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib};
use gtk::{Application, CustomFilter, FilterListModel, NoSelection, SignalListItemFactory};
use log::info;

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::new(&[("application", app)]).expect("Failed to create `Window`.")
    }

    fn model(&self) -> &gio::ListStore {
        // Get state
        let imp = imp::Window::from_instance(self);
        imp.model.get().expect("Could not get model")
    }

    fn filter(&self) -> Option<CustomFilter> {
        // Get state
        let imp = imp::Window::from_instance(self);

        // Get filter_state from settings
        let filter_state: String = imp.settings.get("filter");

        // Create custom filters
        let filter_open = CustomFilter::new(|obj| {
            // Get `TodoObject` from `glib::Object`
            let todo_object = obj
                .downcast_ref::<TodoObject>()
                .expect("The object needs to be of type `TodoObject`.");

            // Only allow completed tasks
            !todo_object.is_completed()
        });
        let filter_done = CustomFilter::new(|obj| {
            // Get `TodoObject` from `glib::Object`
            let todo_object = obj
                .downcast_ref::<TodoObject>()
                .expect("The object needs to be of type `TodoObject`.");

            // Only allow done tasks
            todo_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_model(&self) {
        // Create new model
        let model = gio::ListStore::new(TodoObject::static_type());

        // Get state and set model
        let imp = imp::Window::from_instance(self);
        imp.model.set(model).expect("Could not set model");

        // Wrap model with filter and selection and pass it to the list view
        let filter_model = FilterListModel::new(Some(self.model()), self.filter().as_ref());
        let selection_model = NoSelection::new(Some(&filter_model));
        imp.list_view.set_model(Some(&selection_model));

        // Filter model whenever the value of the key "filter" changes
        imp.settings.connect_changed(
            Some("filter"),
            clone!(@weak self as window, @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<TodoData> =
                serde_json::from_reader(file).expect("Could not get backup data from json file.");

            // Convert `Vec<TodoData>` to `Vec<Object>`
            let todo_objects: Vec<Object> = backup_data
                .into_iter()
                .map(|todo_data| TodoObject::new(todo_data.completed, todo_data.content))
                .map(|todo_object| todo_object.upcast())
                .collect();

            // Insert restored objects into model
            self.model().splice(0, 0, &todo_objects);
        } else {
            info!("Backup file does not exist yet {:?}", data_path());
        }
    }

    fn setup_callbacks(&self) {
        // Get state
        let imp = imp::Window::from_instance(self);
        let model = self.model();

        // Setup callback so that activation
        // creates a new todo object and clears the entry
        imp.entry
            .connect_activate(clone!(@weak model => move |entry| {
                let buffer = entry.buffer();
                let content = buffer.text();
                let todo_object = TodoObject::new(false, content);
                model.append(&todo_object);
                buffer.set_text("");
            }));

        // Setup callback so that click on the clear_button
        // removes all done tasks
        imp.clear_button
            .connect_clicked(clone!(@weak model => move |_| {
                let mut position = 0;
                while let Some(item) = model.item(position) {
                    // Get `TodoObject` from `glib::Object`
                    let todo_object = item
                        .downcast_ref::<TodoObject>()
                        .expect("The object needs to be of type `TodoObject`.");

                    if todo_object.is_completed() {
                        model.remove(position);
                    } else {
                        position += 1;
                    }
                }
            }));
    }

    fn setup_factory(&self) {
        // Create a new factory
        let factory = SignalListItemFactory::new();

        // Create an empty `TodoRow` during setup
        factory.connect_setup(move |_, list_item| {
            // Create `TodoRow`
            let todo_row = TodoRow::new();
            list_item.set_child(Some(&todo_row));
        });

        // Tell factory how to bind `TodoRow` to a `TodoObject`
        factory.connect_bind(move |_, list_item| {
            // Get `TodoObject` from `ListItem`
            let todo_object = list_item
                .item()
                .expect("The item has to exist.")
                .downcast::<TodoObject>()
                .expect("The item has to be an `TodoObject`.");

            // Get `TodoRow` from `ListItem`
            let todo_row = list_item
                .child()
                .expect("The child has to exist.")
                .downcast::<TodoRow>()
                .expect("The child has to be a `TodoRow`.");

            todo_row.bind(&todo_object);
        });

        // Tell factory how to unbind `TodoRow` from `TodoObject`
        factory.connect_unbind(move |_, list_item| {
            // Get `TodoRow` from `ListItem`
            let todo_row = list_item
                .child()
                .expect("The child has to exist.")
                .downcast::<TodoRow>()
                .expect("The child has to be a `TodoRow`.");

            todo_row.unbind();
        });

        // Set the factory of the list view
        let imp = imp::Window::from_instance(self);
        imp.list_view.set_factory(Some(&factory));
    }

    fn setup_shortcut_window(&self) {
        // Get `ShortcutsWindow` via `gtk::Builder`
        let builder = gtk::Builder::from_string(include_str!("shortcuts.ui"));
        let shortcuts = builder
            .object("shortcuts")
            .expect("Could not get object `shortcuts` from builder.");

        // After calling this method,
        // calling the action "win.show-help-overlay" will show the shortcut window
        self.set_help_overlay(Some(&shortcuts));
    }

    fn setup_filter_action(&self) {
        // Get state
        let imp = imp::Window::from_instance(self);

        // Create action from key "filter"
        let filter_action = imp.settings.create_action("filter");

        // Add action "filter" to action group "win"
        self.add_action(&filter_action);
    }
}

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_app/2/window/mod.rs

mod imp;

use std::fs::File;

use crate::todo_object::{TodoData, TodoObject};
use crate::todo_row::TodoRow;
use crate::utils::data_path;
use glib::{clone, Object};
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib};
use gtk::{Application, CustomFilter, FilterListModel, NoSelection, SignalListItemFactory};
use log::info;

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::new(&[("application", app)]).expect("Failed to create `Window`.")
    }

    fn model(&self) -> &gio::ListStore {
        // Get state
        let imp = imp::Window::from_instance(self);
        imp.model.get().expect("Could not get model")
    }

    fn filter(&self) -> Option<CustomFilter> {
        // Get state
        let imp = imp::Window::from_instance(self);

        // Get filter_state from settings
        let filter_state: String = imp.settings.get("filter");

        // Create custom filters
        let filter_open = CustomFilter::new(|obj| {
            // Get `TodoObject` from `glib::Object`
            let todo_object = obj
                .downcast_ref::<TodoObject>()
                .expect("The object needs to be of type `TodoObject`.");

            // Only allow completed tasks
            !todo_object.is_completed()
        });
        let filter_done = CustomFilter::new(|obj| {
            // Get `TodoObject` from `glib::Object`
            let todo_object = obj
                .downcast_ref::<TodoObject>()
                .expect("The object needs to be of type `TodoObject`.");

            // Only allow done tasks
            todo_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_model(&self) {
        // Create new model
        let model = gio::ListStore::new(TodoObject::static_type());

        // Get state and set model
        let imp = imp::Window::from_instance(self);
        imp.model.set(model).expect("Could not set model");

        // Wrap model with filter and selection and pass it to the list view
        let filter_model = FilterListModel::new(Some(self.model()), self.filter().as_ref());
        let selection_model = NoSelection::new(Some(&filter_model));
        imp.list_view.set_model(Some(&selection_model));

        // Filter model whenever the value of the key "filter" changes
        imp.settings.connect_changed(
            Some("filter"),
            clone!(@weak self as window, @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<TodoData> =
                serde_json::from_reader(file).expect("Could not get backup data from json file.");

            // Convert `Vec<TodoData>` to `Vec<Object>`
            let todo_objects: Vec<Object> = backup_data
                .into_iter()
                .map(|todo_data| TodoObject::new(todo_data.completed, todo_data.content))
                .map(|todo_object| todo_object.upcast())
                .collect();

            // Insert restored objects into model
            self.model().splice(0, 0, &todo_objects);
        } else {
            info!("Backup file does not exist yet {:?}", data_path());
        }
    }

    fn setup_callbacks(&self) {
        // Get state
        let imp = imp::Window::from_instance(self);
        let model = self.model();

        // Setup callback so that activation
        // creates a new todo object and clears the entry
        imp.entry
            .connect_activate(clone!(@weak model => move |entry| {
                let buffer = entry.buffer();
                let content = buffer.text();
                let todo_object = TodoObject::new(false, content);
                model.append(&todo_object);
                buffer.set_text("");
            }));

        // Setup callback so that click on the clear_button
        // removes all done tasks
        imp.clear_button
            .connect_clicked(clone!(@weak model => move |_| {
                let mut position = 0;
                while let Some(item) = model.item(position) {
                    // Get `TodoObject` from `glib::Object`
                    let todo_object = item
                        .downcast_ref::<TodoObject>()
                        .expect("The object needs to be of type `TodoObject`.");

                    if todo_object.is_completed() {
                        model.remove(position);
                    } else {
                        position += 1;
                    }
                }
            }));
    }

    fn setup_factory(&self) {
        // Create a new factory
        let factory = SignalListItemFactory::new();

        // Create an empty `TodoRow` during setup
        factory.connect_setup(move |_, list_item| {
            // Create `TodoRow`
            let todo_row = TodoRow::new();
            list_item.set_child(Some(&todo_row));
        });

        // Tell factory how to bind `TodoRow` to a `TodoObject`
        factory.connect_bind(move |_, list_item| {
            // Get `TodoObject` from `ListItem`
            let todo_object = list_item
                .item()
                .expect("The item has to exist.")
                .downcast::<TodoObject>()
                .expect("The item has to be an `TodoObject`.");

            // Get `TodoRow` from `ListItem`
            let todo_row = list_item
                .child()
                .expect("The child has to exist.")
                .downcast::<TodoRow>()
                .expect("The child has to be a `TodoRow`.");

            todo_row.bind(&todo_object);
        });

        // Tell factory how to unbind `TodoRow` from `TodoObject`
        factory.connect_unbind(move |_, list_item| {
            // Get `TodoRow` from `ListItem`
            let todo_row = list_item
                .child()
                .expect("The child has to exist.")
                .downcast::<TodoRow>()
                .expect("The child has to be a `TodoRow`.");

            todo_row.unbind();
        });

        // Set the factory of the list view
        let imp = imp::Window::from_instance(self);
        imp.list_view.set_factory(Some(&factory));
    }

    fn setup_shortcut_window(&self) {
        // Get `ShortcutsWindow` via `gtk::Builder`
        let builder = gtk::Builder::from_string(include_str!("shortcuts.ui"));
        let shortcuts = builder
            .object("shortcuts")
            .expect("Could not get object `shortcuts` from builder.");

        // After calling this method,
        // calling the action "win.show-help-overlay" will show the shortcut window
        self.set_help_overlay(Some(&shortcuts));
    }

    fn setup_filter_action(&self) {
        // Get state
        let imp = imp::Window::from_instance(self);

        // Create action from key "filter"
        let filter_action = imp.settings.create_action("filter");

        // Add action "filter" to action group "win"
        self.add_action(&filter_action);
    }
}

In setup_callbacks, we add a signal handler to clear_button, which removes all completed tasks when activated.

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

mod imp;

use std::fs::File;

use crate::todo_object::{TodoData, TodoObject};
use crate::todo_row::TodoRow;
use crate::utils::data_path;
use glib::{clone, Object};
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib};
use gtk::{Application, CustomFilter, FilterListModel, NoSelection, SignalListItemFactory};
use log::info;

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::new(&[("application", app)]).expect("Failed to create `Window`.")
    }

    fn model(&self) -> &gio::ListStore {
        // Get state
        let imp = imp::Window::from_instance(self);
        imp.model.get().expect("Could not get model")
    }

    fn filter(&self) -> Option<CustomFilter> {
        // Get state
        let imp = imp::Window::from_instance(self);

        // Get filter_state from settings
        let filter_state: String = imp.settings.get("filter");

        // Create custom filters
        let filter_open = CustomFilter::new(|obj| {
            // Get `TodoObject` from `glib::Object`
            let todo_object = obj
                .downcast_ref::<TodoObject>()
                .expect("The object needs to be of type `TodoObject`.");

            // Only allow completed tasks
            !todo_object.is_completed()
        });
        let filter_done = CustomFilter::new(|obj| {
            // Get `TodoObject` from `glib::Object`
            let todo_object = obj
                .downcast_ref::<TodoObject>()
                .expect("The object needs to be of type `TodoObject`.");

            // Only allow done tasks
            todo_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_model(&self) {
        // Create new model
        let model = gio::ListStore::new(TodoObject::static_type());

        // Get state and set model
        let imp = imp::Window::from_instance(self);
        imp.model.set(model).expect("Could not set model");

        // Wrap model with filter and selection and pass it to the list view
        let filter_model = FilterListModel::new(Some(self.model()), self.filter().as_ref());
        let selection_model = NoSelection::new(Some(&filter_model));
        imp.list_view.set_model(Some(&selection_model));

        // Filter model whenever the value of the key "filter" changes
        imp.settings.connect_changed(
            Some("filter"),
            clone!(@weak self as window, @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<TodoData> =
                serde_json::from_reader(file).expect("Could not get backup data from json file.");

            // Convert `Vec<TodoData>` to `Vec<Object>`
            let todo_objects: Vec<Object> = backup_data
                .into_iter()
                .map(|todo_data| TodoObject::new(todo_data.completed, todo_data.content))
                .map(|todo_object| todo_object.upcast())
                .collect();

            // Insert restored objects into model
            self.model().splice(0, 0, &todo_objects);
        } else {
            info!("Backup file does not exist yet {:?}", data_path());
        }
    }

    fn setup_callbacks(&self) {
        // Get state
        let imp = imp::Window::from_instance(self);
        let model = self.model();

        // Setup callback so that activation
        // creates a new todo object and clears the entry
        imp.entry
            .connect_activate(clone!(@weak model => move |entry| {
                let buffer = entry.buffer();
                let content = buffer.text();
                let todo_object = TodoObject::new(false, content);
                model.append(&todo_object);
                buffer.set_text("");
            }));

        // Setup callback so that click on the clear_button
        // removes all done tasks
        imp.clear_button
            .connect_clicked(clone!(@weak model => move |_| {
                let mut position = 0;
                while let Some(item) = model.item(position) {
                    // Get `TodoObject` from `glib::Object`
                    let todo_object = item
                        .downcast_ref::<TodoObject>()
                        .expect("The object needs to be of type `TodoObject`.");

                    if todo_object.is_completed() {
                        model.remove(position);
                    } else {
                        position += 1;
                    }
                }
            }));
    }

    fn setup_factory(&self) {
        // Create a new factory
        let factory = SignalListItemFactory::new();

        // Create an empty `TodoRow` during setup
        factory.connect_setup(move |_, list_item| {
            // Create `TodoRow`
            let todo_row = TodoRow::new();
            list_item.set_child(Some(&todo_row));
        });

        // Tell factory how to bind `TodoRow` to a `TodoObject`
        factory.connect_bind(move |_, list_item| {
            // Get `TodoObject` from `ListItem`
            let todo_object = list_item
                .item()
                .expect("The item has to exist.")
                .downcast::<TodoObject>()
                .expect("The item has to be an `TodoObject`.");

            // Get `TodoRow` from `ListItem`
            let todo_row = list_item
                .child()
                .expect("The child has to exist.")
                .downcast::<TodoRow>()
                .expect("The child has to be a `TodoRow`.");

            todo_row.bind(&todo_object);
        });

        // Tell factory how to unbind `TodoRow` from `TodoObject`
        factory.connect_unbind(move |_, list_item| {
            // Get `TodoRow` from `ListItem`
            let todo_row = list_item
                .child()
                .expect("The child has to exist.")
                .downcast::<TodoRow>()
                .expect("The child has to be a `TodoRow`.");

            todo_row.unbind();
        });

        // Set the factory of the list view
        let imp = imp::Window::from_instance(self);
        imp.list_view.set_factory(Some(&factory));
    }

    fn setup_shortcut_window(&self) {
        // Get `ShortcutsWindow` via `gtk::Builder`
        let builder = gtk::Builder::from_string(include_str!("shortcuts.ui"));
        let shortcuts = builder
            .object("shortcuts")
            .expect("Could not get object `shortcuts` from builder.");

        // After calling this method,
        // calling the action "win.show-help-overlay" will show the shortcut window
        self.set_help_overlay(Some(&shortcuts));
    }

    fn setup_filter_action(&self) {
        // Get state
        let imp = imp::Window::from_instance(self);

        // Create action from key "filter"
        let filter_action = imp.settings.create_action("filter");

        // Add action "filter" to action group "win"
        self.add_action(&filter_action);
    }
}

In setup_shortcut_window, we add a handy way to let users of our app know which shortcuts they can use.

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

mod imp;

use std::fs::File;

use crate::todo_object::{TodoData, TodoObject};
use crate::todo_row::TodoRow;
use crate::utils::data_path;
use glib::{clone, Object};
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib};
use gtk::{Application, CustomFilter, FilterListModel, NoSelection, SignalListItemFactory};
use log::info;

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::new(&[("application", app)]).expect("Failed to create `Window`.")
    }

    fn model(&self) -> &gio::ListStore {
        // Get state
        let imp = imp::Window::from_instance(self);
        imp.model.get().expect("Could not get model")
    }

    fn filter(&self) -> Option<CustomFilter> {
        // Get state
        let imp = imp::Window::from_instance(self);

        // Get filter_state from settings
        let filter_state: String = imp.settings.get("filter");

        // Create custom filters
        let filter_open = CustomFilter::new(|obj| {
            // Get `TodoObject` from `glib::Object`
            let todo_object = obj
                .downcast_ref::<TodoObject>()
                .expect("The object needs to be of type `TodoObject`.");

            // Only allow completed tasks
            !todo_object.is_completed()
        });
        let filter_done = CustomFilter::new(|obj| {
            // Get `TodoObject` from `glib::Object`
            let todo_object = obj
                .downcast_ref::<TodoObject>()
                .expect("The object needs to be of type `TodoObject`.");

            // Only allow done tasks
            todo_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_model(&self) {
        // Create new model
        let model = gio::ListStore::new(TodoObject::static_type());

        // Get state and set model
        let imp = imp::Window::from_instance(self);
        imp.model.set(model).expect("Could not set model");

        // Wrap model with filter and selection and pass it to the list view
        let filter_model = FilterListModel::new(Some(self.model()), self.filter().as_ref());
        let selection_model = NoSelection::new(Some(&filter_model));
        imp.list_view.set_model(Some(&selection_model));

        // Filter model whenever the value of the key "filter" changes
        imp.settings.connect_changed(
            Some("filter"),
            clone!(@weak self as window, @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<TodoData> =
                serde_json::from_reader(file).expect("Could not get backup data from json file.");

            // Convert `Vec<TodoData>` to `Vec<Object>`
            let todo_objects: Vec<Object> = backup_data
                .into_iter()
                .map(|todo_data| TodoObject::new(todo_data.completed, todo_data.content))
                .map(|todo_object| todo_object.upcast())
                .collect();

            // Insert restored objects into model
            self.model().splice(0, 0, &todo_objects);
        } else {
            info!("Backup file does not exist yet {:?}", data_path());
        }
    }

    fn setup_callbacks(&self) {
        // Get state
        let imp = imp::Window::from_instance(self);
        let model = self.model();

        // Setup callback so that activation
        // creates a new todo object and clears the entry
        imp.entry
            .connect_activate(clone!(@weak model => move |entry| {
                let buffer = entry.buffer();
                let content = buffer.text();
                let todo_object = TodoObject::new(false, content);
                model.append(&todo_object);
                buffer.set_text("");
            }));

        // Setup callback so that click on the clear_button
        // removes all done tasks
        imp.clear_button
            .connect_clicked(clone!(@weak model => move |_| {
                let mut position = 0;
                while let Some(item) = model.item(position) {
                    // Get `TodoObject` from `glib::Object`
                    let todo_object = item
                        .downcast_ref::<TodoObject>()
                        .expect("The object needs to be of type `TodoObject`.");

                    if todo_object.is_completed() {
                        model.remove(position);
                    } else {
                        position += 1;
                    }
                }
            }));
    }

    fn setup_factory(&self) {
        // Create a new factory
        let factory = SignalListItemFactory::new();

        // Create an empty `TodoRow` during setup
        factory.connect_setup(move |_, list_item| {
            // Create `TodoRow`
            let todo_row = TodoRow::new();
            list_item.set_child(Some(&todo_row));
        });

        // Tell factory how to bind `TodoRow` to a `TodoObject`
        factory.connect_bind(move |_, list_item| {
            // Get `TodoObject` from `ListItem`
            let todo_object = list_item
                .item()
                .expect("The item has to exist.")
                .downcast::<TodoObject>()
                .expect("The item has to be an `TodoObject`.");

            // Get `TodoRow` from `ListItem`
            let todo_row = list_item
                .child()
                .expect("The child has to exist.")
                .downcast::<TodoRow>()
                .expect("The child has to be a `TodoRow`.");

            todo_row.bind(&todo_object);
        });

        // Tell factory how to unbind `TodoRow` from `TodoObject`
        factory.connect_unbind(move |_, list_item| {
            // Get `TodoRow` from `ListItem`
            let todo_row = list_item
                .child()
                .expect("The child has to exist.")
                .downcast::<TodoRow>()
                .expect("The child has to be a `TodoRow`.");

            todo_row.unbind();
        });

        // Set the factory of the list view
        let imp = imp::Window::from_instance(self);
        imp.list_view.set_factory(Some(&factory));
    }

    fn setup_shortcut_window(&self) {
        // Get `ShortcutsWindow` via `gtk::Builder`
        let builder = gtk::Builder::from_string(include_str!("shortcuts.ui"));
        let shortcuts = builder
            .object("shortcuts")
            .expect("Could not get object `shortcuts` from builder.");

        // After calling this method,
        // calling the action "win.show-help-overlay" will show the shortcut window
        self.set_help_overlay(Some(&shortcuts));
    }

    fn setup_filter_action(&self) {
        // Get state
        let imp = imp::Window::from_instance(self);

        // Create action from key "filter"
        let filter_action = imp.settings.create_action("filter");

        // Add action "filter" to action group "win"
        self.add_action(&filter_action);
    }
}

The entries can be organized with gtk::ShortcutsSection and gtk::ShortcutsGroup. If we specify the action name, we also do not have to repeat the keyboard accelerator. gtk::ShortcutsShortcut looks it up on its own. The shortcuts.ui file looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<interface>
  <object class="GtkShortcutsWindow" id="shortcuts">
    <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>

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 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_app/2/main.rs

mod todo_object;
mod todo_row;
mod utils;
mod window;

use gtk::prelude::*;
use gtk::Application;

use window::Window;

fn main() {
    // Initialize logger
    pretty_env_logger::init();

    // Create a new application
    let app = Application::builder()
        .application_id("org.gtk-rs.Todo")
        .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')", &["<primary>a"]);
    app.set_accels_for_action("win.filter('Open')", &["<primary>o"]);
    app.set_accels_for_action("win.filter('Done')", &["<primary>d"]);
    app.set_accels_for_action("win.show-help-overlay", &["<primary>question"]);
}

fn build_ui(app: &Application) {
    // Create a new custom window and show it
    let window = Window::new(app);
    window.show();
}

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.

[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_app/2/todo_object/mod.rs

mod imp;

use glib::Object;
use gtk::glib;
use gtk::subclass::prelude::*;
use serde::{Deserialize, Serialize};

glib::wrapper! {
    pub struct TodoObject(ObjectSubclass<imp::TodoObject>);
}

impl TodoObject {
    pub fn new(completed: bool, content: String) -> Self {
        Object::new(&[("completed", &completed), ("content", &content)])
            .expect("Failed to create `TodoObject`.")
    }

    pub fn is_completed(&self) -> bool {
        let imp = imp::TodoObject::from_instance(self);
        imp.data.borrow().completed
    }

    pub fn todo_data(&self) -> TodoData {
        let imp = imp::TodoObject::from_instance(self);
        imp.data.borrow().clone()
    }
}

#[derive(Default, Serialize, Deserialize, Clone)]
pub struct TodoData {
    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_app/2/utils.rs

use std::path::PathBuf;

use gtk::glib;

pub fn data_path() -> PathBuf {
    let mut path = glib::user_config_dir();
    path.push("org.gtk-rs.Todo");
    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_app/2/window/imp.rs

use std::fs::File;

use gio::Settings;
use glib::signal::Inhibit;
use glib::subclass::InitializingObject;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib};
use gtk::{Button, CompositeTemplate, Entry, ListView, MenuButton};
use once_cell::sync::OnceCell;

use crate::todo_object::TodoObject;
use crate::utils::data_path;

// Object holding the state
#[derive(CompositeTemplate)]
#[template(file = "window.ui")]
pub struct Window {
    #[template_child]
    pub entry: TemplateChild<Entry>,
    #[template_child]
    pub menu_button: TemplateChild<MenuButton>,
    #[template_child]
    pub list_view: TemplateChild<ListView>,
    #[template_child]
    pub clear_button: TemplateChild<Button>,
    pub model: OnceCell<gio::ListStore>,
    pub settings: Settings,
}

impl Default for Window {
    fn default() -> Self {
        Window {
            entry: TemplateChild::default(),
            menu_button: TemplateChild::default(),
            list_view: TemplateChild::default(),
            clear_button: TemplateChild::default(),
            model: OnceCell::default(),
            settings: Settings::new("org.gtk-rs.Todo"),
        }
    }
}

// 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) {
        Self::bind_template(klass);
    }

    fn instance_init(obj: &InitializingObject<Self>) {
        obj.init_template();
    }
}

// Trait shared by all GObjects
impl ObjectImpl for Window {
    fn constructed(&self, obj: &Self::Type) {
        // Call "constructed" on parent
        self.parent_constructed(obj);

        // Setup
        obj.setup_model();
        obj.restore_data();
        obj.setup_callbacks();
        obj.setup_factory();
        obj.setup_shortcut_window();
        obj.setup_filter_action();
    }
}

// Trait shared by all widgets
impl WidgetImpl for Window {}

// Trait shared by all windows
impl WindowImpl for Window {
    fn close_request(&self, window: &Self::Type) -> Inhibit {
        // Store todo data in vector
        let mut backup_data = Vec::new();
        let mut position = 0;
        while let Some(item) = window.model().item(position) {
            // Get `TodoObject` from `glib::Object`
            let todo_data = item
                .downcast_ref::<TodoObject>()
                .expect("The object needs to be of type `TodoObject`.")
                .todo_data();
            // Add todo data to vector and increase position
            backup_data.push(todo_data);
            position += 1;
        }

        // Save state in file
        let file = File::create(data_path()).expect("Could not create json file.");
        serde_json::to_writer_pretty(file, &backup_data)
            .expect("Could not write data to json file");

        // Pass close request on to the parent
        self.parent_close_request(window)
    }
}

// Trait shared by all application
impl ApplicationWindowImpl for Window {}

Note that we used serde_json::to_writer_pretty here. The pretty suffix suggests that the json file is formatted in a readable way. For your own app you might not care about this and go for serde_json::to_writer which produces smaller files. For this example we like it, because it allows us to see into what a Vec<TodoData> will be serialized.

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_app/2/window/mod.rs

mod imp;

use std::fs::File;

use crate::todo_object::{TodoData, TodoObject};
use crate::todo_row::TodoRow;
use crate::utils::data_path;
use glib::{clone, Object};
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib};
use gtk::{Application, CustomFilter, FilterListModel, NoSelection, SignalListItemFactory};
use log::info;

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::new(&[("application", app)]).expect("Failed to create `Window`.")
    }

    fn model(&self) -> &gio::ListStore {
        // Get state
        let imp = imp::Window::from_instance(self);
        imp.model.get().expect("Could not get model")
    }

    fn filter(&self) -> Option<CustomFilter> {
        // Get state
        let imp = imp::Window::from_instance(self);

        // Get filter_state from settings
        let filter_state: String = imp.settings.get("filter");

        // Create custom filters
        let filter_open = CustomFilter::new(|obj| {
            // Get `TodoObject` from `glib::Object`
            let todo_object = obj
                .downcast_ref::<TodoObject>()
                .expect("The object needs to be of type `TodoObject`.");

            // Only allow completed tasks
            !todo_object.is_completed()
        });
        let filter_done = CustomFilter::new(|obj| {
            // Get `TodoObject` from `glib::Object`
            let todo_object = obj
                .downcast_ref::<TodoObject>()
                .expect("The object needs to be of type `TodoObject`.");

            // Only allow done tasks
            todo_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_model(&self) {
        // Create new model
        let model = gio::ListStore::new(TodoObject::static_type());

        // Get state and set model
        let imp = imp::Window::from_instance(self);
        imp.model.set(model).expect("Could not set model");

        // Wrap model with filter and selection and pass it to the list view
        let filter_model = FilterListModel::new(Some(self.model()), self.filter().as_ref());
        let selection_model = NoSelection::new(Some(&filter_model));
        imp.list_view.set_model(Some(&selection_model));

        // Filter model whenever the value of the key "filter" changes
        imp.settings.connect_changed(
            Some("filter"),
            clone!(@weak self as window, @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<TodoData> =
                serde_json::from_reader(file).expect("Could not get backup data from json file.");

            // Convert `Vec<TodoData>` to `Vec<Object>`
            let todo_objects: Vec<Object> = backup_data
                .into_iter()
                .map(|todo_data| TodoObject::new(todo_data.completed, todo_data.content))
                .map(|todo_object| todo_object.upcast())
                .collect();

            // Insert restored objects into model
            self.model().splice(0, 0, &todo_objects);
        } else {
            info!("Backup file does not exist yet {:?}", data_path());
        }
    }

    fn setup_callbacks(&self) {
        // Get state
        let imp = imp::Window::from_instance(self);
        let model = self.model();

        // Setup callback so that activation
        // creates a new todo object and clears the entry
        imp.entry
            .connect_activate(clone!(@weak model => move |entry| {
                let buffer = entry.buffer();
                let content = buffer.text();
                let todo_object = TodoObject::new(false, content);
                model.append(&todo_object);
                buffer.set_text("");
            }));

        // Setup callback so that click on the clear_button
        // removes all done tasks
        imp.clear_button
            .connect_clicked(clone!(@weak model => move |_| {
                let mut position = 0;
                while let Some(item) = model.item(position) {
                    // Get `TodoObject` from `glib::Object`
                    let todo_object = item
                        .downcast_ref::<TodoObject>()
                        .expect("The object needs to be of type `TodoObject`.");

                    if todo_object.is_completed() {
                        model.remove(position);
                    } else {
                        position += 1;
                    }
                }
            }));
    }

    fn setup_factory(&self) {
        // Create a new factory
        let factory = SignalListItemFactory::new();

        // Create an empty `TodoRow` during setup
        factory.connect_setup(move |_, list_item| {
            // Create `TodoRow`
            let todo_row = TodoRow::new();
            list_item.set_child(Some(&todo_row));
        });

        // Tell factory how to bind `TodoRow` to a `TodoObject`
        factory.connect_bind(move |_, list_item| {
            // Get `TodoObject` from `ListItem`
            let todo_object = list_item
                .item()
                .expect("The item has to exist.")
                .downcast::<TodoObject>()
                .expect("The item has to be an `TodoObject`.");

            // Get `TodoRow` from `ListItem`
            let todo_row = list_item
                .child()
                .expect("The child has to exist.")
                .downcast::<TodoRow>()
                .expect("The child has to be a `TodoRow`.");

            todo_row.bind(&todo_object);
        });

        // Tell factory how to unbind `TodoRow` from `TodoObject`
        factory.connect_unbind(move |_, list_item| {
            // Get `TodoRow` from `ListItem`
            let todo_row = list_item
                .child()
                .expect("The child has to exist.")
                .downcast::<TodoRow>()
                .expect("The child has to be a `TodoRow`.");

            todo_row.unbind();
        });

        // Set the factory of the list view
        let imp = imp::Window::from_instance(self);
        imp.list_view.set_factory(Some(&factory));
    }

    fn setup_shortcut_window(&self) {
        // Get `ShortcutsWindow` via `gtk::Builder`
        let builder = gtk::Builder::from_string(include_str!("shortcuts.ui"));
        let shortcuts = builder
            .object("shortcuts")
            .expect("Could not get object `shortcuts` from builder.");

        // After calling this method,
        // calling the action "win.show-help-overlay" will show the shortcut window
        self.set_help_overlay(Some(&shortcuts));
    }

    fn setup_filter_action(&self) {
        // Get state
        let imp = imp::Window::from_instance(self);

        // Create action from key "filter"
        let filter_action = imp.settings.create_action("filter");

        // Add action "filter" to action group "win"
        self.add_action(&filter_action);
    }
}

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

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

use std::fs::File;

use gio::Settings;
use glib::signal::Inhibit;
use glib::subclass::InitializingObject;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib};
use gtk::{Button, CompositeTemplate, Entry, ListView, MenuButton};
use once_cell::sync::OnceCell;

use crate::todo_object::TodoObject;
use crate::utils::data_path;

// Object holding the state
#[derive(CompositeTemplate)]
#[template(file = "window.ui")]
pub struct Window {
    #[template_child]
    pub entry: TemplateChild<Entry>,
    #[template_child]
    pub menu_button: TemplateChild<MenuButton>,
    #[template_child]
    pub list_view: TemplateChild<ListView>,
    #[template_child]
    pub clear_button: TemplateChild<Button>,
    pub model: OnceCell<gio::ListStore>,
    pub settings: Settings,
}

impl Default for Window {
    fn default() -> Self {
        Window {
            entry: TemplateChild::default(),
            menu_button: TemplateChild::default(),
            list_view: TemplateChild::default(),
            clear_button: TemplateChild::default(),
            model: OnceCell::default(),
            settings: Settings::new("org.gtk-rs.Todo"),
        }
    }
}

// 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) {
        Self::bind_template(klass);
    }

    fn instance_init(obj: &InitializingObject<Self>) {
        obj.init_template();
    }
}

// Trait shared by all GObjects
impl ObjectImpl for Window {
    fn constructed(&self, obj: &Self::Type) {
        // Call "constructed" on parent
        self.parent_constructed(obj);

        // Setup
        obj.setup_model();
        obj.restore_data();
        obj.setup_callbacks();
        obj.setup_factory();
        obj.setup_shortcut_window();
        obj.setup_filter_action();
    }
}

// Trait shared by all widgets
impl WidgetImpl for Window {}

// Trait shared by all windows
impl WindowImpl for Window {
    fn close_request(&self, window: &Self::Type) -> Inhibit {
        // Store todo data in vector
        let mut backup_data = Vec::new();
        let mut position = 0;
        while let Some(item) = window.model().item(position) {
            // Get `TodoObject` from `glib::Object`
            let todo_data = item
                .downcast_ref::<TodoObject>()
                .expect("The object needs to be of type `TodoObject`.")
                .todo_data();
            // Add todo data to vector and increase position
            backup_data.push(todo_data);
            position += 1;
        }

        // Save state in file
        let file = File::create(data_path()).expect("Could not create json file.");
        serde_json::to_writer_pretty(file, &backup_data)
            .expect("Could not write data to json file");

        // Pass close request on to the parent
        self.parent_close_request(window)
    }
}

// Trait shared by all application
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.