You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
324 lines
9.7 KiB
324 lines
9.7 KiB
use adw::subclass::prelude::*; |
|
use gtk::{gdk, glib, glib::clone, prelude::*, CompositeTemplate}; |
|
|
|
use crate::utils::BoundObject; |
|
|
|
mod imp { |
|
use std::cell::{Cell, RefCell}; |
|
|
|
use glib::subclass::InitializingObject; |
|
|
|
use super::*; |
|
|
|
#[repr(C)] |
|
pub struct ContextMenuBinClass { |
|
parent_class: glib::object::Class<adw::Bin>, |
|
pub(super) menu_opened: fn(&super::ContextMenuBin), |
|
} |
|
|
|
unsafe impl ClassStruct for ContextMenuBinClass { |
|
type Type = ContextMenuBin; |
|
} |
|
|
|
pub(super) fn context_menu_bin_menu_opened(this: &super::ContextMenuBin) { |
|
let klass = this.class(); |
|
(klass.as_ref().menu_opened)(this); |
|
} |
|
|
|
#[derive(Debug, Default, CompositeTemplate, glib::Properties)] |
|
#[template(resource = "/org/gnome/Fractal/ui/components/context_menu_bin.ui")] |
|
#[properties(wrapper_type = super::ContextMenuBin)] |
|
pub struct ContextMenuBin { |
|
#[template_child] |
|
click_gesture: TemplateChild<gtk::GestureClick>, |
|
#[template_child] |
|
long_press_gesture: TemplateChild<gtk::GestureLongPress>, |
|
/// Whether this widget has a context menu. |
|
/// |
|
/// If this is set to `false`, all the actions will be disabled. |
|
#[property(get, set = Self::set_has_context_menu, explicit_notify)] |
|
has_context_menu: Cell<bool>, |
|
/// The popover displaying the context menu. |
|
#[property(get, set = Self::set_popover, explicit_notify, nullable)] |
|
popover: BoundObject<gtk::PopoverMenu>, |
|
/// The child widget. |
|
#[property(get, set = Self::set_child, explicit_notify, nullable)] |
|
child: RefCell<Option<gtk::Widget>>, |
|
} |
|
|
|
#[glib::object_subclass] |
|
impl ObjectSubclass for ContextMenuBin { |
|
const NAME: &'static str = "ContextMenuBin"; |
|
const ABSTRACT: bool = true; |
|
type Type = super::ContextMenuBin; |
|
type ParentType = gtk::Widget; |
|
type Class = ContextMenuBinClass; |
|
|
|
fn class_init(klass: &mut Self::Class) { |
|
Self::bind_template(klass); |
|
|
|
klass.set_layout_manager_type::<gtk::BinLayout>(); |
|
|
|
klass.install_action("context-menu.activate", None, |obj, _, _| { |
|
obj.open_menu_at(0, 0); |
|
}); |
|
klass.add_binding_action( |
|
gdk::Key::F10, |
|
gdk::ModifierType::SHIFT_MASK, |
|
"context-menu.activate", |
|
); |
|
klass.add_binding_action( |
|
gdk::Key::Menu, |
|
gdk::ModifierType::empty(), |
|
"context-menu.activate", |
|
); |
|
|
|
klass.install_action("context-menu.close", None, |obj, _, _| { |
|
if let Some(popover) = obj.popover() { |
|
popover.popdown(); |
|
} |
|
}); |
|
} |
|
|
|
fn instance_init(obj: &InitializingObject<Self>) { |
|
obj.init_template(); |
|
} |
|
} |
|
|
|
#[glib::derived_properties] |
|
impl ObjectImpl for ContextMenuBin { |
|
fn constructed(&self) { |
|
let obj = self.obj(); |
|
|
|
self.long_press_gesture.connect_pressed(clone!( |
|
#[weak] |
|
obj, |
|
move |gesture, x, y| { |
|
if obj.has_context_menu() { |
|
gesture.set_state(gtk::EventSequenceState::Claimed); |
|
gesture.reset(); |
|
obj.open_menu_at(x as i32, y as i32); |
|
} |
|
} |
|
)); |
|
|
|
self.click_gesture.connect_released(clone!( |
|
#[weak] |
|
obj, |
|
move |gesture, n_press, x, y| { |
|
if n_press > 1 { |
|
return; |
|
} |
|
|
|
if obj.has_context_menu() { |
|
gesture.set_state(gtk::EventSequenceState::Claimed); |
|
obj.open_menu_at(x as i32, y as i32); |
|
} |
|
} |
|
)); |
|
self.parent_constructed(); |
|
} |
|
|
|
fn dispose(&self) { |
|
if let Some(popover) = self.popover.obj() { |
|
popover.unparent(); |
|
} |
|
|
|
if let Some(child) = self.child.take() { |
|
child.unparent(); |
|
} |
|
} |
|
} |
|
|
|
impl WidgetImpl for ContextMenuBin {} |
|
|
|
impl ContextMenuBin { |
|
/// Set whether this widget has a context menu. |
|
fn set_has_context_menu(&self, has_context_menu: bool) { |
|
if self.has_context_menu.get() == has_context_menu { |
|
return; |
|
} |
|
|
|
self.has_context_menu.set(has_context_menu); |
|
|
|
let obj = self.obj(); |
|
obj.update_property(&[gtk::accessible::Property::HasPopup(has_context_menu)]); |
|
obj.action_set_enabled("context-menu.activate", has_context_menu); |
|
obj.action_set_enabled("context-menu.close", has_context_menu); |
|
|
|
obj.notify_has_context_menu(); |
|
} |
|
|
|
/// Set the popover displaying the context menu. |
|
fn set_popover(&self, popover: Option<gtk::PopoverMenu>) { |
|
let prev_popover = self.popover.obj(); |
|
|
|
if prev_popover == popover { |
|
return; |
|
} |
|
let obj = self.obj(); |
|
|
|
if let Some(popover) = prev_popover { |
|
if popover.parent().is_some_and(|w| w == *obj) { |
|
popover.unparent(); |
|
} |
|
} |
|
self.popover.disconnect_signals(); |
|
|
|
if let Some(popover) = popover { |
|
popover.unparent(); |
|
popover.set_parent(&*obj); |
|
|
|
let parent_handler = popover.connect_parent_notify(clone!( |
|
#[weak] |
|
obj, |
|
move |popover| { |
|
if popover.parent().is_none_or(|w| w != obj) { |
|
obj.imp().popover.disconnect_signals(); |
|
} |
|
} |
|
)); |
|
|
|
self.popover.set(popover, vec![parent_handler]); |
|
} |
|
|
|
obj.notify_popover(); |
|
} |
|
|
|
/// The child widget. |
|
fn child(&self) -> Option<gtk::Widget> { |
|
self.child.borrow().clone() |
|
} |
|
|
|
/// Set the child widget. |
|
fn set_child(&self, child: Option<gtk::Widget>) { |
|
if self.child() == child { |
|
return; |
|
} |
|
|
|
if let Some(child) = &child { |
|
child.set_parent(&*self.obj()); |
|
} |
|
|
|
if let Some(old_child) = self.child.replace(child) { |
|
old_child.unparent(); |
|
} |
|
|
|
self.obj().notify_child(); |
|
} |
|
} |
|
} |
|
|
|
glib::wrapper! { |
|
/// A Bin widget that can have a context menu. |
|
pub struct ContextMenuBin(ObjectSubclass<imp::ContextMenuBin>) |
|
@extends gtk::Widget, @implements gtk::Accessible; |
|
} |
|
|
|
impl ContextMenuBin { |
|
fn open_menu_at(&self, x: i32, y: i32) { |
|
if !self.has_context_menu() { |
|
return; |
|
} |
|
|
|
self.menu_opened(); |
|
|
|
if let Some(popover) = self.popover() { |
|
popover.set_pointing_to(Some(&gdk::Rectangle::new(x, y, 0, 0))); |
|
popover.popup(); |
|
} |
|
} |
|
} |
|
|
|
pub trait ContextMenuBinExt: 'static { |
|
/// Whether this widget has a context menu. |
|
#[allow(dead_code)] |
|
fn has_context_menu(&self) -> bool; |
|
|
|
/// Set whether this widget has a context menu. |
|
fn set_has_context_menu(&self, has_context_menu: bool); |
|
|
|
/// Get the `PopoverMenu` used in the context menu. |
|
#[allow(dead_code)] |
|
fn popover(&self) -> Option<gtk::PopoverMenu>; |
|
|
|
/// Set the `PopoverMenu` used in the context menu. |
|
fn set_popover(&self, popover: Option<gtk::PopoverMenu>); |
|
|
|
/// Get the child widget. |
|
#[allow(dead_code)] |
|
fn child(&self) -> Option<gtk::Widget>; |
|
|
|
/// Set the child widget. |
|
fn set_child(&self, child: Option<&impl IsA<gtk::Widget>>); |
|
|
|
/// Called when the menu was requested to open but before the menu is shown. |
|
fn menu_opened(&self); |
|
} |
|
|
|
impl<O: IsA<ContextMenuBin>> ContextMenuBinExt for O { |
|
fn has_context_menu(&self) -> bool { |
|
self.upcast_ref().has_context_menu() |
|
} |
|
|
|
fn set_has_context_menu(&self, has_context_menu: bool) { |
|
self.upcast_ref().set_has_context_menu(has_context_menu); |
|
} |
|
|
|
fn popover(&self) -> Option<gtk::PopoverMenu> { |
|
self.upcast_ref().popover() |
|
} |
|
|
|
fn set_popover(&self, popover: Option<gtk::PopoverMenu>) { |
|
self.upcast_ref().set_popover(popover); |
|
} |
|
|
|
fn child(&self) -> Option<gtk::Widget> { |
|
self.upcast_ref().child() |
|
} |
|
|
|
fn set_child(&self, child: Option<&impl IsA<gtk::Widget>>) { |
|
self.upcast_ref() |
|
.set_child(child.map(|w| w.clone().upcast())); |
|
} |
|
|
|
fn menu_opened(&self) { |
|
imp::context_menu_bin_menu_opened(self.upcast_ref()); |
|
} |
|
} |
|
|
|
/// Public trait that must be implemented for everything that derives from |
|
/// `ContextMenuBin`. |
|
/// |
|
/// Overriding a method from this Trait overrides also its behavior in |
|
/// `ContextMenuBinExt`. |
|
pub trait ContextMenuBinImpl: WidgetImpl { |
|
/// Called when the menu was requested to open but before the menu is shown. |
|
/// |
|
/// This method should be used to set the popover dynamically. |
|
fn menu_opened(&self) {} |
|
} |
|
|
|
unsafe impl<T> IsSubclassable<T> for ContextMenuBin |
|
where |
|
T: ContextMenuBinImpl, |
|
T::Type: IsA<ContextMenuBin>, |
|
{ |
|
fn class_init(class: &mut glib::Class<Self>) { |
|
Self::parent_class_init::<T>(class.upcast_ref_mut()); |
|
|
|
let klass = class.as_mut(); |
|
|
|
klass.menu_opened = menu_opened_trampoline::<T>; |
|
} |
|
} |
|
|
|
// Virtual method implementation trampolines. |
|
fn menu_opened_trampoline<T>(this: &ContextMenuBin) |
|
where |
|
T: ObjectSubclass + ContextMenuBinImpl, |
|
T::Type: IsA<ContextMenuBin>, |
|
{ |
|
let this = this.downcast_ref::<T::Type>().unwrap(); |
|
this.imp().menu_opened(); |
|
}
|
|
|