explore: Port to glib::Properties macro

This commit is contained in:
Kévin Commaille 2023-12-18 18:45:17 +01:00
parent bffd9463f7
commit f366b02137
No known key found for this signature in database
GPG Key ID: 29A48C1F03620416
9 changed files with 348 additions and 639 deletions

View File

@ -19,22 +19,20 @@ use crate::{components::Spinner, session::model::Session};
mod imp {
use std::cell::RefCell;
use glib::{object::WeakRef, subclass::InitializingObject};
use once_cell::sync::Lazy;
use glib::subclass::InitializingObject;
use super::*;
#[derive(Debug, Default, CompositeTemplate)]
#[derive(Debug, Default, CompositeTemplate, glib::Properties)]
#[template(resource = "/org/gnome/Fractal/ui/session/view/content/explore/mod.ui")]
#[properties(wrapper_type = super::Explore)]
pub struct Explore {
pub session: WeakRef<Session>,
/// The current session.
#[property(get, set = Self::set_session, explicit_notify)]
pub session: glib::WeakRef<Session>,
#[template_child]
pub stack: TemplateChild<gtk::Stack>,
#[template_child]
pub spinner: TemplateChild<Spinner>,
#[template_child]
pub empty_label: TemplateChild<gtk::Label>,
#[template_child]
pub search_entry: TemplateChild<gtk::SearchEntry>,
#[template_child]
pub servers_button: TemplateChild<gtk::MenuButton>,
@ -57,7 +55,10 @@ mod imp {
PublicRoom::static_type();
PublicRoomList::static_type();
PublicRoomRow::static_type();
Spinner::static_type();
Self::bind_template(klass);
klass.set_accessible_role(gtk::AccessibleRole::Group);
}
@ -66,35 +67,8 @@ mod imp {
}
}
#[glib::derived_properties]
impl ObjectImpl for Explore {
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();
@ -116,7 +90,7 @@ mod imp {
self.servers_popover.connect_selected_server_changed(
clone!(@weak obj => move |_, server| {
if let Some(server) = server {
obj.imp().servers_button.set_label(server.name());
obj.imp().servers_button.set_label(&server.name());
obj.trigger_search();
}
}),
@ -126,9 +100,46 @@ mod imp {
impl WidgetImpl for Explore {}
impl BinImpl for Explore {}
impl Explore {
/// Set the current session.
fn set_session(&self, session: Option<Session>) {
if session == self.session.upgrade() {
return;
}
let obj = self.obj();
if let Some(session) = &session {
let public_room_list = PublicRoomList::new(session);
self.listview
.set_model(Some(&gtk::NoSelection::new(Some(public_room_list.clone()))));
public_room_list.connect_notify_local(
Some("loading"),
clone!(@weak obj => move |_, _| {
obj.update_visible_child();
}),
);
public_room_list.connect_notify_local(
Some("empty"),
clone!(@weak obj => move |_, _| {
obj.update_visible_child();
}),
);
self.public_room_list.replace(Some(public_room_list));
obj.update_visible_child();
}
self.session.set(session.as_ref());
obj.notify_session();
}
}
}
glib::wrapper! {
/// A view to explore rooms in the public directory of homeservers.
pub struct Explore(ObjectSubclass<imp::Explore>)
@extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
}
@ -138,18 +149,13 @@ impl Explore {
glib::Object::builder().property("session", session).build()
}
/// The current session.
pub fn session(&self) -> Option<Session> {
self.imp().session.upgrade()
}
pub fn init(&self) {
let imp = self.imp();
imp.servers_popover.init();
if let Some(server) = imp.servers_popover.selected_server() {
imp.servers_button.set_label(server.name());
imp.servers_button.set_label(&server.name());
}
if let Some(public_room_list) = &*imp.public_room_list.borrow() {
@ -159,49 +165,16 @@ impl Explore {
self.imp().search_entry.grab_focus();
}
/// Set the current session.
pub fn set_session(&self, session: Option<Session>) {
let imp = self.imp();
if session == self.session() {
return;
}
if let Some(ref session) = session {
let public_room_list = PublicRoomList::new(session);
imp.listview
.set_model(Some(&gtk::NoSelection::new(Some(public_room_list.clone()))));
public_room_list.connect_notify_local(
Some("loading"),
clone!(@weak self as obj => move |_, _| {
obj.set_visible_child();
}),
);
public_room_list.connect_notify_local(
Some("empty"),
clone!(@weak self as obj => move |_, _| {
obj.set_visible_child();
}),
);
imp.public_room_list.replace(Some(public_room_list));
}
imp.session.set(session.as_ref());
self.notify("session");
}
fn set_visible_child(&self) {
/// Update the visible child according to the current state.
fn update_visible_child(&self) {
let imp = self.imp();
if let Some(public_room_list) = &*imp.public_room_list.borrow() {
if public_room_list.loading() {
imp.stack.set_visible_child(&*imp.spinner);
imp.stack.set_visible_child_name("loading");
} else if public_room_list.empty() {
imp.stack.set_visible_child(&*imp.empty_label);
imp.stack.set_visible_child_name("empty");
} else {
imp.stack.set_visible_child(&*imp.scrolled_window);
imp.stack.set_visible_child_name("results");
}
}
}

View File

@ -41,43 +41,56 @@
<class name="explore"/>
</style>
<child>
<object class="Spinner" id="spinner">
<property name="valign">center</property>
<property name="halign">center</property>
<property name="vexpand">True</property>
<style>
<class name="session-loading-spinner"/>
</style>
</object>
</child>
<child>
<object class="GtkLabel" id="empty_label">
<property name="valign">center</property>
<property name="halign">center</property>
<property name="vexpand">True</property>
<property name="label" translatable="yes">No rooms matching the search were found</property>
<style>
<class name="bold"/>
</style>
</object>
</child>
<child>
<object class="GtkScrolledWindow" id="scrolled_window">
<property name="vexpand">True</property>
<property name="hscrollbar-policy">never</property>
<object class="GtkStackPage">
<property name="name">loading</property>
<property name="child">
<object class="AdwClampScrollable">
<object class="Spinner" id="spinner">
<property name="valign">center</property>
<property name="halign">center</property>
<property name="vexpand">True</property>
<property name="hexpand">True</property>
<property name="maximum-size">750</property>
<property name="tightening-threshold">550</property>
<style>
<class name="session-loading-spinner"/>
</style>
</object>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">empty</property>
<property name="child">
<object class="GtkLabel" id="empty_label">
<property name="valign">center</property>
<property name="halign">center</property>
<property name="vexpand">True</property>
<property name="label" translatable="yes">No rooms matching the search were found</property>
<style>
<class name="bold"/>
</style>
</object>
</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="name">results</property>
<property name="child">
<object class="GtkScrolledWindow" id="scrolled_window">
<property name="vexpand">True</property>
<property name="hscrollbar-policy">never</property>
<property name="child">
<object class="GtkListView" id="listview">
<property name="margin-end">24</property>
<property name="margin-start">24</property>
<property name="factory">
<object class="GtkBuilderListItemFactory">
<property name="bytes"><![CDATA[
<object class="AdwClampScrollable">
<property name="vexpand">True</property>
<property name="hexpand">True</property>
<property name="maximum-size">750</property>
<property name="tightening-threshold">550</property>
<property name="child">
<object class="GtkListView" id="listview">
<property name="margin-end">24</property>
<property name="margin-start">24</property>
<property name="factory">
<object class="GtkBuilderListItemFactory">
<property name="bytes"><![CDATA[
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="GtkListItem">
@ -92,12 +105,14 @@
</property>
</template>
</interface>
]]></property>
]]></property>
</object>
</property>
<accessibility>
<property name="label" translatable="yes">Room List</property>
</accessibility>
</object>
</property>
<accessibility>
<property name="label" translatable="yes">Room List</property>
</accessibility>
</object>
</property>
</object>

View File

@ -4,22 +4,34 @@ use matrix_sdk::ruma::directory::PublicRoomsChunk;
use crate::session::model::{AvatarData, AvatarImage, AvatarUriSource, Room, RoomList};
mod imp {
use std::cell::{Cell, RefCell};
use std::cell::{Cell, OnceCell, RefCell};
use glib::signal::SignalHandlerId;
use once_cell::{sync::Lazy, unsync::OnceCell};
use super::*;
#[derive(Debug, Default)]
#[derive(Debug, Default, glib::Properties)]
#[properties(wrapper_type = super::PublicRoom)]
pub struct PublicRoom {
/// The list of rooms in this session.
#[property(get, construct_only)]
pub room_list: OnceCell<RoomList>,
/// The server that returned the room.
#[property(get, construct_only)]
pub server: OnceCell<String>,
pub matrix_public_room: OnceCell<PublicRoomsChunk>,
/// The [`AvatarData`] of this room.
#[property(get)]
pub avatar_data: OnceCell<AvatarData>,
pub room: OnceCell<Room>,
pub is_pending: Cell<bool>,
/// The `Room` object for this room, if the user is already a member of
/// this room.
#[property(get)]
pub room: RefCell<Option<Room>>,
/// Whether the room is pending.
///
/// A room is pending when the user clicked to join it.
#[property(get)]
pub pending: Cell<bool>,
pub room_handler: RefCell<Option<SignalHandlerId>>,
}
@ -29,52 +41,8 @@ mod imp {
type Type = super::PublicRoom;
}
#[glib::derived_properties]
impl ObjectImpl for PublicRoom {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecObject::builder::<RoomList>("room-list")
.construct_only()
.build(),
glib::ParamSpecString::builder("server")
.construct_only()
.build(),
glib::ParamSpecObject::builder::<Room>("room")
.read_only()
.build(),
glib::ParamSpecBoolean::builder("pending")
.read_only()
.build(),
glib::ParamSpecObject::builder::<AvatarData>("avatar-data")
.read_only()
.build(),
]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"room-list" => self.room_list.set(value.get().unwrap()).unwrap(),
"server" => self.server.set(value.get().unwrap()).unwrap(),
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
let obj = self.obj();
match pspec.name() {
"room-list" => obj.room_list().to_value(),
"server" => obj.server().to_value(),
"avatar-data" => obj.avatar_data().to_value(),
"room" => obj.room().to_value(),
"pending" => obj.is_pending().to_value(),
_ => unimplemented!(),
}
}
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
@ -107,6 +75,7 @@ mod imp {
}
glib::wrapper! {
/// A room in a homeserver's public directory.
pub struct PublicRoom(ObjectSubclass<imp::PublicRoom>);
}
@ -118,48 +87,20 @@ impl PublicRoom {
.build()
}
/// The list of rooms in this session.
pub fn room_list(&self) -> &RoomList {
self.imp().room_list.get().unwrap()
}
/// The server that returned the room.
pub fn server(&self) -> &str {
self.imp().server.get().unwrap()
}
/// The [`AvatarData`] of this room.
pub fn avatar_data(&self) -> &AvatarData {
self.imp().avatar_data.get().unwrap()
}
/// The `Room` object for this room, if the user is already a member of this
/// room.
pub fn room(&self) -> Option<&Room> {
self.imp().room.get()
}
/// Set the `Room` object for this room.
fn set_room(&self, room: Room) {
self.imp().room.set(room).unwrap();
self.notify("room");
self.imp().room.replace(Some(room));
self.notify_room();
}
/// Set whether this room is pending.
fn set_pending(&self, is_pending: bool) {
if self.is_pending() == is_pending {
fn set_pending(&self, pending: bool) {
if self.pending() == pending {
return;
}
self.imp().is_pending.set(is_pending);
self.notify("pending");
}
/// Whether the room is pending.
///
/// A room is pending when the user clicked to join it.
pub fn is_pending(&self) -> bool {
self.imp().is_pending.get()
self.imp().pending.set(pending);
self.notify_pending();
}
pub fn set_matrix_public_room(&self, room: PublicRoomsChunk) {

View File

@ -14,14 +14,15 @@ use super::{PublicRoom, Server};
use crate::{session::model::Session, spawn, spawn_tokio};
mod imp {
use std::cell::{Cell, RefCell};
use glib::object::WeakRef;
use once_cell::sync::Lazy;
use std::{
cell::{Cell, RefCell},
marker::PhantomData,
};
use super::*;
#[derive(Debug, Default)]
#[derive(Debug, Default, glib::Properties)]
#[properties(wrapper_type = super::PublicRoomList)]
pub struct PublicRoomList {
pub list: RefCell<Vec<PublicRoom>>,
pub search_term: RefCell<Option<String>>,
@ -30,7 +31,18 @@ mod imp {
pub next_batch: RefCell<Option<String>>,
pub request_sent: Cell<bool>,
pub total_room_count_estimate: Cell<Option<u64>>,
pub session: WeakRef<Session>,
/// The current session.
#[property(get, construct_only)]
pub session: glib::WeakRef<Session>,
/// Whether the list is loading.
#[property(get = Self::loading)]
pub loading: PhantomData<bool>,
/// Whether the list is empty.
#[property(get = Self::empty)]
pub empty: PhantomData<bool>,
/// Whether all results for the current search were loaded.
#[property(get = Self::complete)]
pub complete: PhantomData<bool>,
}
#[glib::object_subclass]
@ -40,53 +52,18 @@ mod imp {
type Interfaces = (gio::ListModel,);
}
impl ObjectImpl for PublicRoomList {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecObject::builder::<Session>("session")
.construct_only()
.build(),
glib::ParamSpecBoolean::builder("loading")
.read_only()
.build(),
glib::ParamSpecBoolean::builder("empty").read_only().build(),
glib::ParamSpecBoolean::builder("complete")
.read_only()
.build(),
]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"session" => self.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(),
"loading" => obj.loading().to_value(),
"empty" => obj.empty().to_value(),
"complete" => obj.complete().to_value(),
_ => unimplemented!(),
}
}
}
#[glib::derived_properties]
impl ObjectImpl for PublicRoomList {}
impl ListModelImpl for PublicRoomList {
fn item_type(&self) -> glib::Type {
PublicRoom::static_type()
}
fn n_items(&self) -> u32 {
self.list.borrow().len() as u32
}
fn item(&self, position: u32) -> Option<glib::Object> {
self.list
.borrow()
@ -95,9 +72,27 @@ mod imp {
.cloned()
}
}
impl PublicRoomList {
/// Whether the list is loading.
fn loading(&self) -> bool {
self.request_sent.get() && self.list.borrow().is_empty()
}
/// Whether the list is empty.
fn empty(&self) -> bool {
!self.request_sent.get() && self.list.borrow().is_empty()
}
/// Whether all results for the current search were loaded.
fn complete(&self) -> bool {
self.next_batch.borrow().is_none()
}
}
}
glib::wrapper! {
/// A list of rooms in a homeserver's public directory.
pub struct PublicRoomList(ObjectSubclass<imp::PublicRoomList>)
@implements gio::ListModel;
}
@ -107,46 +102,18 @@ impl PublicRoomList {
glib::Object::builder().property("session", session).build()
}
/// The current session.
pub fn session(&self) -> Option<Session> {
self.imp().session.upgrade()
}
/// Set the current session.
fn set_session(&self, session: Option<Session>) {
if session == self.session() {
return;
}
self.imp().session.set(session.as_ref());
self.notify("session");
}
/// Whether the list is loading.
pub fn loading(&self) -> bool {
self.request_sent() && self.imp().list.borrow().is_empty()
}
/// Whether the list is empty.
pub fn empty(&self) -> bool {
!self.request_sent() && self.imp().list.borrow().is_empty()
}
/// Whether all results for the current search were loaded.
pub fn complete(&self) -> bool {
self.imp().next_batch.borrow().is_none()
}
/// Whether a request is in progress.
fn request_sent(&self) -> bool {
self.imp().request_sent.get()
}
/// Set whether a request is in progress.
fn set_request_sent(&self, request_sent: bool) {
self.imp().request_sent.set(request_sent);
self.notify("loading");
self.notify("empty");
self.notify("complete");
self.notify_loading();
self.notify_empty();
self.notify_complete();
}
pub fn init(&self) {
@ -156,21 +123,22 @@ impl PublicRoomList {
}
}
/// Search the given term on the given server.
pub fn search(&self, search_term: Option<String>, server: Server) {
let imp = self.imp();
let network = Some(server.network());
let server = server.server();
if imp.search_term.borrow().as_ref() == search_term.as_ref()
&& imp.server.borrow().as_deref() == server
&& imp.network.borrow().as_deref() == network
if *imp.search_term.borrow() == search_term
&& *imp.server.borrow() == server
&& *imp.network.borrow() == network
{
return;
}
imp.search_term.replace(search_term);
imp.server.replace(server.map(ToOwned::to_owned));
imp.network.replace(network.map(ToOwned::to_owned));
imp.server.replace(server);
imp.network.replace(network);
self.load_public_rooms(true);
}
@ -223,6 +191,7 @@ impl PublicRoomList {
self.set_request_sent(false);
}
/// Whether this is the response for the latest request that was sent.
fn is_valid_response(
&self,
search_term: Option<String>,

View File

@ -7,21 +7,25 @@ use super::PublicRoom;
use crate::{
components::{Avatar, Spinner, SpinnerButton},
prelude::*,
spawn, toast, Window,
spawn, toast,
utils::BoundObject,
Window,
};
mod imp {
use std::cell::RefCell;
use glib::{signal::SignalHandlerId, subclass::InitializingObject};
use once_cell::sync::Lazy;
use glib::subclass::InitializingObject;
use super::*;
#[derive(Debug, Default, CompositeTemplate)]
#[derive(Debug, Default, CompositeTemplate, glib::Properties)]
#[template(resource = "/org/gnome/Fractal/ui/session/view/content/explore/public_room_row.ui")]
#[properties(wrapper_type = super::PublicRoomRow)]
pub struct PublicRoomRow {
pub public_room: RefCell<Option<PublicRoom>>,
/// The public room displayed by this row.
#[property(get, set= Self::set_public_room, explicit_notify)]
pub public_room: BoundObject<PublicRoom>,
#[template_child]
pub avatar: TemplateChild<Avatar>,
#[template_child]
@ -35,8 +39,6 @@ mod imp {
#[template_child]
pub button: TemplateChild<SpinnerButton>,
pub original_child: RefCell<Option<gtk::Widget>>,
pub pending_handler: RefCell<Option<SignalHandlerId>>,
pub room_handler: RefCell<Option<SignalHandlerId>>,
}
#[glib::object_subclass]
@ -54,30 +56,8 @@ mod imp {
}
}
#[glib::derived_properties]
impl ObjectImpl for PublicRoomRow {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![glib::ParamSpecObject::builder::<PublicRoom>("public-room")
.explicit_notify()
.build()]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"public-room" => self.obj().set_public_room(value.get().unwrap()),
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"public-room" => self.obj().public_room().to_value(),
_ => unimplemented!(),
}
}
fn constructed(&self) {
self.parent_constructed();
self.button
@ -85,24 +65,81 @@ mod imp {
imp.obj().join_or_view();
}));
}
fn dispose(&self) {
if let Some(ref old_public_room) = self.obj().public_room() {
if let Some(handler) = self.pending_handler.take() {
old_public_room.disconnect(handler);
}
if let Some(handler_id) = self.room_handler.take() {
old_public_room.disconnect(handler_id);
}
}
}
}
impl WidgetImpl for PublicRoomRow {}
impl BinImpl for PublicRoomRow {}
impl PublicRoomRow {
/// Set the public room displayed by this row.
fn set_public_room(&self, public_room: Option<PublicRoom>) {
if self.public_room.obj() == public_room {
return;
}
let obj = self.obj();
self.public_room.disconnect_signals();
if let Some(public_room) = public_room {
if let Some(child) = self.original_child.take() {
obj.set_child(Some(&child));
}
if let Some(matrix_public_room) = public_room.matrix_public_room() {
self.avatar
.set_data(Some(public_room.avatar_data().clone()));
let display_name = matrix_public_room
.name
.as_deref()
// FIXME: display some other identification for this room
.unwrap_or("Room without name");
self.display_name.set_text(display_name);
if let Some(topic) = &matrix_public_room.topic {
self.description.set_text(topic);
}
self.description
.set_visible(matrix_public_room.topic.is_some());
if let Some(alias) = &matrix_public_room.canonical_alias {
self.alias.set_text(alias.as_str());
}
self.alias
.set_visible(matrix_public_room.canonical_alias.is_some());
self.members_count
.set_text(&matrix_public_room.num_joined_members.to_string());
let pending_handler = public_room.connect_pending_notify(
clone!(@weak obj => move |public_room| {
obj.update_button(public_room);
}),
);
let room_handler =
public_room.connect_room_notify(clone!(@weak obj => move |public_room| {
obj.update_button(public_room);
}));
obj.update_button(&public_room);
self.public_room
.set(public_room, vec![pending_handler, room_handler]);
} else if self.original_child.borrow().is_none() {
let spinner = Spinner::default();
spinner.set_margin_top(12);
spinner.set_margin_bottom(12);
self.original_child.replace(obj.child());
obj.set_child(Some(&spinner));
}
}
obj.notify_public_room();
}
}
}
glib::wrapper! {
/// A row representing a room for a homeserver's public directory.
pub struct PublicRoomRow(ObjectSubclass<imp::PublicRoomRow>)
@extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
}
@ -112,97 +149,6 @@ impl PublicRoomRow {
glib::Object::new()
}
/// The public room displayed by this row.
pub fn public_room(&self) -> Option<PublicRoom> {
self.imp().public_room.borrow().clone()
}
/// Set the public room displayed by this row.
pub fn set_public_room(&self, public_room: Option<PublicRoom>) {
let imp = self.imp();
let old_public_room = self.public_room();
if old_public_room == public_room {
return;
}
if let Some(ref old_public_room) = old_public_room {
if let Some(handler) = imp.room_handler.take() {
old_public_room.disconnect(handler);
}
if let Some(handler) = imp.pending_handler.take() {
old_public_room.disconnect(handler);
}
}
if let Some(ref public_room) = public_room {
if let Some(child) = imp.original_child.take() {
self.set_child(Some(&child));
}
if let Some(matrix_public_room) = public_room.matrix_public_room() {
imp.avatar.set_data(Some(public_room.avatar_data().clone()));
let display_name = matrix_public_room
.name
.as_deref()
.map(AsRef::as_ref)
// FIXME: display some other identification for this room
.unwrap_or("Room without name");
imp.display_name.set_text(display_name);
let has_topic = if let Some(ref topic) = matrix_public_room.topic {
imp.description.set_text(topic);
true
} else {
false
};
imp.description.set_visible(has_topic);
let has_alias = if let Some(ref alias) = matrix_public_room.canonical_alias {
imp.alias.set_text(alias.as_str());
true
} else {
false
};
imp.alias.set_visible(has_alias);
imp.members_count
.set_text(&matrix_public_room.num_joined_members.to_string());
let pending_handler = public_room.connect_notify_local(
Some("pending"),
clone!(@weak self as obj => move |public_room, _| {
obj.update_button(public_room);
}),
);
imp.pending_handler.replace(Some(pending_handler));
let room_handler = public_room.connect_notify_local(
Some("room"),
clone!(@weak self as obj => move |public_room, _| {
obj.update_button(public_room);
}),
);
imp.room_handler.replace(Some(room_handler));
self.update_button(public_room);
} else if imp.original_child.borrow().is_none() {
let spinner = Spinner::default();
spinner.set_margin_top(12);
spinner.set_margin_bottom(12);
imp.original_child.replace(self.child());
self.set_child(Some(&spinner));
}
}
imp.avatar
.set_data(public_room.clone().map(|room| room.avatar_data().clone()));
imp.public_room.replace(public_room);
self.notify("public-room");
}
fn update_button(&self, public_room: &PublicRoom) {
let button = &self.imp().button;
if public_room.room().is_some() {
@ -212,7 +158,7 @@ impl PublicRoomRow {
button.set_label(gettext("Join"));
}
button.set_loading(public_room.is_pending());
button.set_loading(public_room.pending());
}
/// Join or view the public room.

View File

@ -2,22 +2,24 @@ use gtk::{glib, prelude::*, subclass::prelude::*};
use ruma::thirdparty::ProtocolInstance;
mod imp {
use once_cell::{sync::Lazy, unsync::OnceCell};
use std::cell::{OnceCell, RefCell};
use super::*;
#[derive(Debug, Default)]
#[derive(Debug, Default, glib::Properties)]
#[properties(wrapper_type = super::Server)]
pub struct Server {
/// The name of the server that is displayed in the list.
#[property(get, construct_only)]
pub name: OnceCell<String>,
/// The ID of the network that is used during search.
#[property(get, construct_only)]
pub network: OnceCell<String>,
/// The server name that is used during search.
pub server: OnceCell<String>,
#[property(get, construct_only)]
pub server: RefCell<Option<String>>,
/// Whether this server can be deleted from the list.
#[property(get, construct_only)]
pub deletable: OnceCell<bool>,
}
@ -27,54 +29,8 @@ mod imp {
type Type = super::Server;
}
impl ObjectImpl for Server {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecString::builder("name")
.construct_only()
.build(),
glib::ParamSpecString::builder("network")
.construct_only()
.build(),
glib::ParamSpecString::builder("server")
.construct_only()
.build(),
glib::ParamSpecBoolean::builder("deletable")
.construct_only()
.build(),
]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"name" => self.name.set(value.get().unwrap()).unwrap(),
"network" => self.network.set(value.get().unwrap()).unwrap(),
"server" => {
if let Some(server) = value.get().unwrap() {
self.server.set(server).unwrap();
}
}
"deletable" => self.deletable.set(value.get().unwrap()).unwrap(),
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
let obj = self.obj();
match pspec.name() {
"name" => obj.name().to_value(),
"network" => obj.network().to_value(),
"server" => obj.server().to_value(),
"deletable" => obj.deletable().to_value(),
_ => unimplemented!(),
}
}
}
#[glib::derived_properties]
impl ObjectImpl for Server {}
}
glib::wrapper! {
@ -107,24 +63,4 @@ impl Server {
.property("deletable", true)
.build()
}
/// The name of the server.
pub fn name(&self) -> &str {
self.imp().name.get().unwrap()
}
/// The ID of the network that is used during search.
pub fn network(&self) -> &str {
self.imp().network.get().unwrap()
}
/// The server name that is used during search.
pub fn server(&self) -> Option<&str> {
self.imp().server.get().map(String::as_ref)
}
/// Whether this server can be deleted from the list.
pub fn deletable(&self) -> bool {
*self.imp().deletable.get().unwrap()
}
}

View File

@ -8,14 +8,14 @@ use crate::{prelude::*, session::model::Session, spawn, spawn_tokio};
mod imp {
use std::cell::RefCell;
use glib::object::WeakRef;
use once_cell::sync::Lazy;
use super::*;
#[derive(Debug, Default)]
#[derive(Debug, Default, glib::Properties)]
#[properties(wrapper_type = super::ServerList)]
pub struct ServerList {
pub session: WeakRef<Session>,
/// The current session.
#[property(get, set = Self::set_session, construct_only)]
pub session: glib::WeakRef<Session>,
pub list: RefCell<Vec<Server>>,
}
@ -26,31 +26,8 @@ mod imp {
type Interfaces = (gio::ListModel,);
}
impl ObjectImpl for ServerList {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![glib::ParamSpecObject::builder::<Session>("session")
.construct_only()
.build()]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"session" => self.obj().set_session(value.get().unwrap()),
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"session" => self.obj().session().to_value(),
_ => unimplemented!(),
}
}
}
#[glib::derived_properties]
impl ObjectImpl for ServerList {}
impl ListModelImpl for ServerList {
fn item_type(&self) -> glib::Type {
@ -69,6 +46,25 @@ mod imp {
.cloned()
}
}
impl ServerList {
/// Set the current session.
fn set_session(&self, session: Session) {
let obj = self.obj();
self.session.set(Some(&session));
let user_id = session.user_id();
self.list.replace(vec![Server::with_default_server(
user_id.server_name().as_str(),
)]);
obj.items_changed(0, 0, 1);
spawn!(clone!(@weak obj => async move {
obj.load_servers().await;
}));
}
}
}
glib::wrapper! {
@ -82,28 +78,7 @@ impl ServerList {
glib::Object::builder().property("session", session).build()
}
/// Set the current session.
fn set_session(&self, session: Session) {
let imp = self.imp();
imp.session.set(Some(&session));
let user_id = session.user_id();
imp.list.replace(vec![Server::with_default_server(
user_id.server_name().as_str(),
)]);
self.items_changed(0, 0, 1);
spawn!(clone!(@weak self as obj => async move {
obj.load_servers().await;
}));
}
/// The current session.
pub fn session(&self) -> Option<Session> {
self.imp().session.upgrade()
}
/// Load all the servers.
async fn load_servers(&self) {
self.load_protocols().await;
@ -118,6 +93,7 @@ impl ServerList {
self.items_changed(1, 0, (added - 1) as u32);
}
/// Load the protocols of the session's homeserver.
async fn load_protocols(&self) {
let client = self.session().unwrap().client();
@ -132,6 +108,7 @@ impl ServerList {
}
}
/// Add the given protocol to this list.
fn add_protocols(&self, protocols: get_protocols::v3::Response) {
let protocols_servers =
protocols
@ -146,13 +123,15 @@ impl ServerList {
self.imp().list.borrow_mut().extend(protocols_servers)
}
/// Whether this list contains the given Matrix server.
pub fn contains_matrix_server(&self, server: &str) -> bool {
let list = &self.imp().list.borrow();
// The user's matrix server is a special case that doesn't have a "server", so
// use its name.
list[0].name() == server || list.iter().any(|s| s.server() == Some(server))
list[0].name() == server || list.iter().any(|s| s.server().as_deref() == Some(server))
}
/// Add a custom Matrix server.
pub fn add_custom_matrix_server(&self, server_name: String) {
let server = Server::with_custom_matrix_server(&server_name);
let pos = {
@ -172,12 +151,13 @@ impl ServerList {
self.items_changed(pos as u32, 0, 1);
}
/// Remove a custom Matrix server.
pub fn remove_custom_matrix_server(&self, server_name: &str) {
let pos = {
let mut list = self.imp().list.borrow_mut();
let pos = list
.iter()
.position(|s| s.deletable() && s.server() == Some(server_name));
.position(|s| s.deletable() && s.server().as_deref() == Some(server_name));
if let Some(pos) = pos {
list.remove(pos);

View File

@ -3,16 +3,19 @@ use gtk::{glib, prelude::*, subclass::prelude::*, CompositeTemplate};
use super::server::Server;
mod imp {
use std::cell::RefCell;
use glib::subclass::InitializingObject;
use once_cell::{sync::Lazy, unsync::OnceCell};
use super::*;
#[derive(Debug, Default, CompositeTemplate)]
#[derive(Debug, Default, CompositeTemplate, glib::Properties)]
#[template(resource = "/org/gnome/Fractal/ui/session/view/content/explore/server_row.ui")]
#[properties(wrapper_type = super::ExploreServerRow)]
pub struct ExploreServerRow {
/// The server displayed by this row.
pub server: OnceCell<Server>,
#[property(get, construct_only)]
pub server: RefCell<Option<Server>>,
#[template_child]
pub remove_button: TemplateChild<gtk::Button>,
}
@ -32,31 +35,8 @@ mod imp {
}
}
#[glib::derived_properties]
impl ObjectImpl for ExploreServerRow {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![glib::ParamSpecObject::builder::<Server>("server")
.construct_only()
.build()]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"server" => self.server.set(value.get().unwrap()).unwrap(),
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"server" => self.obj().server().to_value(),
_ => unimplemented!(),
}
}
fn constructed(&self) {
self.parent_constructed();
@ -73,6 +53,7 @@ mod imp {
}
glib::wrapper! {
/// A row representing a server to explore.
pub struct ExploreServerRow(ObjectSubclass<imp::ExploreServerRow>)
@extends gtk::Widget, gtk::ListBoxRow, @implements gtk::Accessible;
}
@ -81,9 +62,4 @@ impl ExploreServerRow {
pub fn new(server: &Server) -> Self {
glib::Object::builder().property("server", server).build()
}
/// The server displayed by this row.
pub fn server(&self) -> Option<&Server> {
self.imp().server.get()
}
}

View File

@ -13,15 +13,19 @@ use crate::session::model::Session;
mod imp {
use std::cell::RefCell;
use glib::{object::WeakRef, subclass::InitializingObject};
use once_cell::sync::Lazy;
use glib::subclass::InitializingObject;
use super::*;
#[derive(Debug, Default, CompositeTemplate)]
#[derive(Debug, Default, CompositeTemplate, glib::Properties)]
#[template(resource = "/org/gnome/Fractal/ui/session/view/content/explore/servers_popover.ui")]
#[properties(wrapper_type = super::ExploreServersPopover)]
pub struct ExploreServersPopover {
pub session: WeakRef<Session>,
/// The current session.
#[property(get, set = Self::set_session, explicit_notify)]
pub session: glib::WeakRef<Session>,
/// The server list.
#[property(get)]
pub server_list: RefCell<Option<ServerList>>,
#[template_child]
pub listbox: TemplateChild<gtk::ListBox>,
@ -61,39 +65,8 @@ mod imp {
}
}
#[glib::derived_properties]
impl ObjectImpl for ExploreServersPopover {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecObject::builder::<ServerList>("server-list")
.read_only()
.build(),
glib::ParamSpecObject::builder::<Session>("session")
.explicit_notify()
.build(),
]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"session" => self.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(),
"server-list" => obj.server_list().to_value(),
_ => unimplemented!(),
}
}
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
@ -113,9 +86,22 @@ mod imp {
impl WidgetImpl for ExploreServersPopover {}
impl PopoverImpl for ExploreServersPopover {}
impl ExploreServersPopover {
/// Set the current session.
fn set_session(&self, session: Option<Session>) {
if session == self.session.upgrade() {
return;
}
self.session.set(session.as_ref());
self.obj().notify_session();
}
}
}
glib::wrapper! {
/// A popover that lists the servers that can be explored.
pub struct ExploreServersPopover(ObjectSubclass<imp::ExploreServersPopover>)
@extends gtk::Widget, gtk::Popover, @implements gtk::Accessible;
}
@ -125,21 +111,7 @@ impl ExploreServersPopover {
glib::Object::builder().property("session", session).build()
}
/// The current session.
pub fn session(&self) -> Option<Session> {
self.imp().session.upgrade()
}
/// Set the current session.
pub fn set_session(&self, session: Option<Session>) {
if session == self.session() {
return;
}
self.imp().session.set(session.as_ref());
self.notify("session");
}
/// Initialize the list of servers.
pub fn init(&self) {
let Some(session) = &self.session() else {
return;
@ -156,20 +128,16 @@ impl ExploreServersPopover {
imp.listbox.select_row(imp.listbox.row_at_index(0).as_ref());
imp.server_list.replace(Some(server_list));
self.notify("server-list");
}
/// The server list.
pub fn server_list(&self) -> Option<ServerList> {
self.imp().server_list.borrow().clone()
self.notify_server_list();
}
/// The server that is currently selected, if any.
pub fn selected_server(&self) -> Option<Server> {
self.imp()
.listbox
.selected_row()
.and_downcast::<ExploreServerRow>()
.and_then(|row| row.server().cloned())
.and_then(|row| row.server())
}
pub fn connect_selected_server_changed<F: Fn(&Self, Option<Server>) + 'static>(
@ -179,10 +147,11 @@ impl ExploreServersPopover {
self.imp()
.listbox
.connect_row_selected(clone!(@weak self as obj => move |_, row| {
f(&obj, row.and_then(|row| row.downcast_ref::<ExploreServerRow>()).and_then(|row| row.server().cloned()));
f(&obj, row.and_then(|row| row.downcast_ref::<ExploreServerRow>()).and_then(|row| row.server()));
}))
}
/// Whether the server currently in the text entry can be added.
fn can_add_server(&self) -> bool {
let server = self.imp().server_entry.text();
ServerName::parse(server.as_str()).is_ok()
@ -193,10 +162,13 @@ impl ExploreServersPopover {
.is_some()
}
/// Update the state of the action to add a server according to the current
/// state.
fn update_add_server_state(&self) {
self.action_set_enabled("explore-servers-popover.add-server", self.can_add_server())
}
/// Add the server currently in the text entry.
fn add_server(&self) {
if !self.can_add_server() {
return;
@ -218,6 +190,7 @@ impl ExploreServersPopover {
);
}
/// Remove the given server.
fn remove_server(&self, server: &str) {
let Some(server_list) = self.server_list() else {
return;
@ -226,7 +199,7 @@ impl ExploreServersPopover {
let imp = self.imp();
// If the selected server is gonna be removed, select the first one.
if self.selected_server().unwrap().server() == Some(server) {
if self.selected_server().and_then(|s| s.server()).as_deref() == Some(server) {
imp.listbox.select_row(imp.listbox.row_at_index(0).as_ref());
}