添加“收藏夹”功能
侧边栏
仅仅使用 Libadwaita ,待办事项应用的外观和感觉方面已经有了一个很大的飞跃。 让我们更进一步,添加一种将任务分组为收藏夹的方法。 这些集合将在应用程序的左侧获得自己的边栏。 首先,我们添加一个没有任何功能的空白侧边栏。
要达到这种状态,我们需要经过几个步骤。 首先,我们必须用 adw::ApplicationWindow
替换 gtk::ApplicationWindow
. 两者的主要区别在于 adw::ApplicationWindow
没有标题栏区域。 当我们使用 adw::NavigationSplitView
构建界面时,这就派上了用场。 在上面的截图中,NavigationSplitView
为左边的收藏夹视图添加了一个侧边栏,而任务视图则占据了右边的空间。 使用 adw::ApplicationWindow
时,集合视图和任务视图都有各自的 adw::HeaderBar
栏,而分隔线则穿过整个窗口。
文件名: listings/todo/7/resources/window.ui
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<menu id="main-menu">
<!--Menu implementation-->
</menu>
<template class="TodoWindow" parent="AdwApplicationWindow">
<property name="title" translatable="yes">To-Do</property>
<property name="width-request">360</property>
<property name="height-request">200</property>
<child>
<object class="AdwBreakpoint">
<condition>max-width: 500sp</condition>
<setter object="split_view" property="collapsed">True</setter>
</object>
</child>
<property name="content">
<object class="AdwNavigationSplitView" id="split_view">
<property name="min-sidebar-width">200</property>
<property name="sidebar">
<object class="AdwNavigationPage">
<!--Collection view implementation-->
</object>
</property>
<property name="content">
<object class="AdwNavigationPage">
<!--Task view implementation-->
</object>
</property>
</object>
</property>
</template>
</interface>
NavigationSplitView
还有助于使您的应用程序具有自适应能力。一旦请求的大小太小,无法同时容纳所有子视图,分割视图就会折叠,并开始像 gtk::Stack
那样工作。 这意味着它一次只能显示一个子视图。 仅显示单张的自适应行为允许待办事项应用在较小屏幕尺寸(如手机)上运行,即使添加了收藏夹视图。
我们为收藏夹视图添加了必要的用户界面元素,如带有添加新收藏夹按钮的标题栏,以及稍后显示收藏的列表框 collections_list
. 我们还为 collections_list
添加了导航侧边栏(navigations-sidebar)样式。
文件名: listings/todo/7/resources/window.ui
<object class="AdwNavigationPage">
<property name="title" bind-source="TodoWindow"
bind-property="title" bind-flags="sync-create" />
<property name="child">
<object class="AdwToolbarView">
<child type="top">
<object class="AdwHeaderBar">
<child type="start">
<object class="GtkToggleButton">
<property name="icon-name">list-add-symbolic</property>
<property name="tooltip-text" translatable="yes">New Collection</property>
<property name="action-name">win.new-collection</property>
</object>
</child>
</object>
</child>
<property name="content">
<object class="GtkScrolledWindow">
<property name="child">
<object class="GtkListBox" id="collections_list">
<style>
<class name="navigation-sidebar" />
</style>
</object>
</property>
</object>
</property>
</object>
我们还为任务视图添加了标题栏。
文件名: listings/todo/7/resources/window.ui
<object class="AdwNavigationPage">
<property name="title" translatable="yes">Tasks</property>
<property name="child">
<object class="AdwToolbarView">
<child type="top">
<object class="AdwHeaderBar">
<property name="show-title">False</property>
<child type="end">
<object class="GtkMenuButton">
<property name="icon-name">open-menu-symbolic</property>
<property name="menu-model">main-menu</property>
<property name="tooltip-text" translatable="yes">Main Menu</property>
</object>
</child>
</object>
</child>
<property name="content">
<object class="GtkScrolledWindow">
<property name="child">
<object class="AdwClamp">
<property name="maximum-size">400</property>
<property name="tightening-threshold">300</property>
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="margin-start">12</property>
<property name="margin-end">12</property>
<property name="spacing">12</property>
<child>
<object class="GtkEntry" id="entry">
<property name="placeholder-text" translatable="yes">Enter a Task…</property>
<property name="secondary-icon-name">list-add-symbolic</property>
</object>
</child>
<child>
<object class="GtkListBox" id="tasks_list">
<property name="visible">False</property>
<property name="selection-mode">none</property>
<style>
<class name="boxed-list" />
</style>
</object>
</child>
</object>
</property>
</object>
</property>
</object>
</property>
</object>
</property>
</object>
我们还必须调整窗口的实现。 例如,我们窗口的父类型现在是 adw::ApplicationWindow
,而不是 gtk::ApplicationWindow
.
文件名: listings/todo/7/window/imp.rs
use std::cell::RefCell;
use std::fs::File;
use adw::subclass::prelude::*;
use gio::Settings;
use glib::subclass::InitializingObject;
use adw::prelude::*;
use gtk::{gio, glib, CompositeTemplate, Entry, ListBox};
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/Todo7/window.ui")]
pub struct Window {
#[template_child]
pub entry: TemplateChild<Entry>,
#[template_child]
pub tasks_list: TemplateChild<ListBox>,
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;
// 👇 changed
type ParentType = adw::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_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 {}
// Trait shared by all adwaita application windows
impl AdwApplicationWindowImpl for Window {}
这也意味着我们必须实现 AdwApplicationWindowImpl
trait.
文件名: listings/todo/7/window/imp.rs
use std::cell::RefCell;
use std::fs::File;
use adw::subclass::prelude::*;
use gio::Settings;
use glib::subclass::InitializingObject;
use adw::prelude::*;
use gtk::{gio, glib, CompositeTemplate, Entry, ListBox};
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/Todo7/window.ui")]
pub struct Window {
#[template_child]
pub entry: TemplateChild<Entry>,
#[template_child]
pub tasks_list: TemplateChild<ListBox>,
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;
// 👇 changed
type ParentType = adw::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_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 {}
// Trait shared by all adwaita application windows
impl AdwApplicationWindowImpl for Window {}
最后,我们将 adw::ApplicationWindow
添加到 mod.rs
中 Window
的祖先中。
文件名: listings/todo/7/window/mod.rs
mod imp;
use std::fs::File;
use adw::subclass::prelude::*;
use adw::{prelude::*, ActionRow};
use gio::Settings;
use glib::{clone, Object};
use gtk::{gio, glib, Align, CheckButton, CustomFilter, FilterListModel, NoSelection};
use crate::task_object::{TaskData, TaskObject};
use crate::utils::data_path;
use crate::APP_ID;
glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>)
// 👇 changed
@extends adw::ApplicationWindow, 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: &adw::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 {
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 box
let filter_model = FilterListModel::new(Some(self.tasks()), self.filter());
let selection_model = NoSelection::new(Some(filter_model.clone()));
self.imp().tasks_list.bind_model(
Some(&selection_model),
clone!(
#[weak(rename_to = window)]
self,
#[upgrade_or_panic]
move |obj| {
let task_object = obj
.downcast_ref()
.expect("The object should be of type `TaskObject`.");
let row = window.create_task_row(task_object);
row.upcast()
}
),
);
// 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());
}
),
);
// Assure that the task list is only visible when it is supposed to
self.set_task_list_visible(&self.tasks());
self.tasks().connect_items_changed(clone!(
#[weak(rename_to = window)]
self,
move |tasks, _, _, _| {
window.set_task_list_visible(tasks);
}
));
}
/// Assure that `tasks_list` is only visible
/// if the number of tasks is greater than 0
fn set_task_list_visible(&self, tasks: &gio::ListStore) {
self.imp().tasks_list.set_visible(tasks.n_items() > 0);
}
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 create_task_row(&self, task_object: &TaskObject) -> ActionRow {
// Create check button
let check_button = CheckButton::builder()
.valign(Align::Center)
.can_focus(false)
.build();
// Create row
let row = ActionRow::builder()
.activatable_widget(&check_button)
.build();
row.add_prefix(&check_button);
// Bind properties
task_object
.bind_property("completed", &check_button, "active")
.bidirectional()
.sync_create()
.build();
task_object
.bind_property("content", &row, "title")
.sync_create()
.build();
// Return row
row
}
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_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;
}
}
}
}
占位页面(空页面)
在开始填充收藏夹视图之前,我们还应该考虑一个不同的挑战:待办事项应用程序的空状态。 在此之前,没有任何任务的空状态还算不错。 很明显,您必须在输入栏中添加任务。 但现在情况不同了。 用户必须先添加一个收藏夹,我们必须明确这一点。 GNOME HIG 建议为此使用一个[占位页面](Placeholder Pages - GNOME Human Interface Guidelines)。 在我们的例子中,如果用户在没有任何收藏的情况下打开应用程序,这个占位页面就会显示给用户。
现在,我们将用户界面封装在 gtk::Stack
中。 一个堆栈页面描述占位页面,另一个描述主页面。 我们稍后将在 Rust 代码中设置显示正确堆栈页面的逻辑。
文件名: listings/todo/8/resources/window.ui
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<menu id="main-menu">
<!--Menu implementation-->
</menu>
<template class="TodoWindow" parent="AdwApplicationWindow">
<property name="title" translatable="yes">To-Do</property>
<property name="width-request">360</property>
<property name="height-request">200</property>
<child>
<object class="AdwBreakpoint">
<condition>max-width: 500sp</condition>
<setter object="split_view" property="collapsed">True</setter>
</object>
</child>
<property name="content">
<object class="GtkStack" id="stack">
<property name="transition-type">crossfade</property>
<child>
<object class="GtkStackPage">
<property name="name">placeholder</property>
<property name="child">
<object class="GtkBox">
<!--Placeholder page implementation-->
</object>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">main</property>
<property name="child">
<object class="AdwNavigationSplitView" id="split_view">
<!--Main page implementation-->
</object>
</property>
</object>
</child>
</object>
</property>
</template>
</interface>
为了创建之前显示的分页符页面,我们将扁平标题栏与 adw::StatusPage
结合起来。
文件名: listings/todo/8/resources/window.ui
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="GtkHeaderBar">
<style>
<class name="flat" />
</style>
</object>
</child>
<child>
<object class="GtkWindowHandle">
<property name="vexpand">True</property>
<property name="child">
<object class="AdwStatusPage">
<property name="icon-name">checkbox-checked-symbolic</property>
<property name="title" translatable="yes">No Tasks</property>
<property name="description" translatable="yes">Create some tasks to start using the app.</property>
<property name="child">
<object class="GtkButton">
<property name="label" translatable="yes">_New Collection</property>
<property name="use-underline">True</property>
<property name="halign">center</property>
<property name="action-name">win.new-collection</property>
<style>
<class name="pill" />
<class name="suggested-action" />
</style>
</object>
</property>
</object>
</property>
</object>
</child>
</object>
收藏夹
我们仍然需要一种方法来存储我们的收藏夹。 就像我们已经创建了 TaskObject
一样,现在我们将引入 CollectionObject
. 它将拥有标题(title)
和任务(tasks)
两个成员,这两个成员都将作为公开属性。 像往常一样,点击代码片段右上方的眼睛符号可以看到完整的实现。
文件名: listings/todo/8/collection_object/imp.rs
use std::cell::RefCell;
use adw::prelude::*;
use adw::subclass::prelude::*;
use glib::Properties;
use gtk::{gio, glib};
use std::cell::OnceCell;
// Object holding the state
#[derive(Properties, Default)]
#[properties(wrapper_type = super::CollectionObject)]
pub struct CollectionObject {
#[property(get, set)]
pub title: RefCell<String>,
#[property(get, set)]
pub tasks: OnceCell<gio::ListStore>,
}
// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for CollectionObject {
const NAME: &'static str = "TodoCollectionObject";
type Type = super::CollectionObject;
}
// Trait shared by all GObjects
#[glib::derived_properties]
impl ObjectImpl for CollectionObject {}
我们还添加了结构体收藏夹数据(CollectionData)
,以帮助实现序列化和反序列化。
文件名: listings/todo/8/collection_object/mod.rs
mod imp;
use adw::prelude::*;
use adw::subclass::prelude::*;
use glib::Object;
use gtk::{gio, glib};
use serde::{Deserialize, Serialize};
use crate::task_object::{TaskData, TaskObject};
glib::wrapper! {
pub struct CollectionObject(ObjectSubclass<imp::CollectionObject>);
}
impl CollectionObject {
pub fn new(title: &str, tasks: gio::ListStore) -> Self {
Object::builder()
.property("title", title)
.property("tasks", tasks)
.build()
}
pub fn to_collection_data(&self) -> CollectionData {
let title = self.imp().title.borrow().clone();
let tasks_data = self
.tasks()
.iter::<TaskObject>()
.filter_map(Result::ok)
.map(|task_object| task_object.task_data())
.collect();
CollectionData { title, tasks_data }
}
pub fn from_collection_data(collection_data: CollectionData) -> Self {
let title = collection_data.title;
let tasks_to_extend: Vec<TaskObject> = collection_data
.tasks_data
.into_iter()
.map(TaskObject::from_task_data)
.collect();
let tasks = gio::ListStore::new::<TaskObject>();
tasks.extend_from_slice(&tasks_to_extend);
Self::new(&title, tasks)
}
}
#[derive(Default, Clone, Serialize, Deserialize)]
pub struct CollectionData {
pub title: String,
pub tasks_data: Vec<TaskData>,
}
最后,我们为 CollectionObject
添加方法,以便——
- 用
new
创建它, - 用
tasks
轻松访问ListStore
- 使用
to_collection_data
和from_collection_data
任意转换CollectionData
文件名: listings/todo/8/collection_object/mod.rs
mod imp;
use adw::prelude::*;
use adw::subclass::prelude::*;
use glib::Object;
use gtk::{gio, glib};
use serde::{Deserialize, Serialize};
use crate::task_object::{TaskData, TaskObject};
glib::wrapper! {
pub struct CollectionObject(ObjectSubclass<imp::CollectionObject>);
}
impl CollectionObject {
pub fn new(title: &str, tasks: gio::ListStore) -> Self {
Object::builder()
.property("title", title)
.property("tasks", tasks)
.build()
}
pub fn to_collection_data(&self) -> CollectionData {
let title = self.imp().title.borrow().clone();
let tasks_data = self
.tasks()
.iter::<TaskObject>()
.filter_map(Result::ok)
.map(|task_object| task_object.task_data())
.collect();
CollectionData { title, tasks_data }
}
pub fn from_collection_data(collection_data: CollectionData) -> Self {
let title = collection_data.title;
let tasks_to_extend: Vec<TaskObject> = collection_data
.tasks_data
.into_iter()
.map(TaskObject::from_task_data)
.collect();
let tasks = gio::ListStore::new::<TaskObject>();
tasks.extend_from_slice(&tasks_to_extend);
Self::new(&title, tasks)
}
}
#[derive(Default, Clone, Serialize, Deserialize)]
pub struct CollectionData {
pub title: String,
pub tasks_data: Vec<TaskData>,
}
Window
为了连接新的逻辑,我们必须为 imp::Window
添加更多状态。 我们可以通过 template_child
宏访问额外的控件。 此外,我们还引用了 collections
列表存储、current_collection
和 current_filter_model
. 我们还存储了 tasks_changed_handler_id
. 它的作用将在后面的代码片段中说明。
文件名: listings/todo/8/window/imp.rs
use std::cell::RefCell;
use std::fs::File;
use adw::subclass::prelude::*;
use adw::{prelude::*, NavigationSplitView};
use gio::Settings;
use glib::subclass::InitializingObject;
use gtk::glib::SignalHandlerId;
use gtk::{gio, glib, CompositeTemplate, Entry, FilterListModel, ListBox, Stack};
use std::cell::OnceCell;
use crate::collection_object::{CollectionData, CollectionObject};
use crate::utils::data_path;
// Object holding the state
#[derive(CompositeTemplate, Default)]
#[template(resource = "/org/gtk_rs/Todo8/window.ui")]
pub struct Window {
pub settings: OnceCell<Settings>,
#[template_child]
pub entry: TemplateChild<Entry>,
#[template_child]
pub tasks_list: TemplateChild<ListBox>,
// 👇 all members below are new
#[template_child]
pub collections_list: TemplateChild<ListBox>,
#[template_child]
pub split_view: TemplateChild<NavigationSplitView>,
#[template_child]
pub stack: TemplateChild<Stack>,
pub collections: OnceCell<gio::ListStore>,
pub current_collection: RefCell<Option<CollectionObject>>,
pub current_filter_model: RefCell<Option<FilterListModel>>,
pub tasks_changed_handler_id: RefCell<Option<SignalHandlerId>>,
}
// 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 = adw::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();
});
// Create async action to create new collection and add to action group "win"
klass.install_action_async(
"win.new-collection",
None,
|window, _, _| async move {
window.new_collection().await;
},
);
}
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_collections();
obj.restore_data();
obj.setup_callbacks();
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<CollectionData> = self
.obj()
.collections()
.iter::<CollectionObject>()
.filter_map(|collection_object| collection_object.ok())
.map(|collection_object| collection_object.to_collection_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 {}
// Trait shared by all adwaita application windows
impl AdwApplicationWindowImpl for Window {}
此外,我们还添加了几个 Helper 方法,这些方法稍后会派上用场。
文件名: listings/todo/8/window/mod.rs
mod imp;
use std::fs::File;
use adw::prelude::*;
use adw::subclass::prelude::*;
use adw::{ActionRow, AlertDialog, ResponseAppearance};
use gio::Settings;
use glib::{clone, Object};
use gtk::{
gio, glib, pango, Align, CheckButton, CustomFilter, Entry, FilterListModel, Label,
ListBoxRow, NoSelection,
};
use crate::collection_object::{CollectionData, CollectionObject};
use crate::task_object::TaskObject;
use crate::utils::data_path;
use crate::APP_ID;
glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>)
@extends adw::ApplicationWindow, 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: &adw::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 {
self.current_collection().tasks()
}
fn current_collection(&self) -> CollectionObject {
self.imp()
.current_collection
.borrow()
.clone()
.expect("`current_collection` should be set in `set_current_collections`.")
}
fn collections(&self) -> gio::ListStore {
self.imp()
.collections
.get()
.expect("`collections` should be set in `setup_collections`.")
.clone()
}
fn set_filter(&self) {
self.imp()
.current_filter_model
.borrow()
.clone()
.expect("`current_filter_model` should be set in `set_current_collection`.")
.set_filter(self.filter().as_ref());
}
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_collections(&self) {
let collections = gio::ListStore::new::<CollectionObject>();
self.imp()
.collections
.set(collections.clone())
.expect("Could not set collections");
self.imp().collections_list.bind_model(
Some(&collections),
clone!(
#[weak(rename_to = window)]
self,
#[upgrade_or_panic]
move |obj| {
let collection_object = obj
.downcast_ref()
.expect("The object should be of type `CollectionObject`.");
let row = window.create_collection_row(collection_object);
row.upcast()
}
),
)
}
fn restore_data(&self) {
if let Ok(file) = File::open(data_path()) {
// Deserialize data from file to vector
let backup_data: Vec<CollectionData> = serde_json::from_reader(file)
.expect(
"It should be possible to read `backup_data` from the json file.",
);
// Convert `Vec<CollectionData>` to `Vec<CollectionObject>`
let collections: Vec<CollectionObject> = backup_data
.into_iter()
.map(CollectionObject::from_collection_data)
.collect();
// Insert restored objects into model
self.collections().extend_from_slice(&collections);
// Set first collection as current
if let Some(first_collection) = collections.first() {
self.set_current_collection(first_collection.clone());
}
}
}
fn create_collection_row(
&self,
collection_object: &CollectionObject,
) -> ListBoxRow {
let label = Label::builder()
.ellipsize(pango::EllipsizeMode::End)
.xalign(0.0)
.build();
collection_object
.bind_property("title", &label, "label")
.sync_create()
.build();
ListBoxRow::builder().child(&label).build()
}
fn set_current_collection(&self, collection: CollectionObject) {
// Wrap model with filter and selection and pass it to the list box
let tasks = collection.tasks();
let filter_model = FilterListModel::new(Some(tasks.clone()), self.filter());
let selection_model = NoSelection::new(Some(filter_model.clone()));
self.imp().tasks_list.bind_model(
Some(&selection_model),
clone!(
#[weak(rename_to = window)]
self,
#[upgrade_or_panic]
move |obj| {
let task_object = obj
.downcast_ref()
.expect("The object should be of type `TaskObject`.");
let row = window.create_task_row(task_object);
row.upcast()
}
),
);
// Store filter model
self.imp().current_filter_model.replace(Some(filter_model));
// If present, disconnect old `tasks_changed` handler
if let Some(handler_id) = self.imp().tasks_changed_handler_id.take() {
self.tasks().disconnect(handler_id);
}
// Assure that the task list is only visible when it is supposed to
self.set_task_list_visible(&tasks);
let tasks_changed_handler_id = tasks.connect_items_changed(clone!(
#[weak(rename_to = window)]
self,
move |tasks, _, _, _| {
window.set_task_list_visible(tasks);
}
));
self.imp()
.tasks_changed_handler_id
.replace(Some(tasks_changed_handler_id));
// Set current tasks
self.imp().current_collection.replace(Some(collection));
self.select_collection_row();
}
fn set_task_list_visible(&self, tasks: &gio::ListStore) {
self.imp().tasks_list.set_visible(tasks.n_items() > 0);
}
fn select_collection_row(&self) {
if let Some(index) = self.collections().find(&self.current_collection()) {
let row = self.imp().collections_list.row_at_index(index as i32);
self.imp().collections_list.select_row(row.as_ref());
}
}
fn create_task_row(&self, task_object: &TaskObject) -> ActionRow {
// Create check button
let check_button = CheckButton::builder()
.valign(Align::Center)
.can_focus(false)
.build();
// Create row
let row = ActionRow::builder()
.activatable_widget(&check_button)
.build();
row.add_prefix(&check_button);
// Bind properties
task_object
.bind_property("completed", &check_button, "active")
.bidirectional()
.sync_create()
.build();
task_object
.bind_property("content", &row, "title")
.sync_create()
.build();
// Return row
row
}
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();
}
));
// Filter model whenever the value of the key "filter" changes
self.settings().connect_changed(
Some("filter"),
clone!(
#[weak(rename_to = window)]
self,
move |_, _| {
window.set_filter();
}
),
);
// Setup callback when items of collections change
self.set_stack();
self.collections().connect_items_changed(clone!(
#[weak(rename_to = window)]
self,
move |_, _, _, _| {
window.set_stack();
}
));
// Setup callback for activating a row of collections list
self.imp().collections_list.connect_row_activated(clone!(
#[weak(rename_to = window)]
self,
move |_, row| {
let index = row.index();
let selected_collection = window
.collections()
.item(index as u32)
.expect("There needs to be an object at this position.")
.downcast::<CollectionObject>()
.expect("The object needs to be a `CollectionObject`.");
window.set_current_collection(selected_collection);
window.imp().split_view.set_show_content(true);
}
));
}
fn set_stack(&self) {
if self.collections().n_items() > 0 {
self.imp().stack.set_visible_child_name("main");
} else {
self.imp().stack.set_visible_child_name("placeholder");
}
}
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_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;
}
}
}
async fn new_collection(&self) {
// Create entry
let entry = Entry::builder()
.placeholder_text("Name")
.activates_default(true)
.build();
let cancel_response = "cancel";
let create_response = "create";
// Create new dialog
let dialog = AlertDialog::builder()
.heading("New Collection")
.close_response(cancel_response)
.default_response(create_response)
.extra_child(&entry)
.build();
dialog
.add_responses(&[(cancel_response, "Cancel"), (create_response, "Create")]);
// Make the dialog button insensitive initially
dialog.set_response_enabled(create_response, false);
dialog.set_response_appearance(create_response, ResponseAppearance::Suggested);
// Set entry's css class to "error", when there is no text in it
entry.connect_changed(clone!(
#[weak]
dialog,
move |entry| {
let text = entry.text();
let empty = text.is_empty();
dialog.set_response_enabled(create_response, !empty);
if empty {
entry.add_css_class("error");
} else {
entry.remove_css_class("error");
}
}
));
let response = dialog.choose_future(self).await;
// Return if the user chose `cancel_response`
if response == cancel_response {
return;
}
// Create a new list store
let tasks = gio::ListStore::new::<TaskObject>();
// Create a new collection object from the title the user provided
let title = entry.text().to_string();
let collection = CollectionObject::new(&title, tasks);
// Add new collection object and set current tasks
self.collections().append(&collection);
self.set_current_collection(collection);
// Show the content
self.imp().split_view.set_show_content(true);
}
}
一如既往,我们希望在关闭窗口时保存数据。 由于大部分实现都在 CollectionObject::to_collection_data
方法中,因此 close_request
的实现并没有太大变化。
文件名: listings/todo/8/window/imp.rs
use std::cell::RefCell;
use std::fs::File;
use adw::subclass::prelude::*;
use adw::{prelude::*, NavigationSplitView};
use gio::Settings;
use glib::subclass::InitializingObject;
use gtk::glib::SignalHandlerId;
use gtk::{gio, glib, CompositeTemplate, Entry, FilterListModel, ListBox, Stack};
use std::cell::OnceCell;
use crate::collection_object::{CollectionData, CollectionObject};
use crate::utils::data_path;
// Object holding the state
#[derive(CompositeTemplate, Default)]
#[template(resource = "/org/gtk_rs/Todo8/window.ui")]
pub struct Window {
pub settings: OnceCell<Settings>,
#[template_child]
pub entry: TemplateChild<Entry>,
#[template_child]
pub tasks_list: TemplateChild<ListBox>,
// 👇 all members below are new
#[template_child]
pub collections_list: TemplateChild<ListBox>,
#[template_child]
pub split_view: TemplateChild<NavigationSplitView>,
#[template_child]
pub stack: TemplateChild<Stack>,
pub collections: OnceCell<gio::ListStore>,
pub current_collection: RefCell<Option<CollectionObject>>,
pub current_filter_model: RefCell<Option<FilterListModel>>,
pub tasks_changed_handler_id: RefCell<Option<SignalHandlerId>>,
}
// 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 = adw::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();
});
// Create async action to create new collection and add to action group "win"
klass.install_action_async(
"win.new-collection",
None,
|window, _, _| async move {
window.new_collection().await;
},
);
}
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_collections();
obj.restore_data();
obj.setup_callbacks();
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<CollectionData> = self
.obj()
.collections()
.iter::<CollectionObject>()
.filter_map(|collection_object| collection_object.ok())
.map(|collection_object| collection_object.to_collection_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 {}
// Trait shared by all adwaita application windows
impl AdwApplicationWindowImpl for Window {}
constructed
也基本保持不变。 我们现在不再使用 setup_tasks
,而是使用 setup_collections
.
文件名: listings/todo/8/window/imp.rs
use std::cell::RefCell;
use std::fs::File;
use adw::subclass::prelude::*;
use adw::{prelude::*, NavigationSplitView};
use gio::Settings;
use glib::subclass::InitializingObject;
use gtk::glib::SignalHandlerId;
use gtk::{gio, glib, CompositeTemplate, Entry, FilterListModel, ListBox, Stack};
use std::cell::OnceCell;
use crate::collection_object::{CollectionData, CollectionObject};
use crate::utils::data_path;
// Object holding the state
#[derive(CompositeTemplate, Default)]
#[template(resource = "/org/gtk_rs/Todo8/window.ui")]
pub struct Window {
pub settings: OnceCell<Settings>,
#[template_child]
pub entry: TemplateChild<Entry>,
#[template_child]
pub tasks_list: TemplateChild<ListBox>,
// 👇 all members below are new
#[template_child]
pub collections_list: TemplateChild<ListBox>,
#[template_child]
pub split_view: TemplateChild<NavigationSplitView>,
#[template_child]
pub stack: TemplateChild<Stack>,
pub collections: OnceCell<gio::ListStore>,
pub current_collection: RefCell<Option<CollectionObject>>,
pub current_filter_model: RefCell<Option<FilterListModel>>,
pub tasks_changed_handler_id: RefCell<Option<SignalHandlerId>>,
}
// 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 = adw::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();
});
// Create async action to create new collection and add to action group "win"
klass.install_action_async(
"win.new-collection",
None,
|window, _, _| async move {
window.new_collection().await;
},
);
}
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_collections();
obj.restore_data();
obj.setup_callbacks();
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<CollectionData> = self
.obj()
.collections()
.iter::<CollectionObject>()
.filter_map(|collection_object| collection_object.ok())
.map(|collection_object| collection_object.to_collection_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 {}
// Trait shared by all adwaita application windows
impl AdwApplicationWindowImpl for Window {}
setup_collections
设置 collections
列表存储,并确保模型中的更改会反映在 collections_list
中。 为此,它使用 create_collection_row
方法。
文件名: listings/todo/8/window/mod.rs
mod imp;
use std::fs::File;
use adw::prelude::*;
use adw::subclass::prelude::*;
use adw::{ActionRow, AlertDialog, ResponseAppearance};
use gio::Settings;
use glib::{clone, Object};
use gtk::{
gio, glib, pango, Align, CheckButton, CustomFilter, Entry, FilterListModel, Label,
ListBoxRow, NoSelection,
};
use crate::collection_object::{CollectionData, CollectionObject};
use crate::task_object::TaskObject;
use crate::utils::data_path;
use crate::APP_ID;
glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>)
@extends adw::ApplicationWindow, 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: &adw::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 {
self.current_collection().tasks()
}
fn current_collection(&self) -> CollectionObject {
self.imp()
.current_collection
.borrow()
.clone()
.expect("`current_collection` should be set in `set_current_collections`.")
}
fn collections(&self) -> gio::ListStore {
self.imp()
.collections
.get()
.expect("`collections` should be set in `setup_collections`.")
.clone()
}
fn set_filter(&self) {
self.imp()
.current_filter_model
.borrow()
.clone()
.expect("`current_filter_model` should be set in `set_current_collection`.")
.set_filter(self.filter().as_ref());
}
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_collections(&self) {
let collections = gio::ListStore::new::<CollectionObject>();
self.imp()
.collections
.set(collections.clone())
.expect("Could not set collections");
self.imp().collections_list.bind_model(
Some(&collections),
clone!(
#[weak(rename_to = window)]
self,
#[upgrade_or_panic]
move |obj| {
let collection_object = obj
.downcast_ref()
.expect("The object should be of type `CollectionObject`.");
let row = window.create_collection_row(collection_object);
row.upcast()
}
),
)
}
fn restore_data(&self) {
if let Ok(file) = File::open(data_path()) {
// Deserialize data from file to vector
let backup_data: Vec<CollectionData> = serde_json::from_reader(file)
.expect(
"It should be possible to read `backup_data` from the json file.",
);
// Convert `Vec<CollectionData>` to `Vec<CollectionObject>`
let collections: Vec<CollectionObject> = backup_data
.into_iter()
.map(CollectionObject::from_collection_data)
.collect();
// Insert restored objects into model
self.collections().extend_from_slice(&collections);
// Set first collection as current
if let Some(first_collection) = collections.first() {
self.set_current_collection(first_collection.clone());
}
}
}
fn create_collection_row(
&self,
collection_object: &CollectionObject,
) -> ListBoxRow {
let label = Label::builder()
.ellipsize(pango::EllipsizeMode::End)
.xalign(0.0)
.build();
collection_object
.bind_property("title", &label, "label")
.sync_create()
.build();
ListBoxRow::builder().child(&label).build()
}
fn set_current_collection(&self, collection: CollectionObject) {
// Wrap model with filter and selection and pass it to the list box
let tasks = collection.tasks();
let filter_model = FilterListModel::new(Some(tasks.clone()), self.filter());
let selection_model = NoSelection::new(Some(filter_model.clone()));
self.imp().tasks_list.bind_model(
Some(&selection_model),
clone!(
#[weak(rename_to = window)]
self,
#[upgrade_or_panic]
move |obj| {
let task_object = obj
.downcast_ref()
.expect("The object should be of type `TaskObject`.");
let row = window.create_task_row(task_object);
row.upcast()
}
),
);
// Store filter model
self.imp().current_filter_model.replace(Some(filter_model));
// If present, disconnect old `tasks_changed` handler
if let Some(handler_id) = self.imp().tasks_changed_handler_id.take() {
self.tasks().disconnect(handler_id);
}
// Assure that the task list is only visible when it is supposed to
self.set_task_list_visible(&tasks);
let tasks_changed_handler_id = tasks.connect_items_changed(clone!(
#[weak(rename_to = window)]
self,
move |tasks, _, _, _| {
window.set_task_list_visible(tasks);
}
));
self.imp()
.tasks_changed_handler_id
.replace(Some(tasks_changed_handler_id));
// Set current tasks
self.imp().current_collection.replace(Some(collection));
self.select_collection_row();
}
fn set_task_list_visible(&self, tasks: &gio::ListStore) {
self.imp().tasks_list.set_visible(tasks.n_items() > 0);
}
fn select_collection_row(&self) {
if let Some(index) = self.collections().find(&self.current_collection()) {
let row = self.imp().collections_list.row_at_index(index as i32);
self.imp().collections_list.select_row(row.as_ref());
}
}
fn create_task_row(&self, task_object: &TaskObject) -> ActionRow {
// Create check button
let check_button = CheckButton::builder()
.valign(Align::Center)
.can_focus(false)
.build();
// Create row
let row = ActionRow::builder()
.activatable_widget(&check_button)
.build();
row.add_prefix(&check_button);
// Bind properties
task_object
.bind_property("completed", &check_button, "active")
.bidirectional()
.sync_create()
.build();
task_object
.bind_property("content", &row, "title")
.sync_create()
.build();
// Return row
row
}
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();
}
));
// Filter model whenever the value of the key "filter" changes
self.settings().connect_changed(
Some("filter"),
clone!(
#[weak(rename_to = window)]
self,
move |_, _| {
window.set_filter();
}
),
);
// Setup callback when items of collections change
self.set_stack();
self.collections().connect_items_changed(clone!(
#[weak(rename_to = window)]
self,
move |_, _, _, _| {
window.set_stack();
}
));
// Setup callback for activating a row of collections list
self.imp().collections_list.connect_row_activated(clone!(
#[weak(rename_to = window)]
self,
move |_, row| {
let index = row.index();
let selected_collection = window
.collections()
.item(index as u32)
.expect("There needs to be an object at this position.")
.downcast::<CollectionObject>()
.expect("The object needs to be a `CollectionObject`.");
window.set_current_collection(selected_collection);
window.imp().split_view.set_show_content(true);
}
));
}
fn set_stack(&self) {
if self.collections().n_items() > 0 {
self.imp().stack.set_visible_child_name("main");
} else {
self.imp().stack.set_visible_child_name("placeholder");
}
}
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_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;
}
}
}
async fn new_collection(&self) {
// Create entry
let entry = Entry::builder()
.placeholder_text("Name")
.activates_default(true)
.build();
let cancel_response = "cancel";
let create_response = "create";
// Create new dialog
let dialog = AlertDialog::builder()
.heading("New Collection")
.close_response(cancel_response)
.default_response(create_response)
.extra_child(&entry)
.build();
dialog
.add_responses(&[(cancel_response, "Cancel"), (create_response, "Create")]);
// Make the dialog button insensitive initially
dialog.set_response_enabled(create_response, false);
dialog.set_response_appearance(create_response, ResponseAppearance::Suggested);
// Set entry's css class to "error", when there is no text in it
entry.connect_changed(clone!(
#[weak]
dialog,
move |entry| {
let text = entry.text();
let empty = text.is_empty();
dialog.set_response_enabled(create_response, !empty);
if empty {
entry.add_css_class("error");
} else {
entry.remove_css_class("error");
}
}
));
let response = dialog.choose_future(self).await;
// Return if the user chose `cancel_response`
if response == cancel_response {
return;
}
// Create a new list store
let tasks = gio::ListStore::new::<TaskObject>();
// Create a new collection object from the title the user provided
let title = entry.text().to_string();
let collection = CollectionObject::new(&title, tasks);
// Add new collection object and set current tasks
self.collections().append(&collection);
self.set_current_collection(collection);
// Show the content
self.imp().split_view.set_show_content(true);
}
}
create_collection_row
获取一个 CollectionObject
并根据其信息创建一个 gtk::ListBoxRow
.
文件名: listings/todo/8/window/mod.rs
mod imp;
use std::fs::File;
use adw::prelude::*;
use adw::subclass::prelude::*;
use adw::{ActionRow, AlertDialog, ResponseAppearance};
use gio::Settings;
use glib::{clone, Object};
use gtk::{
gio, glib, pango, Align, CheckButton, CustomFilter, Entry, FilterListModel, Label,
ListBoxRow, NoSelection,
};
use crate::collection_object::{CollectionData, CollectionObject};
use crate::task_object::TaskObject;
use crate::utils::data_path;
use crate::APP_ID;
glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>)
@extends adw::ApplicationWindow, 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: &adw::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 {
self.current_collection().tasks()
}
fn current_collection(&self) -> CollectionObject {
self.imp()
.current_collection
.borrow()
.clone()
.expect("`current_collection` should be set in `set_current_collections`.")
}
fn collections(&self) -> gio::ListStore {
self.imp()
.collections
.get()
.expect("`collections` should be set in `setup_collections`.")
.clone()
}
fn set_filter(&self) {
self.imp()
.current_filter_model
.borrow()
.clone()
.expect("`current_filter_model` should be set in `set_current_collection`.")
.set_filter(self.filter().as_ref());
}
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_collections(&self) {
let collections = gio::ListStore::new::<CollectionObject>();
self.imp()
.collections
.set(collections.clone())
.expect("Could not set collections");
self.imp().collections_list.bind_model(
Some(&collections),
clone!(
#[weak(rename_to = window)]
self,
#[upgrade_or_panic]
move |obj| {
let collection_object = obj
.downcast_ref()
.expect("The object should be of type `CollectionObject`.");
let row = window.create_collection_row(collection_object);
row.upcast()
}
),
)
}
fn restore_data(&self) {
if let Ok(file) = File::open(data_path()) {
// Deserialize data from file to vector
let backup_data: Vec<CollectionData> = serde_json::from_reader(file)
.expect(
"It should be possible to read `backup_data` from the json file.",
);
// Convert `Vec<CollectionData>` to `Vec<CollectionObject>`
let collections: Vec<CollectionObject> = backup_data
.into_iter()
.map(CollectionObject::from_collection_data)
.collect();
// Insert restored objects into model
self.collections().extend_from_slice(&collections);
// Set first collection as current
if let Some(first_collection) = collections.first() {
self.set_current_collection(first_collection.clone());
}
}
}
fn create_collection_row(
&self,
collection_object: &CollectionObject,
) -> ListBoxRow {
let label = Label::builder()
.ellipsize(pango::EllipsizeMode::End)
.xalign(0.0)
.build();
collection_object
.bind_property("title", &label, "label")
.sync_create()
.build();
ListBoxRow::builder().child(&label).build()
}
fn set_current_collection(&self, collection: CollectionObject) {
// Wrap model with filter and selection and pass it to the list box
let tasks = collection.tasks();
let filter_model = FilterListModel::new(Some(tasks.clone()), self.filter());
let selection_model = NoSelection::new(Some(filter_model.clone()));
self.imp().tasks_list.bind_model(
Some(&selection_model),
clone!(
#[weak(rename_to = window)]
self,
#[upgrade_or_panic]
move |obj| {
let task_object = obj
.downcast_ref()
.expect("The object should be of type `TaskObject`.");
let row = window.create_task_row(task_object);
row.upcast()
}
),
);
// Store filter model
self.imp().current_filter_model.replace(Some(filter_model));
// If present, disconnect old `tasks_changed` handler
if let Some(handler_id) = self.imp().tasks_changed_handler_id.take() {
self.tasks().disconnect(handler_id);
}
// Assure that the task list is only visible when it is supposed to
self.set_task_list_visible(&tasks);
let tasks_changed_handler_id = tasks.connect_items_changed(clone!(
#[weak(rename_to = window)]
self,
move |tasks, _, _, _| {
window.set_task_list_visible(tasks);
}
));
self.imp()
.tasks_changed_handler_id
.replace(Some(tasks_changed_handler_id));
// Set current tasks
self.imp().current_collection.replace(Some(collection));
self.select_collection_row();
}
fn set_task_list_visible(&self, tasks: &gio::ListStore) {
self.imp().tasks_list.set_visible(tasks.n_items() > 0);
}
fn select_collection_row(&self) {
if let Some(index) = self.collections().find(&self.current_collection()) {
let row = self.imp().collections_list.row_at_index(index as i32);
self.imp().collections_list.select_row(row.as_ref());
}
}
fn create_task_row(&self, task_object: &TaskObject) -> ActionRow {
// Create check button
let check_button = CheckButton::builder()
.valign(Align::Center)
.can_focus(false)
.build();
// Create row
let row = ActionRow::builder()
.activatable_widget(&check_button)
.build();
row.add_prefix(&check_button);
// Bind properties
task_object
.bind_property("completed", &check_button, "active")
.bidirectional()
.sync_create()
.build();
task_object
.bind_property("content", &row, "title")
.sync_create()
.build();
// Return row
row
}
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();
}
));
// Filter model whenever the value of the key "filter" changes
self.settings().connect_changed(
Some("filter"),
clone!(
#[weak(rename_to = window)]
self,
move |_, _| {
window.set_filter();
}
),
);
// Setup callback when items of collections change
self.set_stack();
self.collections().connect_items_changed(clone!(
#[weak(rename_to = window)]
self,
move |_, _, _, _| {
window.set_stack();
}
));
// Setup callback for activating a row of collections list
self.imp().collections_list.connect_row_activated(clone!(
#[weak(rename_to = window)]
self,
move |_, row| {
let index = row.index();
let selected_collection = window
.collections()
.item(index as u32)
.expect("There needs to be an object at this position.")
.downcast::<CollectionObject>()
.expect("The object needs to be a `CollectionObject`.");
window.set_current_collection(selected_collection);
window.imp().split_view.set_show_content(true);
}
));
}
fn set_stack(&self) {
if self.collections().n_items() > 0 {
self.imp().stack.set_visible_child_name("main");
} else {
self.imp().stack.set_visible_child_name("placeholder");
}
}
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_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;
}
}
}
async fn new_collection(&self) {
// Create entry
let entry = Entry::builder()
.placeholder_text("Name")
.activates_default(true)
.build();
let cancel_response = "cancel";
let create_response = "create";
// Create new dialog
let dialog = AlertDialog::builder()
.heading("New Collection")
.close_response(cancel_response)
.default_response(create_response)
.extra_child(&entry)
.build();
dialog
.add_responses(&[(cancel_response, "Cancel"), (create_response, "Create")]);
// Make the dialog button insensitive initially
dialog.set_response_enabled(create_response, false);
dialog.set_response_appearance(create_response, ResponseAppearance::Suggested);
// Set entry's css class to "error", when there is no text in it
entry.connect_changed(clone!(
#[weak]
dialog,
move |entry| {
let text = entry.text();
let empty = text.is_empty();
dialog.set_response_enabled(create_response, !empty);
if empty {
entry.add_css_class("error");
} else {
entry.remove_css_class("error");
}
}
));
let response = dialog.choose_future(self).await;
// Return if the user chose `cancel_response`
if response == cancel_response {
return;
}
// Create a new list store
let tasks = gio::ListStore::new::<TaskObject>();
// Create a new collection object from the title the user provided
let title = entry.text().to_string();
let collection = CollectionObject::new(&title, tasks);
// Add new collection object and set current tasks
self.collections().append(&collection);
self.set_current_collection(collection);
// Show the content
self.imp().split_view.set_show_content(true);
}
}
我们还调整了 restore_data
. 同样,重头戏来自于 CollectionObject::from_collection_data
,所以我们不必在这里做太多改动。 由于 collections_list
中的行可以被选择,我们必须在还原数据后选择其中之一。 我们选择第一行,然后让 set_current_collection
方法完成剩下的工作。
文件名: listings/todo/8/window/mod.rs
mod imp;
use std::fs::File;
use adw::prelude::*;
use adw::subclass::prelude::*;
use adw::{ActionRow, AlertDialog, ResponseAppearance};
use gio::Settings;
use glib::{clone, Object};
use gtk::{
gio, glib, pango, Align, CheckButton, CustomFilter, Entry, FilterListModel, Label,
ListBoxRow, NoSelection,
};
use crate::collection_object::{CollectionData, CollectionObject};
use crate::task_object::TaskObject;
use crate::utils::data_path;
use crate::APP_ID;
glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>)
@extends adw::ApplicationWindow, 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: &adw::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 {
self.current_collection().tasks()
}
fn current_collection(&self) -> CollectionObject {
self.imp()
.current_collection
.borrow()
.clone()
.expect("`current_collection` should be set in `set_current_collections`.")
}
fn collections(&self) -> gio::ListStore {
self.imp()
.collections
.get()
.expect("`collections` should be set in `setup_collections`.")
.clone()
}
fn set_filter(&self) {
self.imp()
.current_filter_model
.borrow()
.clone()
.expect("`current_filter_model` should be set in `set_current_collection`.")
.set_filter(self.filter().as_ref());
}
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_collections(&self) {
let collections = gio::ListStore::new::<CollectionObject>();
self.imp()
.collections
.set(collections.clone())
.expect("Could not set collections");
self.imp().collections_list.bind_model(
Some(&collections),
clone!(
#[weak(rename_to = window)]
self,
#[upgrade_or_panic]
move |obj| {
let collection_object = obj
.downcast_ref()
.expect("The object should be of type `CollectionObject`.");
let row = window.create_collection_row(collection_object);
row.upcast()
}
),
)
}
fn restore_data(&self) {
if let Ok(file) = File::open(data_path()) {
// Deserialize data from file to vector
let backup_data: Vec<CollectionData> = serde_json::from_reader(file)
.expect(
"It should be possible to read `backup_data` from the json file.",
);
// Convert `Vec<CollectionData>` to `Vec<CollectionObject>`
let collections: Vec<CollectionObject> = backup_data
.into_iter()
.map(CollectionObject::from_collection_data)
.collect();
// Insert restored objects into model
self.collections().extend_from_slice(&collections);
// Set first collection as current
if let Some(first_collection) = collections.first() {
self.set_current_collection(first_collection.clone());
}
}
}
fn create_collection_row(
&self,
collection_object: &CollectionObject,
) -> ListBoxRow {
let label = Label::builder()
.ellipsize(pango::EllipsizeMode::End)
.xalign(0.0)
.build();
collection_object
.bind_property("title", &label, "label")
.sync_create()
.build();
ListBoxRow::builder().child(&label).build()
}
fn set_current_collection(&self, collection: CollectionObject) {
// Wrap model with filter and selection and pass it to the list box
let tasks = collection.tasks();
let filter_model = FilterListModel::new(Some(tasks.clone()), self.filter());
let selection_model = NoSelection::new(Some(filter_model.clone()));
self.imp().tasks_list.bind_model(
Some(&selection_model),
clone!(
#[weak(rename_to = window)]
self,
#[upgrade_or_panic]
move |obj| {
let task_object = obj
.downcast_ref()
.expect("The object should be of type `TaskObject`.");
let row = window.create_task_row(task_object);
row.upcast()
}
),
);
// Store filter model
self.imp().current_filter_model.replace(Some(filter_model));
// If present, disconnect old `tasks_changed` handler
if let Some(handler_id) = self.imp().tasks_changed_handler_id.take() {
self.tasks().disconnect(handler_id);
}
// Assure that the task list is only visible when it is supposed to
self.set_task_list_visible(&tasks);
let tasks_changed_handler_id = tasks.connect_items_changed(clone!(
#[weak(rename_to = window)]
self,
move |tasks, _, _, _| {
window.set_task_list_visible(tasks);
}
));
self.imp()
.tasks_changed_handler_id
.replace(Some(tasks_changed_handler_id));
// Set current tasks
self.imp().current_collection.replace(Some(collection));
self.select_collection_row();
}
fn set_task_list_visible(&self, tasks: &gio::ListStore) {
self.imp().tasks_list.set_visible(tasks.n_items() > 0);
}
fn select_collection_row(&self) {
if let Some(index) = self.collections().find(&self.current_collection()) {
let row = self.imp().collections_list.row_at_index(index as i32);
self.imp().collections_list.select_row(row.as_ref());
}
}
fn create_task_row(&self, task_object: &TaskObject) -> ActionRow {
// Create check button
let check_button = CheckButton::builder()
.valign(Align::Center)
.can_focus(false)
.build();
// Create row
let row = ActionRow::builder()
.activatable_widget(&check_button)
.build();
row.add_prefix(&check_button);
// Bind properties
task_object
.bind_property("completed", &check_button, "active")
.bidirectional()
.sync_create()
.build();
task_object
.bind_property("content", &row, "title")
.sync_create()
.build();
// Return row
row
}
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();
}
));
// Filter model whenever the value of the key "filter" changes
self.settings().connect_changed(
Some("filter"),
clone!(
#[weak(rename_to = window)]
self,
move |_, _| {
window.set_filter();
}
),
);
// Setup callback when items of collections change
self.set_stack();
self.collections().connect_items_changed(clone!(
#[weak(rename_to = window)]
self,
move |_, _, _, _| {
window.set_stack();
}
));
// Setup callback for activating a row of collections list
self.imp().collections_list.connect_row_activated(clone!(
#[weak(rename_to = window)]
self,
move |_, row| {
let index = row.index();
let selected_collection = window
.collections()
.item(index as u32)
.expect("There needs to be an object at this position.")
.downcast::<CollectionObject>()
.expect("The object needs to be a `CollectionObject`.");
window.set_current_collection(selected_collection);
window.imp().split_view.set_show_content(true);
}
));
}
fn set_stack(&self) {
if self.collections().n_items() > 0 {
self.imp().stack.set_visible_child_name("main");
} else {
self.imp().stack.set_visible_child_name("placeholder");
}
}
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_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;
}
}
}
async fn new_collection(&self) {
// Create entry
let entry = Entry::builder()
.placeholder_text("Name")
.activates_default(true)
.build();
let cancel_response = "cancel";
let create_response = "create";
// Create new dialog
let dialog = AlertDialog::builder()
.heading("New Collection")
.close_response(cancel_response)
.default_response(create_response)
.extra_child(&entry)
.build();
dialog
.add_responses(&[(cancel_response, "Cancel"), (create_response, "Create")]);
// Make the dialog button insensitive initially
dialog.set_response_enabled(create_response, false);
dialog.set_response_appearance(create_response, ResponseAppearance::Suggested);
// Set entry's css class to "error", when there is no text in it
entry.connect_changed(clone!(
#[weak]
dialog,
move |entry| {
let text = entry.text();
let empty = text.is_empty();
dialog.set_response_enabled(create_response, !empty);
if empty {
entry.add_css_class("error");
} else {
entry.remove_css_class("error");
}
}
));
let response = dialog.choose_future(self).await;
// Return if the user chose `cancel_response`
if response == cancel_response {
return;
}
// Create a new list store
let tasks = gio::ListStore::new::<TaskObject>();
// Create a new collection object from the title the user provided
let title = entry.text().to_string();
let collection = CollectionObject::new(&title, tasks);
// Add new collection object and set current tasks
self.collections().append(&collection);
self.set_current_collection(collection);
// Show the content
self.imp().split_view.set_show_content(true);
}
}
set_current_collection
可确保所有访问任务的元素都引用当前收藏夹的任务模型。 我们将 tasks_list
与当前收藏夹绑定,并存储过滤模型。 只要当前收藏夹中没有任务,我们就要隐藏任务列表。 否则,列表框会留下一行难看的字。 不过,我们不想在切换集合时累积信号处理器(signal handler)。 这就是为什么我们要存储 tasks_changed_handler_id
,并在设置新的收藏夹时断开旧的处理器(handler)。 最后,我们选择收藏夹行。
文件名: listings/todo/8/window/mod.rs
mod imp;
use std::fs::File;
use adw::prelude::*;
use adw::subclass::prelude::*;
use adw::{ActionRow, AlertDialog, ResponseAppearance};
use gio::Settings;
use glib::{clone, Object};
use gtk::{
gio, glib, pango, Align, CheckButton, CustomFilter, Entry, FilterListModel, Label,
ListBoxRow, NoSelection,
};
use crate::collection_object::{CollectionData, CollectionObject};
use crate::task_object::TaskObject;
use crate::utils::data_path;
use crate::APP_ID;
glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>)
@extends adw::ApplicationWindow, 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: &adw::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 {
self.current_collection().tasks()
}
fn current_collection(&self) -> CollectionObject {
self.imp()
.current_collection
.borrow()
.clone()
.expect("`current_collection` should be set in `set_current_collections`.")
}
fn collections(&self) -> gio::ListStore {
self.imp()
.collections
.get()
.expect("`collections` should be set in `setup_collections`.")
.clone()
}
fn set_filter(&self) {
self.imp()
.current_filter_model
.borrow()
.clone()
.expect("`current_filter_model` should be set in `set_current_collection`.")
.set_filter(self.filter().as_ref());
}
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_collections(&self) {
let collections = gio::ListStore::new::<CollectionObject>();
self.imp()
.collections
.set(collections.clone())
.expect("Could not set collections");
self.imp().collections_list.bind_model(
Some(&collections),
clone!(
#[weak(rename_to = window)]
self,
#[upgrade_or_panic]
move |obj| {
let collection_object = obj
.downcast_ref()
.expect("The object should be of type `CollectionObject`.");
let row = window.create_collection_row(collection_object);
row.upcast()
}
),
)
}
fn restore_data(&self) {
if let Ok(file) = File::open(data_path()) {
// Deserialize data from file to vector
let backup_data: Vec<CollectionData> = serde_json::from_reader(file)
.expect(
"It should be possible to read `backup_data` from the json file.",
);
// Convert `Vec<CollectionData>` to `Vec<CollectionObject>`
let collections: Vec<CollectionObject> = backup_data
.into_iter()
.map(CollectionObject::from_collection_data)
.collect();
// Insert restored objects into model
self.collections().extend_from_slice(&collections);
// Set first collection as current
if let Some(first_collection) = collections.first() {
self.set_current_collection(first_collection.clone());
}
}
}
fn create_collection_row(
&self,
collection_object: &CollectionObject,
) -> ListBoxRow {
let label = Label::builder()
.ellipsize(pango::EllipsizeMode::End)
.xalign(0.0)
.build();
collection_object
.bind_property("title", &label, "label")
.sync_create()
.build();
ListBoxRow::builder().child(&label).build()
}
fn set_current_collection(&self, collection: CollectionObject) {
// Wrap model with filter and selection and pass it to the list box
let tasks = collection.tasks();
let filter_model = FilterListModel::new(Some(tasks.clone()), self.filter());
let selection_model = NoSelection::new(Some(filter_model.clone()));
self.imp().tasks_list.bind_model(
Some(&selection_model),
clone!(
#[weak(rename_to = window)]
self,
#[upgrade_or_panic]
move |obj| {
let task_object = obj
.downcast_ref()
.expect("The object should be of type `TaskObject`.");
let row = window.create_task_row(task_object);
row.upcast()
}
),
);
// Store filter model
self.imp().current_filter_model.replace(Some(filter_model));
// If present, disconnect old `tasks_changed` handler
if let Some(handler_id) = self.imp().tasks_changed_handler_id.take() {
self.tasks().disconnect(handler_id);
}
// Assure that the task list is only visible when it is supposed to
self.set_task_list_visible(&tasks);
let tasks_changed_handler_id = tasks.connect_items_changed(clone!(
#[weak(rename_to = window)]
self,
move |tasks, _, _, _| {
window.set_task_list_visible(tasks);
}
));
self.imp()
.tasks_changed_handler_id
.replace(Some(tasks_changed_handler_id));
// Set current tasks
self.imp().current_collection.replace(Some(collection));
self.select_collection_row();
}
fn set_task_list_visible(&self, tasks: &gio::ListStore) {
self.imp().tasks_list.set_visible(tasks.n_items() > 0);
}
fn select_collection_row(&self) {
if let Some(index) = self.collections().find(&self.current_collection()) {
let row = self.imp().collections_list.row_at_index(index as i32);
self.imp().collections_list.select_row(row.as_ref());
}
}
fn create_task_row(&self, task_object: &TaskObject) -> ActionRow {
// Create check button
let check_button = CheckButton::builder()
.valign(Align::Center)
.can_focus(false)
.build();
// Create row
let row = ActionRow::builder()
.activatable_widget(&check_button)
.build();
row.add_prefix(&check_button);
// Bind properties
task_object
.bind_property("completed", &check_button, "active")
.bidirectional()
.sync_create()
.build();
task_object
.bind_property("content", &row, "title")
.sync_create()
.build();
// Return row
row
}
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();
}
));
// Filter model whenever the value of the key "filter" changes
self.settings().connect_changed(
Some("filter"),
clone!(
#[weak(rename_to = window)]
self,
move |_, _| {
window.set_filter();
}
),
);
// Setup callback when items of collections change
self.set_stack();
self.collections().connect_items_changed(clone!(
#[weak(rename_to = window)]
self,
move |_, _, _, _| {
window.set_stack();
}
));
// Setup callback for activating a row of collections list
self.imp().collections_list.connect_row_activated(clone!(
#[weak(rename_to = window)]
self,
move |_, row| {
let index = row.index();
let selected_collection = window
.collections()
.item(index as u32)
.expect("There needs to be an object at this position.")
.downcast::<CollectionObject>()
.expect("The object needs to be a `CollectionObject`.");
window.set_current_collection(selected_collection);
window.imp().split_view.set_show_content(true);
}
));
}
fn set_stack(&self) {
if self.collections().n_items() > 0 {
self.imp().stack.set_visible_child_name("main");
} else {
self.imp().stack.set_visible_child_name("placeholder");
}
}
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_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;
}
}
}
async fn new_collection(&self) {
// Create entry
let entry = Entry::builder()
.placeholder_text("Name")
.activates_default(true)
.build();
let cancel_response = "cancel";
let create_response = "create";
// Create new dialog
let dialog = AlertDialog::builder()
.heading("New Collection")
.close_response(cancel_response)
.default_response(create_response)
.extra_child(&entry)
.build();
dialog
.add_responses(&[(cancel_response, "Cancel"), (create_response, "Create")]);
// Make the dialog button insensitive initially
dialog.set_response_enabled(create_response, false);
dialog.set_response_appearance(create_response, ResponseAppearance::Suggested);
// Set entry's css class to "error", when there is no text in it
entry.connect_changed(clone!(
#[weak]
dialog,
move |entry| {
let text = entry.text();
let empty = text.is_empty();
dialog.set_response_enabled(create_response, !empty);
if empty {
entry.add_css_class("error");
} else {
entry.remove_css_class("error");
}
}
));
let response = dialog.choose_future(self).await;
// Return if the user chose `cancel_response`
if response == cancel_response {
return;
}
// Create a new list store
let tasks = gio::ListStore::new::<TaskObject>();
// Create a new collection object from the title the user provided
let title = entry.text().to_string();
let collection = CollectionObject::new(&title, tasks);
// Add new collection object and set current tasks
self.collections().append(&collection);
self.set_current_collection(collection);
// Show the content
self.imp().split_view.set_show_content(true);
}
}
之前,我们使用了 set_task_list_visible
方法。 该方法确保 tasks_list
只有在任务数大于 0 时才可见。
文件名: listings/todo/8/window/mod.rs
mod imp;
use std::fs::File;
use adw::prelude::*;
use adw::subclass::prelude::*;
use adw::{ActionRow, AlertDialog, ResponseAppearance};
use gio::Settings;
use glib::{clone, Object};
use gtk::{
gio, glib, pango, Align, CheckButton, CustomFilter, Entry, FilterListModel, Label,
ListBoxRow, NoSelection,
};
use crate::collection_object::{CollectionData, CollectionObject};
use crate::task_object::TaskObject;
use crate::utils::data_path;
use crate::APP_ID;
glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>)
@extends adw::ApplicationWindow, 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: &adw::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 {
self.current_collection().tasks()
}
fn current_collection(&self) -> CollectionObject {
self.imp()
.current_collection
.borrow()
.clone()
.expect("`current_collection` should be set in `set_current_collections`.")
}
fn collections(&self) -> gio::ListStore {
self.imp()
.collections
.get()
.expect("`collections` should be set in `setup_collections`.")
.clone()
}
fn set_filter(&self) {
self.imp()
.current_filter_model
.borrow()
.clone()
.expect("`current_filter_model` should be set in `set_current_collection`.")
.set_filter(self.filter().as_ref());
}
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_collections(&self) {
let collections = gio::ListStore::new::<CollectionObject>();
self.imp()
.collections
.set(collections.clone())
.expect("Could not set collections");
self.imp().collections_list.bind_model(
Some(&collections),
clone!(
#[weak(rename_to = window)]
self,
#[upgrade_or_panic]
move |obj| {
let collection_object = obj
.downcast_ref()
.expect("The object should be of type `CollectionObject`.");
let row = window.create_collection_row(collection_object);
row.upcast()
}
),
)
}
fn restore_data(&self) {
if let Ok(file) = File::open(data_path()) {
// Deserialize data from file to vector
let backup_data: Vec<CollectionData> = serde_json::from_reader(file)
.expect(
"It should be possible to read `backup_data` from the json file.",
);
// Convert `Vec<CollectionData>` to `Vec<CollectionObject>`
let collections: Vec<CollectionObject> = backup_data
.into_iter()
.map(CollectionObject::from_collection_data)
.collect();
// Insert restored objects into model
self.collections().extend_from_slice(&collections);
// Set first collection as current
if let Some(first_collection) = collections.first() {
self.set_current_collection(first_collection.clone());
}
}
}
fn create_collection_row(
&self,
collection_object: &CollectionObject,
) -> ListBoxRow {
let label = Label::builder()
.ellipsize(pango::EllipsizeMode::End)
.xalign(0.0)
.build();
collection_object
.bind_property("title", &label, "label")
.sync_create()
.build();
ListBoxRow::builder().child(&label).build()
}
fn set_current_collection(&self, collection: CollectionObject) {
// Wrap model with filter and selection and pass it to the list box
let tasks = collection.tasks();
let filter_model = FilterListModel::new(Some(tasks.clone()), self.filter());
let selection_model = NoSelection::new(Some(filter_model.clone()));
self.imp().tasks_list.bind_model(
Some(&selection_model),
clone!(
#[weak(rename_to = window)]
self,
#[upgrade_or_panic]
move |obj| {
let task_object = obj
.downcast_ref()
.expect("The object should be of type `TaskObject`.");
let row = window.create_task_row(task_object);
row.upcast()
}
),
);
// Store filter model
self.imp().current_filter_model.replace(Some(filter_model));
// If present, disconnect old `tasks_changed` handler
if let Some(handler_id) = self.imp().tasks_changed_handler_id.take() {
self.tasks().disconnect(handler_id);
}
// Assure that the task list is only visible when it is supposed to
self.set_task_list_visible(&tasks);
let tasks_changed_handler_id = tasks.connect_items_changed(clone!(
#[weak(rename_to = window)]
self,
move |tasks, _, _, _| {
window.set_task_list_visible(tasks);
}
));
self.imp()
.tasks_changed_handler_id
.replace(Some(tasks_changed_handler_id));
// Set current tasks
self.imp().current_collection.replace(Some(collection));
self.select_collection_row();
}
fn set_task_list_visible(&self, tasks: &gio::ListStore) {
self.imp().tasks_list.set_visible(tasks.n_items() > 0);
}
fn select_collection_row(&self) {
if let Some(index) = self.collections().find(&self.current_collection()) {
let row = self.imp().collections_list.row_at_index(index as i32);
self.imp().collections_list.select_row(row.as_ref());
}
}
fn create_task_row(&self, task_object: &TaskObject) -> ActionRow {
// Create check button
let check_button = CheckButton::builder()
.valign(Align::Center)
.can_focus(false)
.build();
// Create row
let row = ActionRow::builder()
.activatable_widget(&check_button)
.build();
row.add_prefix(&check_button);
// Bind properties
task_object
.bind_property("completed", &check_button, "active")
.bidirectional()
.sync_create()
.build();
task_object
.bind_property("content", &row, "title")
.sync_create()
.build();
// Return row
row
}
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();
}
));
// Filter model whenever the value of the key "filter" changes
self.settings().connect_changed(
Some("filter"),
clone!(
#[weak(rename_to = window)]
self,
move |_, _| {
window.set_filter();
}
),
);
// Setup callback when items of collections change
self.set_stack();
self.collections().connect_items_changed(clone!(
#[weak(rename_to = window)]
self,
move |_, _, _, _| {
window.set_stack();
}
));
// Setup callback for activating a row of collections list
self.imp().collections_list.connect_row_activated(clone!(
#[weak(rename_to = window)]
self,
move |_, row| {
let index = row.index();
let selected_collection = window
.collections()
.item(index as u32)
.expect("There needs to be an object at this position.")
.downcast::<CollectionObject>()
.expect("The object needs to be a `CollectionObject`.");
window.set_current_collection(selected_collection);
window.imp().split_view.set_show_content(true);
}
));
}
fn set_stack(&self) {
if self.collections().n_items() > 0 {
self.imp().stack.set_visible_child_name("main");
} else {
self.imp().stack.set_visible_child_name("placeholder");
}
}
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_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;
}
}
}
async fn new_collection(&self) {
// Create entry
let entry = Entry::builder()
.placeholder_text("Name")
.activates_default(true)
.build();
let cancel_response = "cancel";
let create_response = "create";
// Create new dialog
let dialog = AlertDialog::builder()
.heading("New Collection")
.close_response(cancel_response)
.default_response(create_response)
.extra_child(&entry)
.build();
dialog
.add_responses(&[(cancel_response, "Cancel"), (create_response, "Create")]);
// Make the dialog button insensitive initially
dialog.set_response_enabled(create_response, false);
dialog.set_response_appearance(create_response, ResponseAppearance::Suggested);
// Set entry's css class to "error", when there is no text in it
entry.connect_changed(clone!(
#[weak]
dialog,
move |entry| {
let text = entry.text();
let empty = text.is_empty();
dialog.set_response_enabled(create_response, !empty);
if empty {
entry.add_css_class("error");
} else {
entry.remove_css_class("error");
}
}
));
let response = dialog.choose_future(self).await;
// Return if the user chose `cancel_response`
if response == cancel_response {
return;
}
// Create a new list store
let tasks = gio::ListStore::new::<TaskObject>();
// Create a new collection object from the title the user provided
let title = entry.text().to_string();
let collection = CollectionObject::new(&title, tasks);
// Add new collection object and set current tasks
self.collections().append(&collection);
self.set_current_collection(collection);
// Show the content
self.imp().split_view.set_show_content(true);
}
}
select_collection_row
可确保在 collections_list
中选择当前收藏夹的行。
文件名: listings/todo/8/window/mod.rs
mod imp;
use std::fs::File;
use adw::prelude::*;
use adw::subclass::prelude::*;
use adw::{ActionRow, AlertDialog, ResponseAppearance};
use gio::Settings;
use glib::{clone, Object};
use gtk::{
gio, glib, pango, Align, CheckButton, CustomFilter, Entry, FilterListModel, Label,
ListBoxRow, NoSelection,
};
use crate::collection_object::{CollectionData, CollectionObject};
use crate::task_object::TaskObject;
use crate::utils::data_path;
use crate::APP_ID;
glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>)
@extends adw::ApplicationWindow, 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: &adw::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 {
self.current_collection().tasks()
}
fn current_collection(&self) -> CollectionObject {
self.imp()
.current_collection
.borrow()
.clone()
.expect("`current_collection` should be set in `set_current_collections`.")
}
fn collections(&self) -> gio::ListStore {
self.imp()
.collections
.get()
.expect("`collections` should be set in `setup_collections`.")
.clone()
}
fn set_filter(&self) {
self.imp()
.current_filter_model
.borrow()
.clone()
.expect("`current_filter_model` should be set in `set_current_collection`.")
.set_filter(self.filter().as_ref());
}
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_collections(&self) {
let collections = gio::ListStore::new::<CollectionObject>();
self.imp()
.collections
.set(collections.clone())
.expect("Could not set collections");
self.imp().collections_list.bind_model(
Some(&collections),
clone!(
#[weak(rename_to = window)]
self,
#[upgrade_or_panic]
move |obj| {
let collection_object = obj
.downcast_ref()
.expect("The object should be of type `CollectionObject`.");
let row = window.create_collection_row(collection_object);
row.upcast()
}
),
)
}
fn restore_data(&self) {
if let Ok(file) = File::open(data_path()) {
// Deserialize data from file to vector
let backup_data: Vec<CollectionData> = serde_json::from_reader(file)
.expect(
"It should be possible to read `backup_data` from the json file.",
);
// Convert `Vec<CollectionData>` to `Vec<CollectionObject>`
let collections: Vec<CollectionObject> = backup_data
.into_iter()
.map(CollectionObject::from_collection_data)
.collect();
// Insert restored objects into model
self.collections().extend_from_slice(&collections);
// Set first collection as current
if let Some(first_collection) = collections.first() {
self.set_current_collection(first_collection.clone());
}
}
}
fn create_collection_row(
&self,
collection_object: &CollectionObject,
) -> ListBoxRow {
let label = Label::builder()
.ellipsize(pango::EllipsizeMode::End)
.xalign(0.0)
.build();
collection_object
.bind_property("title", &label, "label")
.sync_create()
.build();
ListBoxRow::builder().child(&label).build()
}
fn set_current_collection(&self, collection: CollectionObject) {
// Wrap model with filter and selection and pass it to the list box
let tasks = collection.tasks();
let filter_model = FilterListModel::new(Some(tasks.clone()), self.filter());
let selection_model = NoSelection::new(Some(filter_model.clone()));
self.imp().tasks_list.bind_model(
Some(&selection_model),
clone!(
#[weak(rename_to = window)]
self,
#[upgrade_or_panic]
move |obj| {
let task_object = obj
.downcast_ref()
.expect("The object should be of type `TaskObject`.");
let row = window.create_task_row(task_object);
row.upcast()
}
),
);
// Store filter model
self.imp().current_filter_model.replace(Some(filter_model));
// If present, disconnect old `tasks_changed` handler
if let Some(handler_id) = self.imp().tasks_changed_handler_id.take() {
self.tasks().disconnect(handler_id);
}
// Assure that the task list is only visible when it is supposed to
self.set_task_list_visible(&tasks);
let tasks_changed_handler_id = tasks.connect_items_changed(clone!(
#[weak(rename_to = window)]
self,
move |tasks, _, _, _| {
window.set_task_list_visible(tasks);
}
));
self.imp()
.tasks_changed_handler_id
.replace(Some(tasks_changed_handler_id));
// Set current tasks
self.imp().current_collection.replace(Some(collection));
self.select_collection_row();
}
fn set_task_list_visible(&self, tasks: &gio::ListStore) {
self.imp().tasks_list.set_visible(tasks.n_items() > 0);
}
fn select_collection_row(&self) {
if let Some(index) = self.collections().find(&self.current_collection()) {
let row = self.imp().collections_list.row_at_index(index as i32);
self.imp().collections_list.select_row(row.as_ref());
}
}
fn create_task_row(&self, task_object: &TaskObject) -> ActionRow {
// Create check button
let check_button = CheckButton::builder()
.valign(Align::Center)
.can_focus(false)
.build();
// Create row
let row = ActionRow::builder()
.activatable_widget(&check_button)
.build();
row.add_prefix(&check_button);
// Bind properties
task_object
.bind_property("completed", &check_button, "active")
.bidirectional()
.sync_create()
.build();
task_object
.bind_property("content", &row, "title")
.sync_create()
.build();
// Return row
row
}
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();
}
));
// Filter model whenever the value of the key "filter" changes
self.settings().connect_changed(
Some("filter"),
clone!(
#[weak(rename_to = window)]
self,
move |_, _| {
window.set_filter();
}
),
);
// Setup callback when items of collections change
self.set_stack();
self.collections().connect_items_changed(clone!(
#[weak(rename_to = window)]
self,
move |_, _, _, _| {
window.set_stack();
}
));
// Setup callback for activating a row of collections list
self.imp().collections_list.connect_row_activated(clone!(
#[weak(rename_to = window)]
self,
move |_, row| {
let index = row.index();
let selected_collection = window
.collections()
.item(index as u32)
.expect("There needs to be an object at this position.")
.downcast::<CollectionObject>()
.expect("The object needs to be a `CollectionObject`.");
window.set_current_collection(selected_collection);
window.imp().split_view.set_show_content(true);
}
));
}
fn set_stack(&self) {
if self.collections().n_items() > 0 {
self.imp().stack.set_visible_child_name("main");
} else {
self.imp().stack.set_visible_child_name("placeholder");
}
}
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_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;
}
}
}
async fn new_collection(&self) {
// Create entry
let entry = Entry::builder()
.placeholder_text("Name")
.activates_default(true)
.build();
let cancel_response = "cancel";
let create_response = "create";
// Create new dialog
let dialog = AlertDialog::builder()
.heading("New Collection")
.close_response(cancel_response)
.default_response(create_response)
.extra_child(&entry)
.build();
dialog
.add_responses(&[(cancel_response, "Cancel"), (create_response, "Create")]);
// Make the dialog button insensitive initially
dialog.set_response_enabled(create_response, false);
dialog.set_response_appearance(create_response, ResponseAppearance::Suggested);
// Set entry's css class to "error", when there is no text in it
entry.connect_changed(clone!(
#[weak]
dialog,
move |entry| {
let text = entry.text();
let empty = text.is_empty();
dialog.set_response_enabled(create_response, !empty);
if empty {
entry.add_css_class("error");
} else {
entry.remove_css_class("error");
}
}
));
let response = dialog.choose_future(self).await;
// Return if the user chose `cancel_response`
if response == cancel_response {
return;
}
// Create a new list store
let tasks = gio::ListStore::new::<TaskObject>();
// Create a new collection object from the title the user provided
let title = entry.text().to_string();
let collection = CollectionObject::new(&title, tasks);
// Add new collection object and set current tasks
self.collections().append(&collection);
self.set_current_collection(collection);
// Show the content
self.imp().split_view.set_show_content(true);
}
}
消息对话
目前还没有添加收藏夹的方法。 让我们来实现这一功能。
上面的屏幕录像演示了所需的行为。 当我们按下带有 +
符号的按钮时,会出现一个对话框。 当输入框为空时,"创建"按钮保持不激活。 一旦我们开始键入,按钮就会激活。 当我们删除所有键入的字母,输入框再次变为空时,"创建"按钮就会变暗,输入框就会变成 "错误" 样式。 点击 "创建" 按钮后,一个新的收藏夹就创建好了,然后我们就可以进入其任务视图。
为了实现这一行为,我们首先要在 class_init
方法中添加一个 "new-collection" 动作。 点击 +
按钮和占位页面中的按钮都将激活该操作。 我们使用install_action_async
. 这是一种为子类控件添加异步操作的便捷方法。
文件名: listings/todo/8/window/imp.rs
use std::cell::RefCell;
use std::fs::File;
use adw::subclass::prelude::*;
use adw::{prelude::*, NavigationSplitView};
use gio::Settings;
use glib::subclass::InitializingObject;
use gtk::glib::SignalHandlerId;
use gtk::{gio, glib, CompositeTemplate, Entry, FilterListModel, ListBox, Stack};
use std::cell::OnceCell;
use crate::collection_object::{CollectionData, CollectionObject};
use crate::utils::data_path;
// Object holding the state
#[derive(CompositeTemplate, Default)]
#[template(resource = "/org/gtk_rs/Todo8/window.ui")]
pub struct Window {
pub settings: OnceCell<Settings>,
#[template_child]
pub entry: TemplateChild<Entry>,
#[template_child]
pub tasks_list: TemplateChild<ListBox>,
// 👇 all members below are new
#[template_child]
pub collections_list: TemplateChild<ListBox>,
#[template_child]
pub split_view: TemplateChild<NavigationSplitView>,
#[template_child]
pub stack: TemplateChild<Stack>,
pub collections: OnceCell<gio::ListStore>,
pub current_collection: RefCell<Option<CollectionObject>>,
pub current_filter_model: RefCell<Option<FilterListModel>>,
pub tasks_changed_handler_id: RefCell<Option<SignalHandlerId>>,
}
// 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 = adw::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();
});
// Create async action to create new collection and add to action group "win"
klass.install_action_async(
"win.new-collection",
None,
|window, _, _| async move {
window.new_collection().await;
},
);
}
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_collections();
obj.restore_data();
obj.setup_callbacks();
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<CollectionData> = self
.obj()
.collections()
.iter::<CollectionObject>()
.filter_map(|collection_object| collection_object.ok())
.map(|collection_object| collection_object.to_collection_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 {}
// Trait shared by all adwaita application windows
impl AdwApplicationWindowImpl for Window {}
一旦 "new-collection" 操作被激活,异步(async) new_collection
方法就会被调用。 在这里,我们创建adw::AlertDialog
, 设置按钮并添加条目。 我们为条目添加了一个回调,以确保内容发生变化时,空内容会将 dialog_button
设置为不激活,并为条目添加一个 "错误" CSS 类。 然后,我们等待(await)
用户按下对话框上的按钮。 如果用户点击 "取消",我们就返回。 但是,如果他们点击 "创建",我们就会创建一个新的收藏夹,并将其设置为当前收藏夹。 然后,我们在页面上向前导航,导航到任务视图。
文件名: listings/todo/8/window/mod.rs
mod imp;
use std::fs::File;
use adw::prelude::*;
use adw::subclass::prelude::*;
use adw::{ActionRow, AlertDialog, ResponseAppearance};
use gio::Settings;
use glib::{clone, Object};
use gtk::{
gio, glib, pango, Align, CheckButton, CustomFilter, Entry, FilterListModel, Label,
ListBoxRow, NoSelection,
};
use crate::collection_object::{CollectionData, CollectionObject};
use crate::task_object::TaskObject;
use crate::utils::data_path;
use crate::APP_ID;
glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>)
@extends adw::ApplicationWindow, 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: &adw::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 {
self.current_collection().tasks()
}
fn current_collection(&self) -> CollectionObject {
self.imp()
.current_collection
.borrow()
.clone()
.expect("`current_collection` should be set in `set_current_collections`.")
}
fn collections(&self) -> gio::ListStore {
self.imp()
.collections
.get()
.expect("`collections` should be set in `setup_collections`.")
.clone()
}
fn set_filter(&self) {
self.imp()
.current_filter_model
.borrow()
.clone()
.expect("`current_filter_model` should be set in `set_current_collection`.")
.set_filter(self.filter().as_ref());
}
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_collections(&self) {
let collections = gio::ListStore::new::<CollectionObject>();
self.imp()
.collections
.set(collections.clone())
.expect("Could not set collections");
self.imp().collections_list.bind_model(
Some(&collections),
clone!(
#[weak(rename_to = window)]
self,
#[upgrade_or_panic]
move |obj| {
let collection_object = obj
.downcast_ref()
.expect("The object should be of type `CollectionObject`.");
let row = window.create_collection_row(collection_object);
row.upcast()
}
),
)
}
fn restore_data(&self) {
if let Ok(file) = File::open(data_path()) {
// Deserialize data from file to vector
let backup_data: Vec<CollectionData> = serde_json::from_reader(file)
.expect(
"It should be possible to read `backup_data` from the json file.",
);
// Convert `Vec<CollectionData>` to `Vec<CollectionObject>`
let collections: Vec<CollectionObject> = backup_data
.into_iter()
.map(CollectionObject::from_collection_data)
.collect();
// Insert restored objects into model
self.collections().extend_from_slice(&collections);
// Set first collection as current
if let Some(first_collection) = collections.first() {
self.set_current_collection(first_collection.clone());
}
}
}
fn create_collection_row(
&self,
collection_object: &CollectionObject,
) -> ListBoxRow {
let label = Label::builder()
.ellipsize(pango::EllipsizeMode::End)
.xalign(0.0)
.build();
collection_object
.bind_property("title", &label, "label")
.sync_create()
.build();
ListBoxRow::builder().child(&label).build()
}
fn set_current_collection(&self, collection: CollectionObject) {
// Wrap model with filter and selection and pass it to the list box
let tasks = collection.tasks();
let filter_model = FilterListModel::new(Some(tasks.clone()), self.filter());
let selection_model = NoSelection::new(Some(filter_model.clone()));
self.imp().tasks_list.bind_model(
Some(&selection_model),
clone!(
#[weak(rename_to = window)]
self,
#[upgrade_or_panic]
move |obj| {
let task_object = obj
.downcast_ref()
.expect("The object should be of type `TaskObject`.");
let row = window.create_task_row(task_object);
row.upcast()
}
),
);
// Store filter model
self.imp().current_filter_model.replace(Some(filter_model));
// If present, disconnect old `tasks_changed` handler
if let Some(handler_id) = self.imp().tasks_changed_handler_id.take() {
self.tasks().disconnect(handler_id);
}
// Assure that the task list is only visible when it is supposed to
self.set_task_list_visible(&tasks);
let tasks_changed_handler_id = tasks.connect_items_changed(clone!(
#[weak(rename_to = window)]
self,
move |tasks, _, _, _| {
window.set_task_list_visible(tasks);
}
));
self.imp()
.tasks_changed_handler_id
.replace(Some(tasks_changed_handler_id));
// Set current tasks
self.imp().current_collection.replace(Some(collection));
self.select_collection_row();
}
fn set_task_list_visible(&self, tasks: &gio::ListStore) {
self.imp().tasks_list.set_visible(tasks.n_items() > 0);
}
fn select_collection_row(&self) {
if let Some(index) = self.collections().find(&self.current_collection()) {
let row = self.imp().collections_list.row_at_index(index as i32);
self.imp().collections_list.select_row(row.as_ref());
}
}
fn create_task_row(&self, task_object: &TaskObject) -> ActionRow {
// Create check button
let check_button = CheckButton::builder()
.valign(Align::Center)
.can_focus(false)
.build();
// Create row
let row = ActionRow::builder()
.activatable_widget(&check_button)
.build();
row.add_prefix(&check_button);
// Bind properties
task_object
.bind_property("completed", &check_button, "active")
.bidirectional()
.sync_create()
.build();
task_object
.bind_property("content", &row, "title")
.sync_create()
.build();
// Return row
row
}
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();
}
));
// Filter model whenever the value of the key "filter" changes
self.settings().connect_changed(
Some("filter"),
clone!(
#[weak(rename_to = window)]
self,
move |_, _| {
window.set_filter();
}
),
);
// Setup callback when items of collections change
self.set_stack();
self.collections().connect_items_changed(clone!(
#[weak(rename_to = window)]
self,
move |_, _, _, _| {
window.set_stack();
}
));
// Setup callback for activating a row of collections list
self.imp().collections_list.connect_row_activated(clone!(
#[weak(rename_to = window)]
self,
move |_, row| {
let index = row.index();
let selected_collection = window
.collections()
.item(index as u32)
.expect("There needs to be an object at this position.")
.downcast::<CollectionObject>()
.expect("The object needs to be a `CollectionObject`.");
window.set_current_collection(selected_collection);
window.imp().split_view.set_show_content(true);
}
));
}
fn set_stack(&self) {
if self.collections().n_items() > 0 {
self.imp().stack.set_visible_child_name("main");
} else {
self.imp().stack.set_visible_child_name("placeholder");
}
}
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_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;
}
}
}
async fn new_collection(&self) {
// Create entry
let entry = Entry::builder()
.placeholder_text("Name")
.activates_default(true)
.build();
let cancel_response = "cancel";
let create_response = "create";
// Create new dialog
let dialog = AlertDialog::builder()
.heading("New Collection")
.close_response(cancel_response)
.default_response(create_response)
.extra_child(&entry)
.build();
dialog
.add_responses(&[(cancel_response, "Cancel"), (create_response, "Create")]);
// Make the dialog button insensitive initially
dialog.set_response_enabled(create_response, false);
dialog.set_response_appearance(create_response, ResponseAppearance::Suggested);
// Set entry's css class to "error", when there is no text in it
entry.connect_changed(clone!(
#[weak]
dialog,
move |entry| {
let text = entry.text();
let empty = text.is_empty();
dialog.set_response_enabled(create_response, !empty);
if empty {
entry.add_css_class("error");
} else {
entry.remove_css_class("error");
}
}
));
let response = dialog.choose_future(self).await;
// Return if the user chose `cancel_response`
if response == cancel_response {
return;
}
// Create a new list store
let tasks = gio::ListStore::new::<TaskObject>();
// Create a new collection object from the title the user provided
let title = entry.text().to_string();
let collection = CollectionObject::new(&title, tasks);
// Add new collection object and set current tasks
self.collections().append(&collection);
self.set_current_collection(collection);
// Show the content
self.imp().split_view.set_show_content(true);
}
}
我们还在 setup_callbacks
中添加了更多回调。 重要的是,每当 "过滤(filter)"设置的值发生变化时,我们都要过滤当前的任务模型。 每当收藏夹的项目发生变化时,我们也要设置堆栈。 这样可以确保在没有收藏夹的情况下显示占位页面。 最后,我们确保当我们点击 collections_list
中的一行时,current_collection
将被设置为所选的集合,并分割视图显示任务视图。
文件名: listings/todo/8/window/mod.rs
mod imp;
use std::fs::File;
use adw::prelude::*;
use adw::subclass::prelude::*;
use adw::{ActionRow, AlertDialog, ResponseAppearance};
use gio::Settings;
use glib::{clone, Object};
use gtk::{
gio, glib, pango, Align, CheckButton, CustomFilter, Entry, FilterListModel, Label,
ListBoxRow, NoSelection,
};
use crate::collection_object::{CollectionData, CollectionObject};
use crate::task_object::TaskObject;
use crate::utils::data_path;
use crate::APP_ID;
glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>)
@extends adw::ApplicationWindow, 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: &adw::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 {
self.current_collection().tasks()
}
fn current_collection(&self) -> CollectionObject {
self.imp()
.current_collection
.borrow()
.clone()
.expect("`current_collection` should be set in `set_current_collections`.")
}
fn collections(&self) -> gio::ListStore {
self.imp()
.collections
.get()
.expect("`collections` should be set in `setup_collections`.")
.clone()
}
fn set_filter(&self) {
self.imp()
.current_filter_model
.borrow()
.clone()
.expect("`current_filter_model` should be set in `set_current_collection`.")
.set_filter(self.filter().as_ref());
}
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_collections(&self) {
let collections = gio::ListStore::new::<CollectionObject>();
self.imp()
.collections
.set(collections.clone())
.expect("Could not set collections");
self.imp().collections_list.bind_model(
Some(&collections),
clone!(
#[weak(rename_to = window)]
self,
#[upgrade_or_panic]
move |obj| {
let collection_object = obj
.downcast_ref()
.expect("The object should be of type `CollectionObject`.");
let row = window.create_collection_row(collection_object);
row.upcast()
}
),
)
}
fn restore_data(&self) {
if let Ok(file) = File::open(data_path()) {
// Deserialize data from file to vector
let backup_data: Vec<CollectionData> = serde_json::from_reader(file)
.expect(
"It should be possible to read `backup_data` from the json file.",
);
// Convert `Vec<CollectionData>` to `Vec<CollectionObject>`
let collections: Vec<CollectionObject> = backup_data
.into_iter()
.map(CollectionObject::from_collection_data)
.collect();
// Insert restored objects into model
self.collections().extend_from_slice(&collections);
// Set first collection as current
if let Some(first_collection) = collections.first() {
self.set_current_collection(first_collection.clone());
}
}
}
fn create_collection_row(
&self,
collection_object: &CollectionObject,
) -> ListBoxRow {
let label = Label::builder()
.ellipsize(pango::EllipsizeMode::End)
.xalign(0.0)
.build();
collection_object
.bind_property("title", &label, "label")
.sync_create()
.build();
ListBoxRow::builder().child(&label).build()
}
fn set_current_collection(&self, collection: CollectionObject) {
// Wrap model with filter and selection and pass it to the list box
let tasks = collection.tasks();
let filter_model = FilterListModel::new(Some(tasks.clone()), self.filter());
let selection_model = NoSelection::new(Some(filter_model.clone()));
self.imp().tasks_list.bind_model(
Some(&selection_model),
clone!(
#[weak(rename_to = window)]
self,
#[upgrade_or_panic]
move |obj| {
let task_object = obj
.downcast_ref()
.expect("The object should be of type `TaskObject`.");
let row = window.create_task_row(task_object);
row.upcast()
}
),
);
// Store filter model
self.imp().current_filter_model.replace(Some(filter_model));
// If present, disconnect old `tasks_changed` handler
if let Some(handler_id) = self.imp().tasks_changed_handler_id.take() {
self.tasks().disconnect(handler_id);
}
// Assure that the task list is only visible when it is supposed to
self.set_task_list_visible(&tasks);
let tasks_changed_handler_id = tasks.connect_items_changed(clone!(
#[weak(rename_to = window)]
self,
move |tasks, _, _, _| {
window.set_task_list_visible(tasks);
}
));
self.imp()
.tasks_changed_handler_id
.replace(Some(tasks_changed_handler_id));
// Set current tasks
self.imp().current_collection.replace(Some(collection));
self.select_collection_row();
}
fn set_task_list_visible(&self, tasks: &gio::ListStore) {
self.imp().tasks_list.set_visible(tasks.n_items() > 0);
}
fn select_collection_row(&self) {
if let Some(index) = self.collections().find(&self.current_collection()) {
let row = self.imp().collections_list.row_at_index(index as i32);
self.imp().collections_list.select_row(row.as_ref());
}
}
fn create_task_row(&self, task_object: &TaskObject) -> ActionRow {
// Create check button
let check_button = CheckButton::builder()
.valign(Align::Center)
.can_focus(false)
.build();
// Create row
let row = ActionRow::builder()
.activatable_widget(&check_button)
.build();
row.add_prefix(&check_button);
// Bind properties
task_object
.bind_property("completed", &check_button, "active")
.bidirectional()
.sync_create()
.build();
task_object
.bind_property("content", &row, "title")
.sync_create()
.build();
// Return row
row
}
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();
}
));
// Filter model whenever the value of the key "filter" changes
self.settings().connect_changed(
Some("filter"),
clone!(
#[weak(rename_to = window)]
self,
move |_, _| {
window.set_filter();
}
),
);
// Setup callback when items of collections change
self.set_stack();
self.collections().connect_items_changed(clone!(
#[weak(rename_to = window)]
self,
move |_, _, _, _| {
window.set_stack();
}
));
// Setup callback for activating a row of collections list
self.imp().collections_list.connect_row_activated(clone!(
#[weak(rename_to = window)]
self,
move |_, row| {
let index = row.index();
let selected_collection = window
.collections()
.item(index as u32)
.expect("There needs to be an object at this position.")
.downcast::<CollectionObject>()
.expect("The object needs to be a `CollectionObject`.");
window.set_current_collection(selected_collection);
window.imp().split_view.set_show_content(true);
}
));
}
fn set_stack(&self) {
if self.collections().n_items() > 0 {
self.imp().stack.set_visible_child_name("main");
} else {
self.imp().stack.set_visible_child_name("placeholder");
}
}
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_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;
}
}
}
async fn new_collection(&self) {
// Create entry
let entry = Entry::builder()
.placeholder_text("Name")
.activates_default(true)
.build();
let cancel_response = "cancel";
let create_response = "create";
// Create new dialog
let dialog = AlertDialog::builder()
.heading("New Collection")
.close_response(cancel_response)
.default_response(create_response)
.extra_child(&entry)
.build();
dialog
.add_responses(&[(cancel_response, "Cancel"), (create_response, "Create")]);
// Make the dialog button insensitive initially
dialog.set_response_enabled(create_response, false);
dialog.set_response_appearance(create_response, ResponseAppearance::Suggested);
// Set entry's css class to "error", when there is no text in it
entry.connect_changed(clone!(
#[weak]
dialog,
move |entry| {
let text = entry.text();
let empty = text.is_empty();
dialog.set_response_enabled(create_response, !empty);
if empty {
entry.add_css_class("error");
} else {
entry.remove_css_class("error");
}
}
));
let response = dialog.choose_future(self).await;
// Return if the user chose `cancel_response`
if response == cancel_response {
return;
}
// Create a new list store
let tasks = gio::ListStore::new::<TaskObject>();
// Create a new collection object from the title the user provided
let title = entry.text().to_string();
let collection = CollectionObject::new(&title, tasks);
// Add new collection object and set current tasks
self.collections().append(&collection);
self.set_current_collection(collection);
// Show the content
self.imp().split_view.set_show_content(true);
}
}
之前,我们调用了 set_stack
方法。 该方法确保在至少有一个集合时,显示主页面,否则显示"占位"页面。
文件名: listings/todo/8/window/mod.rs
mod imp;
use std::fs::File;
use adw::prelude::*;
use adw::subclass::prelude::*;
use adw::{ActionRow, AlertDialog, ResponseAppearance};
use gio::Settings;
use glib::{clone, Object};
use gtk::{
gio, glib, pango, Align, CheckButton, CustomFilter, Entry, FilterListModel, Label,
ListBoxRow, NoSelection,
};
use crate::collection_object::{CollectionData, CollectionObject};
use crate::task_object::TaskObject;
use crate::utils::data_path;
use crate::APP_ID;
glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>)
@extends adw::ApplicationWindow, 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: &adw::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 {
self.current_collection().tasks()
}
fn current_collection(&self) -> CollectionObject {
self.imp()
.current_collection
.borrow()
.clone()
.expect("`current_collection` should be set in `set_current_collections`.")
}
fn collections(&self) -> gio::ListStore {
self.imp()
.collections
.get()
.expect("`collections` should be set in `setup_collections`.")
.clone()
}
fn set_filter(&self) {
self.imp()
.current_filter_model
.borrow()
.clone()
.expect("`current_filter_model` should be set in `set_current_collection`.")
.set_filter(self.filter().as_ref());
}
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_collections(&self) {
let collections = gio::ListStore::new::<CollectionObject>();
self.imp()
.collections
.set(collections.clone())
.expect("Could not set collections");
self.imp().collections_list.bind_model(
Some(&collections),
clone!(
#[weak(rename_to = window)]
self,
#[upgrade_or_panic]
move |obj| {
let collection_object = obj
.downcast_ref()
.expect("The object should be of type `CollectionObject`.");
let row = window.create_collection_row(collection_object);
row.upcast()
}
),
)
}
fn restore_data(&self) {
if let Ok(file) = File::open(data_path()) {
// Deserialize data from file to vector
let backup_data: Vec<CollectionData> = serde_json::from_reader(file)
.expect(
"It should be possible to read `backup_data` from the json file.",
);
// Convert `Vec<CollectionData>` to `Vec<CollectionObject>`
let collections: Vec<CollectionObject> = backup_data
.into_iter()
.map(CollectionObject::from_collection_data)
.collect();
// Insert restored objects into model
self.collections().extend_from_slice(&collections);
// Set first collection as current
if let Some(first_collection) = collections.first() {
self.set_current_collection(first_collection.clone());
}
}
}
fn create_collection_row(
&self,
collection_object: &CollectionObject,
) -> ListBoxRow {
let label = Label::builder()
.ellipsize(pango::EllipsizeMode::End)
.xalign(0.0)
.build();
collection_object
.bind_property("title", &label, "label")
.sync_create()
.build();
ListBoxRow::builder().child(&label).build()
}
fn set_current_collection(&self, collection: CollectionObject) {
// Wrap model with filter and selection and pass it to the list box
let tasks = collection.tasks();
let filter_model = FilterListModel::new(Some(tasks.clone()), self.filter());
let selection_model = NoSelection::new(Some(filter_model.clone()));
self.imp().tasks_list.bind_model(
Some(&selection_model),
clone!(
#[weak(rename_to = window)]
self,
#[upgrade_or_panic]
move |obj| {
let task_object = obj
.downcast_ref()
.expect("The object should be of type `TaskObject`.");
let row = window.create_task_row(task_object);
row.upcast()
}
),
);
// Store filter model
self.imp().current_filter_model.replace(Some(filter_model));
// If present, disconnect old `tasks_changed` handler
if let Some(handler_id) = self.imp().tasks_changed_handler_id.take() {
self.tasks().disconnect(handler_id);
}
// Assure that the task list is only visible when it is supposed to
self.set_task_list_visible(&tasks);
let tasks_changed_handler_id = tasks.connect_items_changed(clone!(
#[weak(rename_to = window)]
self,
move |tasks, _, _, _| {
window.set_task_list_visible(tasks);
}
));
self.imp()
.tasks_changed_handler_id
.replace(Some(tasks_changed_handler_id));
// Set current tasks
self.imp().current_collection.replace(Some(collection));
self.select_collection_row();
}
fn set_task_list_visible(&self, tasks: &gio::ListStore) {
self.imp().tasks_list.set_visible(tasks.n_items() > 0);
}
fn select_collection_row(&self) {
if let Some(index) = self.collections().find(&self.current_collection()) {
let row = self.imp().collections_list.row_at_index(index as i32);
self.imp().collections_list.select_row(row.as_ref());
}
}
fn create_task_row(&self, task_object: &TaskObject) -> ActionRow {
// Create check button
let check_button = CheckButton::builder()
.valign(Align::Center)
.can_focus(false)
.build();
// Create row
let row = ActionRow::builder()
.activatable_widget(&check_button)
.build();
row.add_prefix(&check_button);
// Bind properties
task_object
.bind_property("completed", &check_button, "active")
.bidirectional()
.sync_create()
.build();
task_object
.bind_property("content", &row, "title")
.sync_create()
.build();
// Return row
row
}
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();
}
));
// Filter model whenever the value of the key "filter" changes
self.settings().connect_changed(
Some("filter"),
clone!(
#[weak(rename_to = window)]
self,
move |_, _| {
window.set_filter();
}
),
);
// Setup callback when items of collections change
self.set_stack();
self.collections().connect_items_changed(clone!(
#[weak(rename_to = window)]
self,
move |_, _, _, _| {
window.set_stack();
}
));
// Setup callback for activating a row of collections list
self.imp().collections_list.connect_row_activated(clone!(
#[weak(rename_to = window)]
self,
move |_, row| {
let index = row.index();
let selected_collection = window
.collections()
.item(index as u32)
.expect("There needs to be an object at this position.")
.downcast::<CollectionObject>()
.expect("The object needs to be a `CollectionObject`.");
window.set_current_collection(selected_collection);
window.imp().split_view.set_show_content(true);
}
));
}
fn set_stack(&self) {
if self.collections().n_items() > 0 {
self.imp().stack.set_visible_child_name("main");
} else {
self.imp().stack.set_visible_child_name("placeholder");
}
}
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_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;
}
}
}
async fn new_collection(&self) {
// Create entry
let entry = Entry::builder()
.placeholder_text("Name")
.activates_default(true)
.build();
let cancel_response = "cancel";
let create_response = "create";
// Create new dialog
let dialog = AlertDialog::builder()
.heading("New Collection")
.close_response(cancel_response)
.default_response(create_response)
.extra_child(&entry)
.build();
dialog
.add_responses(&[(cancel_response, "Cancel"), (create_response, "Create")]);
// Make the dialog button insensitive initially
dialog.set_response_enabled(create_response, false);
dialog.set_response_appearance(create_response, ResponseAppearance::Suggested);
// Set entry's css class to "error", when there is no text in it
entry.connect_changed(clone!(
#[weak]
dialog,
move |entry| {
let text = entry.text();
let empty = text.is_empty();
dialog.set_response_enabled(create_response, !empty);
if empty {
entry.add_css_class("error");
} else {
entry.remove_css_class("error");
}
}
));
let response = dialog.choose_future(self).await;
// Return if the user chose `cancel_response`
if response == cancel_response {
return;
}
// Create a new list store
let tasks = gio::ListStore::new::<TaskObject>();
// Create a new collection object from the title the user provided
let title = entry.text().to_string();
let collection = CollectionObject::new(&title, tasks);
// Add new collection object and set current tasks
self.collections().append(&collection);
self.set_current_collection(collection);
// Show the content
self.imp().split_view.set_show_content(true);
}
}
就是这样! 现在我们可以欣赏最终成果了。
您可能已经注意到,目前还没有删除收藏夹的方法。 请尝试在本地版本的待办事项应用程序中实现这一缺失功能。 您需要考虑哪些边界情况?