mod account_switcher; mod category; mod category_row; mod category_type; mod entry; mod entry_row; mod entry_type; mod item_list; mod room_row; mod row; mod selection; mod verification_row; pub use self::category::Category; use self::category_row::CategoryRow; pub use self::category_type::CategoryType; pub use self::entry::Entry; use self::entry_row::EntryRow; pub use self::entry_type::EntryType; pub use self::item_list::ItemList; use self::room_row::RoomRow; use self::row::Row; use self::selection::Selection; use self::verification_row::VerificationRow; use adw::{prelude::*, subclass::prelude::*}; use gtk::{gio, glib, glib::closure, subclass::prelude::*, CompositeTemplate, SelectionModel}; use crate::components::Avatar; use crate::session::room::{Room, RoomType}; use crate::session::verification::IdentityVerification; use crate::session::Session; use crate::session::User; use account_switcher::AccountSwitcher; mod imp { use super::*; use glib::subclass::InitializingObject; use once_cell::sync::Lazy; use std::{ cell::{Cell, RefCell}, convert::TryFrom, }; #[derive(Debug, Default, CompositeTemplate)] #[template(resource = "/org/gnome/FractalNext/sidebar.ui")] pub struct Sidebar { pub compact: Cell, pub selected_item: RefCell>, #[template_child] pub headerbar: TemplateChild, #[template_child] pub account_switcher: TemplateChild, #[template_child] pub listview: TemplateChild, #[template_child] pub room_search_entry: TemplateChild, #[template_child] pub room_search: TemplateChild, pub user: RefCell>, /// The type of the source that activated drop mode. pub drop_source_type: Cell>, pub drop_binding: RefCell>, } #[glib::object_subclass] impl ObjectSubclass for Sidebar { const NAME: &'static str = "Sidebar"; type Type = super::Sidebar; type ParentType = adw::Bin; fn class_init(klass: &mut Self::Class) { RoomRow::static_type(); Row::static_type(); Avatar::static_type(); Self::bind_template(klass); klass.set_css_name("sidebar"); klass.install_action( "sidebar.set-drop-source-type", Some("u"), move |obj, _, variant| { obj.set_drop_source_type( variant .and_then(|variant| variant.get::>().flatten()) .and_then(|u| RoomType::try_from(u).ok()), ); }, ); klass.install_action("sidebar.update-drop-targets", None, move |obj, _, _| { if obj.drop_source_type().is_some() { obj.update_drop_targets(); } }); klass.install_action( "sidebar.set-active-drop-category", Some("mu"), move |obj, _, variant| { obj.update_active_drop_targets( variant .and_then(|variant| variant.get::>().flatten()) .and_then(|u| RoomType::try_from(u).ok()), ); }, ); } fn instance_init(obj: &InitializingObject) { obj.init_template(); } } impl ObjectImpl for Sidebar { fn properties() -> &'static [glib::ParamSpec] { static PROPERTIES: Lazy> = Lazy::new(|| { vec![ glib::ParamSpecObject::new( "user", "User", "The logged in user", User::static_type(), glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY, ), glib::ParamSpecBoolean::new( "compact", "Compact", "Whether a compact view is used", false, glib::ParamFlags::READWRITE, ), glib::ParamSpecObject::new( "item-list", "Item List", "The list of items in the sidebar", ItemList::static_type(), glib::ParamFlags::WRITABLE | glib::ParamFlags::EXPLICIT_NOTIFY, ), glib::ParamSpecObject::new( "selected-item", "Selected Item", "The selected item in this sidebar", glib::Object::static_type(), glib::ParamFlags::READWRITE | glib::ParamFlags::EXPLICIT_NOTIFY, ), glib::ParamSpecEnum::new( "drop-source-type", "Drop Source Type", "The type of the source that activated drop mode", CategoryType::static_type(), CategoryType::None as i32, glib::ParamFlags::READABLE | glib::ParamFlags::EXPLICIT_NOTIFY, ), ] }); PROPERTIES.as_ref() } fn set_property( &self, obj: &Self::Type, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec, ) { match pspec.name() { "compact" => { let compact = value.get().unwrap(); self.compact.set(compact); } "user" => { obj.set_user(value.get().unwrap()); } "item-list" => { obj.set_item_list(value.get().unwrap()); } "selected-item" => { let selected_item = value.get().unwrap(); obj.set_selected_item(selected_item); } _ => unimplemented!(), } } fn property(&self, obj: &Self::Type, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { match pspec.name() { "compact" => self.compact.get().to_value(), "user" => obj.user().to_value(), "selected-item" => obj.selected_item().to_value(), "drop-source-type" => obj .drop_source_type() .map(CategoryType::from) .unwrap_or(CategoryType::None) .to_value(), _ => unimplemented!(), } } fn constructed(&self, obj: &Self::Type) { self.parent_constructed(obj); self.listview.connect_activate(move |listview, pos| { let model: Option = listview.model().and_then(|o| o.downcast().ok()); let row: Option = model .as_ref() .and_then(|m| m.item(pos)) .and_then(|o| o.downcast().ok()); let (model, row) = match (model, row) { (Some(model), Some(row)) => (model, row), _ => return, }; match row.item() { Some(o) if o.is::() => row.set_expanded(!row.is_expanded()), Some(o) if o.is::() => model.set_selected(pos), Some(o) if o.is::() => model.set_selected(pos), Some(o) if o.is::() => model.set_selected(pos), _ => {} } }); } } impl WidgetImpl for Sidebar {} impl BinImpl for Sidebar {} } glib::wrapper! { pub struct Sidebar(ObjectSubclass) @extends gtk::Widget, adw::Bin, @implements gtk::Accessible; } impl Sidebar { pub fn new() -> Self { glib::Object::new(&[]).expect("Failed to create Sidebar") } pub fn selected_item(&self) -> Option { let priv_ = imp::Sidebar::from_instance(self); priv_.selected_item.borrow().clone() } pub fn room_search_bar(&self) -> gtk::SearchBar { let priv_ = imp::Sidebar::from_instance(self); priv_.room_search.clone() } pub fn set_item_list(&self, item_list: Option) { let priv_ = imp::Sidebar::from_instance(self); if let Some(binding) = priv_.drop_binding.take() { binding.unbind(); } let item_list = match item_list { Some(item_list) => item_list, None => { priv_.listview.set_model(gtk::SelectionModel::NONE); return; } }; priv_.drop_binding.replace(Some( self.bind_property("drop-source-type", &item_list, "show-all-for-category") .flags(glib::BindingFlags::SYNC_CREATE) .build(), )); let tree_model = gtk::TreeListModel::new(&item_list, false, true, |item| { item.clone().downcast::().ok() }); let room_expression = gtk::ClosureExpression::new::( &[], closure!(|row: gtk::TreeListRow| { row.item() .and_then(|o| o.downcast::().ok()) .map_or(String::new(), |o| o.display_name()) }), ); let filter = gtk::StringFilter::builder() .match_mode(gtk::StringFilterMatchMode::Substring) .expression(&room_expression) .ignore_case(true) .build(); let filter_model = gtk::FilterListModel::new(Some(&tree_model), Some(&filter)); priv_ .room_search_entry .bind_property("text", &filter, "search") .flags(glib::BindingFlags::SYNC_CREATE) .build(); let selection = Selection::new(Some(&filter_model)); self.bind_property("selected-item", &selection, "selected-item") .flags(glib::BindingFlags::SYNC_CREATE | glib::BindingFlags::BIDIRECTIONAL) .build(); priv_.listview.set_model(Some(&selection)); } pub fn set_selected_item(&self, selected_item: Option) { let priv_ = imp::Sidebar::from_instance(self); if self.selected_item() == selected_item { return; } priv_.selected_item.replace(selected_item); self.notify("selected-item"); } pub fn user(&self) -> Option { let priv_ = &imp::Sidebar::from_instance(self); priv_.user.borrow().clone() } fn set_user(&self, user: Option) { let priv_ = imp::Sidebar::from_instance(self); if self.user() == user { return; } priv_.user.replace(user); self.notify("user"); } pub fn set_logged_in_users( &self, sessions_stack_pages: &SelectionModel, session_root: &Session, ) { imp::Sidebar::from_instance(self) .account_switcher .set_logged_in_users(sessions_stack_pages, session_root); } pub fn drop_source_type(&self) -> Option { let priv_ = imp::Sidebar::from_instance(self); priv_.drop_source_type.get() } pub fn set_drop_source_type(&self, source_type: Option) { let priv_ = imp::Sidebar::from_instance(self); if self.drop_source_type() == source_type { return; } priv_.drop_source_type.set(source_type); if source_type.is_some() { priv_.listview.add_css_class("drop-mode"); } else { priv_.listview.remove_css_class("drop-mode"); } self.notify("drop-source-type"); self.update_drop_targets(); } /// Update the disabled or empty state of drop targets. fn update_drop_targets(&self) { let priv_ = imp::Sidebar::from_instance(self); let mut child = priv_.listview.first_child(); while let Some(widget) = child { if let Some(row) = widget .first_child() .and_then(|widget| widget.downcast::().ok()) { if let Some(source_type) = self.drop_source_type() { if row .room_type() .filter(|row_type| source_type.can_change_to(row_type)) .is_some() { row.remove_css_class("drop-disabled"); if row .item() .and_then(|object| object.downcast::().ok()) .filter(|category| category.is_empty()) .is_some() { row.add_css_class("drop-empty"); } else { row.remove_css_class("drop-empty"); } } else { let is_forget_entry = row .entry_type() .filter(|entry_type| entry_type == &EntryType::Forget) .is_some(); if is_forget_entry && source_type == RoomType::Left { row.remove_css_class("drop-disabled"); } else { row.add_css_class("drop-disabled"); row.remove_css_class("drop-empty"); } } } else { // Clear style row.remove_css_class("drop-disabled"); row.remove_css_class("drop-empty"); row.parent().unwrap().remove_css_class("drop-active"); }; if let Some(category_row) = row .child() .and_then(|child| child.downcast::().ok()) { category_row.set_show_label_for_category( self.drop_source_type() .map(CategoryType::from) .unwrap_or(CategoryType::None), ); } } child = widget.next_sibling(); } } /// Update the active state of drop targets. fn update_active_drop_targets(&self, target_type: Option) { let priv_ = imp::Sidebar::from_instance(self); let mut child = priv_.listview.first_child(); while let Some(widget) = child { if let Some((row, row_type)) = widget .first_child() .and_then(|widget| widget.downcast::().ok()) .and_then(|row| { let row_type = row.room_type()?; Some((row, row_type)) }) { if target_type .filter(|target_type| target_type == &row_type) .is_some() { row.parent().unwrap().add_css_class("drop-active"); } else { row.parent().unwrap().remove_css_class("drop-active"); } } child = widget.next_sibling(); } } } impl Default for Sidebar { fn default() -> Self { Self::new() } }