diff --git a/Cargo.toml b/Cargo.toml index c0bfea04..41e764dc 100644 --- a/Cargo.toml +++ b/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" } diff --git a/meson.build b/meson.build index 76ca9cbd..38fb02af 100644 --- a/meson.build +++ b/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') diff --git a/src/session/room/member_list.rs b/src/session/room/member_list.rs index 59bf9c1e..27c92116 100644 --- a/src/session/room/member_list.rs +++ b/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::().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::(closure!( - |_: Option, this_membership: Membership| { + let membership_eq_expr = Member::this_expression("membership").chain_closure::( + closure!(|_: Option, 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(>k::BoolFilter::new(Some(&membership_eq_expr))) + .watch_items(true) + .build() + .upcast() } /// The name of the icon that represents this kind. diff --git a/src/session/sidebar_data/section/mod.rs b/src/session/sidebar_data/section/mod.rs index f253c9a4..6f7991cd 100644 --- a/src/session/sidebar_data/section/mod.rs +++ b/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::() { - 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(); diff --git a/src/session_view/room_history/message_toolbar/completion/member_list.rs b/src/session_view/room_history/message_toolbar/completion/member_list.rs index 45bd64e4..c775bad9 100644 --- a/src/session_view/room_history/message_toolbar/completion/member_list.rs +++ b/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, - /// The members list with expression watches. - members_expr: ExpressionListModel, + /// The filtered members list. + filtered_members: gtk::FilterListModel, permissions_handler: RefCell>, /// 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(); diff --git a/src/session_view/room_history/message_toolbar/completion/room_list.rs b/src/session_view/room_history/message_toolbar/completion/room_list.rs index fd44ccce..d83586db 100644 --- a/src/session_view/room_history/message_toolbar/completion/room_list.rs +++ b/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>, - /// 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::(closure!( - |_obj: Option, category: RoomCategory| { - !matches!(category, RoomCategory::Space | RoomCategory::Outdated) - } + let category_filter = gtk::BoolFilter::new(Some( + Room::this_expression("category").chain_closure::(closure!( + |_obj: Option, 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::("membership") - .chain_closure::(closure!( - |_obj: Option, 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::("membership") + .chain_closure::(closure!( + |_obj: Option, membership: Membership| { + membership == Membership::Join + } + )), + )); - let anyone_can_join_expr = - Room::this_expression("join-rule").chain_property::("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::("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::("alias-string"); let room_search_string_expr = gtk::ClosureExpression::new::( - &[alias_expr.clone(), display_name_expr.clone()], + &[alias_expr, display_name_expr], closure!( |_: Option, 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 { - 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) { if self.rooms() == rooms { return; } - self.rooms_expr.set_model(rooms); + self.filtered_rooms.set_model(rooms.as_ref()); self.obj().notify_rooms(); } diff --git a/src/utils/expression_list_model.rs b/src/utils/expression_list_model.rs index 90af3d4f..e4b73b70 100644 --- a/src/utils/expression_list_model.rs +++ b/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