sidebar: Port to glib::Properties macro

This commit is contained in:
Kévin Commaille 2023-12-17 21:44:43 +01:00
parent 7b8cc7db87
commit e4daec38d6
No known key found for this signature in database
GPG Key ID: 29A48C1F03620416
6 changed files with 499 additions and 675 deletions

View File

@ -1,25 +1,39 @@
use adw::subclass::prelude::BinImpl;
use gettextrs::gettext;
use gtk::{self, accessible, glib, prelude::*, subclass::prelude::*, CompositeTemplate};
use gtk::{glib, prelude::*, subclass::prelude::*, CompositeTemplate};
use crate::session::model::{Category, CategoryType};
mod imp {
use std::cell::{Cell, RefCell};
use std::{
cell::{Cell, RefCell},
marker::PhantomData,
};
use glib::subclass::InitializingObject;
use super::*;
#[derive(Debug, Default, CompositeTemplate)]
#[derive(Debug, Default, CompositeTemplate, glib::Properties)]
#[template(resource = "/org/gnome/Fractal/ui/session/view/sidebar/category_row.ui")]
#[properties(wrapper_type = super::CategoryRow)]
pub struct CategoryRow {
/// The category of this row.
#[property(get, set = Self::set_category, explicit_notify, nullable)]
pub category: RefCell<Option<Category>>,
/// The expanded state of this row.
#[property(get, set = Self::set_expanded, explicit_notify, construct, default = true)]
pub expanded: Cell<bool>,
/// The label to show for this row.
#[property(get = Self::label)]
pub label: PhantomData<Option<String>>,
/// The `CategoryType` to show a label for during a drag-and-drop
/// operation.
///
/// This will change the label according to the action that can be
/// performed when changing from the `CategoryType` to this
/// row's `Category`.
#[property(get, set = Self::set_show_label_for_category, explicit_notify, builder(CategoryType::default()))]
pub show_label_for_category: Cell<CategoryType>,
/// The label showing the category name.
#[template_child]
@ -42,58 +56,112 @@ mod imp {
}
}
impl ObjectImpl for CategoryRow {
fn properties() -> &'static [glib::ParamSpec] {
use once_cell::sync::Lazy;
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecObject::builder::<Category>("category")
.explicit_notify()
.build(),
glib::ParamSpecBoolean::builder("expanded")
.default_value(true)
.explicit_notify()
.construct()
.build(),
glib::ParamSpecString::builder("label").read_only().build(),
glib::ParamSpecEnum::builder::<CategoryType>("show-label-for-category")
.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() {
"category" => obj.set_category(value.get().unwrap()),
"expanded" => obj.set_expanded(value.get().unwrap()),
"show-label-for-category" => obj.set_show_label_for_category(value.get().unwrap()),
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
let obj = self.obj();
match pspec.name() {
"category" => obj.category().to_value(),
"expanded" => obj.expanded().to_value(),
"label" => obj.label().to_value(),
"show-label-for-category" => obj.show_label_for_category().to_value(),
_ => unimplemented!(),
}
}
}
#[glib::derived_properties]
impl ObjectImpl for CategoryRow {}
impl WidgetImpl for CategoryRow {}
impl BinImpl for CategoryRow {}
impl CategoryRow {
/// Set the category represented by this row.
fn set_category(&self, category: Option<Category>) {
if self.category.borrow().clone() == category {
return;
}
self.category.replace(category);
let obj = self.obj();
obj.notify_category();
obj.notify_label();
}
/// The label to show for this row.
fn label(&self) -> Option<String> {
let to_type = self.category.borrow().as_ref()?.r#type();
let from_type = self.show_label_for_category.get();
let label = match from_type {
CategoryType::Invited => match to_type {
// Translators: This is an action to join a room and put it in the "Favorites"
// section.
CategoryType::Favorite => gettext("Join Room as Favorite"),
CategoryType::Normal => gettext("Join Room"),
// Translators: This is an action to join a room and put it in the "Low
// Priority" section.
CategoryType::LowPriority => gettext("Join Room as Low Priority"),
CategoryType::Left => gettext("Reject Invite"),
_ => to_type.to_string(),
},
CategoryType::Favorite => match to_type {
CategoryType::Normal => gettext("Move to Rooms"),
CategoryType::LowPriority => gettext("Move to Low Priority"),
CategoryType::Left => gettext("Leave Room"),
_ => to_type.to_string(),
},
CategoryType::Normal => match to_type {
CategoryType::Favorite => gettext("Move to Favorites"),
CategoryType::LowPriority => gettext("Move to Low Priority"),
CategoryType::Left => gettext("Leave Room"),
_ => to_type.to_string(),
},
CategoryType::LowPriority => match to_type {
CategoryType::Favorite => gettext("Move to Favorites"),
CategoryType::Normal => gettext("Move to Rooms"),
CategoryType::Left => gettext("Leave Room"),
_ => to_type.to_string(),
},
CategoryType::Left => match to_type {
// Translators: This is an action to rejoin a room and put it in the "Favorites"
// section.
CategoryType::Favorite => gettext("Rejoin Room as Favorite"),
CategoryType::Normal => gettext("Rejoin Room"),
// Translators: This is an action to rejoin a room and put it in the "Low
// Priority" section.
CategoryType::LowPriority => gettext("Rejoin Room as Low Priority"),
_ => to_type.to_string(),
},
_ => to_type.to_string(),
};
Some(label)
}
/// Set the expanded state of this row.
fn set_expanded(&self, expanded: bool) {
if self.expanded.get() == expanded {
return;
}
let obj = self.obj();
if expanded {
obj.set_state_flags(gtk::StateFlags::CHECKED, false);
} else {
obj.unset_state_flags(gtk::StateFlags::CHECKED);
}
self.expanded.set(expanded);
obj.set_expanded_accessibility_state(expanded);
obj.notify_expanded();
}
/// Set the `CategoryType` to show a label for.
fn set_show_label_for_category(&self, category: CategoryType) {
if category == self.show_label_for_category.get() {
return;
}
self.show_label_for_category.set(category);
let obj = self.obj();
obj.notify_show_label_for_category();
obj.notify_label();
}
}
}
glib::wrapper! {
/// A sidebar row representing a category.
pub struct CategoryRow(ObjectSubclass<imp::CategoryRow>)
@extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
}
@ -103,123 +171,15 @@ impl CategoryRow {
glib::Object::new()
}
/// The category represented by this row.
pub fn category(&self) -> Option<Category> {
self.imp().category.borrow().clone()
}
/// Set the category represented by this row.
pub fn set_category(&self, category: Option<Category>) {
if self.category() == category {
return;
}
self.imp().category.replace(category);
self.notify("category");
self.notify("label");
}
/// The expanded state of this row.
fn expanded(&self) -> bool {
self.imp().expanded.get()
}
/// Set the expanded state of this row.
fn set_expanded(&self, expanded: bool) {
if self.expanded() == expanded {
return;
}
if expanded {
self.set_state_flags(gtk::StateFlags::CHECKED, false);
} else {
self.unset_state_flags(gtk::StateFlags::CHECKED);
}
self.set_expanded_accessibility_state(expanded);
self.imp().expanded.set(expanded);
self.notify("expanded");
}
/// Set the expanded state of this row for a11y.
fn set_expanded_accessibility_state(&self, expanded: bool) {
if let Some(row) = self.parent() {
row.update_state(&[accessible::State::Expanded(Some(expanded))]);
row.update_state(&[gtk::accessible::State::Expanded(Some(expanded))]);
}
}
/// The label to show for this row.
pub fn label(&self) -> Option<String> {
let to_type = self.category()?.r#type();
let from_type = self.show_label_for_category();
let label = match from_type {
CategoryType::Invited => match to_type {
// Translators: This is an action to join a room and put it in the "Favorites"
// section.
CategoryType::Favorite => gettext("Join Room as Favorite"),
CategoryType::Normal => gettext("Join Room"),
// Translators: This is an action to join a room and put it in the "Low Priority"
// section.
CategoryType::LowPriority => gettext("Join Room as Low Priority"),
CategoryType::Left => gettext("Reject Invite"),
_ => to_type.to_string(),
},
CategoryType::Favorite => match to_type {
CategoryType::Normal => gettext("Move to Rooms"),
CategoryType::LowPriority => gettext("Move to Low Priority"),
CategoryType::Left => gettext("Leave Room"),
_ => to_type.to_string(),
},
CategoryType::Normal => match to_type {
CategoryType::Favorite => gettext("Move to Favorites"),
CategoryType::LowPriority => gettext("Move to Low Priority"),
CategoryType::Left => gettext("Leave Room"),
_ => to_type.to_string(),
},
CategoryType::LowPriority => match to_type {
CategoryType::Favorite => gettext("Move to Favorites"),
CategoryType::Normal => gettext("Move to Rooms"),
CategoryType::Left => gettext("Leave Room"),
_ => to_type.to_string(),
},
CategoryType::Left => match to_type {
// Translators: This is an action to rejoin a room and put it in the "Favorites"
// section.
CategoryType::Favorite => gettext("Rejoin Room as Favorite"),
CategoryType::Normal => gettext("Rejoin Room"),
// Translators: This is an action to rejoin a room and put it in the "Low Priority"
// section.
CategoryType::LowPriority => gettext("Rejoin Room as Low Priority"),
_ => to_type.to_string(),
},
_ => to_type.to_string(),
};
Some(label)
}
/// The `CategoryType` to show a label for.
///
/// This will change the label according to the action that can be performed
/// when changing from the `CategoryType` to this row's `Category`.
pub fn show_label_for_category(&self) -> CategoryType {
self.imp().show_label_for_category.get()
}
/// Set the `CategoryType` to show a label for.
pub fn set_show_label_for_category(&self, category: CategoryType) {
if category == self.show_label_for_category() {
return;
}
self.imp().show_label_for_category.set(category);
self.notify("show-label-for-category");
self.notify("label");
}
/// Returns the display name widget.
pub fn display_name(&self) -> gtk::Label {
self.imp().display_name.clone()
/// The descendant that labels this row for a11y.
pub fn labelled_by(&self) -> &gtk::Accessible {
self.imp().display_name.upcast_ref()
}
}

