|
|
|
|
@ -75,121 +75,19 @@ mod imp {
|
|
|
|
|
|
|
|
|
|
fn constructed(&self) { |
|
|
|
|
self.parent_constructed(); |
|
|
|
|
let obj = self.obj(); |
|
|
|
|
|
|
|
|
|
self.search_entry.connect_pill_removed(clone!( |
|
|
|
|
#[weak(rename_to = imp)] |
|
|
|
|
self, |
|
|
|
|
move |_, source| { |
|
|
|
|
if let Ok(member) = source.downcast::<Member>() { |
|
|
|
|
imp.remove_selected(member.user_id()); |
|
|
|
|
imp.remove_selected(&member); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
)); |
|
|
|
|
|
|
|
|
|
self.list_view.connect_activate(clone!( |
|
|
|
|
#[weak(rename_to = imp)] |
|
|
|
|
self, |
|
|
|
|
move |list_view, index| { |
|
|
|
|
let Some(member) = list_view |
|
|
|
|
.model() |
|
|
|
|
.and_then(|m| m.item(index)) |
|
|
|
|
.and_downcast::<Member>() |
|
|
|
|
else { |
|
|
|
|
return; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
imp.toggle_selected(member); |
|
|
|
|
} |
|
|
|
|
)); |
|
|
|
|
|
|
|
|
|
let user_expr = gtk::ClosureExpression::new::<String>( |
|
|
|
|
&[] as &[gtk::Expression], |
|
|
|
|
closure!(|item: Option<glib::Object>| { |
|
|
|
|
item.and_downcast_ref() |
|
|
|
|
.map(Member::search_string) |
|
|
|
|
.unwrap_or_default() |
|
|
|
|
}), |
|
|
|
|
); |
|
|
|
|
let search_filter = gtk::StringFilter::builder() |
|
|
|
|
.match_mode(gtk::StringFilterMatchMode::Substring) |
|
|
|
|
.expression(expression::normalize_string(user_expr)) |
|
|
|
|
.ignore_case(true) |
|
|
|
|
.build(); |
|
|
|
|
|
|
|
|
|
expression::normalize_string(self.search_entry.property_expression("text")).bind( |
|
|
|
|
&search_filter, |
|
|
|
|
"search", |
|
|
|
|
None::<&glib::Object>, |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
let filter = gtk::EveryFilter::new(); |
|
|
|
|
filter.append(self.power_level_filter.clone()); |
|
|
|
|
filter.append(search_filter); |
|
|
|
|
|
|
|
|
|
self.filtered_model.set_filter(Some(&filter)); |
|
|
|
|
|
|
|
|
|
self.filtered_model.connect_items_changed(clone!( |
|
|
|
|
#[weak(rename_to = imp)] |
|
|
|
|
self, |
|
|
|
|
move |_, _, _, _| { |
|
|
|
|
imp.update_visible_page(); |
|
|
|
|
} |
|
|
|
|
)); |
|
|
|
|
|
|
|
|
|
// Sort members by display name, then user ID.
|
|
|
|
|
let display_name_expr = Member::this_expression("display-name"); |
|
|
|
|
let display_name_sorter = gtk::StringSorter::new(Some(display_name_expr)); |
|
|
|
|
|
|
|
|
|
let user_id_expr = Member::this_expression("user-id-string"); |
|
|
|
|
let user_id_sorter = gtk::StringSorter::new(Some(user_id_expr)); |
|
|
|
|
|
|
|
|
|
let sorter = gtk::MultiSorter::new(); |
|
|
|
|
sorter.append(display_name_sorter); |
|
|
|
|
sorter.append(user_id_sorter); |
|
|
|
|
|
|
|
|
|
let sorted_model = |
|
|
|
|
gtk::SortListModel::new(Some(self.filtered_model.clone()), Some(sorter)); |
|
|
|
|
|
|
|
|
|
self.list_view |
|
|
|
|
.set_model(Some(>k::NoSelection::new(Some(sorted_model)))); |
|
|
|
|
|
|
|
|
|
let factory = gtk::SignalListItemFactory::new(); |
|
|
|
|
factory.connect_setup(clone!( |
|
|
|
|
#[weak] |
|
|
|
|
obj, |
|
|
|
|
move |_, item| { |
|
|
|
|
let Some(item) = item.downcast_ref::<gtk::ListItem>() else { |
|
|
|
|
error!("List item factory did not receive a list item: {item:?}"); |
|
|
|
|
return; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
let row = PermissionsSelectMemberRow::new(); |
|
|
|
|
item.set_child(Some(&row)); |
|
|
|
|
item.bind_property("item", &row, "member") |
|
|
|
|
.sync_create() |
|
|
|
|
.build(); |
|
|
|
|
item.set_selectable(false); |
|
|
|
|
|
|
|
|
|
obj.connect_selection_changed(clone!( |
|
|
|
|
#[weak] |
|
|
|
|
row, |
|
|
|
|
move |obj| { |
|
|
|
|
let Some(member) = row.member() else { |
|
|
|
|
return; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
let selected = obj |
|
|
|
|
.imp() |
|
|
|
|
.selected_members |
|
|
|
|
.borrow() |
|
|
|
|
.contains_key(member.user_id()); |
|
|
|
|
row.set_selected(selected); |
|
|
|
|
} |
|
|
|
|
)); |
|
|
|
|
} |
|
|
|
|
)); |
|
|
|
|
self.list_view.set_factory(Some(&factory)); |
|
|
|
|
self.initialize_filtered_model(); |
|
|
|
|
self.initialize_list_view(); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@ -265,42 +163,58 @@ mod imp {
|
|
|
|
|
self.stack.set_visible_child_name(visible_page); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Whether the member with the given ID is selected.
|
|
|
|
|
fn is_selected(&self, user_id: &OwnedUserId) -> bool { |
|
|
|
|
self.selected_members.borrow().contains_key(user_id) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Toggle whether the given member is selected.
|
|
|
|
|
fn toggle_selected(&self, member: Member) { |
|
|
|
|
let is_empty = { |
|
|
|
|
let mut selected_members = self.selected_members.borrow_mut(); |
|
|
|
|
let is_selected = self.is_selected(member.user_id()); |
|
|
|
|
|
|
|
|
|
if is_selected { |
|
|
|
|
self.remove_selected(&member); |
|
|
|
|
} else { |
|
|
|
|
self.add_selected(member); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Add the given member to the selected list.
|
|
|
|
|
fn add_selected(&self, member: Member) { |
|
|
|
|
{ |
|
|
|
|
let mut selected_members = self.selected_members.borrow_mut(); |
|
|
|
|
let user_id = member.user_id(); |
|
|
|
|
let was_selected = selected_members.remove(user_id).is_some(); |
|
|
|
|
|
|
|
|
|
if was_selected { |
|
|
|
|
self.search_entry.remove_pill(&member.identifier()); |
|
|
|
|
} else { |
|
|
|
|
self.search_entry.add_pill(&member); |
|
|
|
|
selected_members.insert(user_id.clone(), member); |
|
|
|
|
if selected_members.contains_key(user_id) { |
|
|
|
|
// Nothing to do.
|
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
selected_members.is_empty() |
|
|
|
|
}; |
|
|
|
|
self.search_entry.add_pill(&member); |
|
|
|
|
selected_members.insert(user_id.clone(), member); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
self.add_button.set_sensitive(!is_empty); |
|
|
|
|
self.add_button.set_sensitive(true); |
|
|
|
|
self.obj().emit_by_name::<()>("selection-changed", &[]); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Remove the member with the given user ID from the selected list.
|
|
|
|
|
fn remove_selected(&self, user_id: &OwnedUserId) { |
|
|
|
|
let (was_removed, is_empty) = { |
|
|
|
|
/// Remove the given member from the selected list.
|
|
|
|
|
fn remove_selected(&self, member: &Member) { |
|
|
|
|
let is_empty = { |
|
|
|
|
let mut selected_members = self.selected_members.borrow_mut(); |
|
|
|
|
let was_removed = selected_members.remove(user_id).is_some(); |
|
|
|
|
let is_empty = selected_members.is_empty(); |
|
|
|
|
|
|
|
|
|
(was_removed, is_empty) |
|
|
|
|
if selected_members.remove(member.user_id()).is_none() { |
|
|
|
|
// Nothing happened.
|
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
self.search_entry.remove_pill(&member.identifier()); |
|
|
|
|
|
|
|
|
|
selected_members.is_empty() |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
if was_removed { |
|
|
|
|
self.add_button.set_sensitive(!is_empty); |
|
|
|
|
self.obj().emit_by_name::<()>("selection-changed", &[]); |
|
|
|
|
} |
|
|
|
|
self.add_button.set_sensitive(!is_empty); |
|
|
|
|
self.obj().emit_by_name::<()>("selection-changed", &[]); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// Add the selected members to the list of members with custom power
|
|
|
|
|
@ -334,6 +248,128 @@ mod imp {
|
|
|
|
|
self.add_button.set_sensitive(false); |
|
|
|
|
obj.emit_by_name::<()>("selection-changed", &[]); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fn initialize_filtered_model(&self) { |
|
|
|
|
let user_expr = gtk::ClosureExpression::new::<String>( |
|
|
|
|
&[] as &[gtk::Expression], |
|
|
|
|
closure!(|item: Option<glib::Object>| { |
|
|
|
|
item.and_downcast_ref() |
|
|
|
|
.map(Member::search_string) |
|
|
|
|
.unwrap_or_default() |
|
|
|
|
}), |
|
|
|
|
); |
|
|
|
|
let search_filter = gtk::StringFilter::builder() |
|
|
|
|
.match_mode(gtk::StringFilterMatchMode::Substring) |
|
|
|
|
.expression(expression::normalize_string(user_expr)) |
|
|
|
|
.ignore_case(true) |
|
|
|
|
.build(); |
|
|
|
|
|
|
|
|
|
expression::normalize_string(self.search_entry.property_expression("text")).bind( |
|
|
|
|
&search_filter, |
|
|
|
|
"search", |
|
|
|
|
None::<&glib::Object>, |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
let filter = gtk::EveryFilter::new(); |
|
|
|
|
filter.append(self.power_level_filter.clone()); |
|
|
|
|
filter.append(search_filter); |
|
|
|
|
|
|
|
|
|
self.filtered_model.set_filter(Some(&filter)); |
|
|
|
|
|
|
|
|
|
self.filtered_model.connect_items_changed(clone!( |
|
|
|
|
#[weak(rename_to = imp)] |
|
|
|
|
self, |
|
|
|
|
move |_, _, _, _| { |
|
|
|
|
imp.update_visible_page(); |
|
|
|
|
} |
|
|
|
|
)); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fn initialize_list_view(&self) { |
|
|
|
|
self.list_view.connect_activate(clone!( |
|
|
|
|
#[weak(rename_to = imp)] |
|
|
|
|
self, |
|
|
|
|
move |list_view, index| { |
|
|
|
|
let Some(member) = list_view |
|
|
|
|
.model() |
|
|
|
|
.and_then(|m| m.item(index)) |
|
|
|
|
.and_downcast::<Member>() |
|
|
|
|
else { |
|
|
|
|
return; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
imp.toggle_selected(member); |
|
|
|
|
} |
|
|
|
|
)); |
|
|
|
|
|
|
|
|
|
// Sort members by display name, then user ID.
|
|
|
|
|
let display_name_expr = Member::this_expression("display-name"); |
|
|
|
|
let display_name_sorter = gtk::StringSorter::new(Some(display_name_expr)); |
|
|
|
|
|
|
|
|
|
let user_id_expr = Member::this_expression("user-id-string"); |
|
|
|
|
let user_id_sorter = gtk::StringSorter::new(Some(user_id_expr)); |
|
|
|
|
|
|
|
|
|
let sorter = gtk::MultiSorter::new(); |
|
|
|
|
sorter.append(display_name_sorter); |
|
|
|
|
sorter.append(user_id_sorter); |
|
|
|
|
|
|
|
|
|
let sorted_model = |
|
|
|
|
gtk::SortListModel::new(Some(self.filtered_model.clone()), Some(sorter)); |
|
|
|
|
|
|
|
|
|
self.list_view |
|
|
|
|
.set_model(Some(>k::NoSelection::new(Some(sorted_model)))); |
|
|
|
|
|
|
|
|
|
let factory = gtk::SignalListItemFactory::new(); |
|
|
|
|
factory.connect_setup(clone!( |
|
|
|
|
#[weak(rename_to = imp)] |
|
|
|
|
self, |
|
|
|
|
move |_, item| { |
|
|
|
|
let Some(item) = item.downcast_ref::<gtk::ListItem>() else { |
|
|
|
|
error!("List item factory did not receive a list item: {item:?}"); |
|
|
|
|
return; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
let row = PermissionsSelectMemberRow::new(); |
|
|
|
|
item.set_child(Some(&row)); |
|
|
|
|
item.bind_property("item", &row, "member") |
|
|
|
|
.sync_create() |
|
|
|
|
.build(); |
|
|
|
|
item.set_selectable(false); |
|
|
|
|
|
|
|
|
|
// Toggle the selection when the checkbox is toggled.
|
|
|
|
|
row.connect_selected_notify(clone!( |
|
|
|
|
#[weak] |
|
|
|
|
imp, |
|
|
|
|
move |row| { |
|
|
|
|
let Some(member) = row.member() else { |
|
|
|
|
return; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
if row.selected() { |
|
|
|
|
imp.add_selected(member); |
|
|
|
|
} else { |
|
|
|
|
imp.remove_selected(&member); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
)); |
|
|
|
|
|
|
|
|
|
// Toggle the checkbox when the selection changed.
|
|
|
|
|
imp.obj().connect_selection_changed(clone!( |
|
|
|
|
#[weak] |
|
|
|
|
row, |
|
|
|
|
move |obj| { |
|
|
|
|
let Some(member) = row.member() else { |
|
|
|
|
return; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
let selected = obj.imp().is_selected(member.user_id()); |
|
|
|
|
row.set_selected(selected); |
|
|
|
|
} |
|
|
|
|
)); |
|
|
|
|
} |
|
|
|
|
)); |
|
|
|
|
self.list_view.set_factory(Some(&factory)); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|