You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
458 lines
16 KiB
458 lines
16 KiB
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<bool>, |
|
pub selected_item: RefCell<Option<glib::Object>>, |
|
#[template_child] |
|
pub headerbar: TemplateChild<adw::HeaderBar>, |
|
#[template_child] |
|
pub account_switcher: TemplateChild<AccountSwitcher>, |
|
#[template_child] |
|
pub listview: TemplateChild<gtk::ListView>, |
|
#[template_child] |
|
pub room_search_entry: TemplateChild<gtk::SearchEntry>, |
|
#[template_child] |
|
pub room_search: TemplateChild<gtk::SearchBar>, |
|
pub user: RefCell<Option<User>>, |
|
/// The type of the source that activated drop mode. |
|
pub drop_source_type: Cell<Option<RoomType>>, |
|
pub drop_binding: RefCell<Option<glib::Binding>>, |
|
} |
|
|
|
#[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::<Option<u32>>().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::<Option<u32>>().flatten()) |
|
.and_then(|u| RoomType::try_from(u).ok()), |
|
); |
|
}, |
|
); |
|
} |
|
|
|
fn instance_init(obj: &InitializingObject<Self>) { |
|
obj.init_template(); |
|
} |
|
} |
|
|
|
impl ObjectImpl for Sidebar { |
|
fn properties() -> &'static [glib::ParamSpec] { |
|
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = 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<Selection> = listview.model().and_then(|o| o.downcast().ok()); |
|
let row: Option<gtk::TreeListRow> = 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::<Category>() => row.set_expanded(!row.is_expanded()), |
|
Some(o) if o.is::<Room>() => model.set_selected(pos), |
|
Some(o) if o.is::<Entry>() => model.set_selected(pos), |
|
Some(o) if o.is::<IdentityVerification>() => model.set_selected(pos), |
|
_ => {} |
|
} |
|
}); |
|
} |
|
} |
|
|
|
impl WidgetImpl for Sidebar {} |
|
impl BinImpl for Sidebar {} |
|
} |
|
|
|
glib::wrapper! { |
|
pub struct Sidebar(ObjectSubclass<imp::Sidebar>) |
|
@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<glib::Object> { |
|
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<ItemList>) { |
|
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::<gio::ListModel>().ok() |
|
}); |
|
|
|
let room_expression = gtk::ClosureExpression::new::<String, &[gtk::Expression], _>( |
|
&[], |
|
closure!(|row: gtk::TreeListRow| { |
|
row.item() |
|
.and_then(|o| o.downcast::<Room>().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<glib::Object>) { |
|
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<User> { |
|
let priv_ = &imp::Sidebar::from_instance(self); |
|
priv_.user.borrow().clone() |
|
} |
|
|
|
fn set_user(&self, user: Option<User>) { |
|
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<RoomType> { |
|
let priv_ = imp::Sidebar::from_instance(self); |
|
priv_.drop_source_type.get() |
|
} |
|
|
|
pub fn set_drop_source_type(&self, source_type: Option<RoomType>) { |
|
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::<Row>().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::<Category>().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::<CategoryRow>().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<RoomType>) { |
|
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::<Row>().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() |
|
} |
|
}
|
|
|