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

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()
}
}