sidebar-data: Port to glib::Properties macro
This commit is contained in:
parent
f1a923f402
commit
5d9b9e61b3
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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) -> >k::TreeListModel {
|
||||
self.imp().tree_model.get().unwrap()
|
||||
}
|
||||
|
||||
/// The string filter.
|
||||
pub fn string_filter(&self) -> >k::StringFilter {
|
||||
&self.imp().string_filter
|
||||
}
|
||||
|
||||
/// The selection model.
|
||||
pub fn selection_model(&self) -> &Selection {
|
||||
&self.imp().selection_model
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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) {
|
|
@ -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>();
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue