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 {
pub parent_class: glib::object::Class<adw::Bin>,
pub 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]
pub click_gesture: TemplateChild<gtk::GestureClick>,
#[template_child]
pub long_press_gesture: TemplateChild<gtk::GestureLongPress>,
#[property(get, set = Self::set_has_context_menu, explicit_notify)]
pub has_context_menu: Cell<bool>,
#[property(get, set = Self::set_popover, explicit_notify, nullable)]
pub popover: BoundObject<gtk::PopoverMenu>,
#[property(get, set = Self::set_child, explicit_notify, nullable)]
pub 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 {
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();
}
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.upcast_ref::<gtk::Widget>())
{
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_some_and(|w| &w == obj.upcast_ref::<gtk::Widget>())
{
obj.imp().popover.disconnect_signals();
}
}
));
self.popover.set(popover, vec![parent_handler]);
}
obj.notify_popover();
}
fn child(&self) -> Option<gtk::Widget> {
self.child.borrow().clone()
}
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! {
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 {
#[allow(dead_code)]
fn has_context_menu(&self) -> bool;
fn set_has_context_menu(&self, has_context_menu: bool);
#[allow(dead_code)]
fn popover(&self) -> Option<gtk::PopoverMenu>;
fn set_popover(&self, popover: Option<gtk::PopoverMenu>);
#[allow(dead_code)]
fn child(&self) -> Option<gtk::Widget>;
fn set_child(&self, child: Option<&impl IsA<gtk::Widget>>);
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())
}
}
pub trait ContextMenuBinImpl: WidgetImpl {
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>;
}
}
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()
}