sidebar-data: Port to glib::Properties macro

This commit is contained in:
Kévin Commaille 2023-12-12 21:43:25 +01:00
parent f1a923f402
commit 5d9b9e61b3
No known key found for this signature in database
GPG Key ID: 29A48C1F03620416
23 changed files with 817 additions and 1121 deletions

View File

@ -31,8 +31,8 @@ src/session/model/session.rs
src/session/model/room/member_role.rs
src/session/model/room/mod.rs
src/session/model/room_list/mod.rs
src/session/model/sidebar/category/category_type.rs
src/session/model/sidebar/icon_item.rs
src/session/model/sidebar_data/category/category_type.rs
src/session/model/sidebar_data/icon_item.rs
src/session/view/account_settings/devices_page/device_list.rs
src/session/view/account_settings/devices_page/device_row.rs
src/session/view/account_settings/devices_page/device_row.ui

View File

@ -4,7 +4,7 @@ mod room;
mod room_list;
mod session;
mod session_settings;
mod sidebar;
mod sidebar_data;
mod user;
mod verification;
@ -20,7 +20,7 @@ pub use self::{
room_list::RoomList,
session::{Session, SessionState},
session_settings::{SessionSettings, StoredSessionSettings},
sidebar::{
sidebar_data::{
Category, CategoryType, IconItem, ItemList, ItemType, Selection, SidebarItem,
SidebarItemImpl, SidebarListModel,
},

View File

@ -331,11 +331,11 @@ impl Session {
self.imp().settings.get().unwrap()
}
pub fn room_list(&self) -> &RoomList {
pub fn room_list(&self) -> RoomList {
self.sidebar_list_model().item_list().room_list()
}
pub fn verification_list(&self) -> &VerificationList {
pub fn verification_list(&self) -> VerificationList {
self.sidebar_list_model().item_list().verification_list()
}

View File

@ -1,171 +0,0 @@
use gtk::{glib, prelude::*, subclass::prelude::*};
use super::CategoryType;
mod imp {
use std::cell::{Cell, RefCell};
use super::*;
#[derive(Debug, Default)]
pub struct CategoryFilter {
/// The expression to watch.
pub expression: RefCell<Option<gtk::Expression>>,
/// The category type to filter.
pub category_type: Cell<CategoryType>,
}
#[glib::object_subclass]
impl ObjectSubclass for CategoryFilter {
const NAME: &'static str = "CategoryFilter";
type Type = super::CategoryFilter;
type ParentType = gtk::Filter;
}
impl ObjectImpl for CategoryFilter {
fn properties() -> &'static [glib::ParamSpec] {
use once_cell::sync::Lazy;
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
gtk::ParamSpecExpression::builder("expression")
.explicit_notify()
.build(),
glib::ParamSpecEnum::builder::<CategoryType>("category-type")
.explicit_notify()
.build(),
]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
let obj = self.obj();
match pspec.name() {
"expression" => obj.set_expression(value.get().unwrap()),
"category-type" => obj.set_category_type(value.get().unwrap()),
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
let obj = self.obj();
match pspec.name() {
"expression" => obj.expression().to_value(),
"category-type" => obj.category_type().to_value(),
_ => unimplemented!(),
}
}
}
impl FilterImpl for CategoryFilter {
fn strictness(&self) -> gtk::FilterMatch {
if self.category_type.get() == CategoryType::None {
return gtk::FilterMatch::All;
}
if self.expression.borrow().is_none() {
return gtk::FilterMatch::None;
}
gtk::FilterMatch::Some
}
fn match_(&self, item: &glib::Object) -> bool {
let category_type = self.category_type.get();
if category_type == CategoryType::None {
return true;
}
let Some(value) = self
.expression
.borrow()
.as_ref()
.and_then(|e| e.evaluate(Some(item)))
.map(|v| v.get::<CategoryType>().unwrap())
else {
return false;
};
value == category_type
}
}
}
glib::wrapper! {
/// A filter by `CategoryType`.
pub struct CategoryFilter(ObjectSubclass<imp::CategoryFilter>)
@extends gtk::Filter;
}
impl CategoryFilter {
pub fn new(expression: impl AsRef<gtk::Expression>, category_type: CategoryType) -> Self {
glib::Object::builder()
.property("expression", expression.as_ref())
.property("category-type", category_type)
.build()
}
/// The expression to watch.
pub fn expression(&self) -> Option<gtk::Expression> {
self.imp().expression.borrow().clone()
}
/// Set the expression to watch.
///
/// This expression must return a [`CategoryType`].
pub fn set_expression(&self, expression: Option<gtk::Expression>) {
let prev_expression = self.expression();
if prev_expression.is_none() && expression.is_none() {
return;
}
let change = if self.category_type() == CategoryType::None {
None
} else if prev_expression.is_none() {
Some(gtk::FilterChange::LessStrict)
} else if expression.is_none() {
Some(gtk::FilterChange::MoreStrict)
} else {
Some(gtk::FilterChange::Different)
};
self.imp().expression.replace(expression);
if let Some(change) = change {
self.changed(change)
}
self.notify("expression");
}
/// The category type to filter.
pub fn category_type(&self) -> CategoryType {
self.imp().category_type.get()
}
/// Set the category type to filter.
pub fn set_category_type(&self, category_type: CategoryType) {
let prev_category_type = self.category_type();
if prev_category_type == category_type {
return;
}
let change = if self.expression().is_none() {
None
} else if prev_category_type == CategoryType::None {
Some(gtk::FilterChange::MoreStrict)
} else if category_type == CategoryType::None {
Some(gtk::FilterChange::LessStrict)
} else {
Some(gtk::FilterChange::Different)
};
self.imp().category_type.set(category_type);
if let Some(change) = change {
self.changed(change)
}
self.notify("category-type");
}
}

View File

@ -1,218 +0,0 @@
use gtk::{
gio, glib,
glib::{clone, closure},
prelude::*,
subclass::prelude::*,
};
mod category_filter;
mod category_type;
use self::category_filter::CategoryFilter;
pub use self::category_type::CategoryType;
use super::{SidebarItem, SidebarItemExt, SidebarItemImpl};
use crate::{
session::model::{Room, RoomList, RoomType},
utils::ExpressionListModel,
};
mod imp {
use std::cell::Cell;
use once_cell::unsync::OnceCell;
use super::*;
#[derive(Debug, Default)]
pub struct Category {
pub model: OnceCell<gio::ListModel>,
pub type_: Cell<CategoryType>,
pub is_empty: Cell<bool>,
}
#[glib::object_subclass]
impl ObjectSubclass for Category {
const NAME: &'static str = "Category";
type Type = super::Category;
type ParentType = SidebarItem;
type Interfaces = (gio::ListModel,);
}
impl ObjectImpl for Category {
fn properties() -> &'static [glib::ParamSpec] {
use once_cell::sync::Lazy;
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecEnum::builder::<CategoryType>("type")
.construct_only()
.build(),
glib::ParamSpecString::builder("display-name")
.read_only()
.build(),
glib::ParamSpecObject::builder::<gio::ListModel>("model")
.construct_only()
.build(),
glib::ParamSpecBoolean::builder("empty").read_only().build(),
]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"type" => self.type_.set(value.get().unwrap()),
"model" => self.obj().set_model(value.get().unwrap()),
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
let obj = self.obj();
match pspec.name() {
"type" => obj.type_().to_value(),
"display-name" => obj.display_name().to_value(),
"model" => obj.model().to_value(),
"empty" => obj.is_empty().to_value(),
_ => unimplemented!(),
}
}
}
impl ListModelImpl for Category {
fn item_type(&self) -> glib::Type {
SidebarItem::static_type()
}
fn n_items(&self) -> u32 {
self.model.get().unwrap().n_items()
}
fn item(&self, position: u32) -> Option<glib::Object> {
self.model.get().unwrap().item(position)
}
}
impl SidebarItemImpl for Category {
fn update_visibility(&self, for_category: CategoryType) {
let obj = self.obj();
let visible = if !obj.is_empty() {
true
} else {
let room_types =
RoomType::try_from(for_category)
.ok()
.and_then(|source_room_type| {
RoomType::try_from(obj.type_())
.ok()
.map(|target_room_type| (source_room_type, target_room_type))
});
room_types.map_or(false, |(source_room_type, target_room_type)| {
source_room_type.can_change_to(target_room_type)
})
};
obj.set_visible(visible)
}
}
}
glib::wrapper! {
/// A list of Items in the same category implementing ListModel.
///
/// This struct is used in ItemList for the sidebar.
pub struct Category(ObjectSubclass<imp::Category>)
@extends SidebarItem,
@implements gio::ListModel;
}
impl Category {
pub fn new(type_: CategoryType, model: &impl IsA<gio::ListModel>) -> Self {
glib::Object::builder()
.property("type", type_)
.property("model", model)
.build()
}
/// The type of this category.
pub fn type_(&self) -> CategoryType {
self.imp().type_.get()
}
/// The display name of this category.
pub fn display_name(&self) -> String {
self.type_().to_string()
}
/// The filter list model on this category.
pub fn model(&self) -> Option<&gio::ListModel> {
self.imp().model.get()
}
/// Set the filter list model of this category.
fn set_model(&self, model: gio::ListModel) {
let type_ = self.type_();
// Special case room lists so that they are sorted and in the right category
let model = if model.is::<RoomList>() {
let room_category_type = Room::this_expression("category")
.chain_closure::<CategoryType>(closure!(
|_: Option<glib::Object>, room_type: RoomType| {
CategoryType::from(room_type)
}
));
let filter = CategoryFilter::new(&room_category_type, type_);
let category_type_expr_model = ExpressionListModel::new();
category_type_expr_model.set_expressions(vec![room_category_type.upcast()]);
category_type_expr_model.set_model(Some(model));
let filter_model =
gtk::FilterListModel::new(Some(category_type_expr_model), Some(filter));
let room_latest_activity = Room::this_expression("latest-activity");
let sorter = gtk::NumericSorter::builder()
.expression(&room_latest_activity)
.sort_order(gtk::SortType::Descending)
.build();
let latest_activity_expr_model = ExpressionListModel::new();
latest_activity_expr_model.set_expressions(vec![room_latest_activity.upcast()]);
latest_activity_expr_model.set_model(Some(filter_model.upcast()));
let sort_model =
gtk::SortListModel::new(Some(latest_activity_expr_model), Some(sorter));
sort_model.upcast()
} else {
model
};
model.connect_items_changed(
clone!(@weak self as obj => move |model, pos, removed, added| {
obj.items_changed(pos, removed, added);
obj.set_is_empty(model.n_items() == 0);
}),
);
self.set_is_empty(model.n_items() == 0);
self.imp().model.set(model).unwrap();
}
/// Set whether this category is empty.
fn set_is_empty(&self, is_empty: bool) {
if is_empty == self.is_empty() {
return;
}
self.imp().is_empty.set(is_empty);
self.notify("empty");
}
/// Whether this category is empty.
pub fn is_empty(&self) -> bool {
self.imp().is_empty.get()
}
}

View File

@ -1,132 +0,0 @@
use std::fmt;
use gettextrs::gettext;
use gtk::{glib, prelude::*, subclass::prelude::*};
use super::{CategoryType, SidebarItem, SidebarItemExt, SidebarItemImpl};
#[derive(Debug, Default, Hash, Eq, PartialEq, Clone, Copy, glib::Enum)]
#[repr(u32)]
#[enum_type(name = "ItemType")]
pub enum ItemType {
#[default]
Explore = 0,
Forget = 1,
}
impl ItemType {
/// The icon name for this item type.
pub fn icon_name(&self) -> &'static str {
match self {
Self::Explore => "explore-symbolic",
Self::Forget => "user-trash-symbolic",
}
}
}
impl fmt::Display for ItemType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let label = match self {
Self::Explore => gettext("Explore"),
Self::Forget => gettext("Forget Room"),
};
f.write_str(&label)
}
}
mod imp {
use std::cell::Cell;
use super::*;
#[derive(Debug, Default)]
pub struct IconItem {
pub type_: Cell<ItemType>,
}
#[glib::object_subclass]
impl ObjectSubclass for IconItem {
const NAME: &'static str = "IconItem";
type Type = super::IconItem;
type ParentType = SidebarItem;
}
impl ObjectImpl for IconItem {
fn properties() -> &'static [glib::ParamSpec] {
use once_cell::sync::Lazy;
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecEnum::builder::<ItemType>("type")
.construct_only()
.build(),
glib::ParamSpecString::builder("display-name")
.read_only()
.build(),
glib::ParamSpecString::builder("icon-name")
.read_only()
.build(),
]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"type" => {
self.type_.set(value.get().unwrap());
}
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
let obj = self.obj();
match pspec.name() {
"type" => obj.type_().to_value(),
"display-name" => obj.display_name().to_value(),
"icon-name" => obj.icon_name().to_value(),
_ => unimplemented!(),
}
}
}
impl SidebarItemImpl for IconItem {
fn update_visibility(&self, for_category: CategoryType) {
let obj = self.obj();
match obj.type_() {
ItemType::Explore => obj.set_visible(true),
ItemType::Forget => obj.set_visible(for_category == CategoryType::Left),
}
}
}
}
glib::wrapper! {
/// A top-level row in the sidebar with an icon.
pub struct IconItem(ObjectSubclass<imp::IconItem>) @extends SidebarItem;
}
impl IconItem {
pub fn new(type_: ItemType) -> Self {
glib::Object::builder().property("type", type_).build()
}
/// The type of this item.
pub fn type_(&self) -> ItemType {
self.imp().type_.get()
}
/// The display name of this item.
pub fn display_name(&self) -> String {
self.type_().to_string()
}
/// The icon name used for this item.
pub fn icon_name(&self) -> &'static str {
self.type_().icon_name()
}
}

