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.
<?xml version="1.0" encoding="utf-8"?><schemalist><schemaid="org.gtk_rs.Todo2"path="/org/gtk_rs/Todo2/"><keyname="filter"type="s"><choices><choicevalue='All'/><choicevalue='Open'/><choicevalue='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.
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")]pubstructWindow {
#[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 templateconst NAME: &'staticstr = "TodoWindow";
typeType = super::Window;
typeParentType = gtk::ApplicationWindow;
fnclass_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();
});
}
fninstance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
// Trait shared by all GObjectsimpl ObjectImpl for Window {
fnconstructed(&self) {
// Call "constructed" on parentself.parent_constructed();
// Setuplet 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 widgetsimpl WidgetImpl for Window {}
// Trait shared by all windowsimpl WindowImpl for Window {
fnclose_request(&self) -> glib::Propagation {
// Store task data in vectorlet backup_data: Vec<TaskData> = self .obj()
.tasks()
.iter::<TaskObject>()
.filter_map(Result::ok)
.map(|task_object| task_object.task_data())
.collect();
// Save state to filelet 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 parentself.parent_close_request()
}
}
// Trait shared by all application windowsimpl ApplicationWindowImpl for Window {}
Again, we create functions to make it easier to access settings.
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! {
pubstructWindow(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 {
pubfnnew(app: &Application) -> Self {
// Create new window Object::builder().property("application", app).build()
}
fnsetup_settings(&self) {
let settings = Settings::new(APP_ID);
self.imp()
.settings
.set(settings)
.expect("`settings` should not be set before calling `setup_settings`.");
}
fnsettings(&self) -> &Settings {
self.imp()
.settings
.get()
.expect("`settings` should be set in `setup_settings`.")
}
fntasks(&self) -> gio::ListStore {
// Get stateself.imp()
.tasks
.borrow()
.clone()
.expect("Could not get current tasks.")
}
fnfilter(&self) -> Option<CustomFilter> {
// Get filter_state from settingslet filter_state: String = self.settings().get("filter");
// Create custom filterslet 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 filtermatch filter_state.as_str() {
"All" => None,
"Open" => Some(filter_open),
"Done" => Some(filter_done),
_ => unreachable!(),
}
}
fnsetup_tasks(&self) {
// Create new modellet model = gio::ListStore::new::<TaskObject>();
// Get state and set modelself.imp().tasks.replace(Some(model));
// Wrap model with filter and selection and pass it to the list viewlet 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" changesself.settings().connect_changed(
Some("filter"),
clone!(
#[weak(rename_to = window)]self,
#[weak] filter_model,
move |_, _| {
filter_model.set_filter(window.filter().as_ref());
}
),
);
}
fnrestore_data(&self) {
ifletOk(file) = File::open(data_path()) {
// Deserialize data from file to vectorlet 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 modelself.tasks().extend_from_slice(&task_objects);
}
}
fnsetup_callbacks(&self) {
// Setup callback for activation of the entryself.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 entryself.imp().entry.connect_icon_release(clone!(
#[weak(rename_to = window)]self,
move |_, _| {
window.new_task();
}
));
}
fnnew_task(&self) {
// Get content from entry and clear itlet buffer = self.imp().entry.buffer();
let content = buffer.text().to_string();
if content.is_empty() {
return;
}
buffer.set_text("");
// Add new task to modellet task = TaskObject::new(false, content);
self.tasks().append(&task);
}
fnsetup_factory(&self) {
// Create a new factorylet 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 viewself.imp().tasks_list.set_factory(Some(&factory));
}
fnsetup_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);
}
fnremove_done_tasks(&self) {
let tasks = self.tasks();
letmut position = 0;
whileletSome(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.
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! {
pubstructWindow(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 {
pubfnnew(app: &Application) -> Self {
// Create new window Object::builder().property("application", app).build()
}
fnsetup_settings(&self) {
let settings = Settings::new(APP_ID);
self.imp()
.settings
.set(settings)
.expect("`settings` should not be set before calling `setup_settings`.");
}
fnsettings(&self) -> &Settings {
self.imp()
.settings
.get()
.expect("`settings` should be set in `setup_settings`.")
}
fntasks(&self) -> gio::ListStore {
// Get stateself.imp()
.tasks
.borrow()
.clone()
.expect("Could not get current tasks.")
}
fnfilter(&self) -> Option<CustomFilter> {
// Get filter_state from settingslet filter_state: String = self.settings().get("filter");
// Create custom filterslet 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 filtermatch filter_state.as_str() {
"All" => None,
"Open" => Some(filter_open),
"Done" => Some(filter_done),
_ => unreachable!(),
}
}
fnsetup_tasks(&self) {
// Create new modellet model = gio::ListStore::new::<TaskObject>();
// Get state and set modelself.imp().tasks.replace(Some(model));
// Wrap model with filter and selection and pass it to the list viewlet 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" changesself.settings().connect_changed(
Some("filter"),
clone!(
#[weak(rename_to = window)]self,
#[weak] filter_model,
move |_, _| {
filter_model.set_filter(window.filter().as_ref());
}
),
);
}
fnrestore_data(&self) {
ifletOk(file) = File::open(data_path()) {
// Deserialize data from file to vectorlet 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 modelself.tasks().extend_from_slice(&task_objects);
}
}
fnsetup_callbacks(&self) {
// Setup callback for activation of the entryself.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 entryself.imp().entry.connect_icon_release(clone!(
#[weak(rename_to = window)]self,
move |_, _| {
window.new_task();
}
));
}
fnnew_task(&self) {
// Get content from entry and clear itlet buffer = self.imp().entry.buffer();
let content = buffer.text().to_string();
if content.is_empty() {
return;
}
buffer.set_text("");
// Add new task to modellet task = TaskObject::new(false, content);
self.tasks().append(&task);
}
fnsetup_factory(&self) {
// Create a new factorylet 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 viewself.imp().tasks_list.set_factory(Some(&factory));
}
fnsetup_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);
}
fnremove_done_tasks(&self) {
let tasks = self.tasks();
letmut position = 0;
whileletSome(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.
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")]pubstructWindow {
#[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 templateconst NAME: &'staticstr = "TodoWindow";
typeType = super::Window;
typeParentType = gtk::ApplicationWindow;
fnclass_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();
});
}
fninstance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
// Trait shared by all GObjectsimpl ObjectImpl for Window {
fnconstructed(&self) {
// Call "constructed" on parentself.parent_constructed();
// Setuplet 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 widgetsimpl WidgetImpl for Window {}
// Trait shared by all windowsimpl WindowImpl for Window {
fnclose_request(&self) -> glib::Propagation {
// Store task data in vectorlet backup_data: Vec<TaskData> = self .obj()
.tasks()
.iter::<TaskObject>()
.filter_map(Result::ok)
.map(|task_object| task_object.task_data())
.collect();
// Save state to filelet 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 parentself.parent_close_request()
}
}
// Trait shared by all application windowsimpl ApplicationWindowImpl for Window {}
This is the implementation of remove_done_tasks.
We iterate through the gio::ListStore and remove all completed task objects.
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! {
pubstructWindow(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 {
pubfnnew(app: &Application) -> Self {
// Create new window Object::builder().property("application", app).build()
}
fnsetup_settings(&self) {
let settings = Settings::new(APP_ID);
self.imp()
.settings
.set(settings)
.expect("`settings` should not be set before calling `setup_settings`.");
}
fnsettings(&self) -> &Settings {
self.imp()
.settings
.get()
.expect("`settings` should be set in `setup_settings`.")
}
fntasks(&self) -> gio::ListStore {
// Get stateself.imp()
.tasks
.borrow()
.clone()
.expect("Could not get current tasks.")
}
fnfilter(&self) -> Option<CustomFilter> {
// Get filter_state from settingslet filter_state: String = self.settings().get("filter");
// Create custom filterslet 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 filtermatch filter_state.as_str() {
"All" => None,
"Open" => Some(filter_open),
"Done" => Some(filter_done),
_ => unreachable!(),
}
}
fnsetup_tasks(&self) {
// Create new modellet model = gio::ListStore::new::<TaskObject>();
// Get state and set modelself.imp().tasks.replace(Some(model));
// Wrap model with filter and selection and pass it to the list viewlet 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" changesself.settings().connect_changed(
Some("filter"),
clone!(
#[weak(rename_to = window)]self,
#[weak] filter_model,
move |_, _| {
filter_model.set_filter(window.filter().as_ref());
}
),
);
}
fnrestore_data(&self) {
ifletOk(file) = File::open(data_path()) {
// Deserialize data from file to vectorlet 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 modelself.tasks().extend_from_slice(&task_objects);
}
}
fnsetup_callbacks(&self) {
// Setup callback for activation of the entryself.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 entryself.imp().entry.connect_icon_release(clone!(
#[weak(rename_to = window)]self,
move |_, _| {
window.new_task();
}
));
}
fnnew_task(&self) {
// Get content from entry and clear itlet buffer = self.imp().entry.buffer();
let content = buffer.text().to_string();
if content.is_empty() {
return;
}
buffer.set_text("");
// Add new task to modellet task = TaskObject::new(false, content);
self.tasks().append(&task);
}
fnsetup_factory(&self) {
// Create a new factorylet 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 viewself.imp().tasks_list.set_factory(Some(&factory));
}
fnsetup_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);
}
fnremove_done_tasks(&self) {
let tasks = self.tasks();
letmut position = 0;
whileletSome(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.
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! {
pubstructWindow(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 {
pubfnnew(app: &Application) -> Self {
// Create new window Object::builder().property("application", app).build()
}
fnsetup_settings(&self) {
let settings = Settings::new(APP_ID);
self.imp()
.settings
.set(settings)
.expect("`settings` should not be set before calling `setup_settings`.");
}
fnsettings(&self) -> &Settings {
self.imp()
.settings
.get()
.expect("`settings` should be set in `setup_settings`.")
}
fntasks(&self) -> gio::ListStore {
// Get stateself.imp()
.tasks
.borrow()
.clone()
.expect("Could not get current tasks.")
}
fnfilter(&self) -> Option<CustomFilter> {
// Get filter_state from settingslet filter_state: String = self.settings().get("filter");
// Create custom filterslet 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 filtermatch filter_state.as_str() {
"All" => None,
"Open" => Some(filter_open),
"Done" => Some(filter_done),
_ => unreachable!(),
}
}
fnsetup_tasks(&self) {
// Create new modellet model = gio::ListStore::new::<TaskObject>();
// Get state and set modelself.imp().tasks.replace(Some(model));
// Wrap model with filter and selection and pass it to the list viewlet 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" changesself.settings().connect_changed(
Some("filter"),
clone!(
#[weak(rename_to = window)]self,
#[weak] filter_model,
move |_, _| {
filter_model.set_filter(window.filter().as_ref());
}
),
);
}
fnrestore_data(&self) {
ifletOk(file) = File::open(data_path()) {
// Deserialize data from file to vectorlet 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 modelself.tasks().extend_from_slice(&task_objects);
}
}
fnsetup_callbacks(&self) {
// Setup callback for activation of the entryself.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 entryself.imp().entry.connect_icon_release(clone!(
#[weak(rename_to = window)]self,
move |_, _| {
window.new_task();
}
));
}
fnnew_task(&self) {
// Get content from entry and clear itlet buffer = self.imp().entry.buffer();
let content = buffer.text().to_string();
if content.is_empty() {
return;
}
buffer.set_text("");
// Add new task to modellet task = TaskObject::new(false, content);
self.tasks().append(&task);
}
fnsetup_factory(&self) {
// Create a new factorylet 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 viewself.imp().tasks_list.set_factory(Some(&factory));
}
fnsetup_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);
}
fnremove_done_tasks(&self) {
let tasks = self.tasks();
letmut position = 0;
whileletSome(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.
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! {
pubstructWindow(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 {
pubfnnew(app: &Application) -> Self {
// Create new window Object::builder().property("application", app).build()
}
fnsetup_settings(&self) {
let settings = Settings::new(APP_ID);
self.imp()
.settings
.set(settings)
.expect("`settings` should not be set before calling `setup_settings`.");
}
fnsettings(&self) -> &Settings {
self.imp()
.settings
.get()
.expect("`settings` should be set in `setup_settings`.")
}
fntasks(&self) -> gio::ListStore {
// Get stateself.imp()
.tasks
.borrow()
.clone()
.expect("Could not get current tasks.")
}
fnfilter(&self) -> Option<CustomFilter> {
// Get filter_state from settingslet filter_state: String = self.settings().get("filter");
// Create custom filterslet 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 filtermatch filter_state.as_str() {
"All" => None,
"Open" => Some(filter_open),
"Done" => Some(filter_done),
_ => unreachable!(),
}
}
fnsetup_tasks(&self) {
// Create new modellet model = gio::ListStore::new::<TaskObject>();
// Get state and set modelself.imp().tasks.replace(Some(model));
// Wrap model with filter and selection and pass it to the list viewlet 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" changesself.settings().connect_changed(
Some("filter"),
clone!(
#[weak(rename_to = window)]self,
#[weak]
filter_model,
move |_, _| {
filter_model.set_filter(window.filter().as_ref());
}
),
);
}
fnrestore_data(&self) {
ifletOk(file) = File::open(data_path()) {
// Deserialize data from file to vectorlet 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 modelself.tasks().extend_from_slice(&task_objects);
}
}
fnsetup_callbacks(&self) {
// Setup callback for activation of the entryself.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 entryself.imp().entry.connect_icon_release(clone!(
#[weak(rename_to = window)]self,
move |_, _| {
window.new_task();
}
));
}
fnnew_task(&self) {
// Get content from entry and clear itlet buffer = self.imp().entry.buffer();
let content = buffer.text().to_string();
if content.is_empty() {
return;
}
buffer.set_text("");
// Add new task to modellet task = TaskObject::new(false, content);
self.tasks().append(&task);
}
fnsetup_factory(&self) {
// Create a new factorylet 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 viewself.imp().tasks_list.set_factory(Some(&factory));
}
fnsetup_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);
}
fnremove_done_tasks(&self) {
let tasks = self.tasks();
letmut position = 0;
whileletSome(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.
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";
fnmain() -> glib::ExitCode {
// Register and include resources
gio::resources_register_include!("todo_2.gresource")
.expect("Failed to register resources.");
// Create a new applicationlet 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()
}
fnsetup_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"]);
}
fnbuild_ui(app: &Application) {
// Create a new custom window and present itlet 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.
<?xml version="1.0" encoding="UTF-8"?><interface><objectclass="GtkShortcutsWindow"id="help_overlay"><propertyname="modal">True</property><child><objectclass="GtkShortcutsSection"><propertyname="section-name">shortcuts</property><propertyname="max-height">10</property><child><objectclass="GtkShortcutsGroup"><propertyname="title"translatable="yes"context="shortcut window">General</property><child><objectclass="GtkShortcutsShortcut"><propertyname="title"translatable="yes"context="shortcut window">Show shortcuts</property><propertyname="action-name">win.show-help-overlay</property></object></child><child><objectclass="GtkShortcutsShortcut"><propertyname="title"translatable="yes"context="shortcut window">Filter to show all tasks</property><propertyname="action-name">win.filter('All')</property></object></child><child><objectclass="GtkShortcutsShortcut"><propertyname="title"translatable="yes"context="shortcut window">Filter to show only open tasks</property><propertyname="action-name">win.filter('Open')</property></object></child><child><objectclass="GtkShortcutsShortcut"><propertyname="title"translatable="yes"context="shortcut window">Filter to show only completed tasks</property><propertyname="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 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.
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.
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.
use std::path::PathBuf;
use gtk::glib;
use crate::APP_ID;
pubfndata_path() -> PathBuf {
letmut 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.
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")]pubstructWindow {
#[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 templateconst NAME: &'staticstr = "TodoWindow";
typeType = super::Window;
typeParentType = gtk::ApplicationWindow;
fnclass_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();
});
}
fninstance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
// Trait shared by all GObjectsimpl ObjectImpl for Window {
fnconstructed(&self) {
// Call "constructed" on parentself.parent_constructed();
// Setuplet 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 widgetsimpl WidgetImpl for Window {}
// Trait shared by all windowsimpl WindowImpl for Window {
fnclose_request(&self) -> glib::Propagation {
// Store task data in vectorlet backup_data: Vec<TaskData> = self
.obj()
.tasks()
.iter::<TaskObject>()
.filter_map(Result::ok)
.map(|task_object| task_object.task_data())
.collect();
// Save state to filelet 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 parentself.parent_close_request()
}
}
// Trait shared by all application windowsimpl 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.
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! {
pubstructWindow(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 {
pubfnnew(app: &Application) -> Self {
// Create new window Object::builder().property("application", app).build()
}
fnsetup_settings(&self) {
let settings = Settings::new(APP_ID);
self.imp()
.settings
.set(settings)
.expect("`settings` should not be set before calling `setup_settings`.");
}
fnsettings(&self) -> &Settings {
self.imp()
.settings
.get()
.expect("`settings` should be set in `setup_settings`.")
}
fntasks(&self) -> gio::ListStore {
// Get stateself.imp()
.tasks
.borrow()
.clone()
.expect("Could not get current tasks.")
}
fnfilter(&self) -> Option<CustomFilter> {
// Get filter_state from settingslet filter_state: String = self.settings().get("filter");
// Create custom filterslet 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 filtermatch filter_state.as_str() {
"All" => None,
"Open" => Some(filter_open),
"Done" => Some(filter_done),
_ => unreachable!(),
}
}
fnsetup_tasks(&self) {
// Create new modellet model = gio::ListStore::new::<TaskObject>();
// Get state and set modelself.imp().tasks.replace(Some(model));
// Wrap model with filter and selection and pass it to the list viewlet 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" changesself.settings().connect_changed(
Some("filter"),
clone!(
#[weak(rename_to = window)]self,
#[weak] filter_model,
move |_, _| {
filter_model.set_filter(window.filter().as_ref());
}
),
);
}
fnrestore_data(&self) {
ifletOk(file) = File::open(data_path()) {
// Deserialize data from file to vectorlet 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 modelself.tasks().extend_from_slice(&task_objects);
}
}
fnsetup_callbacks(&self) {
// Setup callback for activation of the entryself.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 entryself.imp().entry.connect_icon_release(clone!(
#[weak(rename_to = window)]self,
move |_, _| {
window.new_task();
}
));
}
fnnew_task(&self) {
// Get content from entry and clear itlet buffer = self.imp().entry.buffer();
let content = buffer.text().to_string();
if content.is_empty() {
return;
}
buffer.set_text("");
// Add new task to modellet task = TaskObject::new(false, content);
self.tasks().append(&task);
}
fnsetup_factory(&self) {
// Create a new factorylet 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 viewself.imp().tasks_list.set_factory(Some(&factory));
}
fnsetup_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);
}
fnremove_done_tasks(&self) {
let tasks = self.tasks();
letmut position = 0;
whileletSome(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.
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")]pubstructWindow {
#[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 templateconst NAME: &'staticstr = "TodoWindow";
typeType = super::Window;
typeParentType = gtk::ApplicationWindow;
fnclass_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();
});
}
fninstance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
// Trait shared by all GObjectsimpl ObjectImpl for Window {
fnconstructed(&self) {
// Call "constructed" on parentself.parent_constructed();
// Setuplet 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 widgetsimpl WidgetImpl for Window {}
// Trait shared by all windowsimpl WindowImpl for Window {
fnclose_request(&self) -> glib::Propagation {
// Store task data in vectorlet backup_data: Vec<TaskData> = self .obj()
.tasks()
.iter::<TaskObject>()
.filter_map(Result::ok)
.map(|task_object| task_object.task_data())
.collect();
// Save state to filelet 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 parentself.parent_close_request()
}
}
// Trait shared by all application windowsimpl 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.