1use std::collections::{HashMap, HashSet};
2use std::hash::Hash;
3use std::ops::Not;
4use std::rc::Rc;
5
6use getset::Getters;
7use tray_icon::menu::{CheckMenuItem, MenuId, MenuItemKind};
8
9#[derive(Clone, Getters)]
10#[getset(get = "pub")]
11pub struct MenuItemMeta<G> {
12 kind: MenuItemKind,
13 group: Option<G>,
14}
15
16impl<G> PartialEq for MenuItemMeta<G>
17where
18 G: PartialEq,
19{
20 fn eq(&self, other: &Self) -> bool {
21 self.group == other.group && self.kind.id() == other.kind.id()
22 }
23}
24
25#[derive(Clone, Getters)]
26#[getset(get = "pub")]
27struct RadioGroup {
28 members: HashSet<Rc<MenuId>>,
29 default: Option<MenuId>,
30}
31
32#[derive(Clone)]
33pub struct MenuRegistry<G>
34where
35 G: Clone + Copy + Eq + Hash + PartialEq + std::fmt::Debug,
36{
37 items: HashMap<Rc<MenuId>, MenuItemMeta<G>>,
38 radio_groups: HashMap<G, RadioGroup>,
39 checkbox_groups: HashMap<G, HashSet<Rc<MenuId>>>,
40}
41
42impl<G> Default for MenuRegistry<G>
43where
44 G: Clone + Copy + Eq + Hash + PartialEq + std::fmt::Debug,
45{
46 fn default() -> Self {
47 Self::new()
48 }
49}
50
51impl<G> MenuRegistry<G>
52where
53 G: Clone + Copy + Eq + Hash + PartialEq + std::fmt::Debug,
54{
55 pub fn new() -> Self {
56 Self {
57 items: HashMap::new(),
58 radio_groups: HashMap::new(),
59 checkbox_groups: HashMap::new(),
60 }
61 }
62
63 pub fn register_normal(&mut self, kind: MenuItemKind) {
64 let id = Rc::new(kind.id().clone());
65 self.items.insert(id, MenuItemMeta { kind, group: None });
66 }
67
68 pub fn register_checkbox(&mut self, kind: MenuItemKind, group: G) -> bool {
69 if kind.as_check_menuitem().is_none() {
70 return false;
71 }
72
73 let id = Rc::new(kind.id().clone());
74
75 self.items.insert(
76 id.clone(),
77 MenuItemMeta {
78 kind,
79 group: Some(group),
80 },
81 );
82
83 self.checkbox_groups.entry(group).or_default().insert(id);
84
85 true
86 }
87
88 pub fn register_radio(
89 &mut self,
90 kind: MenuItemKind,
91 group: G,
92 default: Option<MenuId>,
93 ) -> bool {
94 if kind.as_check_menuitem().is_none() {
95 return false;
96 }
97
98 let id = Rc::new(kind.id().clone());
99
100 self.items.insert(
101 id.clone(),
102 MenuItemMeta {
103 kind,
104 group: Some(group),
105 },
106 );
107
108 self.radio_groups
109 .entry(group)
110 .or_insert_with(|| RadioGroup {
111 members: HashSet::new(),
112 default,
113 })
114 .members
115 .insert(id);
116
117 true
118 }
119
120 pub fn deregister_normal(&mut self, id: &MenuId) -> bool {
121 self.items.remove(id).is_some()
122 }
123
124 pub fn deregister_checkbox(&mut self, id: &MenuId, group: G) -> bool {
125 self.items.remove(id);
126
127 self.checkbox_groups
128 .get_mut(&group)
129 .map(|checkbox_group| checkbox_group.remove(id))
130 .unwrap_or_default()
131 }
132
133 pub fn deregister_radio(&mut self, id: &MenuId, group: G) -> bool {
134 self.items.remove(id);
135
136 self.radio_groups
137 .get_mut(&group)
138 .map(|radio_group| radio_group.members.remove(id))
139 .unwrap_or_default()
140 }
141
142 pub fn handle_event(&mut self, id: &MenuId) -> Result<&MenuItemMeta<G>, String> {
143 let menu_item_meta = self
144 .items
145 .get(id)
146 .ok_or_else(|| format!("The menu not found: {id:?}"))?;
147
148 let menu_group = menu_item_meta.group();
149
150 let menu_kind = &menu_item_meta.kind();
151
152 let Some(menu_group) = menu_group else {
154 return Ok(menu_item_meta);
155 };
156
157 let Some(radio_group) = self.radio_groups.get(menu_group) else {
159 if self.checkbox_groups.contains_key(menu_group)
160 && menu_kind.as_check_menuitem().is_some().not()
161 {
162 return Err(format!(
163 "Menu({id:?}) is not a [CheckMenuItem] on the checkbox group({menu_group:?})"
164 ));
165 }
166
167 return Ok(menu_item_meta);
168 };
169
170 let radio_menus_id = radio_group.members();
172
173 let clickd_radio_menu_is_checked = menu_kind
174 .as_check_menuitem()
175 .ok_or_else(|| {
176 format!("Menu({id:?}) is not a [CheckMenuItem] on the radio group({menu_group:?})")
177 })?
178 .is_checked();
179
180 if clickd_radio_menu_is_checked {
182 radio_menus_id
183 .iter()
184 .filter(|menu_id| menu_id.as_ref().ne(&id))
185 .filter_map(|id| self.items.get(id))
186 .filter_map(|menu_meta| menu_meta.kind().as_check_menuitem())
187 .for_each(|check_menu| check_menu.set_checked(false));
188
189 Ok(menu_item_meta)
190 } else {
192 let Some(default_menu_id) = radio_group.default().as_ref() else {
193 self.get_radio_menu_from_group(menu_group)
195 .ok_or_else(|| format!("Failed to get radio menus from {menu_group:?}"))?
196 .iter()
197 .for_each(|check_menu| check_menu.set_checked(false));
198
199 return Ok(menu_item_meta);
200 };
201
202 let default_menu_meta = self
203 .items
204 .get(default_menu_id)
205 .ok_or_else(|| format!("Default menu({default_menu_id:?}) meta not found"))?;
206
207 let default_menu_item = default_menu_meta.kind().as_check_menuitem()
208 .ok_or_else(|| format!("Default Menu({default_menu_id:?}) is not a [CheckMenuItem] on the radio group({menu_group:?})"))?;
209
210 default_menu_item.set_checked(true);
212 radio_menus_id
213 .iter()
214 .filter(|menu_id| menu_id.as_ref().ne(&default_menu_id))
215 .filter_map(|id| self.items.get(id))
216 .filter_map(|menu_meta| menu_meta.kind().as_check_menuitem())
217 .for_each(|check_menu| check_menu.set_checked(false));
218
219 Ok(default_menu_meta)
220 }
221 }
222
223 pub fn get_menu_meta_from_id(&self, id: &MenuId) -> Option<&MenuItemMeta<G>> {
224 self.items.get(id)
225 }
226
227 pub fn get_menu_kind_from_id(&self, id: &MenuId) -> Option<&MenuItemKind> {
228 self.items.get(id).map(|meta| &meta.kind)
229 }
230
231 pub fn get_menu_group_from_id(&self, id: &MenuId) -> Option<G> {
232 self.items.get(id).and_then(|meta| meta.group)
233 }
234
235 pub fn get_checkbox_id_from_group(&self, group: G) -> Option<&HashSet<Rc<MenuId>>> {
236 self.checkbox_groups.get(&group)
237 }
238
239 pub fn get_checkbox_menu_from_group(&self, group: G) -> Option<Vec<&CheckMenuItem>> {
240 self.get_checkbox_id_from_group(group).map(|ids| {
241 ids.iter()
242 .filter_map(|id| self.items.get(id))
243 .filter_map(|meta| meta.kind.as_check_menuitem())
244 .collect::<Vec<_>>()
245 })
246 }
247
248 pub fn get_radio_id_from_group(&self, group: &G) -> Option<&HashSet<Rc<MenuId>>> {
249 self.radio_groups.get(group).map(|r| r.members())
250 }
251
252 pub fn get_radio_menu_from_group(&self, group: &G) -> Option<Vec<&CheckMenuItem>> {
253 self.get_radio_id_from_group(group).map(|ids| {
254 ids.iter()
255 .filter_map(|id| self.items.get(id))
256 .filter_map(|meta| meta.kind.as_check_menuitem())
257 .collect::<Vec<_>>()
258 })
259 }
260}