After we have learned so many concepts, it is finally time to put them into practice.
We are going to build a To-Do app!
For now, we would already be satisfied with a minimal version.
An entry to input new tasks and a list view to display them will suffice.
Something like this:
<?xml version="1.0" encoding="UTF-8"?><interface><templateclass="TodoWindow"parent="GtkApplicationWindow"><propertyname="width-request">360</property><propertyname="title"translatable="yes">To-Do</property><child><objectclass="GtkBox"><propertyname="orientation">vertical</property><propertyname="margin-top">12</property><propertyname="margin-bottom">12</property><propertyname="margin-start">12</property><propertyname="margin-end">12</property><propertyname="spacing">6</property><child><objectclass="GtkEntry"id="entry"><propertyname="placeholder-text"translatable="yes">Enter a Taskā¦</property><propertyname="secondary-icon-name">list-add-symbolic</property></object></child><child><objectclass="GtkScrolledWindow"><propertyname="hscrollbar-policy">never</property><propertyname="min-content-height">360</property><propertyname="vexpand">true</property><child><objectclass="GtkListView"id="tasks_list"><propertyname="valign">start</property></object></child></object></child></object></child></template></interface>
In order to use the composite template, we create a custom widget.
The parent is gtk::ApplicationWindow, so we inherit from it.
As usual, we have to list all ancestors and interfaces apart from GObject and GInitiallyUnowned.
mod imp;
use glib::{clone, Object};
use gtk::subclass::prelude::*;
use gtk::{gio, glib, Application, NoSelection, SignalListItemFactory};
use gtk::{prelude::*, ListItem};
use crate::task_object::TaskObject;
use crate::task_row::TaskRow;
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()
}
fntasks(&self) -> gio::ListStore {
// Get stateself.imp()
.tasks
.borrow()
.clone()
.expect("Could not get current tasks.")
}
fnsetup_tasks(&self) {
// Create new modellet model = gio::ListStore::new::<TaskObject>();
// Get state and set modelself.imp().tasks.replace(Some(model));
// Wrap model with selection and pass it to the list viewlet selection_model = NoSelection::new(Some(self.tasks()));
self.imp().tasks_list.set_model(Some(&selection_model));
}
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));
}
}
Then we initialize the composite template for imp::Window.
We store references to the entry, the list view as well as the list model.
This will come in handy when we later add methods to our window.
After that, we add the typical boilerplate for initializing composite templates.
We only have to assure that the class attribute of the template in window.ui matches NAME.
mod task_object;
mod task_row;
mod window;
use gtk::prelude::*;
use gtk::{gio, glib, Application};
use window::Window;
fnmain() -> glib::ExitCode {
// Register and include resources
gio::resources_register_include!("todo_1.gresource")
.expect("Failed to register resources.");
// Create a new applicationlet app = Application::builder()
.application_id("org.gtk_rs.Todo1")
.build();
// Connect to "activate" signal of `app`
app.connect_activate(build_ui);
// Run the application
app.run()
}
fnbuild_ui(app: &Application) {
// Create a new custom window and present itlet window = Window::new(app);
window.present();
}
Finally, we specify our resources.
Here, they already include task_row.ui which we will handle later in this chapter.
So far so good.
The main user interface is done, but the entry does not react to input yet.
Also, where would the input go?
We haven't even set up the list model yet.
Let's do that!
As discussed in the list widgets chapter,
we start out by creating a custom GObject.
This object will store the state of the task consisting of:
a boolean describing whether the task is completed or not, and
Unlike the lists chapter, the state is stored in a struct rather than in individual members of imp::TaskObject.
This will be very convenient when saving the state in one of the following chapters.
We are going to expose completed and content as properties.
Since the data is now inside a struct rather than individual member variables we have to add more annotations.
For each property we additionally specify the name, the type and which member variable of TaskData we want to access.
use std::cell::RefCell;
use glib::Properties;
use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use super::TaskData;
// Object holding the state#[derive(Properties, Default)]#[properties(wrapper_type = super::TaskObject)]pubstructTaskObject {
#[property(name = "completed", get, set, type = bool, member = completed)]#[property(name = "content", get, set, type = String, member = content)]pub data: RefCell<TaskData>,
}
// The central trait for subclassing a GObject#[glib::object_subclass]impl ObjectSubclass for TaskObject {
const NAME: &'staticstr = "TodoTaskObject";
typeType = super::TaskObject;
}
// Trait shared by all GObjects#[glib::derived_properties]impl ObjectImpl for TaskObject {}
mod imp;
use glib::Object;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{glib, pango};
use pango::{AttrInt, AttrList};
use crate::task_object::TaskObject;
glib::wrapper! {
pubstructTaskRow(ObjectSubclass<imp::TaskRow>)
@extends gtk::Box, gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable;
}
implDefaultfor TaskRow {
fndefault() -> Self {
Self::new()
}
}
impl TaskRow {
pubfnnew() -> Self {
Object::builder().build()
}
pubfnbind(&self, task_object: &TaskObject) {
// Get statelet completed_button = self.imp().completed_button.get();
let content_label = self.imp().content_label.get();
letmut bindings = self.imp().bindings.borrow_mut();
// Bind `task_object.completed` to `task_row.completed_button.active`let completed_button_binding = task_object
.bind_property("completed", &completed_button, "active")
.bidirectional()
.sync_create()
.build();
// Save binding bindings.push(completed_button_binding);
// Bind `task_object.content` to `task_row.content_label.label`let content_label_binding = task_object
.bind_property("content", &content_label, "label")
.sync_create()
.build();
// Save binding bindings.push(content_label_binding);
// Bind `task_object.completed` to `task_row.content_label.attributes`let content_label_binding = task_object
.bind_property("completed", &content_label, "attributes")
.sync_create()
.transform_to(|_, active| {
let attribute_list = AttrList::new();
if active {
// If "active" is true, content of the label will be strikethroughlet attribute = AttrInt::new_strikethrough(true);
attribute_list.insert(attribute);
}
Some(attribute_list.to_value())
})
.build();
// Save binding bindings.push(content_label_binding);
}
pubfnunbind(&self) {
// Unbind all stored bindingsfor binding inself.imp().bindings.borrow_mut().drain(..) {
binding.unbind();
}
}
}
In imp::TaskRow, we hold references to completed_button and content_label.
We also store a mutable vector of bindings.
Why we need that will become clear as soon as we get to bind the state of TaskObject to the corresponding TaskRow.
use std::cell::RefCell;
use glib::Binding;
use gtk::subclass::prelude::*;
use gtk::{glib, CheckButton, CompositeTemplate, Label};
// Object holding the state#[derive(Default, CompositeTemplate)]#[template(resource = "/org/gtk_rs/Todo1/task_row.ui")]pubstructTaskRow {
#[template_child]pub completed_button: TemplateChild<CheckButton>,
#[template_child]pub content_label: TemplateChild<Label>,
// Vector holding the bindings to properties of `TaskObject`pub bindings: RefCell<Vec<Binding>>,
}
// The central trait for subclassing a GObject#[glib::object_subclass]impl ObjectSubclass for TaskRow {
// `NAME` needs to match `class` attribute of templateconst NAME: &'staticstr = "TodoTaskRow";
typeType = super::TaskRow;
typeParentType = gtk::Box;
fnclass_init(klass: &mut Self::Class) {
klass.bind_template();
}
fninstance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
// Trait shared by all GObjectsimpl ObjectImpl for TaskRow {}
// Trait shared by all widgetsimpl WidgetImpl for TaskRow {}
// Trait shared by all boxesimpl BoxImpl for TaskRow {}
Now we can bring everything together.
We override the imp::Window::constructed in order to set up window contents at the time of its construction.
use std::cell::RefCell;
use glib::subclass::InitializingObject;
use gtk::subclass::prelude::*;
use gtk::{gio, glib, CompositeTemplate, Entry, ListView};
// Object holding the state#[derive(CompositeTemplate, Default)]#[template(resource = "/org/gtk_rs/Todo1/window.ui")]pubstructWindow {
#[template_child]pub entry: TemplateChild<Entry>,
#[template_child]pub tasks_list: TemplateChild<ListView>,
pub tasks: RefCell<Option<gio::ListStore>>,
}
// 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();
}
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_tasks();
obj.setup_callbacks();
obj.setup_factory();
}
}
// Trait shared by all widgetsimpl WidgetImpl for Window {}
// Trait shared by all windowsimpl WindowImpl for Window {}
// Trait shared by all application windowsimpl ApplicationWindowImpl for Window {}
Since we need to access the list model quite often, we add the convenience method Window::tasks for that.
In Window::setup_tasks we create a new model.
Then we store a reference to the model in imp::Window as well as in gtk::ListView.
mod imp;
use glib::{clone, Object};
use gtk::subclass::prelude::*;
use gtk::{gio, glib, Application, NoSelection, SignalListItemFactory};
use gtk::{prelude::*, ListItem};
use crate::task_object::TaskObject;
use crate::task_row::TaskRow;
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()
}
fntasks(&self) -> gio::ListStore {
// Get stateself.imp()
.tasks
.borrow()
.clone()
.expect("Could not get current tasks.")
}
fnsetup_tasks(&self) {
// Create new modellet model = gio::ListStore::new::<TaskObject>();
// Get state and set modelself.imp().tasks.replace(Some(model));
// Wrap model with selection and pass it to the list viewlet selection_model = NoSelection::new(Some(self.tasks()));
self.imp().tasks_list.set_model(Some(&selection_model));
}
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));
}
}
We also create a method new_task which takes the content of the entry, clears the entry and uses the content to create a new task.
mod imp;
use glib::{clone, Object};
use gtk::subclass::prelude::*;
use gtk::{gio, glib, Application, NoSelection, SignalListItemFactory};
use gtk::{prelude::*, ListItem};
use crate::task_object::TaskObject;
use crate::task_row::TaskRow;
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()
}
fntasks(&self) -> gio::ListStore {
// Get stateself.imp()
.tasks
.borrow()
.clone()
.expect("Could not get current tasks.")
}
fnsetup_tasks(&self) {
// Create new modellet model = gio::ListStore::new::<TaskObject>();
// Get state and set modelself.imp().tasks.replace(Some(model));
// Wrap model with selection and pass it to the list viewlet selection_model = NoSelection::new(Some(self.tasks()));
self.imp().tasks_list.set_model(Some(&selection_model));
}
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));
}
}
In Window::setup_callbacks we connect to the "activate" signal of the entry.
This signal is triggered when we press the enter key in the entry.
Then a new TaskObject with the content will be created and appended to the model.
Finally, the entry will be cleared.
mod imp;
use glib::{clone, Object};
use gtk::subclass::prelude::*;
use gtk::{gio, glib, Application, NoSelection, SignalListItemFactory};
use gtk::{prelude::*, ListItem};
use crate::task_object::TaskObject;
use crate::task_row::TaskRow;
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()
}
fntasks(&self) -> gio::ListStore {
// Get stateself.imp()
.tasks
.borrow()
.clone()
.expect("Could not get current tasks.")
}
fnsetup_tasks(&self) {
// Create new modellet model = gio::ListStore::new::<TaskObject>();
// Get state and set modelself.imp().tasks.replace(Some(model));
// Wrap model with selection and pass it to the list viewlet selection_model = NoSelection::new(Some(self.tasks()));
self.imp().tasks_list.set_model(Some(&selection_model));
}
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));
}
}
The list elements for the gtk::ListView are produced by a factory.
Before we move on to the implementation, let's take a step back and think about which behavior we expect here.
content_label of TaskRow should follow content of TaskObject.
We also want completed_button of TaskRow follow completed of TaskObject.
This could be achieved with expressions similar to what we did in the lists chapter.
However, if we toggle the state of completed_button of TaskRow, completed of TaskObject should change too.
Unfortunately, expressions cannot handle bidirectional relationships.
This means we have to use property bindings.
We will need to unbind them manually when they are no longer needed.
We will create empty TaskRow objects in the "setup" step in Window::setup_factory and deal with binding in the "bind" and "unbind" steps.
mod imp;
use glib::{clone, Object};
use gtk::subclass::prelude::*;
use gtk::{gio, glib, Application, NoSelection, SignalListItemFactory};
use gtk::{prelude::*, ListItem};
use crate::task_object::TaskObject;
use crate::task_row::TaskRow;
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()
}
fntasks(&self) -> gio::ListStore {
// Get stateself.imp()
.tasks
.borrow()
.clone()
.expect("Could not get current tasks.")
}
fnsetup_tasks(&self) {
// Create new modellet model = gio::ListStore::new::<TaskObject>();
// Get state and set modelself.imp().tasks.replace(Some(model));
// Wrap model with selection and pass it to the list viewlet selection_model = NoSelection::new(Some(self.tasks()));
self.imp().tasks_list.set_model(Some(&selection_model));
}
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));
}
}
Binding properties in TaskRow::bind works just like in former chapters.
The only difference is that we store the bindings in a vector.
This is necessary because a TaskRow will be reused as you scroll through the list.
That means that over time a TaskRow will need to bound to a new TaskObject and has to be unbound from the old one.
Unbinding will only work if it can access the stored glib::Binding.