View File

@ -10,9 +10,12 @@ mod imp {
use super::*;
#[derive(Debug, Default, CompositeTemplate)]
#[derive(Debug, Default, CompositeTemplate, glib::Properties)]
#[template(resource = "/org/gnome/Fractal/ui/session/view/sidebar/icon_item_row.ui")]
#[properties(wrapper_type = super::IconItemRow)]
pub struct IconItemRow {
/// The [`IconItem`] of this row.
#[property(get, set = Self::set_icon_item, explicit_notify, nullable)]
pub icon_item: RefCell<Option<IconItem>>,
}
@ -32,35 +35,33 @@ mod imp {
}
}
impl ObjectImpl for IconItemRow {
fn properties() -> &'static [glib::ParamSpec] {
use once_cell::sync::Lazy;
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![glib::ParamSpecObject::builder::<IconItem>("icon-item")
.explicit_notify()
.build()]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"icon-item" => self.obj().set_icon_item(value.get().unwrap()),
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"icon-item" => self.obj().icon_item().to_value(),
_ => unimplemented!(),
}
}
}
#[glib::derived_properties]
impl ObjectImpl for IconItemRow {}
impl WidgetImpl for IconItemRow {}
impl BinImpl for IconItemRow {}
impl IconItemRow {
/// Set the [`IconItem`] of this row.
fn set_icon_item(&self, icon_item: Option<IconItem>) {
if self.icon_item.borrow().clone() == icon_item {
return;
}
let obj = self.obj();
if icon_item
.as_ref()
.is_some_and(|i| i.r#type() == ItemType::Forget)
{
obj.add_css_class("forget");
} else {
obj.remove_css_class("forget");
}
self.icon_item.replace(icon_item);
obj.notify_icon_item();
}
}
}
glib::wrapper! {
@ -72,28 +73,4 @@ impl IconItemRow {
pub fn new() -> Self {
glib::Object::new()
}
/// The [`IconItem`] of this row.
pub fn icon_item(&self) -> Option<IconItem> {
self.imp().icon_item.borrow().clone()
}
/// Set the [`IconItem`] of this row.
pub fn set_icon_item(&self, icon_item: Option<IconItem>) {
if self.icon_item() == icon_item {
return;
}
if icon_item
.as_ref()
.is_some_and(|i| i.r#type() == ItemType::Forget)
{
self.add_css_class("forget");
} else {
self.remove_css_class("forget");
}
self.imp().icon_item.replace(icon_item);
self.notify("icon-item");
}
}

View File

@ -21,15 +21,18 @@ use crate::{
};
mod imp {
use std::cell::{Cell, RefCell};
use std::{
cell::{Cell, OnceCell, RefCell},
marker::PhantomData,
};
use glib::{signal::SignalHandlerId, subclass::InitializingObject};
use once_cell::{sync::Lazy, unsync::OnceCell};
use glib::subclass::InitializingObject;
use super::*;
#[derive(Debug, Default, CompositeTemplate)]
#[derive(Debug, Default, CompositeTemplate, glib::Properties)]
#[template(resource = "/org/gnome/Fractal/ui/session/view/sidebar/mod.ui")]
#[properties(wrapper_type = super::Sidebar)]
pub struct Sidebar {
#[template_child]
pub scrolled_window: TemplateChild<gtk::ScrolledWindow>,
@ -44,15 +47,24 @@ mod imp {
#[template_child]
pub offline_banner: TemplateChild<adw::Banner>,
pub room_row_popover: OnceCell<gtk::PopoverMenu>,
/// The logged-in user.
#[property(get, set = Self::set_user, explicit_notify, nullable)]
pub user: RefCell<Option<User>>,
/// The type of the source that activated drop mode.
pub drop_source_type: Cell<Option<RoomType>>,
/// The `CategoryType` of the source that activated drop mode.
#[property(get = Self::drop_source_category_type, builder(CategoryType::default()))]
pub drop_source_category_type: PhantomData<CategoryType>,
/// The type of the drop target that is currently hovered.
pub drop_active_target_type: Cell<Option<RoomType>>,
/// The `CategoryType` of the drop target that is currently hovered.
#[property(get = Self::drop_active_target_category_type, builder(CategoryType::default()))]
pub drop_active_target_category_type: PhantomData<CategoryType>,
/// The list model of this sidebar.
#[property(get, set = Self::set_list_model, explicit_notify, nullable)]
pub list_model: glib::WeakRef<SidebarListModel>,
pub bindings: RefCell<Vec<glib::Binding>>,
pub offline_handler_id: RefCell<Option<SignalHandlerId>>,
pub offline_handler_id: RefCell<Option<glib::SignalHandlerId>>,
}
#[glib::object_subclass]
@ -73,58 +85,8 @@ mod imp {
}
}
#[glib::derived_properties]
impl ObjectImpl for Sidebar {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecObject::builder::<User>("user")
.explicit_notify()
.build(),
glib::ParamSpecObject::builder::<SidebarListModel>("list-model")
.explicit_notify()
.build(),
glib::ParamSpecEnum::builder::<CategoryType>("drop-source-type")
.read_only()
.build(),
glib::ParamSpecEnum::builder::<CategoryType>("drop-active-target-type")
.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() {
"user" => obj.set_user(value.get().unwrap()),
"list-model" => obj.set_list_model(value.get().unwrap()),
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
let obj = self.obj();
match pspec.name() {
"user" => obj.user().to_value(),
"list-model" => obj.list_model().to_value(),
"drop-source-type" => obj
.drop_source_type()
.map(CategoryType::from)
.unwrap_or_default()
.to_value(),
"drop-active-target-type" => obj
.drop_active_target_type()
.map(CategoryType::from)
.unwrap_or_default()
.to_value(),
_ => unimplemented!(),
}
}
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
@ -195,6 +157,89 @@ mod imp {
}
impl NavigationPageImpl for Sidebar {}
impl Sidebar {
/// Set the logged-in user.
fn set_user(&self, user: Option<User>) {
let prev_user = self.user.borrow().clone();
if prev_user == user {
return;
}
if let Some(prev_user) = prev_user {
if let Some(handler_id) = self.offline_handler_id.take() {
prev_user.session().disconnect(handler_id);
}
}
if let Some(user) = &user {
let session = user.session();
let handler_id =
session.connect_offline_notify(clone!(@weak self as imp => move |session| {
imp.offline_banner.set_revealed(session.offline());
}));
self.offline_banner.set_revealed(session.offline());
self.offline_handler_id.replace(Some(handler_id));
}
self.user.replace(user);
self.obj().notify_user();
}
/// Set the list model of the sidebar.
fn set_list_model(&self, list_model: Option<SidebarListModel>) {
if self.list_model.upgrade() == list_model {
return;
}
let obj = self.obj();
for binding in self.bindings.take() {
binding.unbind();
}
if let Some(list_model) = &list_model {
let bindings = vec![
obj.bind_property(
"drop-source-category-type",
&list_model.item_list(),
"show-all-for-category",
)
.sync_create()
.build(),
list_model
.string_filter()
.bind_property("search", &*self.room_search_entry, "text")
.sync_create()
.bidirectional()
.build(),
];
self.bindings.replace(bindings);
}
self.listview
.set_model(list_model.as_ref().map(|m| m.selection_model()).as_ref());
self.list_model.set(list_model.as_ref());
obj.notify_list_model();
}
/// The `CategoryType` of the source that activated drop mode.
fn drop_source_category_type(&self) -> CategoryType {
self.drop_source_type
.get()
.map(Into::into)
.unwrap_or_default()
}
/// The `CategoryType` of the drop target that is currently hovered.
fn drop_active_target_category_type(&self) -> CategoryType {
self.drop_active_target_type
.get()
.map(Into::into)
.unwrap_or_default()
}
}
}
glib::wrapper! {
@ -211,84 +256,6 @@ impl Sidebar {
self.imp().room_search.clone()
}
/// The list model of this sidebar.
pub fn list_model(&self) -> Option<SidebarListModel> {
self.imp().list_model.upgrade()
}
/// Set the list model of the sidebar.
pub fn set_list_model(&self, list_model: Option<SidebarListModel>) {
if self.list_model() == list_model {
return;
}
let imp = self.imp();
for binding in imp.bindings.take() {
binding.unbind();
}
if let Some(list_model) = &list_model {
let bindings = vec![
self.bind_property(
"drop-source-type",
&list_model.item_list(),
"show-all-for-category",
)
.sync_create()
.build(),
list_model
.string_filter()
.bind_property("search", &*imp.room_search_entry, "text")
.sync_create()
.bidirectional()
.build(),
];
imp.bindings.replace(bindings);
}
imp.listview
.set_model(list_model.as_ref().map(|m| m.selection_model()).as_ref());
imp.list_model.set(list_model.as_ref());
self.notify("list-model");
}
/// The logged-in user.
pub fn user(&self) -> Option<User> {
self.imp().user.borrow().clone()
}
/// Set the logged-in user.
fn set_user(&self, user: Option<User>) {
let prev_user = self.user();
if prev_user == user {
return;
}
if let Some(prev_user) = prev_user {
if let Some(handler_id) = self.imp().offline_handler_id.take() {
prev_user.session().disconnect(handler_id);
}
}
if let Some(user) = user.as_ref() {
let session = user.session();
let handler_id = session.connect_notify_local(
Some("offline"),
clone!(@weak self as obj => move |session, _| {
obj.imp().offline_banner.set_revealed(session.offline());
}),
);
self.imp().offline_banner.set_revealed(session.offline());
self.imp().offline_handler_id.replace(Some(handler_id));
}
self.imp().user.replace(user);
self.notify("user");
}
/// The type of the source that activated drop mode.
pub fn drop_source_type(&self) -> Option<RoomType> {
self.imp().drop_source_type.get()
@ -310,7 +277,7 @@ impl Sidebar {
imp.listview.remove_css_class("drop-mode");
}
self.notify("drop-source-type");
self.notify_drop_source_category_type();
}
/// The type of the drop target that is currently hovered.
@ -325,7 +292,7 @@ impl Sidebar {
}
self.imp().drop_active_target_type.set(target_type);
self.notify("drop-active-target-type");
self.notify_drop_active_target_category_type();
}
pub fn room_row_popover(&self) -> &gtk::PopoverMenu {

View File

@ -1,6 +1,6 @@
use adw::{prelude::*, subclass::prelude::*};
use gettextrs::gettext;
use gtk::{accessible::Property, gdk, glib, glib::clone, CompositeTemplate};
use gtk::{gdk, glib, glib::clone, CompositeTemplate};
use super::Row;
use crate::{
@ -15,13 +15,15 @@ mod imp {
use std::cell::RefCell;
use glib::subclass::InitializingObject;
use once_cell::sync::Lazy;
use super::*;
#[derive(Debug, Default, CompositeTemplate)]
#[derive(Debug, Default, CompositeTemplate, glib::Properties)]
#[template(resource = "/org/gnome/Fractal/ui/session/view/sidebar/room_row.ui")]
#[properties(wrapper_type = super::RoomRow)]
pub struct RoomRow {
/// The room represented by this row.
#[property(get, set = Self::set_room, explicit_notify, nullable)]
pub room: BoundObject<Room>,
pub binding: RefCell<Option<glib::Binding>>,
#[template_child]
@ -94,31 +96,8 @@ mod imp {
}
}
#[glib::derived_properties]
impl ObjectImpl for RoomRow {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![glib::ParamSpecObject::builder::<Room>("room")
.explicit_notify()
.build()]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"room" => self.obj().set_room(value.get().unwrap()),
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"room" => self.obj().room().to_value(),
_ => unimplemented!(),
}
}
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
@ -158,16 +137,70 @@ mod imp {
if let Some(sidebar) = obj
.parent()
.and_downcast_ref::<Row>()
.map(|row| row.sidebar())
.and_then(|row| row.sidebar())
{
let popover = sidebar.room_row_popover();
obj.set_popover(Some(popover.to_owned()));
}
}
}
impl RoomRow {
/// Set the room represented by this row.
pub fn set_room(&self, room: Option<Room>) {
if self.room.obj() == room {
return;
}
let obj = self.obj();
self.room.disconnect_signals();
if let Some(binding) = self.binding.take() {
binding.unbind();
}
self.display_name.remove_css_class("dim-label");
if let Some(room) = room {
self.binding.replace(Some(
room.bind_property(
"notification-count",
&self.notification_count.get(),
"visible",
)
.sync_create()
.transform_from(|_, count: u64| Some(count > 0))
.build(),
));
let highlight_handler =
room.connect_highlight_notify(clone!(@weak obj => move |_| {
obj.update_highlight();
}));
let direct_handler = room.connect_is_direct_notify(clone!(@weak obj => move |_| {
obj.update_direct_icon();
}));
let name_handler = room.connect_display_name_notify(clone!(@weak obj => move |_| {
obj.update_accessibility_label();
}));
if room.category() == RoomType::Left {
self.display_name.add_css_class("dim-label");
}
self.room
.set(room, vec![highlight_handler, direct_handler, name_handler]);
obj.update_accessibility_label();
}
obj.update_highlight();
obj.update_direct_icon();
obj.update_actions();
obj.notify_room();
}
}
}
glib::wrapper! {
/// A sidebar row representing a room.
pub struct RoomRow(ObjectSubclass<imp::RoomRow>)
@extends gtk::Widget, adw::Bin, ContextMenuBin, @implements gtk::Accessible;
}
@ -177,69 +210,6 @@ impl RoomRow {
glib::Object::new()
}
/// The room represented by this row.
pub fn room(&self) -> Option<Room> {
self.imp().room.obj()
}
/// Set the room represented by this row.
pub fn set_room(&self, room: Option<Room>) {
let imp = self.imp();
if self.room() == room {
return;
}
imp.room.disconnect_signals();
if let Some(binding) = imp.binding.take() {
binding.unbind();
}
imp.display_name.remove_css_class("dim-label");
if let Some(room) = room {
imp.binding.replace(Some(
room.bind_property(
"notification-count",
&imp.notification_count.get(),
"visible",
)
.sync_create()
.transform_from(|_, count: u64| Some(count > 0))
.build(),
));
let highlight_handler = room.connect_notify_local(
Some("highlight"),
clone!(@weak self as obj => move |_, _| {
obj.update_highlight();
}),
);
let direct_handler = room.connect_notify_local(
Some("is-direct"),
clone!(@weak self as obj => move |_, _| {
obj.update_direct_icon();
}),
);
let name_handler =
room.connect_display_name_notify(clone!(@weak self as obj => move |_| {
obj.update_accessibility_label();
}));
if room.category() == RoomType::Left {
imp.display_name.add_css_class("dim-label");
}
imp.room
.set(room, vec![highlight_handler, direct_handler, name_handler]);
self.update_accessibility_label();
}
self.update_highlight();
self.update_direct_icon();
self.update_actions();
self.notify("room");
}
fn update_highlight(&self) {
let imp = self.imp();
if let Some(room) = self.room() {
@ -336,24 +306,41 @@ impl RoomRow {
}
fn drag_prepare(&self, drag: &gtk::DragSource, x: f64, y: f64) -> Option<gdk::ContentProvider> {
let paintable = gtk::WidgetPaintable::new(Some(&self.parent().unwrap()));
// FIXME: The hotspot coordinates don't work.
// See https://gitlab.gnome.org/GNOME/gtk/-/issues/2341
drag.set_icon(Some(&paintable), x as i32, y as i32);
self.room()
.map(|room| gdk::ContentProvider::for_value(&room.to_value()))
let room = self.room()?;
if let Some(parent) = self.parent() {
let paintable = gtk::WidgetPaintable::new(Some(&parent));
// FIXME: The hotspot coordinates don't work.
// See https://gitlab.gnome.org/GNOME/gtk/-/issues/2341
drag.set_icon(Some(&paintable), x as i32, y as i32);
}
Some(gdk::ContentProvider::for_value(&room.to_value()))
}
fn drag_begin(&self) {
let row = self.parent().and_downcast::<Row>().unwrap();
let Some(room) = self.room() else {
return;
};
let Some(row) = self.parent().and_downcast::<Row>() else {
return;
};
let Some(sidebar) = row.sidebar() else {
return;
};
row.add_css_class("drag");
row.sidebar()
.set_drop_source_type(Some(self.room().unwrap().category()));
sidebar.set_drop_source_type(Some(room.category()));
}
fn drag_end(&self) {
let row = self.parent().and_downcast::<Row>().unwrap();
row.sidebar().set_drop_source_type(None);
let Some(row) = self.parent().and_downcast::<Row>() else {
return;
};
let Some(sidebar) = row.sidebar() else {
return;
};
sidebar.set_drop_source_type(None);
row.remove_css_class("drag");
}
@ -422,15 +409,17 @@ impl RoomRow {
}
fn update_accessibility_label(&self) {
self.parent()
.unwrap()
.update_property(&[Property::Label(&self.accessible_label())]);
let Some(parent) = self.parent() else {
return;
};
parent.update_property(&[gtk::accessible::Property::Label(&self.accessible_label())]);
}
fn accessible_label(&self) -> String {
let Some(room) = self.room() else {
return String::new();
};
if room.is_direct() {
gettext_f(
// Translators: Do NOT translate the content between '{' and '}', this is a

View File

@ -13,16 +13,22 @@ use crate::{
};
mod imp {
use std::cell::RefCell;
use once_cell::sync::Lazy;
use std::{cell::RefCell, marker::PhantomData};
use super::*;
#[derive(Debug, Default)]
#[derive(Debug, Default, glib::Properties)]
#[properties(wrapper_type = super::Row)]
pub struct Row {
/// The ancestor sidebar of this row.
#[property(get, set = Self::set_sidebar, construct_only)]
pub sidebar: BoundObjectWeakRef<Sidebar>,
/// The list row to track for expander state.
#[property(get, set = Self::set_list_row, explicit_notify, nullable)]
pub list_row: RefCell<Option<gtk::TreeListRow>>,
/// The sidebar item of this row.
#[property(get = Self::item)]
pub item: PhantomData<Option<SidebarItem>>,
pub bindings: RefCell<Vec<glib::Binding>>,
}
@ -38,46 +44,8 @@ mod imp {
}
}
#[glib::derived_properties]
impl ObjectImpl for Row {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecObject::builder::<glib::Object>("item")
.read_only()
.build(),
glib::ParamSpecObject::builder::<gtk::TreeListRow>("list-row")
.explicit_notify()
.build(),
glib::ParamSpecObject::builder::<Sidebar>("sidebar")
.construct_only()
.build(),
]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
let obj = self.obj();
match pspec.name() {
"list-row" => obj.set_list_row(value.get().unwrap()),
"sidebar" => obj.set_sidebar(value.get().ok().as_ref()),
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
let obj = self.obj();
match pspec.name() {
"item" => obj.item().to_value(),
"list-row" => obj.list_row().to_value(),
"sidebar" => obj.sidebar().to_value(),
_ => unimplemented!(),
}
}
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
@ -104,9 +72,115 @@ mod imp {
impl WidgetImpl for Row {}
impl BinImpl for Row {}
impl Row {
/// Set the ancestor sidebar of this row.
fn set_sidebar(&self, sidebar: Sidebar) {
let obj = self.obj();
let drop_source_type_handler =
sidebar.connect_drop_source_category_type_notify(clone!(@weak obj => move |_| {
obj.update_for_drop_source_type();
}));
let drop_active_target_type_handler = sidebar
.connect_drop_active_target_category_type_notify(clone!(@weak obj => move |_| {
obj.update_for_drop_active_target_type();
}));
self.sidebar.set(
&sidebar,
vec![drop_source_type_handler, drop_active_target_type_handler],
);
}
/// Set the list row to track for expander state.
fn set_list_row(&self, list_row: Option<gtk::TreeListRow>) {
if self.list_row.borrow().clone() == list_row {
return;
}
let obj = self.obj();
for binding in self.bindings.take() {
binding.unbind();
}
self.list_row.replace(list_row.clone());
let mut bindings = vec![];
if let Some((row, item)) = list_row.zip(self.item()) {
if let Some(category) = item.downcast_ref::<Category>() {
let child = if let Some(child) = obj.child().and_downcast::<CategoryRow>() {
child
} else {
let child = CategoryRow::new();
obj.set_child(Some(&child));
obj.update_relation(&[Relation::LabelledBy(&[child.labelled_by()])]);
child
};
child.set_category(Some(category.clone()));
bindings.push(
row.bind_property("expanded", &child, "expanded")
.sync_create()
.build(),
);
} else if let Some(room) = item.downcast_ref::<Room>() {
let child = if let Some(child) = obj.child().and_downcast::<RoomRow>() {
child
} else {
let child = RoomRow::new();
obj.set_child(Some(&child));
child
};
child.set_room(Some(room.clone()));
} else if let Some(icon_item) = item.downcast_ref::<IconItem>() {
let child = if let Some(child) = obj.child().and_downcast::<IconItemRow>() {
child
} else {
let child = IconItemRow::new();
obj.set_child(Some(&child));
child
};
child.set_icon_item(Some(icon_item.clone()));
} else if let Some(verification) = item.downcast_ref::<IdentityVerification>() {
let child = if let Some(child) = obj.child().and_downcast::<VerificationRow>() {
child
} else {
let child = VerificationRow::new();
obj.set_child(Some(&child));
child
};
child.set_identity_verification(Some(verification.clone()));
} else {
panic!("Wrong row item: {item:?}");
}
obj.update_for_drop_source_type();
}
self.bindings.replace(bindings);
obj.notify_item();
obj.notify_list_row();
}
/// The sidebar item of this row.
fn item(&self) -> Option<SidebarItem> {
self.list_row
.borrow()
.as_ref()
.and_then(|r| r.item())
.and_downcast()
}
}
}
glib::wrapper! {
/// A row of the sidebar.
pub struct Row(ObjectSubclass<imp::Row>)
@extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
}
@ -119,129 +193,6 @@ impl Row {
.build()
}
/// The ancestor sidebar of this row.
pub fn sidebar(&self) -> Sidebar {
self.imp().sidebar.obj().unwrap()
}
/// Set the ancestor sidebar of this row.
fn set_sidebar(&self, sidebar: Option<&Sidebar>) {
let Some(sidebar) = sidebar else {
return;
};
let drop_source_type_handler = sidebar.connect_notify_local(
Some("drop-source-type"),
clone!(@weak self as obj => move |_, _| {
obj.update_for_drop_source_type();
}),
);
let drop_active_target_type_handler = sidebar.connect_notify_local(
Some("drop-active-target-type"),
clone!(@weak self as obj => move |_, _| {
obj.update_for_drop_active_target_type();
}),
);
self.imp().sidebar.set(
sidebar,
vec![drop_source_type_handler, drop_active_target_type_handler],
);
}
/// The sidebar item of this row.
pub fn item(&self) -> Option<SidebarItem> {
self.list_row().and_then(|r| r.item()).and_downcast()
}
/// The list row to track for expander state.
pub fn list_row(&self) -> Option<gtk::TreeListRow> {
self.imp().list_row.borrow().clone()
}
/// Set the list row to track for expander state.
pub fn set_list_row(&self, list_row: Option<gtk::TreeListRow>) {
let imp = self.imp();
if self.list_row() == list_row {
return;
}
for binding in imp.bindings.take() {
binding.unbind();
}
let row = if let Some(row) = list_row.clone() {
imp.list_row.replace(list_row);
row
} else {
return;
};
let mut bindings = vec![];
if let Some(item) = self.item() {
if let Some(category) = item.downcast_ref::<Category>() {
let child = if let Some(child) = self.child().and_downcast::<CategoryRow>() {
child
} else {
let child = CategoryRow::new();
self.set_child(Some(&child));
self.update_relation(&[Relation::LabelledBy(&[&child
.display_name()
.upcast()])]);
child
};
child.set_category(Some(category.clone()));
bindings.push(
row.bind_property("expanded", &child, "expanded")
.sync_create()
.build(),
);
} else if let Some(room) = item.downcast_ref::<Room>() {
let child = if let Some(child) = self.child().and_downcast::<RoomRow>() {
child
} else {
let child = RoomRow::new();
self.set_child(Some(&child));
child
};
child.set_room(Some(room.clone()));
} else if let Some(icon_item) = item.downcast_ref::<IconItem>() {
let child = if let Some(child) = self.child().and_downcast::<IconItemRow>() {
child
} else {
let child = IconItemRow::new();
self.set_child(Some(&child));
child
};
child.set_icon_item(Some(icon_item.clone()));
} else if let Some(verification) = item.downcast_ref::<IdentityVerification>() {
let child = if let Some(child) = self.child().and_downcast::<VerificationRow>() {
child
} else {
let child = VerificationRow::new();
self.set_child(Some(&child));
child
};
child.set_identity_verification(Some(verification.clone()));
} else {
panic!("Wrong row item: {item:?}");
}
self.update_for_drop_source_type();
}
imp.bindings.replace(bindings);
self.notify("item");
self.notify("list-row");
}
/// Get the `RoomType` of this item.
///
/// If this is not a `Category` or one of its children, returns `None`.
@ -267,6 +218,10 @@ impl Row {
/// Handle the drag-n-drop hovering this row.
fn drop_accept(&self, drop: &gdk::Drop) -> bool {
let Some(sidebar) = self.sidebar() else {
return false;
};
let room = drop
.drag()
.map(|drag| drag.content())
@ -275,14 +230,13 @@ impl Row {
if let Some(room) = room {
if let Some(target_type) = self.room_type() {
if room.category().can_change_to(target_type) {
self.sidebar()
.set_drop_active_target_type(Some(target_type));
sidebar.set_drop_active_target_type(Some(target_type));
return true;
}
} else if let Some(item_type) = self.item_type() {
if room.category() == RoomType::Left && item_type == ItemType::Forget {
self.add_css_class("drop-active");
self.sidebar().set_drop_active_target_type(None);
sidebar.set_drop_active_target_type(None);
return true;
}
}
@ -293,7 +247,9 @@ impl Row {
/// Handle the drag-n-drop leaving this row.
fn drop_leave(&self) {
self.remove_css_class("drop-active");
self.sidebar().set_drop_active_target_type(None);
if let Some(sidebar) = self.sidebar() {
sidebar.set_drop_active_target_type(None);
}
}
/// Handle the drop on this row.
@ -316,7 +272,9 @@ impl Row {
}
}
}
self.sidebar().set_drop_source_type(None);
if let Some(sidebar) = self.sidebar() {
sidebar.set_drop_source_type(None);
}
ret
}
@ -359,7 +317,7 @@ impl Row {
/// Update the disabled or empty state of this drop target.
fn update_for_drop_source_type(&self) {
let source_type = self.sidebar().drop_source_type();
let source_type = self.sidebar().and_then(|s| s.drop_source_type());
if let Some(source_type) = source_type {
if self
@ -407,9 +365,9 @@ impl Row {
let Some(room_type) = self.room_type() else {
return;
};
let target_type = self.sidebar().drop_active_target_type();
let target_type = self.sidebar().and_then(|s| s.drop_active_target_type());
if target_type.map_or(false, |target_type| target_type == room_type) {
if target_type.is_some_and(|target_type| target_type == room_type) {
self.add_css_class("drop-active");
} else {
self.remove_css_class("drop-active");

View File

@ -7,14 +7,16 @@ mod imp {
use std::cell::RefCell;
use glib::subclass::InitializingObject;
use once_cell::sync::Lazy;
use super::*;
#[derive(Debug, Default, CompositeTemplate)]
#[derive(Debug, Default, CompositeTemplate, glib::Properties)]
#[template(resource = "/org/gnome/Fractal/ui/session/view/sidebar/verification_row.ui")]
#[properties(wrapper_type = super::VerificationRow)]
pub struct VerificationRow {
pub verification: RefCell<Option<IdentityVerification>>,
/// The identity verification represented by this row.
#[property(get, set = Self::set_identity_verification, explicit_notify, nullable)]
pub identity_verification: RefCell<Option<IdentityVerification>>,
}
#[glib::object_subclass]
@ -32,41 +34,27 @@ mod imp {
}
}
impl ObjectImpl for VerificationRow {
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![glib::ParamSpecObject::builder::<IdentityVerification>(
"identity-verification",
)
.explicit_notify()
.build()]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"identity-verification" => {
self.obj().set_identity_verification(value.get().unwrap())
}
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"identity-verification" => self.obj().identity_verification().to_value(),
_ => unimplemented!(),
}
}
}
#[glib::derived_properties]
impl ObjectImpl for VerificationRow {}
impl WidgetImpl for VerificationRow {}
impl BinImpl for VerificationRow {}
impl VerificationRow {
/// Set the identity verification represented by this row.
fn set_identity_verification(&self, verification: Option<IdentityVerification>) {
if self.identity_verification.borrow().clone() == verification {
return;
}
self.identity_verification.replace(verification);
self.obj().notify_identity_verification();
}
}
}
glib::wrapper! {
/// A sidebar row representing an identity verification.
pub struct VerificationRow(ObjectSubclass<imp::VerificationRow>)
@extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
}
@ -75,19 +63,4 @@ impl VerificationRow {
pub fn new() -> Self {
glib::Object::new()
}
/// The identity verification represented by this row.
pub fn identity_verification(&self) -> Option<IdentityVerification> {
self.imp().verification.borrow().clone()
}
/// Set the identity verification represented by this row.
pub fn set_identity_verification(&self, verification: Option<IdentityVerification>) {
if self.identity_verification() == verification {
return;
}
self.imp().verification.replace(verification);
self.notify("identity-verification");
}
}