20 changed files with 706 additions and 399 deletions
@ -0,0 +1,133 @@
|
||||
use gtk::{glib, prelude::*, subclass::prelude::*}; |
||||
|
||||
use super::{item_list::ItemList, selection::Selection}; |
||||
use crate::session::Room; |
||||
|
||||
mod imp { |
||||
use once_cell::{sync::Lazy, unsync::OnceCell}; |
||||
|
||||
use super::*; |
||||
|
||||
#[derive(Debug, Default)] |
||||
pub struct SidebarListModel { |
||||
/// The list of items in the sidebar.
|
||||
pub item_list: OnceCell<ItemList>, |
||||
/// The tree list model.
|
||||
pub tree_model: OnceCell<gtk::TreeListModel>, |
||||
/// The string filter.
|
||||
pub string_filter: gtk::StringFilter, |
||||
/// The selection model.
|
||||
pub selection_model: Selection, |
||||
} |
||||
|
||||
#[glib::object_subclass] |
||||
impl ObjectSubclass for SidebarListModel { |
||||
const NAME: &'static str = "SidebarListModel"; |
||||
type Type = super::SidebarListModel; |
||||
} |
||||
|
||||
impl ObjectImpl for SidebarListModel { |
||||
fn properties() -> &'static [glib::ParamSpec] { |
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| { |
||||
vec![ |
||||
glib::ParamSpecObject::builder::<ItemList>("item-list") |
||||
.construct_only() |
||||
.build(), |
||||
glib::ParamSpecObject::builder::<gtk::TreeListModel>("tree-model") |
||||
.read_only() |
||||
.build(), |
||||
glib::ParamSpecObject::builder::<gtk::StringFilter>("string-filter") |
||||
.read_only() |
||||
.build(), |
||||
glib::ParamSpecObject::builder::<Selection>("selection-model") |
||||
.read_only() |
||||
.build(), |
||||
] |
||||
}); |
||||
|
||||
PROPERTIES.as_ref() |
||||
} |
||||
|
||||
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { |
||||
let obj = self.obj(); |
||||
|
||||
match pspec.name() { |
||||
"item-list" => obj.set_item_list(value.get().unwrap()), |
||||
_ => unimplemented!(), |
||||
} |
||||
} |
||||
|
||||
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { |
||||
let obj = self.obj(); |
||||
|
||||
match pspec.name() { |
||||
"item-list" => obj.item_list().to_value(), |
||||
"tree-model" => obj.tree_model().to_value(), |
||||
"string-filter" => obj.string_filter().to_value(), |
||||
"selection-model" => obj.selection_model().to_value(), |
||||
_ => unimplemented!(), |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
glib::wrapper! { |
||||
/// A wrapper for the sidebar list model of a `Session`.
|
||||
///
|
||||
/// It allows to keep the state for selection and filtering.
|
||||
pub struct SidebarListModel(ObjectSubclass<imp::SidebarListModel>); |
||||
} |
||||
|
||||
impl SidebarListModel { |
||||
/// Create a new `SidebarListModel`.
|
||||
pub fn new(item_list: &ItemList) -> Self { |
||||
glib::Object::builder() |
||||
.property("item-list", item_list) |
||||
.build() |
||||
} |
||||
|
||||
/// The list of items in the sidebar.
|
||||
pub fn item_list(&self) -> &ItemList { |
||||
self.imp().item_list.get().unwrap() |
||||
} |
||||
|
||||
/// Set the list of items in the sidebar.
|
||||
fn set_item_list(&self, item_list: ItemList) { |
||||
let imp = self.imp(); |
||||
|
||||
imp.item_list.set(item_list.clone()).unwrap(); |
||||
|
||||
let tree_model = |
||||
gtk::TreeListModel::new(item_list, false, true, |item| item.clone().downcast().ok()); |
||||
imp.tree_model.set(tree_model.clone()).unwrap(); |
||||
|
||||
let room_expression = |
||||
gtk::TreeListRow::this_expression("item").chain_property::<Room>("display-name"); |
||||
imp.string_filter |
||||
.set_match_mode(gtk::StringFilterMatchMode::Substring); |
||||
imp.string_filter.set_expression(Some(&room_expression)); |
||||
imp.string_filter.set_ignore_case(true); |
||||
// Default to an empty string to be able to bind to GtkEditable::text.
|
||||
imp.string_filter.set_search(Some("")); |
||||
|
||||
let filter_model = |
||||
gtk::FilterListModel::new(Some(tree_model), Some(imp.string_filter.clone())); |
||||
|
||||
imp.selection_model.set_model(Some(&filter_model)); |
||||
} |
||||
|
||||
/// The tree list model.
|
||||
pub fn tree_model(&self) -> >k::TreeListModel { |
||||
self.imp().tree_model.get().unwrap() |
||||
} |
||||
|
||||
/// The string filter.
|
||||
pub fn string_filter(&self) -> >k::StringFilter { |
||||
&self.imp().string_filter |
||||
} |
||||
|
||||
/// The selection model.
|
||||
pub fn selection_model(&self) -> &Selection { |
||||
&self.imp().selection_model |
||||
} |
||||
} |
||||
@ -0,0 +1,339 @@
|
||||
use adw::{prelude::*, subclass::prelude::*}; |
||||
use gtk::{ |
||||
self, gdk, glib, |
||||
glib::{clone, signal::SignalHandlerId}, |
||||
CompositeTemplate, |
||||
}; |
||||
use log::{error, warn}; |
||||
use ruma::RoomId; |
||||
|
||||
use super::{ |
||||
content::Content, |
||||
join_room_dialog::JoinRoomDialog, |
||||
media_viewer::MediaViewer, |
||||
sidebar::{Selection, Sidebar, SidebarListModel}, |
||||
CreateDmDialog, Event, Room, RoomCreation, Session, |
||||
}; |
||||
use crate::{spawn, toast, Window}; |
||||
|
||||
mod imp { |
||||
use std::cell::RefCell; |
||||
|
||||
use glib::subclass::InitializingObject; |
||||
use once_cell::sync::Lazy; |
||||
|
||||
use super::*; |
||||
|
||||
#[derive(Debug, Default, CompositeTemplate)] |
||||
#[template(resource = "/org/gnome/Fractal/session-view.ui")] |
||||
pub struct SessionView { |
||||
#[template_child] |
||||
pub stack: TemplateChild<gtk::Stack>, |
||||
#[template_child] |
||||
pub overlay: TemplateChild<gtk::Overlay>, |
||||
#[template_child] |
||||
pub leaflet: TemplateChild<adw::Leaflet>, |
||||
#[template_child] |
||||
pub sidebar: TemplateChild<Sidebar>, |
||||
#[template_child] |
||||
pub content: TemplateChild<Content>, |
||||
#[template_child] |
||||
pub media_viewer: TemplateChild<MediaViewer>, |
||||
pub session: glib::WeakRef<Session>, |
||||
pub window_active_handler_id: RefCell<Option<SignalHandlerId>>, |
||||
} |
||||
|
||||
#[glib::object_subclass] |
||||
impl ObjectSubclass for SessionView { |
||||
const NAME: &'static str = "SessionView"; |
||||
type Type = super::SessionView; |
||||
type ParentType = adw::Bin; |
||||
|
||||
fn class_init(klass: &mut Self::Class) { |
||||
Self::bind_template(klass); |
||||
|
||||
klass.install_action("session.close-room", None, move |obj, _, _| { |
||||
obj.select_room(None); |
||||
}); |
||||
|
||||
klass.install_action("session.show-room", Some("s"), move |obj, _, parameter| { |
||||
if let Ok(room_id) = |
||||
<&RoomId>::try_from(&*parameter.unwrap().get::<String>().unwrap()) |
||||
{ |
||||
obj.select_room_by_id(room_id); |
||||
} else { |
||||
error!("Cannot show room with invalid ID"); |
||||
} |
||||
}); |
||||
|
||||
klass.install_action("session.logout", None, move |obj, _, _| { |
||||
if let Some(session) = obj.session() { |
||||
spawn!(clone!(@weak obj, @weak session => async move { |
||||
if let Err(error) = session.logout().await { |
||||
toast!(obj, error); |
||||
} |
||||
})); |
||||
} |
||||
}); |
||||
|
||||
klass.install_action("session.show-content", None, move |obj, _, _| { |
||||
obj.show_content(); |
||||
}); |
||||
|
||||
klass.install_action("session.room-creation", None, move |obj, _, _| { |
||||
obj.show_room_creation_dialog(); |
||||
}); |
||||
|
||||
klass.install_action("session.show-join-room", None, move |obj, _, _| { |
||||
spawn!(clone!(@weak obj => async move { |
||||
obj.show_join_room_dialog().await; |
||||
})); |
||||
}); |
||||
|
||||
klass.install_action("session.create-dm", None, move |obj, _, _| { |
||||
obj.show_create_dm_dialog(); |
||||
}); |
||||
|
||||
klass.add_binding_action( |
||||
gdk::Key::Escape, |
||||
gdk::ModifierType::empty(), |
||||
"session.close-room", |
||||
None, |
||||
); |
||||
|
||||
klass.install_action("session.toggle-room-search", None, move |obj, _, _| { |
||||
obj.toggle_room_search(); |
||||
}); |
||||
|
||||
klass.add_binding_action( |
||||
gdk::Key::k, |
||||
gdk::ModifierType::CONTROL_MASK, |
||||
"session.toggle-room-search", |
||||
None, |
||||
); |
||||
} |
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) { |
||||
obj.init_template(); |
||||
} |
||||
} |
||||
|
||||
impl ObjectImpl for SessionView { |
||||
fn properties() -> &'static [glib::ParamSpec] { |
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| { |
||||
vec![glib::ParamSpecObject::builder::<Session>("session") |
||||
.explicit_notify() |
||||
.build()] |
||||
}); |
||||
|
||||
PROPERTIES.as_ref() |
||||
} |
||||
|
||||
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { |
||||
let obj = self.obj(); |
||||
|
||||
match pspec.name() { |
||||
"session" => obj.set_session(value.get().unwrap()), |
||||
_ => unimplemented!(), |
||||
} |
||||
} |
||||
|
||||
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { |
||||
let obj = self.obj(); |
||||
|
||||
match pspec.name() { |
||||
"session" => obj.session().to_value(), |
||||
_ => unimplemented!(), |
||||
} |
||||
} |
||||
|
||||
fn constructed(&self) { |
||||
self.parent_constructed(); |
||||
let obj = self.obj(); |
||||
|
||||
self.sidebar.property_expression("list-model").chain_property::<SidebarListModel>("selection-model").chain_property::<Selection>("selected-item").watch(glib::Object::NONE, |
||||
clone!(@weak self as imp => move || { |
||||
if imp.sidebar.list_model().and_then(|m| m.selection_model().selected_item()).is_none() { |
||||
imp.leaflet.navigate(adw::NavigationDirection::Back); |
||||
} else { |
||||
imp.leaflet.navigate(adw::NavigationDirection::Forward); |
||||
} |
||||
}), |
||||
); |
||||
|
||||
self.content.connect_notify_local( |
||||
Some("item"), |
||||
clone!(@weak obj => move |_, _| { |
||||
let Some(session) = obj.session() else { |
||||
return; |
||||
}; |
||||
let Some(room) = obj.selected_room() else { |
||||
return; |
||||
}; |
||||
|
||||
// When switching to a room, withdraw its notifications.
|
||||
session.notifications().withdraw_all_for_room(&room); |
||||
}), |
||||
); |
||||
|
||||
obj.connect_root_notify(|obj| { |
||||
let Some(window) = obj.parent_window() else { |
||||
return; |
||||
}; |
||||
|
||||
let handler_id = |
||||
window.connect_is_active_notify(clone!(@weak obj => move |window| { |
||||
if !window.is_active() { |
||||
return; |
||||
} |
||||
let Some(session) = obj.session() else { |
||||
return; |
||||
}; |
||||
let Some(room) = obj.selected_room() else { |
||||
return; |
||||
}; |
||||
|
||||
// When the window becomes active, withdraw the notifications
|
||||
// of the room that is displayed.
|
||||
session.notifications().withdraw_all_for_room(&room); |
||||
})); |
||||
obj.imp().window_active_handler_id.replace(Some(handler_id)); |
||||
}); |
||||
} |
||||
|
||||
fn dispose(&self) { |
||||
if let Some(handler_id) = self.window_active_handler_id.take() { |
||||
if let Some(window) = self.obj().parent_window() { |
||||
window.disconnect(handler_id); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
impl WidgetImpl for SessionView {} |
||||
impl BinImpl for SessionView {} |
||||
} |
||||
|
||||
glib::wrapper! { |
||||
/// A view for a Matrix user session.
|
||||
pub struct SessionView(ObjectSubclass<imp::SessionView>) |
||||
@extends gtk::Widget, adw::Bin, @implements gtk::Accessible; |
||||
} |
||||
|
||||
impl SessionView { |
||||
/// Create a new session.
|
||||
pub async fn new() -> Self { |
||||
glib::Object::new() |
||||
} |
||||
|
||||
/// The Matrix user session.
|
||||
pub fn session(&self) -> Option<Session> { |
||||
self.imp().session.upgrade() |
||||
} |
||||
|
||||
/// Set the Matrix user session.
|
||||
pub fn set_session(&self, session: Option<Session>) { |
||||
if self.session() == session { |
||||
return; |
||||
} |
||||
|
||||
self.imp().session.set(session.as_ref()); |
||||
self.notify("session"); |
||||
} |
||||
|
||||
/// The currently selected room, if any.
|
||||
pub fn selected_room(&self) -> Option<Room> { |
||||
self.imp() |
||||
.content |
||||
.item() |
||||
.and_then(|item| item.downcast().ok()) |
||||
} |
||||
|
||||
pub fn select_room(&self, room: Option<Room>) { |
||||
self.select_item(room.map(|item| item.upcast())); |
||||
} |
||||
|
||||
pub fn select_item(&self, item: Option<glib::Object>) { |
||||
let Some(session) = self.session() else { |
||||
return; |
||||
}; |
||||
|
||||
session |
||||
.sidebar_list_model() |
||||
.selection_model() |
||||
.set_selected_item(item); |
||||
} |
||||
|
||||
pub fn select_room_by_id(&self, room_id: &RoomId) { |
||||
if let Some(room) = self.session().and_then(|s| s.room_list().get(room_id)) { |
||||
self.select_room(Some(room)); |
||||
} else { |
||||
warn!("A room with id {room_id} couldn't be found"); |
||||
} |
||||
} |
||||
|
||||
fn toggle_room_search(&self) { |
||||
let room_search = self.imp().sidebar.room_search_bar(); |
||||
room_search.set_search_mode(!room_search.is_search_mode()); |
||||
} |
||||
|
||||
/// Returns the parent GtkWindow containing this widget.
|
||||
fn parent_window(&self) -> Option<Window> { |
||||
self.root()?.downcast().ok() |
||||
} |
||||
|
||||
fn show_room_creation_dialog(&self) { |
||||
let Some(session) = self.session() else { |
||||
return; |
||||
}; |
||||
|
||||
let window = RoomCreation::new(self.parent_window().as_ref(), &session); |
||||
window.present(); |
||||
} |
||||
|
||||
fn show_create_dm_dialog(&self) { |
||||
let Some(session) = self.session() else { |
||||
return; |
||||
}; |
||||
|
||||
let window = CreateDmDialog::new(self.parent_window().as_ref(), &session); |
||||
window.present(); |
||||
} |
||||
|
||||
async fn show_join_room_dialog(&self) { |
||||
let Some(session) = self.session() else { |
||||
return; |
||||
}; |
||||
|
||||
let dialog = JoinRoomDialog::new(self.parent_window().as_ref(), &session); |
||||
dialog.present(); |
||||
} |
||||
|
||||
pub fn handle_paste_action(&self) { |
||||
self.imp().content.handle_paste_action(); |
||||
} |
||||
|
||||
/// Show the content of the session
|
||||
pub fn show_content(&self) { |
||||
let imp = self.imp(); |
||||
|
||||
imp.stack.set_visible_child(&*imp.overlay); |
||||
|
||||
if let Some(window) = self.parent_window() { |
||||
window.switch_to_session_page(); |
||||
} |
||||
} |
||||
|
||||
/// Show a media event.
|
||||
pub fn show_media(&self, event: &Event, source_widget: &impl IsA<gtk::Widget>) { |
||||
let Some(message) = event.message() else { |
||||
error!("Trying to open the media viewer with an event that is not a message"); |
||||
return; |
||||
}; |
||||
|
||||
let imp = self.imp(); |
||||
imp.media_viewer |
||||
.set_message(&event.room(), event.event_id().unwrap(), message); |
||||
imp.media_viewer.reveal(source_widget); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue