fractal/components/
context_menu_bin.rs

1use adw::subclass::prelude::*;
2use gtk::{gdk, glib, glib::clone, prelude::*, CompositeTemplate};
3
4use crate::utils::{key_bindings, BoundObject};
5
6mod imp {
7    use std::cell::{Cell, RefCell};
8
9    use glib::subclass::InitializingObject;
10
11    use super::*;
12
13    #[repr(C)]
14    pub struct ContextMenuBinClass {
15        parent_class: glib::object::Class<adw::Bin>,
16        pub(super) menu_opened: fn(&super::ContextMenuBin),
17    }
18
19    unsafe impl ClassStruct for ContextMenuBinClass {
20        type Type = ContextMenuBin;
21    }
22
23    pub(super) fn context_menu_bin_menu_opened(this: &super::ContextMenuBin) {
24        let klass = this.class();
25        (klass.as_ref().menu_opened)(this);
26    }
27
28    #[derive(Debug, Default, CompositeTemplate, glib::Properties)]
29    #[template(resource = "/org/gnome/Fractal/ui/components/context_menu_bin.ui")]
30    #[properties(wrapper_type = super::ContextMenuBin)]
31    pub struct ContextMenuBin {
32        #[template_child]
33        click_gesture: TemplateChild<gtk::GestureClick>,
34        #[template_child]
35        long_press_gesture: TemplateChild<gtk::GestureLongPress>,
36        /// Whether this widget has a context menu.
37        ///
38        /// If this is set to `false`, all the actions will be disabled.
39        #[property(get, set = Self::set_has_context_menu, explicit_notify)]
40        has_context_menu: Cell<bool>,
41        /// The popover displaying the context menu.
42        #[property(get, set = Self::set_popover, explicit_notify, nullable)]
43        popover: BoundObject<gtk::PopoverMenu>,
44        /// The child widget.
45        #[property(get, set = Self::set_child, explicit_notify, nullable)]
46        child: RefCell<Option<gtk::Widget>>,
47    }
48
49    #[glib::object_subclass]
50    impl ObjectSubclass for ContextMenuBin {
51        const NAME: &'static str = "ContextMenuBin";
52        const ABSTRACT: bool = true;
53        type Type = super::ContextMenuBin;
54        type ParentType = gtk::Widget;
55        type Class = ContextMenuBinClass;
56
57        fn class_init(klass: &mut Self::Class) {
58            Self::bind_template(klass);
59
60            klass.set_layout_manager_type::<gtk::BinLayout>();
61
62            klass.install_action("context-menu.activate", None, |obj, _, _| {
63                obj.open_menu_at(0, 0);
64            });
65            key_bindings::add_context_menu_bindings(klass, "context-menu.activate");
66
67            klass.install_action("context-menu.close", None, |obj, _, _| {
68                if let Some(popover) = obj.popover() {
69                    popover.popdown();
70                }
71            });
72        }
73
74        fn instance_init(obj: &InitializingObject<Self>) {
75            obj.init_template();
76        }
77    }
78
79    #[glib::derived_properties]
80    impl ObjectImpl for ContextMenuBin {
81        fn constructed(&self) {
82            let obj = self.obj();
83
84            self.long_press_gesture.connect_pressed(clone!(
85                #[weak]
86                obj,
87                move |gesture, x, y| {
88                    if obj.has_context_menu() {
89                        gesture.set_state(gtk::EventSequenceState::Claimed);
90                        gesture.reset();
91                        obj.open_menu_at(x as i32, y as i32);
92                    }
93                }
94            ));
95
96            self.click_gesture.connect_released(clone!(
97                #[weak]
98                obj,
99                move |gesture, n_press, x, y| {
100                    if n_press > 1 {
101                        return;
102                    }
103
104                    if obj.has_context_menu() {
105                        gesture.set_state(gtk::EventSequenceState::Claimed);
106                        obj.open_menu_at(x as i32, y as i32);
107                    }
108                }
109            ));
110            self.parent_constructed();
111        }
112
113        fn dispose(&self) {
114            if let Some(popover) = self.popover.obj() {
115                popover.unparent();
116            }
117
118            if let Some(child) = self.child.take() {
119                child.unparent();
120            }
121        }
122    }
123
124    impl WidgetImpl for ContextMenuBin {}
125
126    impl ContextMenuBin {
127        /// Set whether this widget has a context menu.
128        fn set_has_context_menu(&self, has_context_menu: bool) {
129            if self.has_context_menu.get() == has_context_menu {
130                return;
131            }
132
133            self.has_context_menu.set(has_context_menu);
134
135            let obj = self.obj();
136            obj.update_property(&[gtk::accessible::Property::HasPopup(has_context_menu)]);
137            obj.action_set_enabled("context-menu.activate", has_context_menu);
138            obj.action_set_enabled("context-menu.close", has_context_menu);
139
140            obj.notify_has_context_menu();
141        }
142
143        /// Set the popover displaying the context menu.
144        fn set_popover(&self, popover: Option<gtk::PopoverMenu>) {
145            let prev_popover = self.popover.obj();
146
147            if prev_popover == popover {
148                return;
149            }
150            let obj = self.obj();
151
152            if let Some(popover) = prev_popover {
153                if popover.parent().is_some_and(|w| w == *obj) {
154                    popover.unparent();
155                }
156            }
157            self.popover.disconnect_signals();
158
159            if let Some(popover) = popover {
160                popover.unparent();
161                popover.set_parent(&*obj);
162
163                let parent_handler = popover.connect_parent_notify(clone!(
164                    #[weak]
165                    obj,
166                    move |popover| {
167                        if popover.parent().is_none_or(|w| w != obj) {
168                            obj.imp().popover.disconnect_signals();
169                        }
170                    }
171                ));
172
173                self.popover.set(popover, vec![parent_handler]);
174            }
175
176            obj.notify_popover();
177        }
178
179        /// The child widget.
180        fn child(&self) -> Option<gtk::Widget> {
181            self.child.borrow().clone()
182        }
183
184        /// Set the child widget.
185        fn set_child(&self, child: Option<gtk::Widget>) {
186            if self.child() == child {
187                return;
188            }
189
190            if let Some(child) = &child {
191                child.set_parent(&*self.obj());
192            }
193
194            if let Some(old_child) = self.child.replace(child) {
195                old_child.unparent();
196            }
197
198            self.obj().notify_child();
199        }
200    }
201}
202
203glib::wrapper! {
204    /// A Bin widget that can have a context menu.
205    pub struct ContextMenuBin(ObjectSubclass<imp::ContextMenuBin>)
206        @extends gtk::Widget, @implements gtk::Accessible;
207}
208
209impl ContextMenuBin {
210    fn open_menu_at(&self, x: i32, y: i32) {
211        if !self.has_context_menu() {
212            return;
213        }
214
215        self.menu_opened();
216
217        if let Some(popover) = self.popover() {
218            popover.set_pointing_to(Some(&gdk::Rectangle::new(x, y, 0, 0)));
219            popover.popup();
220        }
221    }
222}
223
224pub trait ContextMenuBinExt: 'static {
225    /// Whether this widget has a context menu.
226    #[allow(dead_code)]
227    fn has_context_menu(&self) -> bool;
228
229    /// Set whether this widget has a context menu.
230    fn set_has_context_menu(&self, has_context_menu: bool);
231
232    /// Get the `PopoverMenu` used in the context menu.
233    #[allow(dead_code)]
234    fn popover(&self) -> Option<gtk::PopoverMenu>;
235
236    /// Set the `PopoverMenu` used in the context menu.
237    fn set_popover(&self, popover: Option<gtk::PopoverMenu>);
238
239    /// Get the child widget.
240    #[allow(dead_code)]
241    fn child(&self) -> Option<gtk::Widget>;
242
243    /// Set the child widget.
244    fn set_child(&self, child: Option<&impl IsA<gtk::Widget>>);
245
246    /// Called when the menu was requested to open but before the menu is shown.
247    fn menu_opened(&self);
248}
249
250impl<O: IsA<ContextMenuBin>> ContextMenuBinExt for O {
251    fn has_context_menu(&self) -> bool {
252        self.upcast_ref().has_context_menu()
253    }
254
255    fn set_has_context_menu(&self, has_context_menu: bool) {
256        self.upcast_ref().set_has_context_menu(has_context_menu);
257    }
258
259    fn popover(&self) -> Option<gtk::PopoverMenu> {
260        self.upcast_ref().popover()
261    }
262
263    fn set_popover(&self, popover: Option<gtk::PopoverMenu>) {
264        self.upcast_ref().set_popover(popover);
265    }
266
267    fn child(&self) -> Option<gtk::Widget> {
268        self.upcast_ref().child()
269    }
270
271    fn set_child(&self, child: Option<&impl IsA<gtk::Widget>>) {
272        self.upcast_ref()
273            .set_child(child.map(|w| w.clone().upcast()));
274    }
275
276    fn menu_opened(&self) {
277        imp::context_menu_bin_menu_opened(self.upcast_ref());
278    }
279}
280
281/// Public trait that must be implemented for everything that derives from
282/// `ContextMenuBin`.
283///
284/// Overriding a method from this Trait overrides also its behavior in
285/// `ContextMenuBinExt`.
286pub trait ContextMenuBinImpl: WidgetImpl {
287    /// Called when the menu was requested to open but before the menu is shown.
288    ///
289    /// This method should be used to set the popover dynamically.
290    fn menu_opened(&self) {}
291}
292
293unsafe impl<T> IsSubclassable<T> for ContextMenuBin
294where
295    T: ContextMenuBinImpl,
296    T::Type: IsA<ContextMenuBin>,
297{
298    fn class_init(class: &mut glib::Class<Self>) {
299        Self::parent_class_init::<T>(class.upcast_ref_mut());
300
301        let klass = class.as_mut();
302
303        klass.menu_opened = menu_opened_trampoline::<T>;
304    }
305}
306
307// Virtual method implementation trampolines.
308fn menu_opened_trampoline<T>(this: &ContextMenuBin)
309where
310    T: ObjectSubclass + ContextMenuBinImpl,
311    T::Type: IsA<ContextMenuBin>,
312{
313    let this = this.downcast_ref::<T::Type>().unwrap();
314    this.imp().menu_opened();
315}