fractal/session/view/content/room_history/
item_row_context_menu.rs1use adw::{prelude::*, subclass::prelude::*};
2use gettextrs::gettext;
3use gtk::{
4 gio, glib,
5 glib::{clone, closure_local},
6 CompositeTemplate,
7};
8
9use crate::{session::model::ReactionList, utils::BoundObject};
10
11#[derive(Debug)]
13pub(super) struct ItemRowContextMenu {
14 pub(super) popover: gtk::PopoverMenu,
16 menu_model: gio::Menu,
18 quick_reaction_chooser: QuickReactionChooser,
20}
21
22impl ItemRowContextMenu {
23 const QUICK_REACTION_CHOOSER_ID: &str = "quick-reaction-chooser";
25
26 fn has_quick_reaction_chooser(&self) -> bool {
28 let first_section = self
29 .menu_model
30 .item_link(0, gio::MENU_LINK_SECTION)
31 .and_downcast::<gio::Menu>()
32 .expect("item row context menu has at least one section");
33 first_section
34 .item_attribute_value(0, "custom", Some(&String::static_variant_type()))
35 .and_then(|variant| variant.get::<String>())
36 .is_some_and(|value| value == Self::QUICK_REACTION_CHOOSER_ID)
37 }
38
39 pub(super) fn add_quick_reaction_chooser(&self, reactions: ReactionList) {
42 if !self.has_quick_reaction_chooser() {
43 let section_menu = gio::Menu::new();
44 let item = gio::MenuItem::new(None, None);
45 item.set_attribute_value(
46 "custom",
47 Some(&Self::QUICK_REACTION_CHOOSER_ID.to_variant()),
48 );
49 section_menu.append_item(&item);
50 self.menu_model.insert_section(0, None, §ion_menu);
51
52 self.popover.add_child(
53 &self.quick_reaction_chooser,
54 Self::QUICK_REACTION_CHOOSER_ID,
55 );
56 }
57
58 self.quick_reaction_chooser.set_reactions(Some(reactions));
59 }
60
61 pub(super) fn remove_quick_reaction_chooser(&self) {
63 if !self.has_quick_reaction_chooser() {
64 return;
65 }
66
67 self.popover.remove_child(&self.quick_reaction_chooser);
68 self.menu_model.remove(0);
69 }
70}
71
72impl Default for ItemRowContextMenu {
73 fn default() -> Self {
74 let menu_model = gtk::Builder::from_resource(
75 "/org/gnome/Fractal/ui/session/view/content/room_history/event_context_menu.ui",
76 )
77 .object::<gio::Menu>("event-menu")
78 .expect("resource and menu exist");
79
80 let popover = gtk::PopoverMenu::builder()
81 .has_arrow(false)
82 .halign(gtk::Align::Start)
83 .menu_model(&menu_model)
84 .build();
85 popover.update_property(&[gtk::accessible::Property::Label(&gettext("Context Menu"))]);
86
87 Self {
88 popover,
89 menu_model,
90 quick_reaction_chooser: Default::default(),
91 }
92 }
93}
94
95#[derive(Debug, Clone, Copy)]
97struct QuickReaction {
98 key: &'static str,
100 column: i32,
104 row: i32,
108}
109
110static QUICK_REACTIONS: &[QuickReaction] = &[
112 QuickReaction {
113 key: "👍️",
114 column: 0,
115 row: 0,
116 },
117 QuickReaction {
118 key: "👎️",
119 column: 1,
120 row: 0,
121 },
122 QuickReaction {
123 key: "😄",
124 column: 2,
125 row: 0,
126 },
127 QuickReaction {
128 key: "🎉",
129 column: 3,
130 row: 0,
131 },
132 QuickReaction {
133 key: "😕",
134 column: 0,
135 row: 1,
136 },
137 QuickReaction {
138 key: "❤️",
139 column: 1,
140 row: 1,
141 },
142 QuickReaction {
143 key: "🚀",
144 column: 2,
145 row: 1,
146 },
147];
148
149mod imp {
150
151 use std::{cell::RefCell, collections::HashMap, sync::LazyLock};
152
153 use glib::subclass::{InitializingObject, Signal};
154
155 use super::*;
156
157 #[derive(Debug, Default, CompositeTemplate, glib::Properties)]
158 #[template(
159 resource = "/org/gnome/Fractal/ui/session/view/content/room_history/quick_reaction_chooser.ui"
160 )]
161 #[properties(wrapper_type = super::QuickReactionChooser)]
162 pub struct QuickReactionChooser {
163 #[template_child]
164 reaction_grid: TemplateChild<gtk::Grid>,
165 #[property(get, set = Self::set_reactions, explicit_notify, nullable)]
168 reactions: BoundObject<ReactionList>,
169 reaction_bindings: RefCell<HashMap<String, glib::Binding>>,
170 }
171
172 #[glib::object_subclass]
173 impl ObjectSubclass for QuickReactionChooser {
174 const NAME: &'static str = "QuickReactionChooser";
175 type Type = super::QuickReactionChooser;
176 type ParentType = adw::Bin;
177
178 fn class_init(klass: &mut Self::Class) {
179 Self::bind_template(klass);
180 Self::bind_template_callbacks(klass);
181 }
182
183 fn instance_init(obj: &InitializingObject<Self>) {
184 obj.init_template();
185 }
186 }
187
188 #[glib::derived_properties]
189 impl ObjectImpl for QuickReactionChooser {
190 fn signals() -> &'static [Signal] {
191 static SIGNALS: LazyLock<Vec<Signal>> =
192 LazyLock::new(|| vec![Signal::builder("more-reactions-activated").build()]);
193 SIGNALS.as_ref()
194 }
195
196 fn constructed(&self) {
197 self.parent_constructed();
198
199 let grid = &self.reaction_grid;
201 for reaction in QUICK_REACTIONS {
202 let button = gtk::ToggleButton::builder()
203 .label(reaction.key)
204 .action_name("event.toggle-reaction")
205 .action_target(&reaction.key.to_variant())
206 .css_classes(["flat", "circular"])
207 .build();
208 button.connect_clicked(|button| {
209 button.activate_action("context-menu.close", None).unwrap();
210 });
211 grid.attach(&button, reaction.column, reaction.row, 1, 1);
212 }
213 }
214 }
215
216 impl WidgetImpl for QuickReactionChooser {}
217 impl BinImpl for QuickReactionChooser {}
218
219 #[gtk::template_callbacks]
220 impl QuickReactionChooser {
221 fn set_reactions(&self, reactions: Option<ReactionList>) {
224 let prev_reactions = self.reactions.obj();
225
226 if prev_reactions == reactions {
227 return;
228 }
229
230 self.reactions.disconnect_signals();
231 for (_, binding) in self.reaction_bindings.borrow_mut().drain() {
232 binding.unbind();
233 }
234
235 for row in 0..=1 {
237 for column in 0..=3 {
238 if let Some(button) = self
239 .reaction_grid
240 .child_at(column, row)
241 .and_downcast::<gtk::ToggleButton>()
242 {
243 button.set_active(false);
244 }
245 }
246 }
247
248 if let Some(reactions) = reactions {
249 let signal_handler = reactions.connect_items_changed(clone!(
250 #[weak(rename_to = imp)]
251 self,
252 move |_, _, _, _| {
253 imp.update_reactions();
254 }
255 ));
256 self.reactions.set(reactions, vec![signal_handler]);
257 }
258
259 self.update_reactions();
260 }
261
262 fn update_reactions(&self) {
264 let mut reaction_bindings = self.reaction_bindings.borrow_mut();
265 let reactions = self.reactions.obj();
266
267 for reaction_item in QUICK_REACTIONS {
268 if let Some(reaction) = reactions
269 .as_ref()
270 .and_then(|reactions| reactions.reaction_group_by_key(reaction_item.key))
271 {
272 if reaction_bindings.get(reaction_item.key).is_none() {
273 let button = self
274 .reaction_grid
275 .child_at(reaction_item.column, reaction_item.row)
276 .unwrap();
277 let binding = reaction
278 .bind_property("has-own-user", &button, "active")
279 .sync_create()
280 .build();
281 reaction_bindings.insert(reaction_item.key.to_string(), binding);
282 }
283 } else if let Some(binding) = reaction_bindings.remove(reaction_item.key) {
284 if let Some(button) = self
285 .reaction_grid
286 .child_at(reaction_item.column, reaction_item.row)
287 .and_downcast::<gtk::ToggleButton>()
288 {
289 button.set_active(false);
290 }
291
292 binding.unbind();
293 }
294 }
295 }
296
297 #[template_callback]
299 fn more_reactions_activated(&self) {
300 self.obj()
301 .emit_by_name::<()>("more-reactions-activated", &[]);
302 }
303 }
304}
305
306glib::wrapper! {
307 pub struct QuickReactionChooser(ObjectSubclass<imp::QuickReactionChooser>)
309 @extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
310}
311
312impl QuickReactionChooser {
313 pub fn new() -> Self {
314 glib::Object::new()
315 }
316
317 pub fn connect_more_reactions_activated<F: Fn(&Self) + 'static>(
320 &self,
321 f: F,
322 ) -> glib::SignalHandlerId {
323 self.connect_closure(
324 "more-reactions-activated",
325 true,
326 closure_local!(move |obj: Self| {
327 f(&obj);
328 }),
329 )
330 }
331}
332
333impl Default for QuickReactionChooser {
334 fn default() -> Self {
335 Self::new()
336 }
337}