use std::cell::OnceCell;
use gtk::{glib, prelude::*, subclass::prelude::*};
use crate::{accelerator::MenuAccelerator, items::IconType};
use super::mnemonic::to_gtk_mnemonic;
const MENU_ICON_SIZE: i32 = 16;
mod imp {
use super::*;
#[derive(Default)]
pub struct IconMenuItem {
pub(super) image: OnceCell<gtk::Image>,
pub(super) label: OnceCell<gtk::Label>,
pub(super) accelerator_label: OnceCell<gtk::Label>,
}
#[glib::object_subclass]
impl ObjectSubclass for IconMenuItem {
const NAME: &'static str = "MudaIconMenuItem";
type Type = super::IconMenuItem;
type ParentType = gtk::Button;
fn class_init(klass: &mut Self::Class) {
klass.set_css_name("modelbutton");
klass.set_accessible_role(gtk::AccessibleRole::MenuItem);
}
}
impl ObjectImpl for IconMenuItem {}
impl WidgetImpl for IconMenuItem {}
impl ButtonImpl for IconMenuItem {}
}
glib::wrapper! {
pub struct IconMenuItem(ObjectSubclass<imp::IconMenuItem>)
@extends gtk::Button, gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Actionable;
}
impl IconMenuItem {
pub(super) fn new(
text: &str,
detailed_action: &str,
icon: Option<&IconType>,
accelerator: Option<&MenuAccelerator>,
) -> Self {
let item: Self = glib::Object::new();
item.set_has_frame(false);
item.set_focus_on_click(false);
item.add_css_class("flat");
item.set_detailed_action_name(detailed_action);
item.set_can_focus(true);
item.set_focusable(true);
item.set_halign(gtk::Align::Fill);
item.set_hexpand(true);
let image = gtk::Image::builder()
.halign(gtk::Align::Center)
.valign(gtk::Align::Center)
.width_request(MENU_ICON_SIZE)
.height_request(MENU_ICON_SIZE)
.build();
let label = gtk::Label::builder().xalign(0.0).hexpand(true).build();
let accelerator_label = gtk::Label::builder()
.css_name("accelerator")
.margin_start(24)
.build();
let content = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.spacing(6)
.hexpand(true)
.build();
content.append(&image);
content.append(&label);
content.append(&accelerator_label);
let imp = item.imp();
let _ = imp.image.set(image);
let _ = imp.label.set(label.clone());
let _ = imp.accelerator_label.set(accelerator_label);
item.set_child(Some(&content));
label.set_mnemonic_widget(Some(&item));
item.add_menu_item_controllers();
item.set_label(text);
item.set_icon(icon);
item.set_accelerator(accelerator);
item
}
pub fn set_label(&self, text: &str) {
let Some(label) = self.imp().label.get() else {
return;
};
label.set_text_with_mnemonic(&to_gtk_mnemonic(text));
label.set_mnemonic_widget(Some(self));
}
pub(super) fn set_icon(&self, icon: Option<&IconType>) {
let Some(image) = self.imp().image.get() else {
return;
};
match icon {
Some(IconType::Custom(icon)) => {
image.set_icon_name(None);
let texture = icon.inner.texture();
image.set_paintable(Some(&texture));
}
Some(IconType::Native(icon)) => {
image.set_paintable(gtk::gdk::Paintable::NONE);
image.set_icon_name(Some(icon.freedesktop_name()));
}
None => {
image.set_paintable(gtk::gdk::Paintable::NONE);
image.set_icon_name(None);
}
}
}
pub(super) fn set_accelerator(&self, accelerator: Option<&MenuAccelerator>) {
let Some(accelerator_label) = self.imp().accelerator_label.get() else {
return;
};
let accelerator = accelerator.and_then(|accelerator| {
let (key, mods) = gtk::accelerator_parse(accelerator.to_gtk()?)?;
let label = gtk::accelerator_get_label(key, mods);
(!label.is_empty()).then_some(label)
});
if let Some(accelerator) = accelerator {
accelerator_label.set_label(&accelerator);
accelerator_label.set_visible(true);
} else {
accelerator_label.set_label("");
accelerator_label.set_visible(false);
}
}
fn add_menu_item_controllers(&self) {
let focus = gtk::EventControllerFocus::new();
focus.connect_enter({
move |controller| {
if let Some(widget) = controller.widget() {
clear_sibling_selected_visual_state(&widget);
widget.set_state_flags(gtk::StateFlags::PRELIGHT, false);
}
}
});
focus.connect_leave({
move |controller| {
if let Some(widget) = controller.widget() {
widget.unset_state_flags(gtk::StateFlags::PRELIGHT);
}
}
});
self.add_controller(focus);
self.connect_clicked(|button| {
if let Some(popover) = button
.ancestor(gtk::Popover::static_type())
.and_then(|widget| widget.downcast::<gtk::Popover>().ok())
{
popover.popdown();
}
});
let motion = gtk::EventControllerMotion::new();
motion.connect_enter({
move |controller, _, _| {
let Some(widget) = controller.widget() else {
return;
};
if let Some(popover) = find_sibling_popover_menu(&widget) {
popover.set_visible(false);
}
}
});
self.add_controller(motion);
}
}
fn find_sibling_popover_menu(widget: >k::Widget) -> Option<gtk::PopoverMenu> {
let parent = widget.parent()?.parent()?;
let mut child = parent.first_child();
while let Some(sibling) = child {
if let Some(menu) = find_child_popover_menu(&sibling) {
return Some(menu);
}
child = sibling.next_sibling();
}
None
}
fn find_child_popover_menu(widget: >k::Widget) -> Option<gtk::PopoverMenu> {
let mut child = widget.first_child();
while let Some(sibling) = child {
if let Some(menu) = sibling.downcast_ref::<gtk::PopoverMenu>() {
return Some(menu.clone());
}
child = sibling.next_sibling();
}
None
}
fn clear_sibling_selected_visual_state(widget: >k::Widget) {
let Some(row) = widget.parent() else {
return;
};
let Some(parent) = row.parent() else {
return;
};
let mut child = parent.first_child();
while let Some(sibling) = child {
if sibling != row {
sibling.unset_state_flags(gtk::StateFlags::PRELIGHT | gtk::StateFlags::SELECTED);
}
child = sibling.next_sibling();
}
}