让待办事项应用使用 Libadwaita
在本章中,我们将调整待办事项应用程序,使其遵循 GNOME 的 HIG. 首先,让我们安装 Libadwaita,并将 libadwaita
crate 添加到我们的依赖关系中,如上一章所述。
使用 Libadwaita 的最简单方法是将 gtk::Application
替换为 adw::Application
.
mod task_object;
mod task_row;
mod utils;
mod window;
use gtk::prelude::*;
use gtk::{gio, glib};
use window::Window;
const APP_ID: &str = "org.gtk_rs.Todo5";
fn main() -> glib::ExitCode {
gio::resources_register_include!("todo_5.gresource")
.expect("Failed to register resources.");
// Create a new application
// 👇 changed
let app = adw::Application::builder().application_id(APP_ID).build();
// Connect to signals
app.connect_startup(setup_shortcuts);
app.connect_activate(build_ui);
// Run the application
app.run()
}
// 👇 changed
fn setup_shortcuts(app: &adw::Application) {
app.set_accels_for_action("win.filter('All')", &["<Ctrl>a"]);
app.set_accels_for_action("win.filter('Open')", &["<Ctrl>o"]);
app.set_accels_for_action("win.filter('Done')", &["<Ctrl>d"]);
}
// 👇 changed
fn build_ui(app: &adw::Application) {
// Create a new custom window and present it
let window = Window::new(app);
window.present();
}
文件名: listings/todo/5/window/mod.rs
mod imp;
use std::fs::File;
use gio::Settings;
use glib::{clone, Object};
use gtk::subclass::prelude::*;
use gtk::{
gio, glib, CustomFilter, FilterListModel, NoSelection, SignalListItemFactory,
};
use gtk::{prelude::*, ListItem};
use crate::task_object::{TaskData, TaskObject};
use crate::task_row::TaskRow;
use crate::utils::data_path;
use crate::APP_ID;
glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>)
@extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}
impl Window {
pub fn new(app: &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 view
let filter_model = FilterListModel::new(Some(self.tasks()), self.filter());
let selection_model = NoSelection::new(Some(filter_model.clone()));
self.imp().tasks_list.set_model(Some(&selection_model));
// Filter model whenever the value of the key "filter" changes
self.settings().connect_changed(
Some("filter"),
clone!(
#[weak(rename_to = window)]
self,
#[weak]
filter_model,
move |_, _| {
filter_model.set_filter(window.filter().as_ref());
}
),
);
}
fn restore_data(&self) {
if let Ok(file) = File::open(data_path()) {
// Deserialize data from file to vector
let backup_data: Vec<TaskData> = serde_json::from_reader(file).expect(
"It should be possible to read `backup_data` from the json file.",
);
// Convert `Vec<TaskData>` to `Vec<TaskObject>`
let task_objects: Vec<TaskObject> = backup_data
.into_iter()
.map(TaskObject::from_task_data)
.collect();
// Insert restored objects into model
self.tasks().extend_from_slice(&task_objects);
}
}
fn setup_callbacks(&self) {
// Setup callback for activation of the entry
self.imp().entry.connect_activate(clone!(
#[weak(rename_to = window)]
self,
move |_| {
window.new_task();
}
));
// Setup callback for clicking (and the releasing) the icon of the entry
self.imp().entry.connect_icon_release(clone!(
#[weak(rename_to = window)]
self,
move |_, _| {
window.new_task();
}
));
}
fn new_task(&self) {
// Get content from entry and clear it
let buffer = self.imp().entry.buffer();
let content = buffer.text().to_string();
if content.is_empty() {
return;
}
buffer.set_text("");
// Add new task to model
let task = TaskObject::new(false, content);
self.tasks().append(&task);
}
fn setup_factory(&self) {
// Create a new factory
let factory = SignalListItemFactory::new();
// Create an empty `TaskRow` during setup
factory.connect_setup(move |_, list_item| {
// Create `TaskRow`
let task_row = TaskRow::new();
list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.set_child(Some(&task_row));
});
// Tell factory how to bind `TaskRow` to a `TaskObject`
factory.connect_bind(move |_, list_item| {
// Get `TaskObject` from `ListItem`
let task_object = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.item()
.and_downcast::<TaskObject>()
.expect("The item has to be an `TaskObject`.");
// Get `TaskRow` from `ListItem`
let task_row = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.child()
.and_downcast::<TaskRow>()
.expect("The child has to be a `TaskRow`.");
task_row.bind(&task_object);
});
// Tell factory how to unbind `TaskRow` from `TaskObject`
factory.connect_unbind(move |_, list_item| {
// Get `TaskRow` from `ListItem`
let task_row = list_item
.downcast_ref::<ListItem>()
.expect("Needs to be ListItem")
.child()
.and_downcast::<TaskRow>()
.expect("The child has to be a `TaskRow`.");
task_row.unbind();
});
// Set the factory of the list view
self.imp().tasks_list.set_factory(Some(&factory));
}
fn setup_actions(&self) {
// Create action from key "filter" and add to action group "win"
let action_filter = self.settings().create_action("filter");
self.add_action(&action_filter);
}
fn remove_done_tasks(&self) {
let tasks = self.tasks();
let mut position = 0;
while let Some(item) = tasks.item(position) {
// Get `TaskObject` from `glib::Object`
let task_object = item
.downcast_ref::<TaskObject>()
.expect("The object needs to be of type `TaskObject`.");
if task_object.is_completed() {
tasks.remove(position);
} else {
position += 1;
}
}
}
}
adw::Application
在内部调用 adw::init
,确保为 Libadwaita 正确设置翻译、类型、样式表和图标。 只要命名[正确](Application in libadwaita - Rust),它还会自动从资源中加载样式表。
看看我们的待办事项应用,我们可以发现其控件的外观发生了变化。 这是因为 GTK 提供的默认
样式表已被 Libadwaita 提供的 Adwaita
样式表所取代。
此外,我们的应用现在可以与系统一起切换到暗黑风格。
Boxed lists
当然,Libadwaita 不仅仅是几个样式表和一个样式管理器
。 但在我们进入有趣的内容之前,我们将用 adw::prelude
和 adw::subclass::prelude
替换所有出现的adw::prelude
和 adw::subclass::prelude
,让我们的未来生活更轻松。 这是因为 adw
prelude 除了 Libadwaita 特有的 trait 外,还重新导出了相应的 gtk
prelude。
现在,我们要让我们的任务项采用 boxed lists pattern. HIG 并不要求我们使用这种样式,这是有原因的:它与循环列表不兼容。 这意味着它们不能与 [list views](List & Column Views - GNOME Human Interface Guidelines) 一起使用,因此只适用于相对较小的列表。
尝试用代码添加任务,看看需要添加多少个任务才能使用户界面明显变慢。 自己判断一下这个数字是否合理,或者我们是否应该坚持使用列表视图。
我们可以使用 gtk::ListBox
代替 gtk::ListView
来使用 boxed lists. 我们还将添加 Libadwaita 提供的boxed-list
样式类。
让我们在 window.ui
文件中实现所有这些更改。 所有更改都限制在 ApplicationWindow
的第二个子对象中。 要查看完整文件,只需点击"文件名"后面的链接。
文件名: listings/todo/6/resources/window.ui
<child>
<object class="GtkScrolledWindow">
<property name="hscrollbar-policy">never</property>
<property name="min-content-height">420</property>
<property name="vexpand">True</property>
<property name="child">
<object class="AdwClamp">
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">18</property>
<property name="margin-top">24</property>
<property name="margin-bottom">24</property>
<property name="margin-start">12</property>
<property name="margin-end">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>
</child>
为了遵循 boxed list 模式,我们改用了 gtk::ListBox
, 将其属性 "selection-mode"设置为 "none",并添加了boxed-list
样式类。
让我们继续查看 window/imp.rs
. 成员变量 tasks_list
现在描述的是 ListBox
而不是 ListView
.
文件名: listings/todo/6/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/Todo6/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;
type ParentType = gtk::ApplicationWindow;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
// Create action to remove done tasks and add to action group "win"
klass.install_action("win.remove-done-tasks", None, |window, _, _| {
window.remove_done_tasks();
});
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
// Trait shared by all GObjects
impl ObjectImpl for Window {
fn constructed(&self) {
// Call "constructed" on parent
self.parent_constructed();
// Setup
let obj = self.obj();
obj.setup_settings();
obj.setup_tasks();
obj.restore_data();
obj.setup_callbacks();
obj.setup_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 {}
现在我们转到 window/mod.rs
. ListBox
可以很好地支持模型(model),但在没有任何控件回收的情况下,我们不再需要工厂。 setup_factory
可以被安全地删除。要设置 ListBox
,我们需要在 setup_tasks
中调用 bind_model
. 在这里,我们指定了模型,以及描述如何将给定的 GObject 转换为 list box 可以显示的控件的闭包。
文件名: listings/todo/6/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>)
@extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}
impl Window {
pub fn new(app: &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;
}
}
}
}
我们仍然需要指定 create_task_row
方法。 在这里,我们创建了一个以 gtk::CheckButton
作为可激活控件的 adw::ActionRow
. 如果没有回收,GObject 将始终属于同一个控件。 这意味着我们可以直接绑定它们的属性,而不必担心以后会取消绑定。
文件名: listings/todo/6/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>)
@extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}
impl Window {
pub fn new(app: &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;
}
}
}
}
使用 boxed lists 时,还必须注意在没有任务时隐藏 ListBox
.
文件名: listings/todo/6/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>)
@extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}
impl Window {
pub fn new(app: &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;
}
}
}
}
最后,我们定义了 set_task_list_visible
方法。
文件名: listings/todo/6/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>)
@extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}
impl Window {
pub fn new(app: &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;
}
}
}
}
这就是 boxed list 样式在我们应用程序中的样子。