muda/platform_impl/gtk/
mod.rs

1// Copyright 2022-2022 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5mod accelerator;
6mod icon;
7
8pub(crate) use icon::PlatformIcon;
9
10use crate::{
11    accelerator::Accelerator,
12    dpi::Position,
13    icon::{Icon, NativeIcon},
14    items::*,
15    util::{AddOp, Counter},
16    IsMenuItem, MenuEvent, MenuId, MenuItemKind, MenuItemType,
17};
18use accelerator::{from_gtk_mnemonic, parse_accelerator, to_gtk_mnemonic};
19use glib::translate::ToGlibPtr;
20use gtk::{gdk, glib, prelude::*, AboutDialog, Container, Orientation};
21use std::{
22    cell::RefCell,
23    collections::{hash_map::Entry, HashMap},
24    rc::Rc,
25    sync::atomic::{AtomicBool, Ordering},
26};
27
28static COUNTER: Counter = Counter::new();
29
30macro_rules! is_item_supported {
31    ($item:tt) => {{
32        let child = $item.child();
33        let child_ = child.borrow();
34        let supported = if let Some(predefined_item_type) = &child_.predefined_item_type {
35            matches!(
36                predefined_item_type,
37                PredefinedMenuItemType::Separator
38                    | PredefinedMenuItemType::Copy
39                    | PredefinedMenuItemType::Cut
40                    | PredefinedMenuItemType::Paste
41                    | PredefinedMenuItemType::SelectAll
42                    | PredefinedMenuItemType::About(_)
43            )
44        } else {
45            true
46        };
47        drop(child_);
48        supported
49    }};
50}
51
52macro_rules! return_if_item_not_supported {
53    ($item:tt) => {
54        if !is_item_supported!($item) {
55            return Ok(());
56        }
57    };
58}
59
60pub struct Menu {
61    id: MenuId,
62    children: Vec<Rc<RefCell<MenuChild>>>,
63    // TODO: maybe save a reference to the window?
64    gtk_menubars: HashMap<u32, gtk::MenuBar>,
65    accel_group: Option<gtk::AccelGroup>,
66    gtk_menu: (u32, Option<gtk::Menu>), // dedicated menu for tray or context menus
67}
68
69impl Drop for Menu {
70    fn drop(&mut self) {
71        for (id, menu) in &self.gtk_menubars {
72            drop_children_from_menu_and_destroy(*id, menu, &self.children);
73            unsafe { menu.destroy() }
74        }
75
76        if let (id, Some(menu)) = &self.gtk_menu {
77            drop_children_from_menu_and_destroy(*id, menu, &self.children);
78            unsafe { menu.destroy() }
79        }
80    }
81}
82
83impl Menu {
84    pub fn new(id: Option<MenuId>) -> Self {
85        Self {
86            id: id.unwrap_or_else(|| MenuId(COUNTER.next().to_string())),
87            children: Vec::new(),
88            gtk_menubars: HashMap::new(),
89            accel_group: None,
90            gtk_menu: (COUNTER.next(), None),
91        }
92    }
93
94    pub fn id(&self) -> &MenuId {
95        &self.id
96    }
97
98    pub fn add_menu_item(&mut self, item: &dyn crate::IsMenuItem, op: AddOp) -> crate::Result<()> {
99        if is_item_supported!(item) {
100            for (menu_id, menu_bar) in &self.gtk_menubars {
101                let gtk_item =
102                    item.make_gtk_menu_item(*menu_id, self.accel_group.as_ref(), true, true)?;
103                match op {
104                    AddOp::Append => menu_bar.append(&gtk_item),
105                    AddOp::Insert(position) => menu_bar.insert(&gtk_item, position as i32),
106                }
107                gtk_item.show();
108            }
109
110            if let (menu_id, Some(menu)) = &self.gtk_menu {
111                let gtk_item =
112                    item.make_gtk_menu_item(*menu_id, self.accel_group.as_ref(), true, false)?;
113                match op {
114                    AddOp::Append => menu.append(&gtk_item),
115                    AddOp::Insert(position) => menu.insert(&gtk_item, position as i32),
116                }
117                gtk_item.show();
118            }
119        }
120
121        match op {
122            AddOp::Append => self.children.push(item.child()),
123            AddOp::Insert(position) => self.children.insert(position, item.child()),
124        }
125
126        Ok(())
127    }
128
129    fn add_menu_item_with_id(&self, item: &dyn crate::IsMenuItem, id: u32) -> crate::Result<()> {
130        return_if_item_not_supported!(item);
131
132        for (menu_id, menu_bar) in self.gtk_menubars.iter().filter(|m| *m.0 == id) {
133            let gtk_item =
134                item.make_gtk_menu_item(*menu_id, self.accel_group.as_ref(), true, true)?;
135            menu_bar.append(&gtk_item);
136            gtk_item.show();
137        }
138
139        Ok(())
140    }
141
142    fn add_menu_item_to_context_menu(&self, item: &dyn crate::IsMenuItem) -> crate::Result<()> {
143        return_if_item_not_supported!(item);
144
145        if let (menu_id, Some(menu)) = &self.gtk_menu {
146            let gtk_item =
147                item.make_gtk_menu_item(*menu_id, self.accel_group.as_ref(), true, false)?;
148            menu.append(&gtk_item);
149            gtk_item.show();
150        }
151
152        Ok(())
153    }
154
155    pub fn remove(&mut self, item: &dyn crate::IsMenuItem) -> crate::Result<()> {
156        self.remove_inner(item, true, None)
157    }
158
159    fn remove_inner(
160        &mut self,
161        item: &dyn crate::IsMenuItem,
162        remove_from_cache: bool,
163        id: Option<u32>,
164    ) -> crate::Result<()> {
165        // get child
166        let child = {
167            let index = self
168                .children
169                .iter()
170                .position(|e| e.borrow().id == item.id())
171                .ok_or(crate::Error::NotAChildOfThisMenu)?;
172            if remove_from_cache {
173                self.children.remove(index)
174            } else {
175                self.children.get(index).cloned().unwrap()
176            }
177        };
178
179        for (menu_id, menu_bar) in &self.gtk_menubars {
180            // check if we are removing this item from all gtk_menubars
181            //      which is usually when this is the item the user is actaully removing
182            // or if we are removing from a specific menu (id)
183            //      which is when the actual item being removed is a submenu
184            //      and we are iterating through its children and removing
185            //      each child gtk items that are related to this submenu.
186            if id.map(|i| i == *menu_id).unwrap_or(true) {
187                // bail this is not a supported item like a close_window predefined menu item
188                if is_item_supported!(item) {
189                    let mut child_ = child.borrow_mut();
190
191                    if child_.item_type == MenuItemType::Submenu {
192                        let menus = child_.gtk_menus.as_ref().unwrap().get(menu_id).cloned();
193                        if let Some(menus) = menus {
194                            for (id, menu) in menus {
195                                // iterate through children and only remove the gtk items
196                                // related to this submenu
197                                for item in child_.items() {
198                                    child_.remove_inner(item.as_ref(), false, Some(id))?;
199                                }
200                                unsafe { menu.destroy() };
201                            }
202                        }
203                        child_.gtk_menus.as_mut().unwrap().remove(menu_id);
204                    }
205
206                    // remove all the gtk items that are related to this gtk menubar and destroy it
207                    if let Some(items) = child_.gtk_menu_items.borrow_mut().remove(menu_id) {
208                        for item in items {
209                            menu_bar.remove(&item);
210                            if let Some(accel_group) = &child_.accel_group {
211                                if let Some((mods, key)) = child_.gtk_accelerator {
212                                    item.remove_accelerator(accel_group, key, mods);
213                                }
214                            }
215                            unsafe { item.destroy() };
216                        }
217                    };
218                }
219            }
220        }
221
222        // remove from the gtk menu assigned to the context menu
223        if remove_from_cache {
224            if let (id, Some(menu)) = &self.gtk_menu {
225                let child_ = child.borrow_mut();
226                if let Some(items) = child_.gtk_menu_items.borrow_mut().remove(id) {
227                    for item in items {
228                        menu.remove(&item);
229                        if let Some(accel_group) = &child_.accel_group {
230                            if let Some((mods, key)) = child_.gtk_accelerator {
231                                item.remove_accelerator(accel_group, key, mods);
232                            }
233                        }
234                        unsafe { item.destroy() };
235                    }
236                };
237            }
238        }
239        Ok(())
240    }
241
242    pub fn items(&self) -> Vec<MenuItemKind> {
243        self.children
244            .iter()
245            .map(|c| c.borrow().kind(c.clone()))
246            .collect()
247    }
248
249    pub fn init_for_gtk_window<W, C>(
250        &mut self,
251        window: &W,
252        container: Option<&C>,
253    ) -> crate::Result<()>
254    where
255        W: IsA<gtk::Window>,
256        W: IsA<gtk::Container>,
257        C: IsA<gtk::Container>,
258    {
259        let id = window.as_ptr() as u32;
260
261        if self.accel_group.is_none() {
262            self.accel_group = Some(gtk::AccelGroup::new());
263        }
264
265        // This is the first time this method has been called on this window
266        // so we need to create the menubar and its parent box
267        if let Entry::Vacant(e) = self.gtk_menubars.entry(id) {
268            let menu_bar = gtk::MenuBar::new();
269            e.insert(menu_bar);
270        } else {
271            return Err(crate::Error::AlreadyInitialized);
272        }
273
274        // Construct the entries of the menubar
275        let menu_bar = &self.gtk_menubars[&id];
276
277        window.add_accel_group(self.accel_group.as_ref().unwrap());
278
279        for item in self.items() {
280            self.add_menu_item_with_id(item.as_ref(), id)?;
281        }
282
283        // add the menubar to the specified widget, otherwise to the window
284        if let Some(container) = container {
285            if container.type_().name() == "GtkBox" {
286                let gtk_box = container.dynamic_cast_ref::<gtk::Box>().unwrap();
287                gtk_box.pack_start(menu_bar, false, false, 0);
288                gtk_box.reorder_child(menu_bar, 0);
289            } else {
290                container.add(menu_bar);
291            }
292        } else {
293            window.add(menu_bar);
294        }
295
296        // Show the menubar
297        menu_bar.show();
298
299        Ok(())
300    }
301
302    pub fn remove_for_gtk_window<W>(&mut self, window: &W) -> crate::Result<()>
303    where
304        W: IsA<gtk::Window>,
305    {
306        let id = window.as_ptr() as u32;
307
308        // Remove from our cache
309        let menu_bar = self
310            .gtk_menubars
311            .remove(&id)
312            .ok_or(crate::Error::NotInitialized)?;
313
314        for item in self.items() {
315            let _ = self.remove_inner(item.as_ref(), false, Some(id));
316        }
317
318        // Remove the [`gtk::Menubar`] from the widget tree
319        unsafe { menu_bar.destroy() };
320        // Detach the accelerators from the window
321        window.remove_accel_group(self.accel_group.as_ref().unwrap());
322        Ok(())
323    }
324
325    pub fn hide_for_gtk_window<W>(&mut self, window: &W) -> crate::Result<()>
326    where
327        W: IsA<gtk::Window>,
328    {
329        self.gtk_menubars
330            .get(&(window.as_ptr() as u32))
331            .ok_or(crate::Error::NotInitialized)?
332            .hide();
333        Ok(())
334    }
335
336    pub fn show_for_gtk_window<W>(&self, window: &W) -> crate::Result<()>
337    where
338        W: IsA<gtk::Window>,
339    {
340        self.gtk_menubars
341            .get(&(window.as_ptr() as u32))
342            .ok_or(crate::Error::NotInitialized)?
343            .show_all();
344        Ok(())
345    }
346
347    pub fn is_visible_on_gtk_window<W>(&self, window: &W) -> bool
348    where
349        W: IsA<gtk::Window>,
350    {
351        self.gtk_menubars
352            .get(&(window.as_ptr() as u32))
353            .map(|m| m.get_visible())
354            .unwrap_or(false)
355    }
356
357    pub fn gtk_menubar_for_gtk_window<W>(&self, window: &W) -> Option<gtk::MenuBar>
358    where
359        W: gtk::prelude::IsA<gtk::Window>,
360    {
361        self.gtk_menubars.get(&(window.as_ptr() as u32)).cloned()
362    }
363
364    pub fn show_context_menu_for_gtk_window(
365        &mut self,
366        widget: &impl IsA<gtk::Widget>,
367        position: Option<Position>,
368    ) -> bool {
369        show_context_menu(self.gtk_context_menu(), widget, position)
370    }
371
372    pub fn gtk_context_menu(&mut self) -> gtk::Menu {
373        let mut add_items = false;
374
375        {
376            if self.gtk_menu.1.is_none() {
377                self.gtk_menu.1 = Some(gtk::Menu::new());
378                add_items = true;
379            }
380        }
381
382        if add_items {
383            for item in self.items() {
384                self.add_menu_item_to_context_menu(item.as_ref()).unwrap();
385            }
386        }
387
388        self.gtk_menu.1.as_ref().unwrap().clone()
389    }
390}
391
392/// A generic child in a menu
393#[derive(Debug, Default)]
394pub struct MenuChild {
395    // shared fields between submenus and menu items
396    item_type: MenuItemType,
397    text: String,
398    enabled: bool,
399    id: MenuId,
400
401    gtk_menu_items: Rc<RefCell<HashMap<u32, Vec<gtk::MenuItem>>>>,
402
403    // menu item fields
404    accelerator: Option<Accelerator>,
405    gtk_accelerator: Option<(gdk::ModifierType, u32)>,
406
407    // predefined menu item fields
408    predefined_item_type: Option<PredefinedMenuItemType>,
409
410    // check menu item fields
411    checked: Option<Rc<AtomicBool>>,
412    is_syncing_checked_state: Option<Rc<AtomicBool>>,
413
414    // icon menu item fields
415    icon: Option<Icon>,
416
417    // submenu fields
418    pub children: Option<Vec<Rc<RefCell<MenuChild>>>>,
419    gtk_menus: Option<HashMap<u32, Vec<(u32, gtk::Menu)>>>,
420    gtk_menu: Option<(u32, Option<gtk::Menu>)>, // dedicated menu for tray or context menus
421    accel_group: Option<gtk::AccelGroup>,
422}
423
424impl Drop for MenuChild {
425    fn drop(&mut self) {
426        if self.item_type == MenuItemType::Submenu {
427            for menus in self.gtk_menus.as_ref().unwrap().values() {
428                for (id, menu) in menus {
429                    drop_children_from_menu_and_destroy(*id, menu, self.children.as_ref().unwrap());
430                    unsafe { menu.destroy() };
431                }
432            }
433
434            if let Some((id, Some(menu))) = &self.gtk_menu {
435                drop_children_from_menu_and_destroy(*id, menu, self.children.as_ref().unwrap());
436                unsafe { menu.destroy() };
437            }
438        }
439
440        for items in self.gtk_menu_items.borrow().values() {
441            for item in items {
442                if let Some(accel_group) = &self.accel_group {
443                    if let Some((mods, key)) = self.gtk_accelerator {
444                        item.remove_accelerator(accel_group, key, mods);
445                    }
446                }
447                unsafe { item.destroy() };
448            }
449        }
450    }
451}
452
453fn drop_children_from_menu_and_destroy(
454    id: u32,
455    menu: &impl IsA<Container>,
456    children: &Vec<Rc<RefCell<MenuChild>>>,
457) {
458    for child in children {
459        let mut child_ = child.borrow_mut();
460        {
461            let mut menu_items = child_.gtk_menu_items.borrow_mut();
462            if let Some(items) = menu_items.remove(&id) {
463                for item in items {
464                    menu.remove(&item);
465                    if let Some(accel_group) = &child_.accel_group {
466                        if let Some((mods, key)) = child_.gtk_accelerator {
467                            item.remove_accelerator(accel_group, key, mods);
468                        }
469                    }
470                    unsafe { item.destroy() }
471                }
472            }
473        }
474
475        if child_.item_type == MenuItemType::Submenu {
476            if let Some(menus) = child_.gtk_menus.as_mut().unwrap().remove(&id) {
477                for (id, menu) in menus {
478                    let children = child_.children.as_ref().unwrap();
479                    drop_children_from_menu_and_destroy(id, &menu, children);
480                    unsafe { menu.destroy() }
481                }
482            }
483        }
484    }
485}
486
487/// Constructors
488impl MenuChild {
489    pub fn new(
490        text: &str,
491        enabled: bool,
492        accelerator: Option<Accelerator>,
493        id: Option<MenuId>,
494    ) -> Self {
495        Self {
496            text: text.to_string(),
497            enabled,
498            accelerator,
499            id: id.unwrap_or_else(|| MenuId(COUNTER.next().to_string())),
500            item_type: MenuItemType::MenuItem,
501            gtk_menu_items: Rc::new(RefCell::new(HashMap::new())),
502            accel_group: None,
503            checked: None,
504            children: None,
505            gtk_accelerator: None,
506            gtk_menu: None,
507            gtk_menus: None,
508            icon: None,
509            is_syncing_checked_state: None,
510            predefined_item_type: None,
511        }
512    }
513
514    pub fn new_submenu(text: &str, enabled: bool, id: Option<MenuId>) -> Self {
515        Self {
516            text: text.to_string(),
517            enabled,
518            id: id.unwrap_or_else(|| MenuId(COUNTER.next().to_string())),
519            children: Some(Vec::new()),
520            item_type: MenuItemType::Submenu,
521            gtk_menu: Some((COUNTER.next(), None)),
522            gtk_menu_items: Rc::new(RefCell::new(HashMap::new())),
523            gtk_menus: Some(HashMap::new()),
524            accel_group: None,
525            gtk_accelerator: None,
526            icon: None,
527            is_syncing_checked_state: None,
528            predefined_item_type: None,
529            accelerator: None,
530            checked: None,
531        }
532    }
533
534    pub(crate) fn new_predefined(item_type: PredefinedMenuItemType, text: Option<String>) -> Self {
535        Self {
536            text: text.unwrap_or_else(|| item_type.text().to_string()),
537            enabled: true,
538            accelerator: item_type.accelerator(),
539            id: MenuId(COUNTER.next().to_string()),
540            item_type: MenuItemType::Predefined,
541            predefined_item_type: Some(item_type),
542            gtk_menu_items: Rc::new(RefCell::new(HashMap::new())),
543            accel_group: None,
544            checked: None,
545            children: None,
546            gtk_accelerator: None,
547            gtk_menu: None,
548            gtk_menus: None,
549            icon: None,
550            is_syncing_checked_state: None,
551        }
552    }
553
554    pub fn new_check(
555        text: &str,
556        enabled: bool,
557        checked: bool,
558        accelerator: Option<Accelerator>,
559        id: Option<MenuId>,
560    ) -> Self {
561        Self {
562            text: text.to_string(),
563            enabled,
564            checked: Some(Rc::new(AtomicBool::new(checked))),
565            is_syncing_checked_state: Some(Rc::new(AtomicBool::new(false))),
566            accelerator,
567            id: id.unwrap_or_else(|| MenuId(COUNTER.next().to_string())),
568            item_type: MenuItemType::Check,
569            gtk_menu_items: Rc::new(RefCell::new(HashMap::new())),
570            accel_group: None,
571            children: None,
572            gtk_accelerator: None,
573            gtk_menu: None,
574            gtk_menus: None,
575            icon: None,
576            predefined_item_type: None,
577        }
578    }
579
580    pub fn new_icon(
581        text: &str,
582        enabled: bool,
583        icon: Option<Icon>,
584        accelerator: Option<Accelerator>,
585        id: Option<MenuId>,
586    ) -> Self {
587        Self {
588            text: text.to_string(),
589            enabled,
590            icon,
591            accelerator,
592            id: id.unwrap_or_else(|| MenuId(COUNTER.next().to_string())),
593            item_type: MenuItemType::Icon,
594            gtk_menu_items: Rc::new(RefCell::new(HashMap::new())),
595            accel_group: None,
596            checked: None,
597            children: None,
598            gtk_accelerator: None,
599            gtk_menu: None,
600            gtk_menus: None,
601            is_syncing_checked_state: None,
602            predefined_item_type: None,
603        }
604    }
605
606    pub fn new_native_icon(
607        text: &str,
608        enabled: bool,
609        _native_icon: Option<NativeIcon>,
610        accelerator: Option<Accelerator>,
611        id: Option<MenuId>,
612    ) -> Self {
613        Self {
614            text: text.to_string(),
615            enabled,
616            accelerator,
617            id: id.unwrap_or_else(|| MenuId(COUNTER.next().to_string())),
618            item_type: MenuItemType::Icon,
619            gtk_menu_items: Rc::new(RefCell::new(HashMap::new())),
620            accel_group: None,
621            checked: None,
622            children: None,
623            gtk_accelerator: None,
624            gtk_menu: None,
625            gtk_menus: None,
626            icon: None,
627            is_syncing_checked_state: None,
628            predefined_item_type: None,
629        }
630    }
631}
632
633/// Shared methods
634impl MenuChild {
635    pub(crate) fn item_type(&self) -> MenuItemType {
636        self.item_type
637    }
638
639    pub fn id(&self) -> &MenuId {
640        &self.id
641    }
642
643    pub fn text(&self) -> String {
644        match self
645            .gtk_menu_items
646            .borrow()
647            .values()
648            .collect::<Vec<_>>()
649            .first()
650            .map(|v| v.first())
651            .map(|e| e.map(|i| i.label().map(from_gtk_mnemonic)))
652        {
653            Some(Some(Some(text))) => text,
654            _ => self.text.clone(),
655        }
656    }
657
658    pub fn set_text(&mut self, text: &str) {
659        self.text = text.to_string();
660        let text = to_gtk_mnemonic(text);
661        for items in self.gtk_menu_items.borrow().values() {
662            for i in items {
663                i.set_label(&text);
664            }
665        }
666    }
667
668    pub fn is_enabled(&self) -> bool {
669        match self
670            .gtk_menu_items
671            .borrow()
672            .values()
673            .collect::<Vec<_>>()
674            .first()
675            .map(|v| v.first())
676            .map(|e| e.map(|i| i.is_sensitive()))
677        {
678            Some(Some(enabled)) => enabled,
679            _ => self.enabled,
680        }
681    }
682
683    pub fn set_enabled(&mut self, enabled: bool) {
684        self.enabled = enabled;
685        for items in self.gtk_menu_items.borrow().values() {
686            for i in items {
687                i.set_sensitive(enabled);
688            }
689        }
690    }
691
692    pub fn set_accelerator(&mut self, accelerator: Option<Accelerator>) -> crate::Result<()> {
693        let prev_accel = self.gtk_accelerator.as_ref();
694        let new_accel = accelerator.as_ref().map(parse_accelerator).transpose()?;
695
696        for items in self.gtk_menu_items.borrow().values() {
697            for i in items {
698                if let Some((mods, key)) = prev_accel {
699                    if let Some(accel_group) = &self.accel_group {
700                        i.remove_accelerator(accel_group, *key, *mods);
701                    }
702                }
703                if let Some((mods, key)) = new_accel {
704                    if let Some(accel_group) = &self.accel_group {
705                        i.add_accelerator(
706                            "activate",
707                            accel_group,
708                            key,
709                            mods,
710                            gtk::AccelFlags::VISIBLE,
711                        )
712                    }
713                }
714            }
715        }
716
717        self.gtk_accelerator = new_accel;
718        self.accelerator = accelerator;
719
720        Ok(())
721    }
722}
723
724/// CheckMenuItem methods
725impl MenuChild {
726    pub fn is_checked(&self) -> bool {
727        match self
728            .gtk_menu_items
729            .borrow()
730            .values()
731            .collect::<Vec<_>>()
732            .first()
733            .map(|v| v.first())
734            .map(|e| e.map(|i| i.downcast_ref::<gtk::CheckMenuItem>().unwrap().is_active()))
735        {
736            Some(Some(checked)) => checked,
737            _ => self.checked.as_ref().unwrap().load(Ordering::Relaxed),
738        }
739    }
740
741    pub fn set_checked(&mut self, checked: bool) {
742        self.checked
743            .as_ref()
744            .unwrap()
745            .store(checked, Ordering::Release);
746        let is_syncing = self.is_syncing_checked_state.as_ref().unwrap();
747        is_syncing.store(true, Ordering::Release);
748        for items in self.gtk_menu_items.borrow().values() {
749            for i in items {
750                i.downcast_ref::<gtk::CheckMenuItem>()
751                    .unwrap()
752                    .set_active(checked);
753            }
754        }
755        is_syncing.store(false, Ordering::Release);
756    }
757}
758
759/// IconMenuItem methods
760impl MenuChild {
761    pub fn set_icon(&mut self, icon: Option<Icon>) {
762        self.icon.clone_from(&icon);
763
764        let pixbuf = icon.map(|i| i.inner.to_pixbuf_scale(16, 16));
765        for items in self.gtk_menu_items.borrow().values() {
766            for i in items {
767                let box_container = i.child().unwrap().downcast::<gtk::Box>().unwrap();
768                box_container.children()[0]
769                    .downcast_ref::<gtk::Image>()
770                    .unwrap()
771                    .set_pixbuf(pixbuf.as_ref())
772            }
773        }
774    }
775}
776
777/// Submenu methods
778impl MenuChild {
779    pub fn add_menu_item(&mut self, item: &dyn crate::IsMenuItem, op: AddOp) -> crate::Result<()> {
780        if is_item_supported!(item) {
781            for menus in self.gtk_menus.as_ref().unwrap().values() {
782                for (menu_id, menu) in menus {
783                    let gtk_item =
784                        item.make_gtk_menu_item(*menu_id, self.accel_group.as_ref(), true, false)?;
785                    match op {
786                        AddOp::Append => menu.append(&gtk_item),
787                        AddOp::Insert(position) => menu.insert(&gtk_item, position as i32),
788                    }
789                    gtk_item.show();
790                }
791            }
792
793            if let Some((menu_id, Some(menu))) = self.gtk_menu.as_ref() {
794                let gtk_item =
795                    item.make_gtk_menu_item(*menu_id, self.accel_group.as_ref(), true, false)?;
796                match op {
797                    AddOp::Append => menu.append(&gtk_item),
798                    AddOp::Insert(position) => menu.insert(&gtk_item, position as i32),
799                }
800                gtk_item.show();
801            }
802        }
803
804        match op {
805            AddOp::Append => self.children.as_mut().unwrap().push(item.child()),
806            AddOp::Insert(position) => self
807                .children
808                .as_mut()
809                .unwrap()
810                .insert(position, item.child()),
811        }
812
813        Ok(())
814    }
815
816    fn add_menu_item_with_id(&self, item: &dyn crate::IsMenuItem, id: u32) -> crate::Result<()> {
817        return_if_item_not_supported!(item);
818
819        for menus in self.gtk_menus.as_ref().unwrap().values() {
820            for (menu_id, menu) in menus.iter().filter(|m| m.0 == id) {
821                let gtk_item =
822                    item.make_gtk_menu_item(*menu_id, self.accel_group.as_ref(), true, false)?;
823                menu.append(&gtk_item);
824                gtk_item.show();
825            }
826        }
827
828        Ok(())
829    }
830
831    fn add_menu_item_to_context_menu(&self, item: &dyn crate::IsMenuItem) -> crate::Result<()> {
832        return_if_item_not_supported!(item);
833
834        if let Some((menu_id, Some(menu))) = self.gtk_menu.as_ref() {
835            let gtk_item =
836                item.make_gtk_menu_item(*menu_id, self.accel_group.as_ref(), true, false)?;
837            menu.append(&gtk_item);
838            gtk_item.show();
839        }
840
841        Ok(())
842    }
843
844    pub fn remove(&mut self, item: &dyn crate::IsMenuItem) -> crate::Result<()> {
845        self.remove_inner(item, true, None)
846    }
847
848    fn remove_inner(
849        &mut self,
850        item: &dyn crate::IsMenuItem,
851        remove_from_cache: bool,
852        id: Option<u32>,
853    ) -> crate::Result<()> {
854        // get child
855        let child = {
856            let index = self
857                .children
858                .as_ref()
859                .unwrap()
860                .iter()
861                .position(|e| e.borrow().id == item.id())
862                .ok_or(crate::Error::NotAChildOfThisMenu)?;
863            if remove_from_cache {
864                self.children.as_mut().unwrap().remove(index)
865            } else {
866                self.children.as_ref().unwrap().get(index).cloned().unwrap()
867            }
868        };
869
870        for menus in self.gtk_menus.as_ref().unwrap().values() {
871            for (menu_id, menu) in menus {
872                // check if we are removing this item from all gtk_menus
873                //      which is usually when this is the item the user is actaully removing
874                // or if we are removing from a specific menu (id)
875                //      which is when the actual item being removed is a submenu
876                //      and we are iterating through its children and removing
877                //      each child gtk items that are related to this submenu.
878                if id.map(|i| i == *menu_id).unwrap_or(true) {
879                    // bail this is not a supported item like a close_window predefined menu item
880                    if is_item_supported!(item) {
881                        let mut child_ = child.borrow_mut();
882
883                        if child_.item_type == MenuItemType::Submenu {
884                            let menus = child_.gtk_menus.as_ref().unwrap().get(menu_id).cloned();
885                            if let Some(menus) = menus {
886                                for (id, menu) in menus {
887                                    // iterate through children and only remove the gtk items
888                                    // related to this submenu
889                                    for item in child_.items() {
890                                        child_.remove_inner(item.as_ref(), false, Some(id))?;
891                                    }
892                                    unsafe { menu.destroy() };
893                                }
894                            }
895                            child_.gtk_menus.as_mut().unwrap().remove(menu_id);
896                        }
897
898                        // remove all the gtk items that are related to this gtk menu and destroy it
899                        if let Some(items) = child_.gtk_menu_items.borrow_mut().remove(menu_id) {
900                            for item in items {
901                                menu.remove(&item);
902                                if let Some(accel_group) = &child_.accel_group {
903                                    if let Some((mods, key)) = child_.gtk_accelerator {
904                                        item.remove_accelerator(accel_group, key, mods);
905                                    }
906                                }
907                                unsafe { item.destroy() };
908                            }
909                        };
910                    }
911                }
912            }
913        }
914
915        // remove from the gtk menu assigned to the context menu
916        if remove_from_cache {
917            if let (id, Some(menu)) = self.gtk_menu.as_ref().unwrap() {
918                let child_ = child.borrow_mut();
919                if let Some(items) = child_.gtk_menu_items.borrow_mut().remove(id) {
920                    for item in items {
921                        menu.remove(&item);
922                        if let Some(accel_group) = &child_.accel_group {
923                            if let Some((mods, key)) = child_.gtk_accelerator {
924                                item.remove_accelerator(accel_group, key, mods);
925                            }
926                        }
927                        unsafe { item.destroy() };
928                    }
929                };
930            }
931        }
932
933        Ok(())
934    }
935
936    pub fn items(&self) -> Vec<MenuItemKind> {
937        self.children
938            .as_ref()
939            .unwrap()
940            .iter()
941            .map(|c| c.borrow().kind(c.clone()))
942            .collect()
943    }
944
945    pub fn show_context_menu_for_gtk_window(
946        &mut self,
947        widget: &impl IsA<gtk::Widget>,
948        position: Option<Position>,
949    ) -> bool {
950        show_context_menu(self.gtk_context_menu(), widget, position)
951    }
952
953    pub fn gtk_context_menu(&mut self) -> gtk::Menu {
954        let mut add_items = false;
955        {
956            let gtk_menu = self.gtk_menu.as_mut().unwrap();
957            if gtk_menu.1.is_none() {
958                gtk_menu.1 = Some(gtk::Menu::new());
959                add_items = true;
960            }
961        }
962
963        if add_items {
964            for item in self.items() {
965                self.add_menu_item_to_context_menu(item.as_ref()).unwrap();
966            }
967        }
968
969        self.gtk_menu.as_ref().unwrap().1.as_ref().unwrap().clone()
970    }
971}
972
973macro_rules! register_accel {
974    ($self:ident, $item:ident, $accel_group:ident) => {
975        $self.gtk_accelerator = $self
976            .accelerator
977            .as_ref()
978            .map(parse_accelerator)
979            .transpose()?;
980
981        if let Some((mods, key)) = &$self.gtk_accelerator {
982            if let Some(accel_group) = $accel_group {
983                $item.add_accelerator(
984                    "activate",
985                    accel_group,
986                    *key,
987                    *mods,
988                    gtk::AccelFlags::VISIBLE,
989                )
990            }
991        }
992    };
993}
994
995/// Gtk menu item creation methods
996impl MenuChild {
997    fn create_gtk_item_for_submenu(
998        &mut self,
999        menu_id: u32,
1000        accel_group: Option<&gtk::AccelGroup>,
1001        add_to_cache: bool,
1002        for_menu_bar: bool,
1003    ) -> crate::Result<gtk::MenuItem> {
1004        let submenu = gtk::Menu::new();
1005
1006        let image = self
1007            .icon
1008            .as_ref()
1009            .map(|icon| gtk::Image::from_pixbuf(Some(&icon.inner.to_pixbuf_scale(16, 16))))
1010            .unwrap_or_default();
1011
1012        let label = gtk::AccelLabel::builder()
1013            .label(to_gtk_mnemonic(&self.text))
1014            .use_underline(true)
1015            .xalign(0.0)
1016            .build();
1017
1018        let box_container = gtk::Box::new(gtk::Orientation::Horizontal, 6);
1019        if !for_menu_bar {
1020            let style_context = box_container.style_context();
1021            let css_provider = gtk::CssProvider::new();
1022            let theme = r#"
1023            box {
1024                margin-left: -22px;
1025                }
1026                "#;
1027            let _ = css_provider.load_from_data(theme.as_bytes());
1028            style_context.add_provider(&css_provider, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION);
1029        }
1030        box_container.pack_start(&image, false, false, 0);
1031        box_container.pack_start(&label, true, true, 0);
1032        box_container.show_all();
1033
1034        let item = gtk::MenuItem::builder()
1035            .child(&box_container)
1036            .sensitive(self.enabled)
1037            .build();
1038
1039        item.set_submenu(Some(&submenu));
1040        item.show();
1041
1042        self.accel_group = accel_group.cloned();
1043
1044        let mut id = 0;
1045        if add_to_cache {
1046            id = COUNTER.next();
1047
1048            self.gtk_menu_items
1049                .borrow_mut()
1050                .entry(menu_id)
1051                .or_default()
1052                .push(item.clone());
1053            self.gtk_menus
1054                .as_mut()
1055                .unwrap()
1056                .entry(menu_id)
1057                .or_default()
1058                .push((id, submenu.clone()));
1059        }
1060
1061        for child_item in self.items() {
1062            if add_to_cache {
1063                self.add_menu_item_with_id(child_item.as_ref(), id)?;
1064            } else {
1065                let gtk_child_item = child_item.make_gtk_menu_item(0, None, false, false)?;
1066                submenu.append(&gtk_child_item);
1067            }
1068        }
1069
1070        Ok(item)
1071    }
1072
1073    fn create_gtk_item_for_menu_item(
1074        &mut self,
1075        menu_id: u32,
1076        accel_group: Option<&gtk::AccelGroup>,
1077        add_to_cache: bool,
1078    ) -> crate::Result<gtk::MenuItem> {
1079        let item = gtk::MenuItem::builder()
1080            .label(to_gtk_mnemonic(&self.text))
1081            .use_underline(true)
1082            .sensitive(self.enabled)
1083            .build();
1084
1085        self.accel_group = accel_group.cloned();
1086
1087        register_accel!(self, item, accel_group);
1088
1089        let id = self.id.clone();
1090        item.connect_activate(move |_| {
1091            MenuEvent::send(crate::MenuEvent { id: id.clone() });
1092        });
1093
1094        if add_to_cache {
1095            self.gtk_menu_items
1096                .borrow_mut()
1097                .entry(menu_id)
1098                .or_default()
1099                .push(item.clone());
1100        }
1101
1102        Ok(item)
1103    }
1104
1105    fn create_gtk_item_for_predefined_menu_item(
1106        &mut self,
1107        menu_id: u32,
1108        accel_group: Option<&gtk::AccelGroup>,
1109        add_to_cache: bool,
1110    ) -> crate::Result<gtk::MenuItem> {
1111        let text = self.text.clone();
1112        self.gtk_accelerator = self
1113            .accelerator
1114            .as_ref()
1115            .map(parse_accelerator)
1116            .transpose()?;
1117        let predefined_item_type = self.predefined_item_type.clone().unwrap();
1118
1119        let make_item = || {
1120            gtk::MenuItem::builder()
1121                .label(to_gtk_mnemonic(&text))
1122                .use_underline(true)
1123                .sensitive(true)
1124                .build()
1125        };
1126        let register_accel = |item: &gtk::MenuItem| {
1127            if let Some((mods, key)) = &self.gtk_accelerator {
1128                if let Some(accel_group) = accel_group {
1129                    item.add_accelerator(
1130                        "activate",
1131                        accel_group,
1132                        *key,
1133                        *mods,
1134                        gtk::AccelFlags::VISIBLE,
1135                    )
1136                }
1137            }
1138        };
1139
1140        let item = match predefined_item_type {
1141            PredefinedMenuItemType::Separator => {
1142                gtk::SeparatorMenuItem::new().upcast::<gtk::MenuItem>()
1143            }
1144            PredefinedMenuItemType::Copy
1145            | PredefinedMenuItemType::Cut
1146            | PredefinedMenuItemType::Paste
1147            | PredefinedMenuItemType::SelectAll => {
1148                let item = make_item();
1149                let (mods, key) =
1150                    parse_accelerator(&predefined_item_type.accelerator().unwrap()).unwrap();
1151                item.child()
1152                    .unwrap()
1153                    .downcast::<gtk::AccelLabel>()
1154                    .unwrap()
1155                    .set_accel(key, mods);
1156                item.connect_activate(move |_| {
1157                    // TODO: wayland
1158                    #[cfg(feature = "libxdo")]
1159                    if let Ok(xdo) = libxdo::XDo::new(None) {
1160                        let _ = xdo.send_keysequence(predefined_item_type.xdo_keys(), 0);
1161                    }
1162                });
1163                item
1164            }
1165            PredefinedMenuItemType::About(metadata) => {
1166                let item = make_item();
1167                register_accel(&item);
1168                item.connect_activate(move |_| {
1169                    if let Some(metadata) = &metadata {
1170                        let mut builder = AboutDialog::builder().modal(true).resizable(false);
1171
1172                        if let Some(name) = &metadata.name {
1173                            builder = builder.program_name(name);
1174                        }
1175                        if let Some(version) = &metadata.full_version() {
1176                            builder = builder.version(version);
1177                        }
1178                        if let Some(authors) = &metadata.authors {
1179                            builder = builder.authors(authors.clone());
1180                        }
1181                        if let Some(comments) = &metadata.comments {
1182                            builder = builder.comments(comments);
1183                        }
1184                        if let Some(copyright) = &metadata.copyright {
1185                            builder = builder.copyright(copyright);
1186                        }
1187                        if let Some(license) = &metadata.license {
1188                            builder = builder.license(license);
1189                        }
1190                        if let Some(website) = &metadata.website {
1191                            builder = builder.website(website);
1192                        }
1193                        if let Some(website_label) = &metadata.website_label {
1194                            builder = builder.website_label(website_label);
1195                        }
1196                        if let Some(icon) = &metadata.icon {
1197                            builder = builder.logo(&icon.inner.to_pixbuf());
1198                        }
1199
1200                        let about = builder.build();
1201                        about.run();
1202                        unsafe {
1203                            about.destroy();
1204                        }
1205                    }
1206                });
1207                item
1208            }
1209            _ => unreachable!(),
1210        };
1211
1212        if add_to_cache {
1213            self.gtk_menu_items
1214                .borrow_mut()
1215                .entry(menu_id)
1216                .or_default()
1217                .push(item.clone());
1218        }
1219        Ok(item)
1220    }
1221
1222    fn create_gtk_item_for_check_menu_item(
1223        &mut self,
1224        menu_id: u32,
1225        accel_group: Option<&gtk::AccelGroup>,
1226        add_to_cache: bool,
1227    ) -> crate::Result<gtk::MenuItem> {
1228        let item = gtk::CheckMenuItem::builder()
1229            .label(to_gtk_mnemonic(&self.text))
1230            .use_underline(true)
1231            .sensitive(self.enabled)
1232            .active(self.checked.as_ref().unwrap().load(Ordering::Relaxed))
1233            .build();
1234
1235        self.accel_group = accel_group.cloned();
1236
1237        register_accel!(self, item, accel_group);
1238
1239        let id = self.id.clone();
1240        let is_syncing_checked_state = self.is_syncing_checked_state.clone().unwrap();
1241        let checked = self.checked.clone().unwrap();
1242        let store = self.gtk_menu_items.clone();
1243        item.connect_toggled(move |i| {
1244            let should_dispatch = is_syncing_checked_state
1245                .compare_exchange(false, true, Ordering::Release, Ordering::Relaxed)
1246                .is_ok();
1247
1248            if should_dispatch {
1249                let c = i.is_active();
1250                checked.store(c, Ordering::Release);
1251
1252                for items in store.borrow().values() {
1253                    for i in items {
1254                        i.downcast_ref::<gtk::CheckMenuItem>()
1255                            .unwrap()
1256                            .set_active(c);
1257                    }
1258                }
1259
1260                is_syncing_checked_state.store(false, Ordering::Release);
1261
1262                MenuEvent::send(crate::MenuEvent { id: id.clone() });
1263            }
1264        });
1265
1266        let item = item.upcast::<gtk::MenuItem>();
1267
1268        if add_to_cache {
1269            self.gtk_menu_items
1270                .borrow_mut()
1271                .entry(menu_id)
1272                .or_default()
1273                .push(item.clone());
1274        }
1275
1276        Ok(item)
1277    }
1278
1279    fn create_gtk_item_for_icon_menu_item(
1280        &mut self,
1281        menu_id: u32,
1282        accel_group: Option<&gtk::AccelGroup>,
1283        add_to_cache: bool,
1284        for_menu_bar: bool,
1285    ) -> crate::Result<gtk::MenuItem> {
1286        let image = self
1287            .icon
1288            .as_ref()
1289            .map(|i| gtk::Image::from_pixbuf(Some(&i.inner.to_pixbuf_scale(16, 16))))
1290            .unwrap_or_default();
1291
1292        self.accel_group = accel_group.cloned();
1293
1294        let label = gtk::AccelLabel::builder()
1295            .label(to_gtk_mnemonic(&self.text))
1296            .use_underline(true)
1297            .xalign(0.0)
1298            .build();
1299
1300        let box_container = gtk::Box::new(Orientation::Horizontal, 6);
1301        if !for_menu_bar {
1302            let style_context = box_container.style_context();
1303            let css_provider = gtk::CssProvider::new();
1304            let theme = r#"
1305            box {
1306                margin-left: -22px;
1307                }
1308                "#;
1309            let _ = css_provider.load_from_data(theme.as_bytes());
1310            style_context.add_provider(&css_provider, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION);
1311        }
1312        box_container.pack_start(&image, false, false, 0);
1313        box_container.pack_start(&label, true, true, 0);
1314        box_container.show_all();
1315
1316        let item = gtk::MenuItem::builder()
1317            .child(&box_container)
1318            .sensitive(self.enabled)
1319            .build();
1320
1321        register_accel!(self, item, accel_group);
1322
1323        let id = self.id.clone();
1324        item.connect_activate(move |_| {
1325            MenuEvent::send(crate::MenuEvent { id: id.clone() });
1326        });
1327
1328        if add_to_cache {
1329            self.gtk_menu_items
1330                .borrow_mut()
1331                .entry(menu_id)
1332                .or_default()
1333                .push(item.clone());
1334        }
1335
1336        Ok(item)
1337    }
1338}
1339
1340impl MenuItemKind {
1341    fn make_gtk_menu_item(
1342        &self,
1343        menu_id: u32,
1344        accel_group: Option<&gtk::AccelGroup>,
1345        add_to_cache: bool,
1346        for_menu_bar: bool,
1347    ) -> crate::Result<gtk::MenuItem> {
1348        let mut child = self.child_mut();
1349        match child.item_type() {
1350            MenuItemType::Submenu => {
1351                child.create_gtk_item_for_submenu(menu_id, accel_group, add_to_cache, for_menu_bar)
1352            }
1353            MenuItemType::MenuItem => {
1354                child.create_gtk_item_for_menu_item(menu_id, accel_group, add_to_cache)
1355            }
1356            MenuItemType::Predefined => {
1357                child.create_gtk_item_for_predefined_menu_item(menu_id, accel_group, add_to_cache)
1358            }
1359            MenuItemType::Check => {
1360                child.create_gtk_item_for_check_menu_item(menu_id, accel_group, add_to_cache)
1361            }
1362            MenuItemType::Icon => child.create_gtk_item_for_icon_menu_item(
1363                menu_id,
1364                accel_group,
1365                add_to_cache,
1366                for_menu_bar,
1367            ),
1368        }
1369    }
1370}
1371
1372impl dyn IsMenuItem + '_ {
1373    fn make_gtk_menu_item(
1374        &self,
1375        menu_id: u32,
1376        accel_group: Option<&gtk::AccelGroup>,
1377        add_to_cache: bool,
1378        for_menu_bar: bool,
1379    ) -> crate::Result<gtk::MenuItem> {
1380        self.kind()
1381            .make_gtk_menu_item(menu_id, accel_group, add_to_cache, for_menu_bar)
1382    }
1383}
1384
1385fn show_context_menu(
1386    gtk_menu: gtk::Menu,
1387    widget: &impl IsA<gtk::Widget>,
1388    position: Option<Position>,
1389) -> bool {
1390    let (pos, window) = if let Some(pos) = position {
1391        let window = widget.window();
1392        (
1393            pos.to_logical::<i32>(window.as_ref().map(|w| w.scale_factor()).unwrap_or(1) as _)
1394                .into(),
1395            window,
1396        )
1397    } else {
1398        let window = widget.screen().and_then(|s| s.root_window());
1399        (
1400            window
1401                .as_ref()
1402                .and_then(|w| {
1403                    w.display()
1404                        .default_seat()
1405                        .and_then(|s| s.pointer())
1406                        .map(|s| {
1407                            let p = s.position();
1408                            (p.1, p.2)
1409                        })
1410                })
1411                .unwrap_or_default(),
1412            window,
1413        )
1414    };
1415
1416    if let Some(window) = window {
1417        let mut event = gdk::Event::new(gdk::EventType::ButtonPress);
1418        event.set_device(
1419            window
1420                .display()
1421                .default_seat()
1422                .and_then(|d| d.pointer())
1423                .as_ref(),
1424        );
1425
1426        // Set the time of the event otherwise GTK will close the menu
1427        // when right click is released
1428        let event_ffi: *mut gdk::ffi::GdkEvent = event.to_glib_none().0;
1429        if !event_ffi.is_null() {
1430            let time = glib::monotonic_time() / 1000;
1431            unsafe {
1432                (*event_ffi).button.time = time as _;
1433            }
1434        }
1435
1436        let (tx, rx) = crossbeam_channel::unbounded();
1437        let tx_clone = tx.clone();
1438        let id = gtk_menu.connect_cancel(move |_| tx_clone.send(false).unwrap_or(()));
1439        let id2 = gtk_menu.connect_selection_done(move |_| tx.send(true).unwrap_or(()));
1440        gtk_menu.popup_at_rect(
1441            &window,
1442            &gdk::Rectangle::new(pos.0, pos.1, 0, 0),
1443            gdk::Gravity::NorthWest,
1444            gdk::Gravity::NorthWest,
1445            Some(&event),
1446        );
1447
1448        loop {
1449            gtk::main_iteration();
1450
1451            match rx.try_recv() {
1452                Ok(result) => {
1453                    gtk_menu.disconnect(id);
1454                    gtk_menu.disconnect(id2);
1455                    return result;
1456                }
1457                Err(err) => {
1458                    if err.is_disconnected() {
1459                        gtk_menu.disconnect(id);
1460                        gtk_menu.disconnect(id2);
1461                        return false;
1462                    }
1463                }
1464            }
1465        }
1466    }
1467
1468    false
1469}
1470
1471impl PredefinedMenuItemType {
1472    #[cfg(feature = "libxdo")]
1473    fn xdo_keys(&self) -> &str {
1474        match self {
1475            PredefinedMenuItemType::Copy => "ctrl+c",
1476            PredefinedMenuItemType::Cut => "ctrl+X",
1477            PredefinedMenuItemType::Paste => "ctrl+v",
1478            PredefinedMenuItemType::SelectAll => "ctrl+a",
1479            _ => unreachable!(),
1480        }
1481    }
1482}