View File

@ -1,133 +0,0 @@
use gtk::{glib, prelude::*, subclass::prelude::*};
use super::{item_list::ItemList, selection::Selection};
use crate::session::model::Room;
mod imp {
use once_cell::{sync::Lazy, unsync::OnceCell};
use super::*;
#[derive(Debug, Default)]
pub struct SidebarListModel {
/// The list of items in the sidebar.
pub item_list: OnceCell<ItemList>,
/// The tree list model.
pub tree_model: OnceCell<gtk::TreeListModel>,
/// The string filter.
pub string_filter: gtk::StringFilter,
/// The selection model.
pub selection_model: Selection,
}
#[glib::object_subclass]
impl ObjectSubclass for SidebarListModel {
const NAME: &'static str = "SidebarListModel";
type Type = super::SidebarListModel;
}
impl ObjectImpl for SidebarListModel {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecObject::builder::<ItemList>("item-list")
.construct_only()
.build(),
glib::ParamSpecObject::builder::<gtk::TreeListModel>("tree-model")
.read_only()
.build(),
glib::ParamSpecObject::builder::<gtk::StringFilter>("string-filter")
.read_only()
.build(),
glib::ParamSpecObject::builder::<Selection>("selection-model")
.read_only()
.build(),
]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
let obj = self.obj();
match pspec.name() {
"item-list" => obj.set_item_list(value.get().unwrap()),
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
let obj = self.obj();
match pspec.name() {
"item-list" => obj.item_list().to_value(),
"tree-model" => obj.tree_model().to_value(),
"string-filter" => obj.string_filter().to_value(),
"selection-model" => obj.selection_model().to_value(),
_ => unimplemented!(),
}
}
}
}
glib::wrapper! {
/// A wrapper for the sidebar list model of a `Session`.
///
/// It allows to keep the state for selection and filtering.
pub struct SidebarListModel(ObjectSubclass<imp::SidebarListModel>);
}
impl SidebarListModel {
/// Create a new `SidebarListModel`.
pub fn new(item_list: &ItemList) -> Self {
glib::Object::builder()
.property("item-list", item_list)
.build()
}
/// The list of items in the sidebar.
pub fn item_list(&self) -> &ItemList {
self.imp().item_list.get().unwrap()
}
/// Set the list of items in the sidebar.
fn set_item_list(&self, item_list: ItemList) {
let imp = self.imp();
imp.item_list.set(item_list.clone()).unwrap();
let tree_model =
gtk::TreeListModel::new(item_list, false, true, |item| item.clone().downcast().ok());
imp.tree_model.set(tree_model.clone()).unwrap();
let room_expression =
gtk::TreeListRow::this_expression("item").chain_property::<Room>("display-name");
imp.string_filter
.set_match_mode(gtk::StringFilterMatchMode::Substring);
imp.string_filter.set_expression(Some(&room_expression));
imp.string_filter.set_ignore_case(true);
// Default to an empty string to be able to bind to GtkEditable::text.
imp.string_filter.set_search(Some(""));
let filter_model =
gtk::FilterListModel::new(Some(tree_model), Some(imp.string_filter.clone()));
imp.selection_model.set_model(Some(&filter_model));
}
/// The tree list model.
pub fn tree_model(&self) -> &gtk::TreeListModel {
self.imp().tree_model.get().unwrap()
}
/// The string filter.
pub fn string_filter(&self) -> &gtk::StringFilter {
&self.imp().string_filter
}
/// The selection model.
pub fn selection_model(&self) -> &Selection {
&self.imp().selection_model
}
}

