1mod 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 gtk_menubars: HashMap<u32, gtk::MenuBar>,
65 accel_group: Option<gtk::AccelGroup>,
66 gtk_menu: (u32, Option<gtk::Menu>), }
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(>k_item),
105 AddOp::Insert(position) => menu_bar.insert(>k_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(>k_item),
115 AddOp::Insert(position) => menu.insert(>k_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(>k_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(>k_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 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 if id.map(|i| i == *menu_id).unwrap_or(true) {
187 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 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 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 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 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 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 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 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 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 unsafe { menu_bar.destroy() };
320 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#[derive(Debug, Default)]
394pub struct MenuChild {
395 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 accelerator: Option<Accelerator>,
405 gtk_accelerator: Option<(gdk::ModifierType, u32)>,
406
407 predefined_item_type: Option<PredefinedMenuItemType>,
409
410 checked: Option<Rc<AtomicBool>>,
412 is_syncing_checked_state: Option<Rc<AtomicBool>>,
413
414 icon: Option<Icon>,
416
417 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>)>, 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
487impl 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
633impl 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
724impl 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
759impl 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
777impl 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(>k_item),
787 AddOp::Insert(position) => menu.insert(>k_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(>k_item),
798 AddOp::Insert(position) => menu.insert(>k_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(>k_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(>k_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 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 if id.map(|i| i == *menu_id).unwrap_or(true) {
879 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 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 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 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
995impl MenuChild {
997 fn create_gtk_item_for_submenu(
998 &mut self,
999 menu_id: u32,
1000 accel_group: Option<>k::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(>k_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<>k::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<>k::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: >k::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 #[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<>k::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<>k::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<>k::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<>k::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 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}