Browse Source

session: Use new GtkFilterListModel watch-items property

To replace ExpressionListModel where possible. It should be more
performant because it only triggers an `items-changed` signal if the
item filtering changed.

This was added in GTK 4.20 but we need at least 4.20.2 which includes a
bug fix.
fractal-13
Kévin Commaille 5 months ago
parent
commit
a537fe564a
No known key found for this signature in database
GPG Key ID: F26F4BE20A08255B
  1. 4
      Cargo.toml
  2. 4
      meson.build
  3. 37
      src/session/room/member_list.rs
  4. 20
      src/session/sidebar_data/section/mod.rs
  5. 43
      src/session_view/room_history/message_toolbar/completion/member_list.rs
  6. 77
      src/session_view/room_history/message_toolbar/completion/room_list.rs
  7. 6
      src/utils/expression_list_model.rs

4
Cargo.toml

@ -61,14 +61,14 @@ wtinylfu = "0.2"
zeroize = "1"
# gtk-rs project and dependents. These usually need to be updated together.
adw = { package = "libadwaita", version = "0.8", features = ["v1_7"] }
adw = { package = "libadwaita", version = "0.8", features = ["v1_8"] }
glycin = { version = "3", default-features = false, features = ["tokio", "gdk4"] }
gst = { version = "0.24", package = "gstreamer" }
gst_app = { version = "0.24", package = "gstreamer-app" }
gst_pbutils = { version = "0.24", package = "gstreamer-pbutils" }
gst_play = { version = "0.24", package = "gstreamer-play" }
gst_video = { version = "0.24", package = "gstreamer-video" }
gtk = { package = "gtk4", version = "0.10", features = ["gnome_47"] }
gtk = { package = "gtk4", version = "0.10", features = ["gnome_48", "v4_20"] } # FIXME: Should be gnome_49 feature
shumate = { package = "libshumate", version = "0.7", features = ["v1_1"] }
sourceview = { package = "sourceview5", version = "0.10" }

4
meson.build

@ -24,8 +24,8 @@ full_version = version
dependency('glib-2.0', version: '>= 2.82') # update when changing gtk version
dependency('gio-2.0', version: '>= 2.82') # always same version as glib
dependency('gtk4', version: '>= 4.19.3')
dependency('libadwaita-1', version: '>= 1.8.beta')
dependency('gtk4', version: '>= 4.20.2')
dependency('libadwaita-1', version: '>= 1.8.0')
# Please keep these dependencies sorted.
dependency('gstreamer-1.0', version: '>= 1.20')

37
src/session/room/member_list.rs