View File

@ -1,325 +0,0 @@
use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*};
mod imp {
use std::cell::{Cell, RefCell};
use once_cell::sync::Lazy;
use super::*;
#[derive(Debug, Default)]
pub struct Selection {
pub model: RefCell<Option<gio::ListModel>>,
pub selected: Cell<u32>,
pub selected_item: RefCell<Option<glib::Object>>,
pub signal_handler: RefCell<Option<glib::SignalHandlerId>>,
}
#[glib::object_subclass]
impl ObjectSubclass for Selection {
const NAME: &'static str = "SidebarSelection";
type Type = super::Selection;
type Interfaces = (gio::ListModel, gtk::SelectionModel);
fn new() -> Self {
Self {
selected: Cell::new(gtk::INVALID_LIST_POSITION),
..Default::default()
}
}
}
impl ObjectImpl for Selection {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecObject::builder::<gio::ListModel>("model")
.explicit_notify()
.build(),
glib::ParamSpecUInt::builder("selected")
.default_value(gtk::INVALID_LIST_POSITION)
.explicit_notify()
.build(),
glib::ParamSpecObject::builder::<glib::Object>("selected-item")
.explicit_notify()
.build(),
]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
let obj = self.obj();
match pspec.name() {
"model" => {
let model: Option<gio::ListModel> = value.get().unwrap();
obj.set_model(model.as_ref());
}
"selected" => obj.set_selected(value.get().unwrap()),
"selected-item" => {
obj.set_selected_item(value.get::<Option<glib::Object>>().unwrap())
}
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
let obj = self.obj();
match pspec.name() {
"model" => obj.model().to_value(),
"selected" => obj.selected().to_value(),
"selected-item" => obj.selected_item().to_value(),
_ => unimplemented!(),
}
}
}
impl ListModelImpl for Selection {
fn item_type(&self) -> glib::Type {
gtk::TreeListRow::static_type()
}
fn n_items(&self) -> u32 {
self.model
.borrow()
.as_ref()
.map(|m| m.n_items())
.unwrap_or(0)
}
fn item(&self, position: u32) -> Option<glib::Object> {
self.model.borrow().as_ref().and_then(|m| m.item(position))
}
}
impl SelectionModelImpl for Selection {
fn selection_in_range(&self, _position: u32, _n_items: u32) -> gtk::Bitset {
let bitset = gtk::Bitset::new_empty();
let selected = self.selected.get();
if selected != gtk::INVALID_LIST_POSITION {
bitset.add(selected);
}
bitset
}
fn is_selected(&self, position: u32) -> bool {
self.selected.get() == position
}
}
}
glib::wrapper! {
pub struct Selection(ObjectSubclass<imp::Selection>)
@implements gio::ListModel, gtk::SelectionModel;
}
impl Selection {
pub fn new<P: IsA<gio::ListModel>>(model: Option<&P>) -> Selection {
let model = model.map(|m| m.clone().upcast());
glib::Object::builder().property("model", &model).build()
}
/// The underlying model.
pub fn model(&self) -> Option<gio::ListModel> {
self.imp().model.borrow().clone()
}
/// The position of the selected item.
pub fn selected(&self) -> u32 {
self.imp().selected.get()
}
/// The selected item.
pub fn selected_item(&self) -> Option<glib::Object> {
self.imp().selected_item.borrow().clone()
}
/// Set the underlying model.
pub fn set_model<P: IsA<gio::ListModel>>(&self, model: Option<&P>) {
let imp = self.imp();
let _guard = self.freeze_notify();
let model = model.map(|m| m.clone().upcast());
let old_model = self.model();
if old_model == model {
return;
}
let n_items_before = old_model
.map(|model| {
if let Some(id) = imp.signal_handler.take() {
model.disconnect(id);
}
model.n_items()
})
.unwrap_or(0);
if let Some(model) = model {
imp.signal_handler.replace(Some(model.connect_items_changed(
clone!(@weak self as obj => move |m, p, r, a| {
obj.items_changed_cb(m, p, r, a);
}),
)));
self.items_changed_cb(&model, 0, n_items_before, model.n_items());
imp.model.replace(Some(model));
} else {
imp.model.replace(None);
if self.selected() != gtk::INVALID_LIST_POSITION {
imp.selected.replace(gtk::INVALID_LIST_POSITION);
self.notify("selected");
}
if self.selected_item().is_some() {
imp.selected_item.replace(None);
self.notify("selected-item");
}
self.items_changed(0, n_items_before, 0);
}
self.notify("model");
}
/// Set the selected item by its position.
pub fn set_selected(&self, position: u32) {
let imp = self.imp();
let old_selected = self.selected();
if old_selected == position {
return;
}
let selected_item = self
.model()
.and_then(|m| m.item(position))
.and_downcast::<gtk::TreeListRow>()
.and_then(|r| r.item());
let selected = if selected_item.is_none() {
gtk::INVALID_LIST_POSITION
} else {
position
};
if old_selected == selected {
return;
}
imp.selected.replace(selected);
imp.selected_item.replace(selected_item);
if old_selected == gtk::INVALID_LIST_POSITION {
self.selection_changed(selected, 1);
} else if selected == gtk::INVALID_LIST_POSITION {
self.selection_changed(old_selected, 1);
} else if selected < old_selected {
self.selection_changed(selected, old_selected - selected + 1);
} else {
self.selection_changed(old_selected, selected - old_selected + 1);
}
self.notify("selected");
self.notify("selected-item");
}
/// Set the selected item.
pub fn set_selected_item(&self, item: Option<impl IsA<glib::Object>>) {
let imp = self.imp();
let item = item.and_upcast();
let selected_item = self.selected_item();
if selected_item == item {
return;
}
let old_selected = self.selected();
let mut selected = gtk::INVALID_LIST_POSITION;
if item.is_some() {
if let Some(model) = self.model() {
for i in 0..model.n_items() {
let current_item = model
.item(i)
.and_downcast::<gtk::TreeListRow>()
.and_then(|r| r.item());
if current_item == item {
selected = i;
break;
}
}
}
}
imp.selected_item.replace(item);
if old_selected != selected {
imp.selected.replace(selected);
if old_selected == gtk::INVALID_LIST_POSITION {
self.selection_changed(selected, 1);
} else if selected == gtk::INVALID_LIST_POSITION {
self.selection_changed(old_selected, 1);
} else if selected < old_selected {
self.selection_changed(selected, old_selected - selected + 1);
} else {
self.selection_changed(old_selected, selected - old_selected + 1);
}
self.notify("selected");
}
self.notify("selected-item");
}
fn items_changed_cb(&self, model: &gio::ListModel, position: u32, removed: u32, added: u32) {
let imp = self.imp();
let _guard = self.freeze_notify();
let selected = self.selected();
let selected_item = self.selected_item();
if selected_item.is_none() || selected < position {
// unchanged
} else if selected != gtk::INVALID_LIST_POSITION && selected >= position + removed {
imp.selected.replace(selected + added - removed);
self.notify("selected");
} else {
for i in 0..=added {
if i == added {
// the item really was deleted
imp.selected.replace(gtk::INVALID_LIST_POSITION);
self.notify("selected");
} else {
let item = model
.item(position + i)
.and_downcast::<gtk::TreeListRow>()
.and_then(|r| r.item());
if item == selected_item {
// the item moved
if selected != position + i {
imp.selected.replace(position + i);
self.notify("selected");
}
break;
}
}
}
}
self.items_changed(position, removed, added);
}
}
impl Default for Selection {
fn default() -> Self {
Self::new(gio::ListModel::NONE)
}
}

