fractal/session/view/content/room_history/
item_row_context_menu.rsuse adw::{prelude::*, subclass::prelude::*};
use gettextrs::gettext;
use gtk::{
gio, glib,
glib::{clone, closure_local},
CompositeTemplate,
};
use crate::{session::model::ReactionList, utils::BoundObject};
#[derive(Debug)]
pub(super) struct ItemRowContextMenu {
pub(super) popover: gtk::PopoverMenu,
menu_model: gio::Menu,
quick_reaction_chooser: QuickReactionChooser,
}
impl ItemRowContextMenu {
const QUICK_REACTION_CHOOSER_ID: &str = "quick-reaction-chooser";
fn has_quick_reaction_chooser(&self) -> bool {
let first_section = self
.menu_model
.item_link(0, gio::MENU_LINK_SECTION)
.and_downcast::<gio::Menu>()
.expect("item row context menu has at least one section");
first_section
.item_attribute_value(0, "custom", Some(&String::static_variant_type()))
.and_then(|variant| variant.get::<String>())
.is_some_and(|value| value == Self::QUICK_REACTION_CHOOSER_ID)
}
pub(super) fn add_quick_reaction_chooser(&self, reactions: ReactionList) {
if !self.has_quick_reaction_chooser() {
let section_menu = gio::Menu::new();
let item = gio::MenuItem::new(None, None);
item.set_attribute_value(
"custom",
Some(&Self::QUICK_REACTION_CHOOSER_ID.to_variant()),
);
section_menu.append_item(&item);
self.menu_model.insert_section(0, None, §ion_menu);
self.popover.add_child(
&self.quick_reaction_chooser,
Self::QUICK_REACTION_CHOOSER_ID,
);
}
self.quick_reaction_chooser.set_reactions(Some(reactions));
}
pub(super) fn remove_quick_reaction_chooser(&self) {
if !self.has_quick_reaction_chooser() {
return;
}
self.popover.remove_child(&self.quick_reaction_chooser);
self.menu_model.remove(0);
}
}
impl Default for ItemRowContextMenu {
fn default() -> Self {
let menu_model = gtk::Builder::from_resource(
"/org/gnome/Fractal/ui/session/view/content/room_history/event_context_menu.ui",
)
.object::<gio::Menu>("event-menu")
.expect("resource and menu exist");
let popover = gtk::PopoverMenu::builder()
.has_arrow(false)
.halign(gtk::Align::Start)
.menu_model(&menu_model)
.build();
popover.update_property(&[gtk::accessible::Property::Label(&gettext("Context Menu"))]);
Self {
popover,
menu_model,
quick_reaction_chooser: Default::default(),
}
}
}
#[derive(Debug, Clone, Copy)]
struct QuickReaction {
key: &'static str,
column: i32,
row: i32,
}
static QUICK_REACTIONS: &[QuickReaction] = &[
QuickReaction {
key: "👍️",
column: 0,
row: 0,
},
QuickReaction {
key: "👎️",
column: 1,
row: 0,
},
QuickReaction {
key: "😄",
column: 2,
row: 0,
},
QuickReaction {
key: "🎉",
column: 3,
row: 0,
},
QuickReaction {
key: "😕",
column: 0,
row: 1,
},
QuickReaction {
key: "❤️",
column: 1,
row: 1,
},
QuickReaction {
key: "🚀",
column: 2,
row: 1,
},
];
mod imp {
use std::{cell::RefCell, collections::HashMap, sync::LazyLock};
use glib::subclass::{InitializingObject, Signal};
use super::*;
#[derive(Debug, Default, CompositeTemplate, glib::Properties)]
#[template(
resource = "/org/gnome/Fractal/ui/session/view/content/room_history/quick_reaction_chooser.ui"
)]
#[properties(wrapper_type = super::QuickReactionChooser)]
pub struct QuickReactionChooser {
#[template_child]
reaction_grid: TemplateChild<gtk::Grid>,
#[property(get, set = Self::set_reactions, explicit_notify, nullable)]
reactions: BoundObject<ReactionList>,
reaction_bindings: RefCell<HashMap<String, glib::Binding>>,
}
#[glib::object_subclass]
impl ObjectSubclass for QuickReactionChooser {
const NAME: &'static str = "QuickReactionChooser";
type Type = super::QuickReactionChooser;
type ParentType = adw::Bin;
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
Self::bind_template_callbacks(klass);
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
#[glib::derived_properties]
impl ObjectImpl for QuickReactionChooser {
fn signals() -> &'static [Signal] {
static SIGNALS: LazyLock<Vec<Signal>> =
LazyLock::new(|| vec![Signal::builder("more-reactions-activated").build()]);
SIGNALS.as_ref()
}
fn constructed(&self) {
self.parent_constructed();
let grid = &self.reaction_grid;
for reaction in QUICK_REACTIONS {
let button = gtk::ToggleButton::builder()
.label(reaction.key)
.action_name("event.toggle-reaction")
.action_target(&reaction.key.to_variant())
.css_classes(["flat", "circular"])
.build();
button.connect_clicked(|button| {
button.activate_action("context-menu.close", None).unwrap();
});
grid.attach(&button, reaction.column, reaction.row, 1, 1);
}
}
}
impl WidgetImpl for QuickReactionChooser {}
impl BinImpl for QuickReactionChooser {}
#[gtk::template_callbacks]
impl QuickReactionChooser {
fn set_reactions(&self, reactions: Option<ReactionList>) {
let prev_reactions = self.reactions.obj();
if prev_reactions == reactions {
return;
}
self.reactions.disconnect_signals();
for (_, binding) in self.reaction_bindings.borrow_mut().drain() {
binding.unbind();
}
for row in 0..=1 {
for column in 0..=3 {
if let Some(button) = self
.reaction_grid
.child_at(column, row)
.and_downcast::<gtk::ToggleButton>()
{
button.set_active(false);
}
}
}
if let Some(reactions) = reactions {
let signal_handler = reactions.connect_items_changed(clone!(
#[weak(rename_to = imp)]
self,
move |_, _, _, _| {
imp.update_reactions();
}
));
self.reactions.set(reactions, vec![signal_handler]);
}
self.update_reactions();
}
fn update_reactions(&self) {
let mut reaction_bindings = self.reaction_bindings.borrow_mut();
let reactions = self.reactions.obj();
for reaction_item in QUICK_REACTIONS {
if let Some(reaction) = reactions
.as_ref()
.and_then(|reactions| reactions.reaction_group_by_key(reaction_item.key))
{
if reaction_bindings.get(reaction_item.key).is_none() {
let button = self
.reaction_grid
.child_at(reaction_item.column, reaction_item.row)
.unwrap();
let binding = reaction
.bind_property("has-own-user", &button, "active")
.sync_create()
.build();
reaction_bindings.insert(reaction_item.key.to_string(), binding);
}
} else if let Some(binding) = reaction_bindings.remove(reaction_item.key) {
if let Some(button) = self
.reaction_grid
.child_at(reaction_item.column, reaction_item.row)
.and_downcast::<gtk::ToggleButton>()
{
button.set_active(false);
}
binding.unbind();
}
}
}
#[template_callback]
fn more_reactions_activated(&self) {
self.obj()
.emit_by_name::<()>("more-reactions-activated", &[]);
}
}
}
glib::wrapper! {
pub struct QuickReactionChooser(ObjectSubclass<imp::QuickReactionChooser>)
@extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
}
impl QuickReactionChooser {
pub fn new() -> Self {
glib::Object::new()
}
pub fn connect_more_reactions_activated<F: Fn(&Self) + 'static>(
&self,
f: F,
) -> glib::SignalHandlerId {
self.connect_closure(
"more-reactions-activated",
true,
closure_local!(move |obj: Self| {
f(&obj);
}),
)
}
}
impl Default for QuickReactionChooser {
fn default() -> Self {
Self::new()
}
}