Skip to main content

fractal/components/
context_menu_bin.rs

1use gtk::{gdk, glib, glib::clone, prelude::*, subclass::prelude::*};
2
3use crate::utils::{BoundObject, key_bindings};
4
5mod imp {
6    use std::cell::{Cell, RefCell};
7
8    use glib::subclass::InitializingObject;
9
10    use super::*;
11
12    #[repr(C)]
13    pub struct ContextMenuBinClass {
14        parent_class: glib::object::Class<gtk::Widget>,
15        pub(super) menu_opened: fn(&super::ContextMenuBin),
16    }
17
18    unsafe impl ClassStruct for ContextMenuBinClass {
19        type Type = ContextMenuBin;
20    }
21
22    pub(super) fn context_menu_bin_menu_opened(this: &super::ContextMenuBin) {
23        let klass = this.class();
24        (klass.as_ref().menu_opened)(this);
25    }
26
27    #[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)]
28    #[template(resource = "/org/gnome/Fractal/ui/components/context_menu_bin.ui")]
29    #[properties(wrapper_type = super::ContextMenuBin)]
30    pub struct ContextMenuBin {
31        #[template_child]
32        click_gesture: TemplateChild<gtk::GestureClick>,
33        #[template_child]
34        long_press_gesture: TemplateChild<gtk::GestureLongPress>,
35        /// Whether this widget has a context menu.
36        ///
37        /// If this is set to `false`, all the actions will be disabled.
38        #[property(get, set = Self::set_has_context_menu, explicit_notify)]
39        has_context_menu: Cell<bool>,
40        /// The popover displaying the context menu.
41        #[property(get, set = Self::set_popover, explicit_notify, nullable)]
42        popover: BoundObject<gtk::PopoverMenu>,
43        /// The child widget.
44        #[property(get, set = Self::set_child, explicit_notify, nullable)]
45        child: RefCell<Option<gtk::Widget>>,
46    }
47
48    #[glib::object_subclass]
49    impl ObjectSubclass for ContextMenuBin {
50        const NAME: &'static str = "ContextMenuBin";
51        const ABSTRACT: bool = true;
52        type Type = super::ContextMenuBin;
53        type ParentType = gtk::Widget;
54        type Class = ContextMenuBinClass;
55
56        fn class_init(klass: &mut Self::Class) {
57            Self::bind_template(klass);
58
59            klass.set_layout_manager_type::<gtk::BinLayout>();
60
61            klass.install_action("context-menu.activate", None, |obj, _, _| {
62                obj.open_menu_at(0, 0);
63            });
64            key_bindings::add_context_menu_bindings(klass, "context-menu.activate");
65
66            klass.install_action("context-menu.close", None, |obj, _, _| {
67                if let Some(popover) = obj.popover() {
68                    popover.popdown();
69                }
70            });
71        }
72
73        fn instance_init(obj: &InitializingObject<Self>) {
74            obj.init_template();
75        }
76    }
77
78    #[glib::derived_properties]
79    impl ObjectImpl for ContextMenuBin {
80        fn constructed(&self) {
81            let obj = self.obj();
82
83            self.long_press_gesture.connect_pressed(clone!(
84                #[weak]
85                obj,
86                move |gesture, x, y| {
87                    if obj.has_context_menu() {
88                        gesture.set_state(gtk::EventSequenceState::Claimed);
89                        gesture.reset();
90                        obj.open_menu_at(x as i32, y as i32);
91                    }
92                }
93            ));
94
95            self.click_gesture.connect_released(clone!(
96                #[weak]
97                obj,
98                move |gesture, n_press, x, y| {
99                    if n_press > 1 {
100                        return;
101                    }
102
103                    if obj.has_context_menu() {
104                        gesture.set_state(gtk::EventSequenceState::Claimed);
105                        obj.open_menu_at(x as i32, y as i32);
106                    }
107                }
108            ));
109            self.parent_constructed();
110        }
111
112        fn dispose(&self) {
113            if let Some(popover) = self.popover.obj() {
114                popover.unparent();
115            }
116
117            if let Some(child) = self.child.take() {
118                child.unparent();
119            }
120        }
121    }
122
123    impl WidgetImpl for ContextMenuBin {}
124
125    impl ContextMenuBin {
126        /// Set whether this widget has a context menu.
127        fn set_has_context_menu(&self, has_context_menu: bool) {
128            if self.has_context_menu.get() == has_context_menu {
129                return;
130            }
131
132            self.has_context_menu.set(has_context_menu);
133
134            let obj = self.obj();
135            obj.update_property(&[gtk::accessible::Property::HasPopup(has_context_menu)]);
136            obj.action_set_enabled("context-menu.activate", has_context_menu);
137            obj.action_set_enabled("context-menu.close", has_context_menu);
138
139            obj.notify_has_context_menu();
140        }
141
142        /// Set the popover displaying the context menu.
143        fn set_popover(&self, popover: Option<gtk::PopoverMenu>) {
144            let prev_popover = self.popover.obj();
145
146            if prev_popover == popover {
147                return;
148            }
149            let obj = self.obj();
150
151            if let Some(popover) = prev_popover
152                && popover.parent().is_some_and(|w| w == *obj)
153            {
154                popover.unparent();
155            }
156            self.popover.disconnect_signals();
157
158            if let Some(popover) = popover {
159                popover.unparent();
160                popover.set_parent(&*obj);
161
162                let parent_handler = popover.connect_parent_notify(clone!(
163                    #[weak]
164                    obj,
165                    move |popover| {
166                        if popover.parent().is_none_or(|w| w != obj) {
167                            obj.imp().popover.disconnect_signals();
168                        }
169                    }
170                ));
171
172                self.popover.set(popover, vec![parent_handler]);
173            }
174
175            obj.notify_popover();
176        }
177
178        /// The child widget.
179        fn child(&self) -> Option<gtk::Widget> {
180            self.child.borrow().clone()
181        }
182
183        /// Set the child widget.
184        fn set_child(&self, child: Option<gtk::Widget>) {
185            if self.child() == child {
186                return;
187            }
188
189            if let Some(child) = &child {
190                child.set_parent(&*self.obj());
191            }
192
193            if let Some(old_child) = self.child.replace(child) {
194                old_child.unparent();
195            }
196
197            self.obj().notify_child();
198        }
199    }
200}
201
202glib::wrapper! {
203    /// A Bin widget that can have a context menu.
204    pub struct ContextMenuBin(ObjectSubclass<imp::ContextMenuBin>)
205        @extends gtk::Widget,
206        @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
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}