控制待办事项应用的状态
过滤任务
现在是继续开发待办事项应用的时候了。 一个值得添加的功能是:过滤任务。 这是一个利用我们新学到的关于 Action 的知识的好机会! 使用 Action,我们可以通过菜单和键盘快捷键访问过滤器。 这就是我们希望的最终效果:
让我们先在 window.ui
中添加菜单和标题栏。 在阅读了动作一章后,我们应该会对添加的代码感到熟悉。
文件名:listings/todo/2/resources/window.ui
<?xml version="1.0" encoding="UTF-8"?>
<interface>
+ <menu id="main-menu">
+ <submenu>
+ <attribute name="label" translatable="yes">_Filter</attribute>
+ <item>
+ <attribute name="label" translatable="yes">_All</attribute>
+ <attribute name="action">win.filter</attribute>
+ <attribute name="target">All</attribute>
+ </item>
+ <item>
+ <attribute name="label" translatable="yes">_Open</attribute>
+ <attribute name="action">win.filter</attribute>
+ <attribute name="target">Open</attribute>
+ </item>
+ <item>
+ <attribute name="label" translatable="yes">_Done</attribute>
+ <attribute name="action">win.filter</attribute>
+ <attribute name="target">Done</attribute>
+ </item>
+ </submenu>
+ <item>
+ <attribute name="label" translatable="yes">_Remove Done Tasks</attribute>
+ <attribute name="action">win.remove-done-tasks</attribute>
+ </item>
+ <item>
+ <attribute name="label" translatable="yes">_Keyboard Shortcuts</attribute>
+ <attribute name="action">win.show-help-overlay</attribute>
+ </item>
+ </menu>
<template class="TodoWindow" parent="GtkApplicationWindow">
<property name="width-request">360</property>
<property name="title" translatable="yes">To-Do</property>
+ <child type="titlebar">
+ <object class="GtkHeaderBar">
+ <child type="end">
+ <object class="GtkMenuButton" id="menu_button">
+ <property name="icon-name">open-menu-symbolic</property>
+ <property name="menu-model">main-menu</property>
+ </object>
+ </child>
+ </object>
+ </child>
然后,我们创建一个设置 schema。 同样,"过滤器"设置与菜单调用的有状态的动作(Action)相对应。
文件名:listings/todo/2/org.gtk_rs.Todo2.gschema.xml
<?xml version="1.0" encoding="utf-8"?>
<schemalist>
<schema id="org.gtk_rs.Todo2" path="/org/gtk_rs/Todo2/">
<key name="filter" type="s">
<choices>
<choice value='All'/>
<choice value='Open'/>
<choice value='Done'/>
</choices>
<default>'All'</default>
<summary>Filter of the tasks</summary>
</key>
</schema>
</schemalist>
我们按照设置一章中的描述安装 schema. 然后,我们将设置(settings)
的引用添加到 imp::Window
.
文件名:listings/todo/2/window/imp.rs
use std::cell::RefCell;
use std::fs::File;
use gio::Settings;
use glib::subclass::InitializingObject;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib, CompositeTemplate, Entry, ListView};
use std::cell::OnceCell;
use crate::task_object::{TaskData, TaskObject};
use crate::utils::data_path;
// Object holding the state
#[derive(CompositeTemplate, Default)]
#[template(resource = "/org/gtk_rs/Todo2/window.ui")]
pub struct Window {
#[template_child]
pub entry: TemplateChild<Entry>,
#[template_child]
pub tasks_list: TemplateChild<ListView>,
pub tasks: RefCell<Option<gio::ListStore>>,
pub settings: OnceCell<Settings>,
}
// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for Window {
// `NAME` needs to match `class` attribute of template
const NAME: &'static str = "TodoWindow";
type Type = super::Window;
type ParentType = gtk::ApplicationWindow;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
// Create action to remove done tasks and add to action group "win"
klass.install_action("win.remove-done-tasks", None, |window, _, _| {
window.remove_done_tasks();
});
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
// Trait shared by all GObjects
impl ObjectImpl for Window {
fn constructed(&self) {
// Call "constructed" on parent
self.parent_constructed();
// Setup
let obj = self.obj();
obj.setup_settings();
obj.setup_tasks();
obj.restore_data();
obj.setup_callbacks();
obj.setup_factory();
obj.setup_actions();
}
}
// Trait shared by all widgets
impl WidgetImpl for Window {}
// Trait shared by all windows
impl WindowImpl for Window {
fn close_request(&self) -> glib::Propagation {
// Store task data in vector
let backup_data: Vec<TaskData> = self
.obj()
.tasks()
.iter::<TaskObject>()
.filter_map(Result::ok)
.map(|task_object| task_object.task_data())
.collect();
// Save state to file
let file = File::create(data_path()).expect("Could not create json file.");
serde_json::to_writer(file, &backup_data)
.expect("Could not write data to json file");
// Pass close request on to the parent
self.parent_close_request()
}
}
// Trait shared by all application windows
impl ApplicationWindowImpl for Window {}
同样,我们创建了一些函数,以方便访问设置。
文件名:listings/todo/2/window/mod.rs
mod imp;
use std::fs::File;
use gio::Settings;
use glib::{clone, Object};
use gtk::subclass::prelude::*;
use gtk::{
gio, glib, Application, CustomFilter, FilterListModel, NoSelection,
SignalListItemFactory,
};
use gtk::{prelude::*, ListItem};
use crate::task_object::{TaskData, TaskObject};
use crate::task_row::TaskRow;
use crate::utils::data_path;
use crate::APP_ID;
glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>)
@extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}
impl Window {
pub fn new(app: &Application) -> Self {
// Create new window
Object::builder().property("application", app).build()
}
fn setup_settings(&self) {
let settings = Settings::new(APP_ID);
self.imp()
.settings
.set(settings)
.expect("`settings` should not be set before calling `setup_settings`.");
}
fn settings(&self) -> &Settings {
self.imp()
.settings
.get()
.expect("`settings` should be set in `setup_settings`.")
}
fn tasks(&self) -> gio::ListStore {
// Get state
self.imp()
.tasks
.borrow()
.clone()
.expect("Could not get current tasks.")
}
fn filter(&self) -> Option<CustomFilter> {
// Get filter_state from settings
let filter_state: String = self.settings().get("filter");
// Create custom filters
let filter_open = CustomFilter::new(|obj| {
// Get `TaskObject` from `glib::Object`
let task_object = obj
.downcast_ref::<TaskObject>()
.expect("The object needs to be of type `TaskObject`.");
// Only allow completed tasks
!task_object.is_completed()
});
let filter_done = CustomFilter::new(|obj| {
// Get `TaskObject` from `glib::Object`
let task_object = obj
.downcast_ref::<TaskObject>()
.expect("The object needs to be of type `TaskObject`.");
// Only allow done tasks
task_object.is_completed()
});
// Return the correct filter
match filter_state.as_str() {
"All" => None,
"Open" => Some(filter_open),
"Done" => Some(filter_done),
_ => unreachable!(),
}
}
fn setup_tasks(&self) {
// Create new model
let model = gio::ListStore::new::<TaskObject>();
// Get state and set model
self.imp().tasks.replace(Some(model));
// Wrap model with filter and selection and pass it to the list view
let filter_model = FilterListModel::new(Some(self.tasks()), self.filter());
let selection_model = NoSelection::new(Some(filter_model.clone()));
self.imp().tasks_list.set_model(Some(&selection_model));
// Filter model whenever the value of the key "filter" changes
self.settings().connect_changed(
Some("filter"),
clone!(
#[weak(rename_to = window)]
self,
#[weak]
filter_model,
move |_, _| {
filter_model.set_filter(window.filter().as_ref());
}
),
);
}
fn restore_data(&self) {
if let Ok(file) = File::open(data_path()) {
// Deserialize data from file to vector
let backup_data: Vec<TaskData> = serde_json::from_reader(file).expect(
"It should be possible to read `backup_data` from the json file.",
);
// Convert `Vec<TaskData>` to `Vec<TaskObject>`
let task_objects: Vec<TaskObject> = backup_data
.into_iter()
.map(TaskObject::from_task_data)
.collect();
// Insert restored objects into model
self.tasks().extend_from_slice(&task_objects);
}
}
fn setup_callbacks(&self) {
// Setup callback for activation of the entry
self.imp().entry.connect_activate(clone!(
#[weak(rename_to = window)]
self,
move |_| {
window.new_task();
}
));
// Setup callback for clicking (and the releasing) the icon of the entry
self.imp().entry.connect_icon_release(clone!(
#[weak(rename_to = window)]
self,
move |_, _| {
window.new_task();
}
));
}
fn new_task(&self) {
// Get content from entry and clear it
let buffer = self.imp().entry.buffer();
let content = buffer.text().to_string();
if content.is_empty() {
return;
}
buffer.set_text("");
// Add new task to model
let task = TaskObject::new(false, content);
self.tasks().append(&task);
}
fn setup_factory(&self) {
// Create a new factory
let factory = SignalListItemFactory::new();
// Create an empty `TaskRow` during setup
factory.connect_setup(move |_, list_item| {
// Create `TaskRow`
let task_row = TaskRow::new();
list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.set_child(Some(&task_row));
});
// Tell factory how to bind `TaskRow` to a `TaskObject`
factory.connect_bind(move |_, list_item| {
// Get `TaskObject` from `ListItem`
let task_object = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.item()
.and_downcast::<TaskObject>()
.expect("The item has to be an `TaskObject`.");
// Get `TaskRow` from `ListItem`
let task_row = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.child()
.and_downcast::<TaskRow>()
.expect("The child has to be a `TaskRow`.");
task_row.bind(&task_object);
});
// Tell factory how to unbind `TaskRow` from `TaskObject`
factory.connect_unbind(move |_, list_item| {
// Get `TaskRow` from `ListItem`
let task_row = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.child()
.and_downcast::<TaskRow>()
.expect("The child has to be a `TaskRow`.");
task_row.unbind();
});
// Set the factory of the list view
self.imp().tasks_list.set_factory(Some(&factory));
}
fn setup_actions(&self) {
// Create action from key "filter" and add to action group "win"
let action_filter = self.settings().create_action("filter");
self.add_action(&action_filter);
}
fn remove_done_tasks(&self) {
let tasks = self.tasks();
let mut position = 0;
while let Some(item) = tasks.item(position) {
// Get `TaskObject` from `glib::Object`
let task_object = item
.downcast_ref::<TaskObject>()
.expect("The object needs to be of type `TaskObject`.");
if task_object.is_completed() {
tasks.remove(position);
} else {
position += 1;
}
}
}
}
我们还为 TaskObject
添加了 is_completed
、task_data
和 from_task_data
方法。 我们将在下面的代码段中使用它们。
文件名:listings/todo/2/task_object/mod.rs
mod imp;
use glib::Object;
use gtk::glib;
use gtk::subclass::prelude::*;
use serde::{Deserialize, Serialize};
glib::wrapper! {
pub struct TaskObject(ObjectSubclass<imp::TaskObject>);
}
impl TaskObject {
pub fn new(completed: bool, content: String) -> Self {
Object::builder()
.property("completed", completed)
.property("content", content)
.build()
}
pub fn is_completed(&self) -> bool {
self.imp().data.borrow().completed
}
pub fn task_data(&self) -> TaskData {
self.imp().data.borrow().clone()
}
pub fn from_task_data(task_data: TaskData) -> Self {
Self::new(task_data.completed, task_data.content)
}
}
#[derive(Default, Clone, Serialize, Deserialize)]
pub struct TaskData {
pub completed: bool,
pub content: String,
}
与前一章类似,我们让设置(settings)
创建动作。 然后,我们将新创建的动作"过滤器(filter)"添加到窗口中。
文件名:listings/todo/2/window/mod.rs
mod imp;
use std::fs::File;
use gio::Settings;
use glib::{clone, Object};
use gtk::subclass::prelude::*;
use gtk::{
gio, glib, Application, CustomFilter, FilterListModel, NoSelection,
SignalListItemFactory,
};
use gtk::{prelude::*, ListItem};
use crate::task_object::{TaskData, TaskObject};
use crate::task_row::TaskRow;
use crate::utils::data_path;
use crate::APP_ID;
glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>)
@extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}
impl Window {
pub fn new(app: &Application) -> Self {
// Create new window
Object::builder().property("application", app).build()
}
fn setup_settings(&self) {
let settings = Settings::new(APP_ID);
self.imp()
.settings
.set(settings)
.expect("`settings` should not be set before calling `setup_settings`.");
}
fn settings(&self) -> &Settings {
self.imp()
.settings
.get()
.expect("`settings` should be set in `setup_settings`.")
}
fn tasks(&self) -> gio::ListStore {
// Get state
self.imp()
.tasks
.borrow()
.clone()
.expect("Could not get current tasks.")
}
fn filter(&self) -> Option<CustomFilter> {
// Get filter_state from settings
let filter_state: String = self.settings().get("filter");
// Create custom filters
let filter_open = CustomFilter::new(|obj| {
// Get `TaskObject` from `glib::Object`
let task_object = obj
.downcast_ref::<TaskObject>()
.expect("The object needs to be of type `TaskObject`.");
// Only allow completed tasks
!task_object.is_completed()
});
let filter_done = CustomFilter::new(|obj| {
// Get `TaskObject` from `glib::Object`
let task_object = obj
.downcast_ref::<TaskObject>()
.expect("The object needs to be of type `TaskObject`.");
// Only allow done tasks
task_object.is_completed()
});
// Return the correct filter
match filter_state.as_str() {
"All" => None,
"Open" => Some(filter_open),
"Done" => Some(filter_done),
_ => unreachable!(),
}
}
fn setup_tasks(&self) {
// Create new model
let model = gio::ListStore::new::<TaskObject>();
// Get state and set model
self.imp().tasks.replace(Some(model));
// Wrap model with filter and selection and pass it to the list view
let filter_model = FilterListModel::new(Some(self.tasks()), self.filter());
let selection_model = NoSelection::new(Some(filter_model.clone()));
self.imp().tasks_list.set_model(Some(&selection_model));
// Filter model whenever the value of the key "filter" changes
self.settings().connect_changed(
Some("filter"),
clone!(
#[weak(rename_to = window)]
self,
#[weak]
filter_model,
move |_, _| {
filter_model.set_filter(window.filter().as_ref());
}
),
);
}
fn restore_data(&self) {
if let Ok(file) = File::open(data_path()) {
// Deserialize data from file to vector
let backup_data: Vec<TaskData> = serde_json::from_reader(file).expect(
"It should be possible to read `backup_data` from the json file.",
);
// Convert `Vec<TaskData>` to `Vec<TaskObject>`
let task_objects: Vec<TaskObject> = backup_data
.into_iter()
.map(TaskObject::from_task_data)
.collect();
// Insert restored objects into model
self.tasks().extend_from_slice(&task_objects);
}
}
fn setup_callbacks(&self) {
// Setup callback for activation of the entry
self.imp().entry.connect_activate(clone!(
#[weak(rename_to = window)]
self,
move |_| {
window.new_task();
}
));
// Setup callback for clicking (and the releasing) the icon of the entry
self.imp().entry.connect_icon_release(clone!(
#[weak(rename_to = window)]
self,
move |_, _| {
window.new_task();
}
));
}
fn new_task(&self) {
// Get content from entry and clear it
let buffer = self.imp().entry.buffer();
let content = buffer.text().to_string();
if content.is_empty() {
return;
}
buffer.set_text("");
// Add new task to model
let task = TaskObject::new(false, content);
self.tasks().append(&task);
}
fn setup_factory(&self) {
// Create a new factory
let factory = SignalListItemFactory::new();
// Create an empty `TaskRow` during setup
factory.connect_setup(move |_, list_item| {
// Create `TaskRow`
let task_row = TaskRow::new();
list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.set_child(Some(&task_row));
});
// Tell factory how to bind `TaskRow` to a `TaskObject`
factory.connect_bind(move |_, list_item| {
// Get `TaskObject` from `ListItem`
let task_object = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.item()
.and_downcast::<TaskObject>()
.expect("The item has to be an `TaskObject`.");
// Get `TaskRow` from `ListItem`
let task_row = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.child()
.and_downcast::<TaskRow>()
.expect("The child has to be a `TaskRow`.");
task_row.bind(&task_object);
});
// Tell factory how to unbind `TaskRow` from `TaskObject`
factory.connect_unbind(move |_, list_item| {
// Get `TaskRow` from `ListItem`
let task_row = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.child()
.and_downcast::<TaskRow>()
.expect("The child has to be a `TaskRow`.");
task_row.unbind();
});
// Set the factory of the list view
self.imp().tasks_list.set_factory(Some(&factory));
}
fn setup_actions(&self) {
// Create action from key "filter" and add to action group "win"
let action_filter = self.settings().create_action("filter");
self.add_action(&action_filter);
}
fn remove_done_tasks(&self) {
let tasks = self.tasks();
let mut position = 0;
while let Some(item) = tasks.item(position) {
// Get `TaskObject` from `glib::Object`
let task_object = item
.downcast_ref::<TaskObject>()
.expect("The object needs to be of type `TaskObject`.");
if task_object.is_completed() {
tasks.remove(position);
} else {
position += 1;
}
}
}
}
我们还添加了一个动作,允许我们删除已完成的任务。 这次我们使用另一种名为 install_action
的方法。 这种方法有一些限制。 它只能在子类化部件时使用,而且不支持有状态的动作。 但是,它的用法很简洁,而且有一个相应的姊妹方法 install_action_async
,我们将在以后的章节中使用它。
文件名:listings/todo/2/window/imp.rs
use std::cell::RefCell;
use std::fs::File;
use gio::Settings;
use glib::subclass::InitializingObject;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib, CompositeTemplate, Entry, ListView};
use std::cell::OnceCell;
use crate::task_object::{TaskData, TaskObject};
use crate::utils::data_path;
// Object holding the state
#[derive(CompositeTemplate, Default)]
#[template(resource = "/org/gtk_rs/Todo2/window.ui")]
pub struct Window {
#[template_child]
pub entry: TemplateChild<Entry>,
#[template_child]
pub tasks_list: TemplateChild<ListView>,
pub tasks: RefCell<Option<gio::ListStore>>,
pub settings: OnceCell<Settings>,
}
// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for Window {
// `NAME` needs to match `class` attribute of template
const NAME: &'static str = "TodoWindow";
type Type = super::Window;
type ParentType = gtk::ApplicationWindow;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
// Create action to remove done tasks and add to action group "win"
klass.install_action("win.remove-done-tasks", None, |window, _, _| {
window.remove_done_tasks();
});
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
// Trait shared by all GObjects
impl ObjectImpl for Window {
fn constructed(&self) {
// Call "constructed" on parent
self.parent_constructed();
// Setup
let obj = self.obj();
obj.setup_settings();
obj.setup_tasks();
obj.restore_data();
obj.setup_callbacks();
obj.setup_factory();
obj.setup_actions();
}
}
// Trait shared by all widgets
impl WidgetImpl for Window {}
// Trait shared by all windows
impl WindowImpl for Window {
fn close_request(&self) -> glib::Propagation {
// Store task data in vector
let backup_data: Vec<TaskData> = self
.obj()
.tasks()
.iter::<TaskObject>()
.filter_map(Result::ok)
.map(|task_object| task_object.task_data())
.collect();
// Save state to file
let file = File::create(data_path()).expect("Could not create json file.");
serde_json::to_writer(file, &backup_data)
.expect("Could not write data to json file");
// Pass close request on to the parent
self.parent_close_request()
}
}
// Trait shared by all application windows
impl ApplicationWindowImpl for Window {}
这是 remove_done_tasks
的实现。 我们遍历 gio::ListStore
并删除所有已完成的任务对象。
文件名:listings/todo/2/window/mod.rs
mod imp;
use std::fs::File;
use gio::Settings;
use glib::{clone, Object};
use gtk::subclass::prelude::*;
use gtk::{
gio, glib, Application, CustomFilter, FilterListModel, NoSelection,
SignalListItemFactory,
};
use gtk::{prelude::*, ListItem};
use crate::task_object::{TaskData, TaskObject};
use crate::task_row::TaskRow;
use crate::utils::data_path;
use crate::APP_ID;
glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>)
@extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}
impl Window {
pub fn new(app: &Application) -> Self {
// Create new window
Object::builder().property("application", app).build()
}
fn setup_settings(&self) {
let settings = Settings::new(APP_ID);
self.imp()
.settings
.set(settings)
.expect("`settings` should not be set before calling `setup_settings`.");
}
fn settings(&self) -> &Settings {
self.imp()
.settings
.get()
.expect("`settings` should be set in `setup_settings`.")
}
fn tasks(&self) -> gio::ListStore {
// Get state
self.imp()
.tasks
.borrow()
.clone()
.expect("Could not get current tasks.")
}
fn filter(&self) -> Option<CustomFilter> {
// Get filter_state from settings
let filter_state: String = self.settings().get("filter");
// Create custom filters
let filter_open = CustomFilter::new(|obj| {
// Get `TaskObject` from `glib::Object`
let task_object = obj
.downcast_ref::<TaskObject>()
.expect("The object needs to be of type `TaskObject`.");
// Only allow completed tasks
!task_object.is_completed()
});
let filter_done = CustomFilter::new(|obj| {
// Get `TaskObject` from `glib::Object`
let task_object = obj
.downcast_ref::<TaskObject>()
.expect("The object needs to be of type `TaskObject`.");
// Only allow done tasks
task_object.is_completed()
});
// Return the correct filter
match filter_state.as_str() {
"All" => None,
"Open" => Some(filter_open),
"Done" => Some(filter_done),
_ => unreachable!(),
}
}
fn setup_tasks(&self) {
// Create new model
let model = gio::ListStore::new::<TaskObject>();
// Get state and set model
self.imp().tasks.replace(Some(model));
// Wrap model with filter and selection and pass it to the list view
let filter_model = FilterListModel::new(Some(self.tasks()), self.filter());
let selection_model = NoSelection::new(Some(filter_model.clone()));
self.imp().tasks_list.set_model(Some(&selection_model));
// Filter model whenever the value of the key "filter" changes
self.settings().connect_changed(
Some("filter"),
clone!(
#[weak(rename_to = window)]
self,
#[weak]
filter_model,
move |_, _| {
filter_model.set_filter(window.filter().as_ref());
}
),
);
}
fn restore_data(&self) {
if let Ok(file) = File::open(data_path()) {
// Deserialize data from file to vector
let backup_data: Vec<TaskData> = serde_json::from_reader(file).expect(
"It should be possible to read `backup_data` from the json file.",
);
// Convert `Vec<TaskData>` to `Vec<TaskObject>`
let task_objects: Vec<TaskObject> = backup_data
.into_iter()
.map(TaskObject::from_task_data)
.collect();
// Insert restored objects into model
self.tasks().extend_from_slice(&task_objects);
}
}
fn setup_callbacks(&self) {
// Setup callback for activation of the entry
self.imp().entry.connect_activate(clone!(
#[weak(rename_to = window)]
self,
move |_| {
window.new_task();
}
));
// Setup callback for clicking (and the releasing) the icon of the entry
self.imp().entry.connect_icon_release(clone!(
#[weak(rename_to = window)]
self,
move |_, _| {
window.new_task();
}
));
}
fn new_task(&self) {
// Get content from entry and clear it
let buffer = self.imp().entry.buffer();
let content = buffer.text().to_string();
if content.is_empty() {
return;
}
buffer.set_text("");
// Add new task to model
let task = TaskObject::new(false, content);
self.tasks().append(&task);
}
fn setup_factory(&self) {
// Create a new factory
let factory = SignalListItemFactory::new();
// Create an empty `TaskRow` during setup
factory.connect_setup(move |_, list_item| {
// Create `TaskRow`
let task_row = TaskRow::new();
list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.set_child(Some(&task_row));
});
// Tell factory how to bind `TaskRow` to a `TaskObject`
factory.connect_bind(move |_, list_item| {
// Get `TaskObject` from `ListItem`
let task_object = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.item()
.and_downcast::<TaskObject>()
.expect("The item has to be an `TaskObject`.");
// Get `TaskRow` from `ListItem`
let task_row = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.child()
.and_downcast::<TaskRow>()
.expect("The child has to be a `TaskRow`.");
task_row.bind(&task_object);
});
// Tell factory how to unbind `TaskRow` from `TaskObject`
factory.connect_unbind(move |_, list_item| {
// Get `TaskRow` from `ListItem`
let task_row = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.child()
.and_downcast::<TaskRow>()
.expect("The child has to be a `TaskRow`.");
task_row.unbind();
});
// Set the factory of the list view
self.imp().tasks_list.set_factory(Some(&factory));
}
fn setup_actions(&self) {
// Create action from key "filter" and add to action group "win"
let action_filter = self.settings().create_action("filter");
self.add_action(&action_filter);
}
fn remove_done_tasks(&self) {
let tasks = self.tasks();
let mut position = 0;
while let Some(item) = tasks.item(position) {
// Get `TaskObject` from `glib::Object`
let task_object = item
.downcast_ref::<TaskObject>()
.expect("The object needs to be of type `TaskObject`.");
if task_object.is_completed() {
tasks.remove(position);
} else {
position += 1;
}
}
}
}
激活 "win.filter" 操作后,相应的设置将被更改。 因此我们需要一个方法,将此设置转换为 gtk::FilterListModel
可以理解的过滤器。 可能的状态有 "全部"、"待办"和 "已完成"。 对于 "待办"和 "已完成",我们返回 Some(filter)
。 如果状态为 "全部",则无需过滤任何内容,因此我们返回 None
.
文件名:listings/todo/2/window/mod.rs
mod imp;
use std::fs::File;
use gio::Settings;
use glib::{clone, Object};
use gtk::subclass::prelude::*;
use gtk::{
gio, glib, Application, CustomFilter, FilterListModel, NoSelection,
SignalListItemFactory,
};
use gtk::{prelude::*, ListItem};
use crate::task_object::{TaskData, TaskObject};
use crate::task_row::TaskRow;
use crate::utils::data_path;
use crate::APP_ID;
glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>)
@extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}
impl Window {
pub fn new(app: &Application) -> Self {
// Create new window
Object::builder().property("application", app).build()
}
fn setup_settings(&self) {
let settings = Settings::new(APP_ID);
self.imp()
.settings
.set(settings)
.expect("`settings` should not be set before calling `setup_settings`.");
}
fn settings(&self) -> &Settings {
self.imp()
.settings
.get()
.expect("`settings` should be set in `setup_settings`.")
}
fn tasks(&self) -> gio::ListStore {
// Get state
self.imp()
.tasks
.borrow()
.clone()
.expect("Could not get current tasks.")
}
fn filter(&self) -> Option<CustomFilter> {
// Get filter_state from settings
let filter_state: String = self.settings().get("filter");
// Create custom filters
let filter_open = CustomFilter::new(|obj| {
// Get `TaskObject` from `glib::Object`
let task_object = obj
.downcast_ref::<TaskObject>()
.expect("The object needs to be of type `TaskObject`.");
// Only allow completed tasks
!task_object.is_completed()
});
let filter_done = CustomFilter::new(|obj| {
// Get `TaskObject` from `glib::Object`
let task_object = obj
.downcast_ref::<TaskObject>()
.expect("The object needs to be of type `TaskObject`.");
// Only allow done tasks
task_object.is_completed()
});
// Return the correct filter
match filter_state.as_str() {
"All" => None,
"Open" => Some(filter_open),
"Done" => Some(filter_done),
_ => unreachable!(),
}
}
fn setup_tasks(&self) {
// Create new model
let model = gio::ListStore::new::<TaskObject>();
// Get state and set model
self.imp().tasks.replace(Some(model));
// Wrap model with filter and selection and pass it to the list view
let filter_model = FilterListModel::new(Some(self.tasks()), self.filter());
let selection_model = NoSelection::new(Some(filter_model.clone()));
self.imp().tasks_list.set_model(Some(&selection_model));
// Filter model whenever the value of the key "filter" changes
self.settings().connect_changed(
Some("filter"),
clone!(
#[weak(rename_to = window)]
self,
#[weak]
filter_model,
move |_, _| {
filter_model.set_filter(window.filter().as_ref());
}
),
);
}
fn restore_data(&self) {
if let Ok(file) = File::open(data_path()) {
// Deserialize data from file to vector
let backup_data: Vec<TaskData> = serde_json::from_reader(file).expect(
"It should be possible to read `backup_data` from the json file.",
);
// Convert `Vec<TaskData>` to `Vec<TaskObject>`
let task_objects: Vec<TaskObject> = backup_data
.into_iter()
.map(TaskObject::from_task_data)
.collect();
// Insert restored objects into model
self.tasks().extend_from_slice(&task_objects);
}
}
fn setup_callbacks(&self) {
// Setup callback for activation of the entry
self.imp().entry.connect_activate(clone!(
#[weak(rename_to = window)]
self,
move |_| {
window.new_task();
}
));
// Setup callback for clicking (and the releasing) the icon of the entry
self.imp().entry.connect_icon_release(clone!(
#[weak(rename_to = window)]
self,
move |_, _| {
window.new_task();
}
));
}
fn new_task(&self) {
// Get content from entry and clear it
let buffer = self.imp().entry.buffer();
let content = buffer.text().to_string();
if content.is_empty() {
return;
}
buffer.set_text("");
// Add new task to model
let task = TaskObject::new(false, content);
self.tasks().append(&task);
}
fn setup_factory(&self) {
// Create a new factory
let factory = SignalListItemFactory::new();
// Create an empty `TaskRow` during setup
factory.connect_setup(move |_, list_item| {
// Create `TaskRow`
let task_row = TaskRow::new();
list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.set_child(Some(&task_row));
});
// Tell factory how to bind `TaskRow` to a `TaskObject`
factory.connect_bind(move |_, list_item| {
// Get `TaskObject` from `ListItem`
let task_object = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.item()
.and_downcast::<TaskObject>()
.expect("The item has to be an `TaskObject`.");
// Get `TaskRow` from `ListItem`
let task_row = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.child()
.and_downcast::<TaskRow>()
.expect("The child has to be a `TaskRow`.");
task_row.bind(&task_object);
});
// Tell factory how to unbind `TaskRow` from `TaskObject`
factory.connect_unbind(move |_, list_item| {
// Get `TaskRow` from `ListItem`
let task_row = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.child()
.and_downcast::<TaskRow>()
.expect("The child has to be a `TaskRow`.");
task_row.unbind();
});
// Set the factory of the list view
self.imp().tasks_list.set_factory(Some(&factory));
}
fn setup_actions(&self) {
// Create action from key "filter" and add to action group "win"
let action_filter = self.settings().create_action("filter");
self.add_action(&action_filter);
}
fn remove_done_tasks(&self) {
let tasks = self.tasks();
let mut position = 0;
while let Some(item) = tasks.item(position) {
// Get `TaskObject` from `glib::Object`
let task_object = item
.downcast_ref::<TaskObject>()
.expect("The object needs to be of type `TaskObject`.");
if task_object.is_completed() {
tasks.remove(position);
} else {
position += 1;
}
}
}
}
现在,我们可以设置模型了。 通过调用 filter
方法,用设置中的状态初始化 filter_model
。 每当关键字 "filter" 的状态发生变化时,我们就会再次调用 filter
方法来获取更新后的 filter_model
.
文件名:listings/todo/2/window/mod.rs
mod imp;
use std::fs::File;
use gio::Settings;
use glib::{clone, Object};
use gtk::subclass::prelude::*;
use gtk::{
gio, glib, Application, CustomFilter, FilterListModel, NoSelection,
SignalListItemFactory,
};
use gtk::{prelude::*, ListItem};
use crate::task_object::{TaskData, TaskObject};
use crate::task_row::TaskRow;
use crate::utils::data_path;
use crate::APP_ID;
glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>)
@extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}
impl Window {
pub fn new(app: &Application) -> Self {
// Create new window
Object::builder().property("application", app).build()
}
fn setup_settings(&self) {
let settings = Settings::new(APP_ID);
self.imp()
.settings
.set(settings)
.expect("`settings` should not be set before calling `setup_settings`.");
}
fn settings(&self) -> &Settings {
self.imp()
.settings
.get()
.expect("`settings` should be set in `setup_settings`.")
}
fn tasks(&self) -> gio::ListStore {
// Get state
self.imp()
.tasks
.borrow()
.clone()
.expect("Could not get current tasks.")
}
fn filter(&self) -> Option<CustomFilter> {
// Get filter_state from settings
let filter_state: String = self.settings().get("filter");
// Create custom filters
let filter_open = CustomFilter::new(|obj| {
// Get `TaskObject` from `glib::Object`
let task_object = obj
.downcast_ref::<TaskObject>()
.expect("The object needs to be of type `TaskObject`.");
// Only allow completed tasks
!task_object.is_completed()
});
let filter_done = CustomFilter::new(|obj| {
// Get `TaskObject` from `glib::Object`
let task_object = obj
.downcast_ref::<TaskObject>()
.expect("The object needs to be of type `TaskObject`.");
// Only allow done tasks
task_object.is_completed()
});
// Return the correct filter
match filter_state.as_str() {
"All" => None,
"Open" => Some(filter_open),
"Done" => Some(filter_done),
_ => unreachable!(),
}
}
fn setup_tasks(&self) {
// Create new model
let model = gio::ListStore::new::<TaskObject>();
// Get state and set model
self.imp().tasks.replace(Some(model));
// Wrap model with filter and selection and pass it to the list view
let filter_model = FilterListModel::new(Some(self.tasks()), self.filter());
let selection_model = NoSelection::new(Some(filter_model.clone()));
self.imp().tasks_list.set_model(Some(&selection_model));
// Filter model whenever the value of the key "filter" changes
self.settings().connect_changed(
Some("filter"),
clone!(
#[weak(rename_to = window)]
self,
#[weak]
filter_model,
move |_, _| {
filter_model.set_filter(window.filter().as_ref());
}
),
);
}
fn restore_data(&self) {
if let Ok(file) = File::open(data_path()) {
// Deserialize data from file to vector
let backup_data: Vec<TaskData> = serde_json::from_reader(file).expect(
"It should be possible to read `backup_data` from the json file.",
);
// Convert `Vec<TaskData>` to `Vec<TaskObject>`
let task_objects: Vec<TaskObject> = backup_data
.into_iter()
.map(TaskObject::from_task_data)
.collect();
// Insert restored objects into model
self.tasks().extend_from_slice(&task_objects);
}
}
fn setup_callbacks(&self) {
// Setup callback for activation of the entry
self.imp().entry.connect_activate(clone!(
#[weak(rename_to = window)]
self,
move |_| {
window.new_task();
}
));
// Setup callback for clicking (and the releasing) the icon of the entry
self.imp().entry.connect_icon_release(clone!(
#[weak(rename_to = window)]
self,
move |_, _| {
window.new_task();
}
));
}
fn new_task(&self) {
// Get content from entry and clear it
let buffer = self.imp().entry.buffer();
let content = buffer.text().to_string();
if content.is_empty() {
return;
}
buffer.set_text("");
// Add new task to model
let task = TaskObject::new(false, content);
self.tasks().append(&task);
}
fn setup_factory(&self) {
// Create a new factory
let factory = SignalListItemFactory::new();
// Create an empty `TaskRow` during setup
factory.connect_setup(move |_, list_item| {
// Create `TaskRow`
let task_row = TaskRow::new();
list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.set_child(Some(&task_row));
});
// Tell factory how to bind `TaskRow` to a `TaskObject`
factory.connect_bind(move |_, list_item| {
// Get `TaskObject` from `ListItem`
let task_object = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.item()
.and_downcast::<TaskObject>()
.expect("The item has to be an `TaskObject`.");
// Get `TaskRow` from `ListItem`
let task_row = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.child()
.and_downcast::<TaskRow>()
.expect("The child has to be a `TaskRow`.");
task_row.bind(&task_object);
});
// Tell factory how to unbind `TaskRow` from `TaskObject`
factory.connect_unbind(move |_, list_item| {
// Get `TaskRow` from `ListItem`
let task_row = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.child()
.and_downcast::<TaskRow>()
.expect("The child has to be a `TaskRow`.");
task_row.unbind();
});
// Set the factory of the list view
self.imp().tasks_list.set_factory(Some(&factory));
}
fn setup_actions(&self) {
// Create action from key "filter" and add to action group "win"
let action_filter = self.settings().create_action("filter");
self.add_action(&action_filter);
}
fn remove_done_tasks(&self) {
let tasks = self.tasks();
let mut position = 0;
while let Some(item) = tasks.item(position) {
// Get `TaskObject` from `glib::Object`
let task_object = item
.downcast_ref::<TaskObject>()
.expect("The object needs to be of type `TaskObject`.");
if task_object.is_completed() {
tasks.remove(position);
} else {
position += 1;
}
}
}
}
然后,我们使用 set_accels_for_action
将快捷方式与其动作绑定。 这里也使用了详细的动作名称。 由于这必须在应用程序级别完成,setup_shortcuts
将 gtk::Application
作为参数。
mod task_object;
mod task_row;
mod utils;
mod window;
use gtk::prelude::*;
use gtk::{gio, glib, Application};
use window::Window;
const APP_ID: &str = "org.gtk_rs.Todo2";
fn main() -> glib::ExitCode {
// Register and include resources
gio::resources_register_include!("todo_2.gresource")
.expect("Failed to register resources.");
// Create a new application
let app = Application::builder().application_id(APP_ID).build();
// Connect to signals
app.connect_startup(setup_shortcuts);
app.connect_activate(build_ui);
// Run the application
app.run()
}
fn setup_shortcuts(app: &Application) {
app.set_accels_for_action("win.filter('All')", &["<Ctrl>a"]);
app.set_accels_for_action("win.filter('Open')", &["<Ctrl>o"]);
app.set_accels_for_action("win.filter('Done')", &["<Ctrl>d"]);
}
fn build_ui(app: &Application) {
// Create a new custom window and present it
let window = Window::new(app);
window.present();
}
现在,我们创建了所有这些漂亮的快捷方式,我们希望用户能找到它们。 为此,我们需要创建一个快捷方式窗口。 我们再次使用一个 ui
文件来描述它,但这里我们不想使用它作为我们自定义控件模板。 相反,我们用它实例化了一个现有类 gtk::ShortcutsWindow
的控件。
文件名:listings/todo/2/resources/shortcuts.ui
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<object class="GtkShortcutsWindow" id="help_overlay">
<property name="modal">True</property>
<child>
<object class="GtkShortcutsSection">
<property name="section-name">shortcuts</property>
<property name="max-height">10</property>
<child>
<object class="GtkShortcutsGroup">
<property name="title" translatable="yes" context="shortcut window">General</property>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes" context="shortcut window">Show shortcuts</property>
<property name="action-name">win.show-help-overlay</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes" context="shortcut window">Filter to show all tasks</property>
<property name="action-name">win.filter('All')</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes" context="shortcut window">Filter to show only open tasks</property>
<property name="action-name">win.filter('Open')</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="title" translatable="yes" context="shortcut window">Filter to show only completed tasks</property>
<property name="action-name">win.filter('Done')</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</interface>
这些条目可以用 gtk::ShortcutsSection
和 gtk::ShortcutsGroup
来组织。 如果我们指定了动作名称,我们就不必重复键盘快键键。 gtk::ShortcutsShortcut
会自行查找。
请注意我们为
ShortcutsShortcut
设置action-name
的方式。 我们没有为目标使用单独的属性,而是使用了一个详细的操作名称。 详细名称看上去类似:action_group.action_name(target)
. 目标的格式取决于其类型,此处有相关说明。 尤其是字符串,必须用单引号括起来,如本例所示。
最后,我们必须将 shortcuts.ui
添加到资源中。 请注意,我们给它起的别名是 gtk/help-overlay.ui
. 这样做是为了利用此处记录的一个便利功能。 它将在 gtk/help-overlay.ui
中查找资源,该资源定义了一个 ID 为 help_overlay
的快捷方式窗口(ShortcutsWindow)
。 如果能找到,它将创建一个操作 win.show-help-overlay
来显示该窗口,并将快捷键 Ctrl + ? 与之关联。
文件名:listings/todo/2/resources/resources.gresource.xml
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/org/gtk_rs/Todo2/">
<file compressed="true" preprocess="xml-stripblanks" alias="gtk/help-overlay.ui">shortcuts.ui</file>
<file compressed="true" preprocess="xml-stripblanks">task_row.ui</file>
<file compressed="true" preprocess="xml-stripblanks">window.ui</file>
</gresource>
</gresources>
保存和恢复任务
由于我们使用的是设置(Settings)
,因此我们的过滤器状态将在会话之间持续存在。 但是,任务本身不会。 让我们来实现这一点。 我们可以将任务存储在设置
中,但这样会很不方便。 说到序列化和反序列化,没有什么比 serde
更好的了。 结合 serde_json
,我们可以将任务保存为序列化的 json 文件。
首先,我们使用 serde
和 serde_json
crate 扩展 Cargo.toml
.
cargo add serde --features derive
cargo add serde_json
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Serde 是一个用于序列化和反序列化 Rust 数据结构的框架。 通过派生(derive)
功能,我们只需一行代码就能使我们的结构(去)序列化。 我们还使用了 rc
功能,这样 Serde 就能处理 std::rc::Rc
对象。 这就是为什么我们将 TodoObject
的数据存储在一个不同的 TodoData
结构中。 这样我们就可以为 TodoData
派生出序列化(Serialize)
和反序列化(Deserialize)
.
文件名:listings/todo/2/task_object/mod.rs
mod imp;
use glib::Object;
use gtk::glib;
use gtk::subclass::prelude::*;
use serde::{Deserialize, Serialize};
glib::wrapper! {
pub struct TaskObject(ObjectSubclass<imp::TaskObject>);
}
impl TaskObject {
pub fn new(completed: bool, content: String) -> Self {
Object::builder()
.property("completed", completed)
.property("content", content)
.build()
}
pub fn is_completed(&self) -> bool {
self.imp().data.borrow().completed
}
pub fn task_data(&self) -> TaskData {
self.imp().data.borrow().clone()
}
pub fn from_task_data(task_data: TaskData) -> Self {
Self::new(task_data.completed, task_data.content)
}
}
#[derive(Default, Clone, Serialize, Deserialize)]
pub struct TaskData {
pub completed: bool,
pub content: String,
}
我们计划将数据存储为文件,因此我们创建了一个工具函数,为我们提供合适的文件路径。 我们使用 glib::user_config_dir
获取配置目录的路径,并为应用程序创建一个新的子目录。 然后返回文件路径。
use std::path::PathBuf;
use gtk::glib;
use crate::APP_ID;
pub fn data_path() -> PathBuf {
let mut path = glib::user_data_dir();
path.push(APP_ID);
std::fs::create_dir_all(&path).expect("Could not create directory.");
path.push("data.json");
path
}
我们重写了 close_request
虚函数,以便在窗口关闭时保存任务。 为此,我们首先遍历所有条目并将其存储在一个 Vec
中。 然后将 Vec
序列化,并将数据存储为 json 文件。
文件名:listings/todo/2/window/imp.rs
use std::cell::RefCell;
use std::fs::File;
use gio::Settings;
use glib::subclass::InitializingObject;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib, CompositeTemplate, Entry, ListView};
use std::cell::OnceCell;
use crate::task_object::{TaskData, TaskObject};
use crate::utils::data_path;
// Object holding the state
#[derive(CompositeTemplate, Default)]
#[template(resource = "/org/gtk_rs/Todo2/window.ui")]
pub struct Window {
#[template_child]
pub entry: TemplateChild<Entry>,
#[template_child]
pub tasks_list: TemplateChild<ListView>,
pub tasks: RefCell<Option<gio::ListStore>>,
pub settings: OnceCell<Settings>,
}
// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for Window {
// `NAME` needs to match `class` attribute of template
const NAME: &'static str = "TodoWindow";
type Type = super::Window;
type ParentType = gtk::ApplicationWindow;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
// Create action to remove done tasks and add to action group "win"
klass.install_action("win.remove-done-tasks", None, |window, _, _| {
window.remove_done_tasks();
});
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
// Trait shared by all GObjects
impl ObjectImpl for Window {
fn constructed(&self) {
// Call "constructed" on parent
self.parent_constructed();
// Setup
let obj = self.obj();
obj.setup_settings();
obj.setup_tasks();
obj.restore_data();
obj.setup_callbacks();
obj.setup_factory();
obj.setup_actions();
}
}
// Trait shared by all widgets
impl WidgetImpl for Window {}
// Trait shared by all windows
impl WindowImpl for Window {
fn close_request(&self) -> glib::Propagation {
// Store task data in vector
let backup_data: Vec<TaskData> = self
.obj()
.tasks()
.iter::<TaskObject>()
.filter_map(Result::ok)
.map(|task_object| task_object.task_data())
.collect();
// Save state to file
let file = File::create(data_path()).expect("Could not create json file.");
serde_json::to_writer(file, &backup_data)
.expect("Could not write data to json file");
// Pass close request on to the parent
self.parent_close_request()
}
}
// Trait shared by all application windows
impl ApplicationWindowImpl for Window {}
让我们来看看 Vec<TaskData>
将被序列化成什么样子。 请注意,serde_json::to_writer
会以更简洁但可读性更差的方式保存数据。 若要创建等价但格式友好的 json,只需将 to_writer
替换为 serde_json::to_writer_pretty
.
文件名: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"
}
]
当我们启动应用程序时,我们希望恢复已保存的数据。 让我们为此添加一个 restore_data
方法。 我们将确保处理启动时没有数据文件的情况。 这可能是我们第一次启动应用程序,因此没有以前的会话可以恢复。
文件名:listings/todo/2/window/mod.rs
mod imp;
use std::fs::File;
use gio::Settings;
use glib::{clone, Object};
use gtk::subclass::prelude::*;
use gtk::{
gio, glib, Application, CustomFilter, FilterListModel, NoSelection,
SignalListItemFactory,
};
use gtk::{prelude::*, ListItem};
use crate::task_object::{TaskData, TaskObject};
use crate::task_row::TaskRow;
use crate::utils::data_path;
use crate::APP_ID;
glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>)
@extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}
impl Window {
pub fn new(app: &Application) -> Self {
// Create new window
Object::builder().property("application", app).build()
}
fn setup_settings(&self) {
let settings = Settings::new(APP_ID);
self.imp()
.settings
.set(settings)
.expect("`settings` should not be set before calling `setup_settings`.");
}
fn settings(&self) -> &Settings {
self.imp()
.settings
.get()
.expect("`settings` should be set in `setup_settings`.")
}
fn tasks(&self) -> gio::ListStore {
// Get state
self.imp()
.tasks
.borrow()
.clone()
.expect("Could not get current tasks.")
}
fn filter(&self) -> Option<CustomFilter> {
// Get filter_state from settings
let filter_state: String = self.settings().get("filter");
// Create custom filters
let filter_open = CustomFilter::new(|obj| {
// Get `TaskObject` from `glib::Object`
let task_object = obj
.downcast_ref::<TaskObject>()
.expect("The object needs to be of type `TaskObject`.");
// Only allow completed tasks
!task_object.is_completed()
});
let filter_done = CustomFilter::new(|obj| {
// Get `TaskObject` from `glib::Object`
let task_object = obj
.downcast_ref::<TaskObject>()
.expect("The object needs to be of type `TaskObject`.");
// Only allow done tasks
task_object.is_completed()
});
// Return the correct filter
match filter_state.as_str() {
"All" => None,
"Open" => Some(filter_open),
"Done" => Some(filter_done),
_ => unreachable!(),
}
}
fn setup_tasks(&self) {
// Create new model
let model = gio::ListStore::new::<TaskObject>();
// Get state and set model
self.imp().tasks.replace(Some(model));
// Wrap model with filter and selection and pass it to the list view
let filter_model = FilterListModel::new(Some(self.tasks()), self.filter());
let selection_model = NoSelection::new(Some(filter_model.clone()));
self.imp().tasks_list.set_model(Some(&selection_model));
// Filter model whenever the value of the key "filter" changes
self.settings().connect_changed(
Some("filter"),
clone!(
#[weak(rename_to = window)]
self,
#[weak]
filter_model,
move |_, _| {
filter_model.set_filter(window.filter().as_ref());
}
),
);
}
fn restore_data(&self) {
if let Ok(file) = File::open(data_path()) {
// Deserialize data from file to vector
let backup_data: Vec<TaskData> = serde_json::from_reader(file).expect(
"It should be possible to read `backup_data` from the json file.",
);
// Convert `Vec<TaskData>` to `Vec<TaskObject>`
let task_objects: Vec<TaskObject> = backup_data
.into_iter()
.map(TaskObject::from_task_data)
.collect();
// Insert restored objects into model
self.tasks().extend_from_slice(&task_objects);
}
}
fn setup_callbacks(&self) {
// Setup callback for activation of the entry
self.imp().entry.connect_activate(clone!(
#[weak(rename_to = window)]
self,
move |_| {
window.new_task();
}
));
// Setup callback for clicking (and the releasing) the icon of the entry
self.imp().entry.connect_icon_release(clone!(
#[weak(rename_to = window)]
self,
move |_, _| {
window.new_task();
}
));
}
fn new_task(&self) {
// Get content from entry and clear it
let buffer = self.imp().entry.buffer();
let content = buffer.text().to_string();
if content.is_empty() {
return;
}
buffer.set_text("");
// Add new task to model
let task = TaskObject::new(false, content);
self.tasks().append(&task);
}
fn setup_factory(&self) {
// Create a new factory
let factory = SignalListItemFactory::new();
// Create an empty `TaskRow` during setup
factory.connect_setup(move |_, list_item| {
// Create `TaskRow`
let task_row = TaskRow::new();
list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.set_child(Some(&task_row));
});
// Tell factory how to bind `TaskRow` to a `TaskObject`
factory.connect_bind(move |_, list_item| {
// Get `TaskObject` from `ListItem`
let task_object = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.item()
.and_downcast::<TaskObject>()
.expect("The item has to be an `TaskObject`.");
// Get `TaskRow` from `ListItem`
let task_row = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.child()
.and_downcast::<TaskRow>()
.expect("The child has to be a `TaskRow`.");
task_row.bind(&task_object);
});
// Tell factory how to unbind `TaskRow` from `TaskObject`
factory.connect_unbind(move |_, list_item| {
// Get `TaskRow` from `ListItem`
let task_row = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.child()
.and_downcast::<TaskRow>()
.expect("The child has to be a `TaskRow`.");
task_row.unbind();
});
// Set the factory of the list view
self.imp().tasks_list.set_factory(Some(&factory));
}
fn setup_actions(&self) {
// Create action from key "filter" and add to action group "win"
let action_filter = self.settings().create_action("filter");
self.add_action(&action_filter);
}
fn remove_done_tasks(&self) {
let tasks = self.tasks();
let mut position = 0;
while let Some(item) = tasks.item(position) {
// Get `TaskObject` from `glib::Object`
let task_object = item
.downcast_ref::<TaskObject>()
.expect("The object needs to be of type `TaskObject`.");
if task_object.is_completed() {
tasks.remove(position);
} else {
position += 1;
}
}
}
}
最后,我们要确保一切在 constructed
中配置好。
文件名:listings/todo/2/window/imp.rs
use std::cell::RefCell;
use std::fs::File;
use gio::Settings;
use glib::subclass::InitializingObject;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib, CompositeTemplate, Entry, ListView};
use std::cell::OnceCell;
use crate::task_object::{TaskData, TaskObject};
use crate::utils::data_path;
// Object holding the state
#[derive(CompositeTemplate, Default)]
#[template(resource = "/org/gtk_rs/Todo2/window.ui")]
pub struct Window {
#[template_child]
pub entry: TemplateChild<Entry>,
#[template_child]
pub tasks_list: TemplateChild<ListView>,
pub tasks: RefCell<Option<gio::ListStore>>,
pub settings: OnceCell<Settings>,
}
// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for Window {
// `NAME` needs to match `class` attribute of template
const NAME: &'static str = "TodoWindow";
type Type = super::Window;
type ParentType = gtk::ApplicationWindow;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
// Create action to remove done tasks and add to action group "win"
klass.install_action("win.remove-done-tasks", None, |window, _, _| {
window.remove_done_tasks();
});
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
// Trait shared by all GObjects
impl ObjectImpl for Window {
fn constructed(&self) {
// Call "constructed" on parent
self.parent_constructed();
// Setup
let obj = self.obj();
obj.setup_settings();
obj.setup_tasks();
obj.restore_data();
obj.setup_callbacks();
obj.setup_factory();
obj.setup_actions();
}
}
// Trait shared by all widgets
impl WidgetImpl for Window {}
// Trait shared by all windows
impl WindowImpl for Window {
fn close_request(&self) -> glib::Propagation {
// Store task data in vector
let backup_data: Vec<TaskData> = self
.obj()
.tasks()
.iter::<TaskObject>()
.filter_map(Result::ok)
.map(|task_object| task_object.task_data())
.collect();
// Save state to file
let file = File::create(data_path()).expect("Could not create json file.");
serde_json::to_writer(file, &backup_data)
.expect("Could not write data to json file");
// Pass close request on to the parent
self.parent_close_request()
}
}
// Trait shared by all application windows
impl ApplicationWindowImpl for Window {}
我们的待办事项应用突然变得更有用了。 我们不仅可以过滤任务,还可以在会话之间保留任务。