fractal/components/
context_menu_bin.rs1use 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 #[property(get, set = Self::set_has_context_menu, explicit_notify)]
40 has_context_menu: Cell<bool>,
41 #[property(get, set = Self::set_popover, explicit_notify, nullable)]
43 popover: BoundObject<gtk::PopoverMenu>,
44 #[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 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 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 fn child(&self) -> Option<gtk::Widget> {
181 self.child.borrow().clone()
182 }
183
184 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 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 #[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}