View File

@ -0,0 +1,134 @@
use gtk::{glib, prelude::*, subclass::prelude::*};
use super::CategoryType;
mod imp {
use std::cell::{Cell, RefCell};
use super::*;
#[derive(Debug, Default, glib::Properties)]
#[properties(wrapper_type = super::CategoryFilter)]
pub struct CategoryFilter {
/// The expression to watch.
#[property(get, set = Self::set_expression, explicit_notify)]
pub expression: RefCell<Option<gtk::Expression>>,
/// The category type to filter.
#[property(get, set = Self::set_category_type, explicit_notify, builder(CategoryType::default()))]
pub category_type: Cell<CategoryType>,
}
#[glib::object_subclass]
impl ObjectSubclass for CategoryFilter {
const NAME: &'static str = "CategoryFilter";
type Type = super::CategoryFilter;
type ParentType = gtk::Filter;
}
#[glib::derived_properties]
impl ObjectImpl for CategoryFilter {}
impl FilterImpl for CategoryFilter {
fn strictness(&self) -> gtk::FilterMatch {
if self.category_type.get() == CategoryType::None {
return gtk::FilterMatch::All;
}
if self.expression.borrow().is_none() {
return gtk::FilterMatch::None;
}
gtk::FilterMatch::Some
}
fn match_(&self, item: &glib::Object) -> bool {
let category_type = self.category_type.get();
if category_type == CategoryType::None {
return true;
}
let Some(value) = self
.expression
.borrow()
.as_ref()
.and_then(|e| e.evaluate(Some(item)))
.map(|v| v.get::<CategoryType>().unwrap())
else {
return false;
};
value == category_type
}
}
impl CategoryFilter {
/// Set the expression to watch.
///
/// This expression must return a [`CategoryType`].
fn set_expression(&self, expression: Option<gtk::Expression>) {
let prev_expression = self.expression.borrow().clone();
if prev_expression.is_none() && expression.is_none() {
return;
}
let obj = self.obj();
let change = if self.category_type.get() == CategoryType::None {
None
} else if prev_expression.is_none() {
Some(gtk::FilterChange::LessStrict)
} else if expression.is_none() {
Some(gtk::FilterChange::MoreStrict)
} else {
Some(gtk::FilterChange::Different)
};
self.expression.replace(expression);
if let Some(change) = change {
obj.changed(change)
}
obj.notify_expression();
}
/// Set the category type to filter.
fn set_category_type(&self, category_type: CategoryType) {
let prev_category_type = self.category_type.get();
if prev_category_type == category_type {
return;
}
let obj = self.obj();
let change = if self.expression.borrow().is_none() {
None
} else if prev_category_type == CategoryType::None {
Some(gtk::FilterChange::MoreStrict)
} else if category_type == CategoryType::None {
Some(gtk::FilterChange::LessStrict)
} else {
Some(gtk::FilterChange::Different)
};
self.category_type.set(category_type);
if let Some(change) = change {
obj.changed(change)
}
obj.notify_category_type();
}
}
}
glib::wrapper! {
/// A filter by `CategoryType`.
pub struct CategoryFilter(ObjectSubclass<imp::CategoryFilter>)
@extends gtk::Filter;
}
impl CategoryFilter {
pub fn new(expression: impl AsRef<gtk::Expression>, category_type: CategoryType) -> Self {
glib::Object::builder()
.property("expression", expression.as_ref())
.property("category-type", category_type)
.build()
}
}