@ -12,11 +12,7 @@ use ruma::{OwnedUserId, UserId, events::room::power_levels::RoomPowerLevels};
use tracing::error;
use super::{Event, Member, Membership, Room};
use crate::{
prelude::*,
spawn, spawn_tokio,
utils::{ExpressionListModel, LoadingState},
};
use crate::{prelude::*, spawn, spawn_tokio, utils::LoadingState};
mod imp {
use std::cell::{Cell, RefCell};
@ -100,7 +96,7 @@ mod imp {
}
// Construct the list if it doesn't exist.
let list = kind.filtered_list_model(self.obj().upcast_ref::<gio::ListModel>().clone());
let list = kind.filtered_list_model(self.obj().upcast_ref());
self.membership_lists
.borrow_mut()
.insert(kind, list.clone());
@ -346,27 +342,20 @@ pub enum MembershipListKind {
impl MembershipListKind {
/// Build a `GListModel` that filters the given list model containing
/// [`Member`]s with this kind, and add it to the given map.
fn filtered_list_model(self, members: gio::ListModel) -> gio::ListModel {
let membership_expr = Member::this_expression("membership");
// We need to notify when the membership changes so the filter can update the
// list.
let expr_members = ExpressionListModel::new();
expr_members.set_expressions(vec![membership_expr.clone().upcast()]);
expr_members.set_model(Some(members));
fn filtered_list_model(self, members: &gio::ListModel) -> gio::ListModel {
let membership = Membership::from(self);
let membership_eq_expr = membership_expr.chain_closure::<bool>(closure!(
|_: Option<glib::Object>, this_membership: Membership| {
let membership_eq_expr = Member::this_expression("membership").chain_closure::<bool>(
closure!(|_: Option<glib::Object>, this_membership: Membership| {
this_membership == membership
}
));
}),
);
gtk::FilterListModel::new(
Some(expr_members),
Some(gtk::BoolFilter::new(Some(&membership_eq_expr))),
)
.upcast()
gtk::FilterListModel::builder()
.model(members)
.filter(&gtk::BoolFilter::new(Some(&membership_eq_expr)))
.watch_items(true)
.build()
.upcast()
}
/// The name of the icon that represents this kind.

20
src/session/sidebar_data/section/mod.rs

@ -105,19 +105,16 @@ mod imp {
// Special-case room lists so that they are sorted and in the right section.
let inner_model = if model.is::<RoomList>() {
let room_category = Room::this_expression("category");
// Filter the list to only show rooms for the proper category.
self.filter
.set_expression(Some(room_category.clone().upcast()));
let section_name_expr_model = ExpressionListModel::new();
section_name_expr_model.set_expressions(vec![room_category.upcast()]);
section_name_expr_model.set_model(Some(model));
let filter_model = gtk::FilterListModel::new(
Some(section_name_expr_model),
Some(self.filter.clone()),
);
.set_expression(Some(Room::this_expression("category").upcast()));
let filter_model = gtk::FilterListModel::builder()
.model(&model)
.filter(&self.filter)
.watch_items(true)
.build();
// Sort the list by activity.
let room_latest_activity = Room::this_expression("latest-activity");
let sorter = gtk::NumericSorter::builder()
.expression(&room_latest_activity)
@ -131,6 +128,7 @@ mod imp {
let sort_model =
gtk::SortListModel::new(Some(latest_activity_expr_model), Some(sorter));
// Watch for notification count and highlight changes in the filtered room list.
let room_notification_count = Room::this_expression("notification-count");
let room_highlight = Room::this_expression("highlight");
let notification_and_highlight_expr_model = ExpressionListModel::new();

43
src/session_view/room_history/message_toolbar/completion/member_list.rs

@ -25,8 +25,8 @@ mod imp {
/// The list of room members used for completion.
#[property(get)]
joined_members: glib::WeakRef<gio::ListModel>,
/// The members list with expression watches.
members_expr: ExpressionListModel,
/// The filtered members list.
filtered_members: gtk::FilterListModel,
permissions_handler: RefCell<Option<glib::SignalHandlerId>>,
/// The list model for the `@room` item.
at_room_model: SingleItemListModel,
@ -52,12 +52,12 @@ mod imp {
// - not our user
// - not ignored
let not_own_user = gtk::BoolFilter::builder()
.expression(expression::not(Member::this_expression("is-own-user")))
.expression(Member::this_expression("is-own-user"))
.invert(true)
.build();
let ignored_expr = Member::this_expression("is-ignored");
let not_ignored = gtk::BoolFilter::builder()
.expression(&ignored_expr)
.expression(Member::this_expression("is-ignored"))
.invert(true)
.build();
@ -65,22 +65,29 @@ mod imp {
filter.append(not_own_user);
filter.append(not_ignored);
let first_model = gtk::FilterListModel::builder()
.filter(&filter)
.model(&self.members_expr)
.build();
self.filtered_members.set_filter(Some(&filter));
self.filtered_members.set_watch_items(true);
// Sort the members list by activity, then display name.
// Watch the activity and display name of members.
let latest_activity_expr = Member::this_expression("latest-activity");
let display_name_expr = Member::this_expression("display-name");
let expr_model = ExpressionListModel::new();
expr_model.set_expressions(vec![
latest_activity_expr.clone().upcast(),
display_name_expr.clone().upcast(),
]);
expr_model.set_model(Some(self.filtered_members.clone()));
// Sort the members list by activity, then display name.
let activity = gtk::NumericSorter::builder()
.sort_order(gtk::SortType::Descending)
.expression(&latest_activity_expr)
.expression(latest_activity_expr)
.build();
let display_name_expr = Member::this_expression("display-name");
let display_name = gtk::StringSorter::builder()
.ignore_case(true)
.expression(&display_name_expr)
.expression(display_name_expr)
.build();
let sorter = gtk::MultiSorter::new();
@ -88,7 +95,7 @@ mod imp {
sorter.append(display_name);
let sorted_members_model = gtk::SortListModel::builder()
.sorter(&sorter)
.model(&first_model)
.model(&expr_model)
.build();
// Add `@room` model.
@ -117,12 +124,6 @@ mod imp {
self.list.set_filter(Some(&self.search_filter));
self.list.set_model(Some(&flatten_model));
self.members_expr.set_expressions(vec![
ignored_expr.upcast(),
latest_activity_expr.upcast(),
display_name_expr.upcast(),
]);
}
fn dispose(&self) {
@ -175,7 +176,7 @@ mod imp {
let joined_members = room
.map(Room::get_or_create_members)
.map(|members| members.membership_list(MembershipListKind::Join));
self.members_expr.set_model(joined_members);
self.filtered_members.set_model(joined_members.as_ref());
self.update_at_room_model();
self.obj().notify_joined_members();

77
src/session_view/room_history/message_toolbar/completion/room_list.rs

@ -16,8 +16,8 @@ mod imp {
/// The rooms used for completion.
#[property(get = Self::rooms, set = Self::set_rooms, explicit_notify, nullable)]
rooms: PhantomData<Option<RoomList>>,
/// The room list with expression watches.
rooms_expr: ExpressionListModel,
/// The filtered room list.
filtered_rooms: gtk::FilterListModel,
/// The search filter.
search_filter: gtk::StringFilter,
/// The list of sorted and filtered rooms.
@ -41,55 +41,58 @@ mod imp {
// - joined
// - anyone can join
let category_expr = Room::this_expression("category").chain_closure::<bool>(closure!(
|_obj: Option<glib::Object>, category: RoomCategory| {
!matches!(category, RoomCategory::Space | RoomCategory::Outdated)
}
let category_filter = gtk::BoolFilter::new(Some(
Room::this_expression("category").chain_closure::<bool>(closure!(
|_obj: Option<glib::Object>, category: RoomCategory| {
!matches!(category, RoomCategory::Space | RoomCategory::Outdated)
}
)),
));
let category_filter = gtk::BoolFilter::new(Some(&category_expr));
let joined_expr = Room::this_expression("own-member")
.chain_property::<Member>("membership")
.chain_closure::<bool>(closure!(
|_obj: Option<glib::Object>, membership: Membership| {
membership == Membership::Join
}
));
let joined_filter = gtk::BoolFilter::new(Some(&joined_expr));
let joined_filter = gtk::BoolFilter::new(Some(
Room::this_expression("own-member")
.chain_property::<Member>("membership")
.chain_closure::<bool>(closure!(
|_obj: Option<glib::Object>, membership: Membership| {
membership == Membership::Join
}
)),
));
let anyone_can_join_expr =
Room::this_expression("join-rule").chain_property::<JoinRule>("anyone-can-join");
let anyone_can_join_filter = gtk::BoolFilter::builder()
.expression(&anyone_can_join_expr)
.build();
let anyone_can_join_filter = gtk::BoolFilter::new(Some(
Room::this_expression("join-rule").chain_property::<JoinRule>("anyone-can-join"),
));
let filter = gtk::EveryFilter::new();
filter.append(category_filter);
filter.append(joined_filter);
filter.append(anyone_can_join_filter);
let first_model = gtk::FilterListModel::builder()
.filter(&filter)
.model(&self.rooms_expr)
.build();
self.filtered_rooms.set_filter(Some(&filter));
// Sort list by display name.
// Watch display name to update the sorter.
let display_name_expr = Room::this_expression("display-name");
let expr_model = ExpressionListModel::new();
expr_model.set_expressions(vec![display_name_expr.clone().upcast()]);
expr_model.set_model(Some(self.filtered_rooms.clone()));
// Sort list by display name.
let display_name_sorter = gtk::StringSorter::builder()
.ignore_case(true)
.expression(&display_name_expr)
.build();
let second_model = gtk::SortListModel::builder()
let sorted_model = gtk::SortListModel::builder()
.sorter(&display_name_sorter)
.model(&first_model)
.model(&expr_model)
.build();
// Setup the search filter.
// Set up the search filter.
let alias_expr =
Room::this_expression("aliases").chain_property::<RoomAliases>("alias-string");
let room_search_string_expr = gtk::ClosureExpression::new::<String>(
&[alias_expr.clone(), display_name_expr.clone()],
&[alias_expr, display_name_expr],
closure!(
|_: Option<glib::Object>, alias: Option<&str>, display_name: &str| {
if let Some(alias) = alias {
@ -107,31 +110,25 @@ mod imp {
.set_expression(Some(expression::normalize_string(room_search_string_expr)));
self.list.set_filter(Some(&self.search_filter));
self.list.set_model(Some(&second_model));
self.rooms_expr.set_expressions(vec![
category_expr.upcast(),
joined_expr.upcast(),
anyone_can_join_expr.upcast(),
alias_expr.upcast(),
display_name_expr.upcast(),
]);
self.list.set_watch_items(true);
self.list.set_model(Some(&sorted_model));
}
}
impl CompletionRoomList {
/// The rooms used for completion.
fn rooms(&self) -> Option<RoomList> {
self.rooms_expr.model().and_downcast()
self.filtered_rooms.model().and_downcast()
}
/// Set the rooms used for completion.
#[allow(clippy::needless_pass_by_value)]
fn set_rooms(&self, rooms: Option<RoomList>) {
if self.rooms() == rooms {
return;
}
self.rooms_expr.set_model(rooms);
self.filtered_rooms.set_model(rooms.as_ref());
self.obj().notify_rooms();
}

6
src/utils/expression_list_model.rs

@ -77,7 +77,7 @@ mod imp {
let added = model.n_items();
self.model.set(model, vec![items_changed_handler]);
self.watch_items(0, 0, added);
self.watch_items(0, removed, added);
added
} else {
0
@ -95,7 +95,9 @@ mod imp {
}
self.expressions.replace(expressions);
self.watch_items(0, 0, self.n_items());
let n_items = self.n_items();
self.watch_items(0, n_items, n_items);
}
/// Watch and unwatch items according to changes in the underlying

Loading…
Cancel
Save