fractal/src/components/context_menu_bin.rs

246 lines
7.5 KiB
Rust

use adw::subclass::prelude::*;
use gtk::{gdk, glib, glib::clone, prelude::*, CompositeTemplate};
use log::debug;
mod imp {
use std::cell::RefCell;
use glib::{subclass::InitializingObject, SignalHandlerId};
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)]
#[template(resource = "/org/gnome/Fractal/ui/components/context_menu_bin.ui")]
pub struct ContextMenuBin {
#[template_child]
pub click_gesture: TemplateChild<gtk::GestureClick>,
#[template_child]
pub long_press_gesture: TemplateChild<gtk::GestureLongPress>,
pub popover: RefCell<Option<gtk::PopoverMenu>>,
pub signal_handler: RefCell<Option<SignalHandlerId>>,
}
#[glib::object_subclass]
impl ObjectSubclass for ContextMenuBin {
const NAME: &'static str = "ContextMenuBin";
const ABSTRACT: bool = true;
type Type = super::ContextMenuBin;
type ParentType = adw::Bin;
type Class = ContextMenuBinClass;
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
klass.install_action("context-menu.activate", None, move |widget, _, _| {
widget.open_menu_at(0, 0)
});
klass.add_binding_action(
gdk::Key::F10,
gdk::ModifierType::SHIFT_MASK,
"context-menu.activate",
None,
);
klass.add_binding_action(
gdk::Key::Menu,
gdk::ModifierType::empty(),
"context-menu.activate",
None,
);
klass.install_action("context-menu.close", None, move |widget, _, _| {
if let Some(popover) = widget.popover() {
popover.popdown();
}
});
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for ContextMenuBin {
fn properties() -> &'static [glib::ParamSpec] {
use once_cell::sync::Lazy;
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
vec![
glib::ParamSpecObject::builder::<gtk::PopoverMenu>("popover")
.explicit_notify()
.build(),
]
});
PROPERTIES.as_ref()
}
fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) {
match pspec.name() {
"popover" => self.obj().set_popover(value.get().unwrap()),
_ => unimplemented!(),
}
}
fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value {
match pspec.name() {
"popover" => self.obj().popover().to_value(),
_ => unimplemented!(),
}
}
fn constructed(&self) {
let obj = self.obj();
self.long_press_gesture
.connect_pressed(clone!(@weak obj => move |gesture, x, y| {
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;
}
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.take() {
popover.unparent()
}
}
}
impl WidgetImpl for ContextMenuBin {}
impl BinImpl for ContextMenuBin {}
}
glib::wrapper! {
/// A Bin widget that adds a context menu.
pub struct ContextMenuBin(ObjectSubclass<imp::ContextMenuBin>)
@extends gtk::Widget, adw::Bin, @implements gtk::Accessible;
}
impl ContextMenuBin {
fn open_menu_at(&self, x: i32, y: i32) {
debug!("Open menu at ({x}, {y})");
self.menu_opened();
if let Some(popover) = self.popover() {
debug!("Context menu was activated");
popover.set_pointing_to(Some(&gdk::Rectangle::new(x, y, 0, 0)));
popover.popup();
}
}
}
pub trait ContextMenuBinExt: 'static {
/// Get the `PopoverMenu` used in the context menu.
fn popover(&self) -> Option<gtk::PopoverMenu>;
/// Set the `PopoverMenu` used in the context menu.
fn set_popover(&self, popover: Option<gtk::PopoverMenu>);
/// 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 popover(&self) -> Option<gtk::PopoverMenu> {
self.upcast_ref().imp().popover.borrow().clone()
}
fn set_popover(&self, popover: Option<gtk::PopoverMenu>) {
let obj = self.upcast_ref();
if obj.popover() == popover {
return;
}
let imp = obj.imp();
if let Some(popover) = &popover {
popover.unparent();
popover.set_parent(obj);
imp.signal_handler
.replace(Some(popover.connect_parent_notify(
clone!(@weak obj => move |popover| {
if popover.parent().as_ref() != Some(obj.upcast_ref()) {
let imp = obj.imp();
if let Some(popover) = imp.popover.take() {
if let Some(signal_handler) = imp.signal_handler.take() {
popover.disconnect(signal_handler)
}
}
}
}),
)));
}
obj.imp().popover.replace(popover);
obj.notify("popover");
}
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: BinImpl {
/// 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()
}