View File

@ -0,0 +1,176 @@
use gtk::{
gio, glib,
glib::{clone, closure},
prelude::*,
subclass::prelude::*,
};
mod category_filter;
mod category_type;
use self::category_filter::CategoryFilter;
pub use self::category_type::CategoryType;
use super::{SidebarItem, SidebarItemExt, SidebarItemImpl};
use crate::{
session::model::{Room, RoomList, RoomType},
utils::ExpressionListModel,
};
mod imp {
use std::{
cell::{Cell, OnceCell},
marker::PhantomData,
};
use super::*;
#[derive(Debug, Default, glib::Properties)]
#[properties(wrapper_type = super::Category)]
pub struct Category {
/// The filter list model on this category.
#[property(get, set = Self::set_model, construct_only)]
pub model: OnceCell<gio::ListModel>,
/// The type of this category.
#[property(get, construct_only, builder(CategoryType::default()))]
pub r#type: Cell<CategoryType>,
/// Whether this category is empty.
#[property(get)]
pub empty: Cell<bool>,
/// The display name of this category.
#[property(get = Self::display_name)]
pub display_name: PhantomData<String>,
}
#[glib::object_subclass]
impl ObjectSubclass for Category {
const NAME: &'static str = "Category";
type Type = super::Category;
type ParentType = SidebarItem;
type Interfaces = (gio::ListModel,);
}
#[glib::derived_properties]
impl ObjectImpl for Category {}
impl ListModelImpl for Category {
fn item_type(&self) -> glib::Type {
SidebarItem::static_type()
}
fn n_items(&self) -> u32 {
self.model.get().unwrap().n_items()
}
fn item(&self, position: u32) -> Option<glib::Object> {
self.model.get().unwrap().item(position)
}
}
impl SidebarItemImpl for Category {
fn update_visibility(&self, for_category: CategoryType) {
let obj = self.obj();
let visible = if !obj.empty() {
true
} else {
let room_types =
RoomType::try_from(for_category)
.ok()
.and_then(|source_room_type| {
RoomType::try_from(obj.r#type())
.ok()
.map(|target_room_type| (source_room_type, target_room_type))
});
room_types.map_or(false, |(source_room_type, target_room_type)| {
source_room_type.can_change_to(target_room_type)
})
};
obj.set_visible(visible)
}
}
impl Category {
/// Set the filter list model of this category.
fn set_model(&self, model: gio::ListModel) {
let obj = self.obj();
let type_ = self.r#type.get();
// Special case room lists so that they are sorted and in the right category
let model = if model.is::<RoomList>() {
let room_category_type = Room::this_expression("category")
.chain_closure::<CategoryType>(closure!(
|_: Option<glib::Object>, room_type: RoomType| {
CategoryType::from(room_type)
}
));
let filter = CategoryFilter::new(&room_category_type, type_);
let category_type_expr_model = ExpressionListModel::new();
category_type_expr_model.set_expressions(vec![room_category_type.upcast()]);
category_type_expr_model.set_model(Some(model));
let filter_model =
gtk::FilterListModel::new(Some(category_type_expr_model), Some(filter));
let room_latest_activity = Room::this_expression("latest-activity");
let sorter = gtk::NumericSorter::builder()
.expression(&room_latest_activity)
.sort_order(gtk::SortType::Descending)
.build();
let latest_activity_expr_model = ExpressionListModel::new();
latest_activity_expr_model.set_expressions(vec![room_latest_activity.upcast()]);
latest_activity_expr_model.set_model(Some(filter_model.upcast()));
let sort_model =
gtk::SortListModel::new(Some(latest_activity_expr_model), Some(sorter));
sort_model.upcast()
} else {
model
};
model.connect_items_changed(clone!(@weak obj => move |model, pos, removed, added| {
obj.items_changed(pos, removed, added);
obj.imp().set_empty(model.n_items() == 0);
}));
self.set_empty(model.n_items() == 0);
self.model.set(model).unwrap();
}
/// Set whether this category is empty.
fn set_empty(&self, empty: bool) {
if empty == self.empty.get() {
return;
}
self.empty.set(empty);
self.obj().notify_empty();
}
/// The display name of this category.
fn display_name(&self) -> String {
self.r#type.get().to_string()
}
}
}
glib::wrapper! {
/// A list of Items in the same category, implementing ListModel.
///
/// This struct is used in ItemList for the sidebar.
pub struct Category(ObjectSubclass<imp::Category>)
@extends SidebarItem,
@implements gio::ListModel;
}
impl Category {
pub fn new(type_: CategoryType, model: &impl IsA<gio::ListModel>) -> Self {
glib::Object::builder()
.property("type", type_)
.property("model", model)
.build()
}
}

View File

@ -0,0 +1,100 @@
use std::fmt;
use gettextrs::gettext;
use gtk::{glib, prelude::*, subclass::prelude::*};
use super::{CategoryType, SidebarItem, SidebarItemExt, SidebarItemImpl};
#[derive(Debug, Default, Hash, Eq, PartialEq, Clone, Copy, glib::Enum)]
#[repr(u32)]
#[enum_type(name = "ItemType")]
pub enum ItemType {
#[default]
Explore = 0,
Forget = 1,
}
impl ItemType {
/// The icon name for this item type.
pub fn icon_name(&self) -> &'static str {
match self {
Self::Explore => "explore-symbolic",
Self::Forget => "user-trash-symbolic",
}
}
}
impl fmt::Display for ItemType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let label = match self {
Self::Explore => gettext("Explore"),
Self::Forget => gettext("Forget Room"),
};
f.write_str(&label)
}
}
mod imp {
use std::{cell::Cell, marker::PhantomData};
use super::*;
#[derive(Debug, Default, glib::Properties)]
#[properties(wrapper_type = super::IconItem)]
pub struct IconItem {
/// The type of this item.
#[property(get, construct_only, builder(ItemType::default()))]
pub r#type: Cell<ItemType>,
/// The display name of this item.
#[property(get = Self::display_name)]
pub display_name: PhantomData<String>,
/// The icon name used for this item.
#[property(get = Self::icon_name)]
pub icon_name: PhantomData<String>,
}
#[glib::object_subclass]
impl ObjectSubclass for IconItem {
const NAME: &'static str = "IconItem";
type Type = super::IconItem;
type ParentType = SidebarItem;
}
#[glib::derived_properties]
impl ObjectImpl for IconItem {}
impl SidebarItemImpl for IconItem {
fn update_visibility(&self, for_category: CategoryType) {
let obj = self.obj();
match self.r#type.get() {
ItemType::Explore => obj.set_visible(true),
ItemType::Forget => obj.set_visible(for_category == CategoryType::Left),
}
}
}
impl IconItem {
/// The display name of this item.
fn display_name(&self) -> String {
self.r#type.get().to_string()
}
/// The icon name used for this item.
fn icon_name(&self) -> String {
self.r#type.get().icon_name().to_owned()
}
}
}
glib::wrapper! {
/// A top-level row in the sidebar with an icon.
pub struct IconItem(ObjectSubclass<imp::IconItem>) @extends SidebarItem;
}
impl IconItem {
pub fn new(type_: ItemType) -> Self {
glib::Object::builder().property("type", type_).build()
}
}

