fractal/components/
context_menu_bin.rs1use 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 #[property(get, set = Self::set_has_context_menu, explicit_notify)]
39 has_context_menu: Cell<bool>,
40 #[property(get, set = Self::set_popover, explicit_notify, nullable)]
42 popover: BoundObject<gtk::PopoverMenu>,
43 #[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 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 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 fn child(&self) -> Option<gtk::Widget> {
180 self.child.borrow().clone()
181 }
182
183 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 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 #[allow(dead_code)]
227 fn has_context_menu(&self) -> bool;
228
229 fn set_has_context_menu(&self, has_context_menu: bool);
231
232 #[allow(dead_code)]
234 fn popover(&self) -> Option<gtk::PopoverMenu>;
235
236 fn set_popover(&self, popover: Option<gtk::PopoverMenu>);
238
239 #[allow(dead_code)]
241 fn child(&self) -> Option<gtk::Widget>;
242
243 fn set_child(&self, child: Option<&impl IsA<gtk::Widget>>);
245
246 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
281pub trait ContextMenuBinImpl: WidgetImpl {
287 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
307fn 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}