View File

@ -5,8 +5,6 @@ use super::CategoryType;
mod imp {
use std::cell::Cell;
use once_cell::sync::Lazy;
use super::*;
#[repr(C)]
@ -27,9 +25,11 @@ mod imp {
(klass.as_ref().update_visibility)(this, for_category)
}
#[derive(Debug)]
#[derive(Debug, glib::Properties)]
#[properties(wrapper_type = super::SidebarItem)]
pub struct SidebarItem {
/// Whether this item is visible.
#[property(get, set = Self::set_visible, explicit_notify, default = true)]
pub visible: Cell<bool>,
}
@ -49,30 +49,18 @@ mod imp {
type Class = SidebarItemClass;
}
impl ObjectImpl for SidebarItem {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![glib::ParamSpecBoolean::builder("visible")
.default_value(true)
.explicit_notify()
.build()]
});
#[glib::derived_properties]
impl ObjectImpl for SidebarItem {}
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"visible" => self.obj().set_visible(value.get().unwrap()),
_ => unimplemented!(),
impl SidebarItem {
/// Set whether this item is visible.
fn set_visible(&self, visible: bool) {
if self.visible.get() == visible {
return;
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"visible" => self.obj().visible().to_value(),
_ => unimplemented!(),
}
self.visible.set(visible);
self.obj().notify_visible();
}
}
}
@ -102,16 +90,11 @@ pub trait SidebarItemExt: 'static {
impl<O: IsA<SidebarItem>> SidebarItemExt for O {
fn visible(&self) -> bool {
self.upcast_ref().imp().visible.get()
self.upcast_ref().visible()
}
fn set_visible(&self, visible: bool) {
if self.visible() == visible {
return;
}
self.upcast_ref().imp().visible.set(visible);
self.notify("visible");
self.upcast_ref().set_visible(visible);
}
fn update_visibility(&self, for_category: CategoryType) {

View File

@ -4,20 +4,25 @@ use super::{Category, CategoryType, IconItem, ItemType, SidebarItem, SidebarItem
use crate::session::model::{RoomList, VerificationList};
mod imp {
use std::cell::Cell;
use once_cell::{sync::Lazy, unsync::OnceCell};
use std::cell::{Cell, OnceCell};
use super::*;
#[derive(Debug, Default)]
#[derive(Debug, Default, glib::Properties)]
#[properties(wrapper_type = super::ItemList)]
pub struct ItemList {
pub list: OnceCell<[SidebarItem; 8]>,
/// The list of rooms.
#[property(get, construct_only)]
pub room_list: OnceCell<RoomList>,
/// The list of verification requests.
#[property(get, construct_only)]
pub verification_list: OnceCell<VerificationList>,
/// The `CategoryType` to show all compatible categories for.
///
/// Uses `RoomType::can_change_to` to find compatible categories.
/// The UI is updated to show possible actions for the list items
/// according to the `CategoryType`.
#[property(get, set = Self::set_show_all_for_category, explicit_notify, builder(CategoryType::default()))]
pub show_all_for_category: Cell<CategoryType>,
}
@ -28,47 +33,8 @@ mod imp {
type Interfaces = (gio::ListModel,);
}
#[glib::derived_properties]
impl ObjectImpl for ItemList {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecObject::builder::<RoomList>("room-list")
.construct_only()
.build(),
glib::ParamSpecObject::builder::<VerificationList>("verification-list")
.construct_only()
.build(),
glib::ParamSpecEnum::builder::<CategoryType>("show-all-for-category")
.explicit_notify()
.build(),
]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
let obj = self.obj();
match pspec.name() {
"room-list" => obj.set_room_list(value.get().unwrap()),
"verification-list" => obj.set_verification_list(value.get().unwrap()),
"show-all-for-category" => obj.set_show_all_for_category(value.get().unwrap()),
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
let obj = self.obj();
match pspec.name() {
"room-list" => obj.room_list().to_value(),
"verification-list" => obj.verification_list().to_value(),
"show-all-for-category" => obj.show_all_for_category().to_value(),
_ => unimplemented!(),
}
}
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
@ -78,12 +44,12 @@ mod imp {
let list: [SidebarItem; 8] = [
IconItem::new(ItemType::Explore).upcast(),
Category::new(CategoryType::VerificationRequest, verification_list).upcast(),
Category::new(CategoryType::Invited, room_list).upcast(),
Category::new(CategoryType::Favorite, room_list).upcast(),
Category::new(CategoryType::Normal, room_list).upcast(),
Category::new(CategoryType::LowPriority, room_list).upcast(),
Category::new(CategoryType::Left, room_list).upcast(),
Category::new(CategoryType::VerificationRequest, &verification_list).upcast(),
Category::new(CategoryType::Invited, &room_list).upcast(),
Category::new(CategoryType::Favorite, &room_list).upcast(),
Category::new(CategoryType::Normal, &room_list).upcast(),
Category::new(CategoryType::LowPriority, &room_list).upcast(),
Category::new(CategoryType::Left, &room_list).upcast(),
IconItem::new(ItemType::Forget).upcast(),
];
@ -128,6 +94,23 @@ mod imp {
.map(|item| item.upcast())
}
}
impl ItemList {
/// Set the `CategoryType` to show all compatible categories for.
fn set_show_all_for_category(&self, category: CategoryType) {
if category == self.show_all_for_category.get() {
return;
}
let obj = self.obj();
self.show_all_for_category.set(category);
for item in self.list.get().unwrap().iter() {
obj.update_item(item)
}
obj.notify_show_all_for_category();
}
}
}
glib::wrapper! {
@ -147,50 +130,6 @@ impl ItemList {
.build()
}
/// The `CategoryType` to show all compatible categories for.
///
/// The UI is updated to show possible actions for the list items according
/// to the `CategoryType`.
pub fn show_all_for_category(&self) -> CategoryType {
self.imp().show_all_for_category.get()
}
/// Set the `CategoryType` to show all compatible categories for.
pub fn set_show_all_for_category(&self, category: CategoryType) {
let imp = self.imp();
if category == self.show_all_for_category() {
return;
}
imp.show_all_for_category.set(category);
for item in imp.list.get().unwrap().iter() {
self.update_item(item)
}
self.notify("show-all-for-category");
}
/// Set the list of rooms.
fn set_room_list(&self, room_list: RoomList) {
self.imp().room_list.set(room_list).unwrap();
}
/// Set the list of verification requests.
fn set_verification_list(&self, verification_list: VerificationList) {
self.imp().verification_list.set(verification_list).unwrap();
}
/// The list of rooms.
pub fn room_list(&self) -> &RoomList {
self.imp().room_list.get().unwrap()
}
/// The list of verification requests.
pub fn verification_list(&self) -> &VerificationList {
self.imp().verification_list.get().unwrap()
}
fn update_item(&self, item: &impl IsA<SidebarItem>) {
let imp = self.imp();
let item = item.upcast_ref::<SidebarItem>();

View File

@ -0,0 +1,78 @@
use gtk::{glib, prelude::*, subclass::prelude::*};
use super::{item_list::ItemList, selection::Selection};
use crate::session::model::Room;
mod imp {
use std::cell::OnceCell;
use super::*;
#[derive(Debug, Default, glib::Properties)]
#[properties(wrapper_type = super::SidebarListModel)]
pub struct SidebarListModel {
/// The list of items in the sidebar.
#[property(get, set = Self::set_item_list, construct_only)]
pub item_list: OnceCell<ItemList>,
/// The tree list model.
#[property(get)]
pub tree_model: OnceCell<gtk::TreeListModel>,
/// The string filter.
#[property(get)]
pub string_filter: gtk::StringFilter,
/// The selection model.
#[property(get)]
pub selection_model: Selection,
}
#[glib::object_subclass]
impl ObjectSubclass for SidebarListModel {
const NAME: &'static str = "SidebarListModel";
type Type = super::SidebarListModel;
}
#[glib::derived_properties]
impl ObjectImpl for SidebarListModel {}
impl SidebarListModel {
/// Set the list of items in the sidebar.
fn set_item_list(&self, item_list: ItemList) {
self.item_list.set(item_list.clone()).unwrap();
let tree_model = gtk::TreeListModel::new(item_list, false, true, |item| {
item.clone().downcast().ok()
});
self.tree_model.set(tree_model.clone()).unwrap();
let room_expression =
gtk::TreeListRow::this_expression("item").chain_property::<Room>("display-name");
self.string_filter
.set_match_mode(gtk::StringFilterMatchMode::Substring);
self.string_filter.set_expression(Some(&room_expression));
self.string_filter.set_ignore_case(true);
// Default to an empty string to be able to bind to GtkEditable::text.
self.string_filter.set_search(Some(""));
let filter_model =
gtk::FilterListModel::new(Some(tree_model), Some(self.string_filter.clone()));
self.selection_model.set_model(Some(filter_model));
}
}
}
glib::wrapper! {
/// A wrapper for the sidebar list model of a `Session`.
///
/// It allows to keep the state for selection and filtering.
pub struct SidebarListModel(ObjectSubclass<imp::SidebarListModel>);
}
impl SidebarListModel {
/// Create a new `SidebarListModel`.
pub fn new(item_list: &ItemList) -> Self {
glib::Object::builder()
.property("item-list", item_list)
.build()
}
}

View File

@ -0,0 +1,265 @@
use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*};
use crate::utils::BoundObject;
mod imp {
use std::cell::{Cell, RefCell};
use super::*;
#[derive(Debug, glib::Properties)]
#[properties(wrapper_type = super::Selection)]
pub struct Selection {
/// The underlying model.
#[property(get, set = Self::set_model, explicit_notify, nullable)]
pub model: BoundObject<gio::ListModel>,
/// The position of the selected item.
#[property(get, set = Self::set_selected, explicit_notify, default = gtk::INVALID_LIST_POSITION)]
pub selected: Cell<u32>,
/// The selected item.
#[property(get, set = Self::set_selected_item, explicit_notify, nullable)]
pub selected_item: RefCell<Option<glib::Object>>,
}
impl Default for Selection {
fn default() -> Self {
Self {
model: Default::default(),
selected: Cell::new(gtk::INVALID_LIST_POSITION),
selected_item: Default::default(),
}
}
}
#[glib::object_subclass]
impl ObjectSubclass for Selection {
const NAME: &'static str = "SidebarSelection";
type Type = super::Selection;
type Interfaces = (gio::ListModel, gtk::SelectionModel);
}
#[glib::derived_properties]
impl ObjectImpl for Selection {}
impl ListModelImpl for Selection {
fn item_type(&self) -> glib::Type {
gtk::TreeListRow::static_type()
}
fn n_items(&self) -> u32 {
self.model
.obj()
.as_ref()
.map(|m| m.n_items())
.unwrap_or_default()
}
fn item(&self, position: u32) -> Option<glib::Object> {
self.model.obj().as_ref().and_then(|m| m.item(position))
}
}
impl SelectionModelImpl for Selection {
fn selection_in_range(&self, _position: u32, _n_items: u32) -> gtk::Bitset {
let bitset = gtk::Bitset::new_empty();
let selected = self.selected.get();
if selected != gtk::INVALID_LIST_POSITION {
bitset.add(selected);
}
bitset
}
fn is_selected(&self, position: u32) -> bool {
self.selected.get() == position
}
}
impl Selection {
/// Set the underlying model.
fn set_model(&self, model: Option<gio::ListModel>) {
let obj = self.obj();
let _guard = obj.freeze_notify();
let model = model.map(|m| m.clone().upcast());
let old_model = self.model.obj();
if old_model == model {
return;
}
let n_items_before = old_model.map(|model| model.n_items()).unwrap_or(0);
self.model.disconnect_signals();
if let Some(model) = model {
let items_changed_handler =
model.connect_items_changed(clone!(@weak obj => move |m, p, r, a| {
obj.items_changed_cb(m, p, r, a);
}));
obj.items_changed_cb(&model, 0, n_items_before, model.n_items());
self.model.set(model, vec![items_changed_handler]);
} else {
if self.selected.get() != gtk::INVALID_LIST_POSITION {
self.selected.replace(gtk::INVALID_LIST_POSITION);
obj.notify_selected();
}
if self.selected_item.borrow().is_some() {
self.selected_item.replace(None);
obj.notify_selected_item();
}
obj.items_changed(0, n_items_before, 0);
}
obj.notify_model();
}
/// Set the selected item by its position.
fn set_selected(&self, position: u32) {
let old_selected = self.selected.get();
if old_selected == position {
return;
}
let selected_item = self
.model
.obj()
.and_then(|m| m.item(position))
.and_downcast::<gtk::TreeListRow>()
.and_then(|r| r.item());
let selected = if selected_item.is_none() {
gtk::INVALID_LIST_POSITION
} else {
position
};
if old_selected == selected {
return;
}
let obj = self.obj();
self.selected.replace(selected);
self.selected_item.replace(selected_item);
if old_selected == gtk::INVALID_LIST_POSITION {
obj.selection_changed(selected, 1);
} else if selected == gtk::INVALID_LIST_POSITION {
obj.selection_changed(old_selected, 1);
} else if selected < old_selected {
obj.selection_changed(selected, old_selected - selected + 1);
} else {
obj.selection_changed(old_selected, selected - old_selected + 1);
}
obj.notify_selected();
obj.notify_selected_item();
}
/// Set the selected item.
fn set_selected_item(&self, item: Option<glib::Object>) {
if self.selected_item.borrow().as_ref() == item.as_ref() {
return;
}
let obj = self.obj();
let old_selected = self.selected.get();
let mut selected = gtk::INVALID_LIST_POSITION;
if item.is_some() {
if let Some(model) = self.model.obj() {
for i in 0..model.n_items() {
let current_item = model
.item(i)
.and_downcast::<gtk::TreeListRow>()
.and_then(|r| r.item());
if current_item == item {
selected = i;
break;
}
}
}
}
self.selected_item.replace(item);
if old_selected != selected {
self.selected.replace(selected);
if old_selected == gtk::INVALID_LIST_POSITION {
obj.selection_changed(selected, 1);
} else if selected == gtk::INVALID_LIST_POSITION {
obj.selection_changed(old_selected, 1);
} else if selected < old_selected {
obj.selection_changed(selected, old_selected - selected + 1);
} else {
obj.selection_changed(old_selected, selected - old_selected + 1);
}
obj.notify_selected();
}
obj.notify_selected_item();
}
}
}
glib::wrapper! {
/// A `GtkSelectionModel` that keeps track of the selected item even if its position changes or it is removed from the list.
pub struct Selection(ObjectSubclass<imp::Selection>)
@implements gio::ListModel, gtk::SelectionModel;
}
impl Selection {
pub fn new<P: IsA<gio::ListModel>>(model: Option<&P>) -> Selection {
let model = model.map(|m| m.clone().upcast());
glib::Object::builder().property("model", &model).build()
}
fn items_changed_cb(&self, model: &gio::ListModel, position: u32, removed: u32, added: u32) {
let imp = self.imp();
let _guard = self.freeze_notify();
let selected = self.selected();
let selected_item = self.selected_item();
if selected_item.is_none() || selected < position {
// unchanged
} else if selected != gtk::INVALID_LIST_POSITION && selected >= position + removed {
imp.selected.replace(selected + added - removed);
self.notify_selected();
} else {
for i in 0..=added {
if i == added {
// the item really was deleted
imp.selected.replace(gtk::INVALID_LIST_POSITION);
self.notify_selected();
} else {
let item = model
.item(position + i)
.and_downcast::<gtk::TreeListRow>()
.and_then(|r| r.item());
if item == selected_item {
// the item moved
if selected != position + i {
imp.selected.replace(position + i);
self.notify_selected();
}
break;
}
}
}
}
self.items_changed(position, removed, added);
}
}
impl Default for Selection {
fn default() -> Self {
Self::new(gio::ListModel::NONE)
}
}

View File

@ -192,7 +192,7 @@ impl PublicRoomList {
.chunk
.into_iter()
.map(|matrix_room| {
let room = PublicRoom::new(room_list, &server);
let room = PublicRoom::new(&room_list, &server);
room.set_matrix_public_room(matrix_room);
room
})
@ -200,7 +200,7 @@ impl PublicRoomList {
let empty_row = list
.pop()
.unwrap_or_else(|| PublicRoom::new(room_list, &server));
.unwrap_or_else(|| PublicRoom::new(&room_list, &server));
list.append(&mut new_rooms);
if !self.complete() {

View File

@ -253,7 +253,7 @@ impl Content {
}
Some(o)
if o.downcast_ref::<IconItem>()
.is_some_and(|i| i.type_() == ItemType::Explore) =>
.is_some_and(|i| i.r#type() == ItemType::Explore) =>
{
imp.explore.init();
imp.stack.set_visible_child(&*imp.explore);

View File

@ -146,7 +146,7 @@ impl CategoryRow {
/// The label to show for this row.
pub fn label(&self) -> Option<String> {
let to_type = self.category()?.type_();
let to_type = self.category()?.r#type();
let from_type = self.show_label_for_category();
let label = match from_type {

View File

@ -86,7 +86,7 @@ impl IconItemRow {
if icon_item
.as_ref()
.is_some_and(|i| i.type_() == ItemType::Forget)
.is_some_and(|i| i.r#type() == ItemType::Forget)
{
self.add_css_class("forget");
} else {

View File

@ -233,7 +233,7 @@ impl Sidebar {
let bindings = vec![
self.bind_property(
"drop-source-type",
list_model.item_list(),
&list_model.item_list(),
"show-all-for-category",
)
.sync_create()
@ -250,7 +250,7 @@ impl Sidebar {
}
imp.listview
.set_model(list_model.as_ref().map(|m| m.selection_model()));
.set_model(list_model.as_ref().map(|m| m.selection_model()).as_ref());
imp.list_model.set(list_model.as_ref());
self.notify("list-model");
}

View File

@ -248,7 +248,7 @@ impl Row {
Some(room.category())
} else {
item.downcast_ref::<Category>()
.and_then(|category| RoomType::try_from(category.type_()).ok())
.and_then(|category| RoomType::try_from(category.r#type()).ok())
}
}
@ -258,7 +258,7 @@ impl Row {
pub fn item_type(&self) -> Option<ItemType> {
self.item()
.and_downcast_ref::<IconItem>()
.map(|i| i.type_())
.map(|i| i.r#type())
}
/// Handle the drag-n-drop hovering this row.
@ -367,7 +367,7 @@ impl Row {
if self
.item()
.and_downcast::<Category>()
.map_or(false, |category| category.is_empty())
.map_or(false, |category| category.empty())
{
self.add_css_class("drop-empty");
} else {