Serenity Operating System
1/*
2 * Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org>
3 * Copyright (c) 2020, Shannon Booth <shannon.ml.booth@gmail.com>
4 *
5 * SPDX-License-Identifier: BSD-2-Clause
6 */
7
8#include <AK/Badge.h>
9#include <WindowServer/ConnectionFromClient.h>
10#include <WindowServer/MenuManager.h>
11#include <WindowServer/Screen.h>
12#include <WindowServer/WindowManager.h>
13
14namespace WindowServer {
15
16static MenuManager* s_the;
17
18MenuManager& MenuManager::the()
19{
20 VERIFY(s_the);
21 return *s_the;
22}
23
24MenuManager::MenuManager()
25{
26 s_the = this;
27}
28
29bool MenuManager::is_open(Menu const& menu) const
30{
31 for (size_t i = 0; i < m_open_menu_stack.size(); ++i) {
32 if (&menu == m_open_menu_stack[i].ptr())
33 return true;
34 }
35 return false;
36}
37
38void MenuManager::refresh()
39{
40 ConnectionFromClient::for_each_client([&](ConnectionFromClient& client) {
41 client.for_each_menu([&](Menu& menu) {
42 menu.redraw();
43 return IterationDecision::Continue;
44 });
45 });
46}
47
48void MenuManager::event(Core::Event& event)
49{
50 auto& wm = WindowManager::the();
51
52 if (static_cast<Event&>(event).is_mouse_event()) {
53 handle_mouse_event(static_cast<MouseEvent&>(event));
54 return;
55 }
56
57 if (static_cast<Event&>(event).is_key_event()) {
58 auto& key_event = static_cast<KeyEvent const&>(event);
59
60 if (key_event.type() == Event::KeyUp && key_event.key() == Key_Escape) {
61 close_everyone();
62 return;
63 }
64
65 if (m_current_menu && event.type() == Event::KeyDown
66 && ((key_event.key() >= Key_A && key_event.key() <= Key_Z)
67 || (key_event.key() >= Key_0 && key_event.key() <= Key_9))) {
68
69 if (auto* shortcut_item_indices = m_current_menu->items_with_alt_shortcut(key_event.code_point())) {
70 VERIFY(!shortcut_item_indices->is_empty());
71 auto it = shortcut_item_indices->find_if([&](int const& i) { return i > m_current_menu->hovered_item_index(); });
72 auto index = shortcut_item_indices->at(it.is_end() ? 0 : it.index());
73 auto& item = m_current_menu->item(index);
74 m_current_menu->set_hovered_index(index);
75 if (shortcut_item_indices->size() > 1)
76 return;
77 if (item.is_submenu())
78 m_current_menu->descend_into_submenu_at_hovered_item();
79 else
80 m_current_menu->open_hovered_item(false);
81 }
82
83 return;
84 }
85
86 if (event.type() == Event::KeyDown) {
87
88 if (key_event.key() == Key_Left) {
89 auto it = m_open_menu_stack.find_if([&](auto const& other) { return m_current_menu == other.ptr(); });
90 VERIFY(!it.is_end());
91
92 // Going "back" a menu should be the previous menu in the stack
93 if (it.index() > 0)
94 set_current_menu(m_open_menu_stack.at(it.index() - 1));
95 else {
96 if (m_current_menu->hovered_item())
97 m_current_menu->set_hovered_index(-1);
98 else {
99 auto* target_menu = previous_menu(m_current_menu);
100 if (target_menu) {
101 target_menu->ensure_menu_window(target_menu->rect_in_window_menubar().bottom_left().translated(wm.window_with_active_menu()->frame().rect().location()).translated(wm.window_with_active_menu()->frame().menubar_rect().location()));
102 open_menu(*target_menu);
103 wm.window_with_active_menu()->invalidate_menubar();
104 }
105 }
106 }
107 close_everyone_not_in_lineage(*m_current_menu);
108 return;
109 }
110
111 if (key_event.key() == Key_Right) {
112 auto hovered_item = m_current_menu->hovered_item();
113 if (hovered_item && hovered_item->is_submenu())
114 m_current_menu->descend_into_submenu_at_hovered_item();
115 else if (m_open_menu_stack.size() <= 1 && wm.window_with_active_menu()) {
116 auto* target_menu = next_menu(m_current_menu);
117 if (target_menu) {
118 target_menu->ensure_menu_window(target_menu->rect_in_window_menubar().bottom_left().translated(wm.window_with_active_menu()->frame().rect().location()).translated(wm.window_with_active_menu()->frame().menubar_rect().location()));
119 open_menu(*target_menu);
120 wm.window_with_active_menu()->invalidate_menubar();
121 close_everyone_not_in_lineage(*target_menu);
122 }
123 }
124 return;
125 }
126
127 if (key_event.key() == Key_Return) {
128 auto hovered_item = m_current_menu->hovered_item();
129 if (!hovered_item || !hovered_item->is_enabled())
130 return;
131 if (hovered_item->is_submenu())
132 m_current_menu->descend_into_submenu_at_hovered_item();
133 else
134 m_current_menu->open_hovered_item(key_event.modifiers() & KeyModifier::Mod_Ctrl);
135 return;
136 }
137
138 if (key_event.key() == Key_Space) {
139 auto* hovered_item = m_current_menu->hovered_item();
140 if (!hovered_item || !hovered_item->is_enabled())
141 return;
142 if (!hovered_item->is_checkable())
143 return;
144
145 m_current_menu->open_hovered_item(true);
146 }
147
148 m_current_menu->dispatch_event(event);
149 }
150 }
151
152 return Core::Object::event(event);
153}
154
155void MenuManager::handle_mouse_event(MouseEvent& mouse_event)
156{
157 if (!has_open_menu())
158 return;
159 auto* topmost_menu = m_open_menu_stack.last().ptr();
160 VERIFY(topmost_menu);
161 auto* window = topmost_menu->menu_window();
162 if (!window) {
163 dbgln("MenuManager::handle_mouse_event: No menu window");
164 return;
165 }
166 VERIFY(window->is_visible());
167
168 bool event_is_inside_current_menu = window->rect().contains(mouse_event.position());
169 if (event_is_inside_current_menu) {
170 WindowManager::the().set_hovered_window(window);
171 WindowManager::the().deliver_mouse_event(*window, mouse_event);
172 return;
173 }
174
175 if (topmost_menu->hovered_item())
176 topmost_menu->clear_hovered_item();
177 if (mouse_event.type() == Event::MouseDown || mouse_event.type() == Event::MouseUp) {
178 auto* window_menu_of = topmost_menu->window_menu_of();
179 if (window_menu_of) {
180 bool event_is_inside_taskbar_button = window_menu_of->taskbar_rect().contains(mouse_event.position());
181 if (event_is_inside_taskbar_button && !topmost_menu->is_window_menu_open()) {
182 topmost_menu->set_window_menu_open(true);
183 return;
184 }
185 }
186
187 if (mouse_event.type() == Event::MouseDown) {
188 for (auto& menu : m_open_menu_stack) {
189 if (!menu)
190 continue;
191 if (!menu->menu_window()->rect().contains(mouse_event.position()))
192 continue;
193 return;
194 }
195 MenuManager::the().close_everyone();
196 topmost_menu->set_window_menu_open(false);
197 }
198 }
199
200 if (mouse_event.type() == Event::MouseMove) {
201 for (auto& menu : m_open_menu_stack.in_reverse()) {
202 if (!menu)
203 continue;
204 if (!menu->menu_window()->rect().contains(mouse_event.position()))
205 continue;
206 WindowManager::the().set_hovered_window(menu->menu_window());
207 WindowManager::the().deliver_mouse_event(*menu->menu_window(), mouse_event);
208 break;
209 }
210 }
211}
212
213void MenuManager::close_all_menus_from_client(Badge<ConnectionFromClient>, ConnectionFromClient& client)
214{
215 if (!has_open_menu())
216 return;
217 if (m_open_menu_stack.first()->client() != &client)
218 return;
219 close_everyone();
220}
221
222void MenuManager::close_everyone()
223{
224 for (auto& menu : m_open_menu_stack) {
225 VERIFY(menu);
226 menu->set_visible(false);
227 menu->clear_hovered_item();
228 }
229 m_open_menu_stack.clear();
230 clear_current_menu();
231}
232
233Menu* MenuManager::closest_open_ancestor_of(Menu const& other) const
234{
235 for (auto& menu : m_open_menu_stack.in_reverse())
236 if (menu->is_menu_ancestor_of(other))
237 return menu.ptr();
238 return nullptr;
239}
240
241void MenuManager::close_everyone_not_in_lineage(Menu& menu)
242{
243 Vector<Menu&> menus_to_close;
244 for (auto& open_menu : m_open_menu_stack) {
245 if (!open_menu)
246 continue;
247 if (&menu == open_menu.ptr() || open_menu->is_menu_ancestor_of(menu))
248 continue;
249 menus_to_close.append(*open_menu);
250 }
251 close_menus(menus_to_close);
252}
253
254void MenuManager::close_menus(Vector<Menu&>& menus)
255{
256 for (auto& menu : menus) {
257 if (&menu == m_current_menu)
258 clear_current_menu();
259 menu.set_visible(false);
260 menu.clear_hovered_item();
261 m_open_menu_stack.remove_first_matching([&](auto& entry) {
262 return entry == &menu;
263 });
264 }
265}
266
267static void collect_menu_subtree(Menu& menu, Vector<Menu&>& menus)
268{
269 menus.append(menu);
270 for (size_t i = 0; i < menu.item_count(); ++i) {
271 auto& item = menu.item(i);
272 if (!item.is_submenu())
273 continue;
274 collect_menu_subtree(*item.submenu(), menus);
275 }
276}
277
278void MenuManager::close_menu_and_descendants(Menu& menu)
279{
280 Vector<Menu&> menus_to_close;
281 collect_menu_subtree(menu, menus_to_close);
282 close_menus(menus_to_close);
283}
284
285void MenuManager::set_hovered_menu(Menu* menu)
286{
287 if (m_hovered_menu == menu)
288 return;
289 if (menu) {
290 m_hovered_menu = menu->make_weak_ptr<Menu>();
291 } else {
292 // FIXME: This is quite aggressive. If we knew which window the previously hovered menu was in,
293 // we could just invalidate that one instead of iterating all windows in the client.
294 if (auto* client = m_hovered_menu->client()) {
295 client->for_each_window([&](Window& window) {
296 window.invalidate_menubar();
297 return IterationDecision::Continue;
298 });
299 }
300 m_hovered_menu = nullptr;
301 }
302}
303
304void MenuManager::open_menu(Menu& menu, bool as_current_menu)
305{
306 if (menu.is_open()) {
307 if (as_current_menu || current_menu() != &menu) {
308 // This menu is already open. If requested, or if the current
309 // window doesn't match this one, then set it to this
310 set_current_menu(&menu);
311 }
312 return;
313 }
314
315 m_open_menu_stack.append(menu);
316
317 menu.set_visible(true);
318
319 if (!menu.is_empty()) {
320 menu.redraw_if_theme_changed();
321 auto* window = menu.menu_window();
322 VERIFY(window);
323 window->set_visible(true);
324 }
325
326 if (as_current_menu || !current_menu()) {
327 // Only make this menu the current menu if requested, or if no
328 // other menu is current
329 set_current_menu(&menu);
330 }
331}
332
333void MenuManager::clear_current_menu()
334{
335 if (m_current_menu) {
336 auto& wm = WindowManager::the();
337 if (auto* window = wm.window_with_active_menu()) {
338 window->invalidate_menubar();
339 }
340 wm.set_window_with_active_menu(nullptr);
341 }
342 m_current_menu = nullptr;
343}
344
345void MenuManager::set_current_menu(Menu* menu)
346{
347 if (!menu) {
348 clear_current_menu();
349 return;
350 }
351
352 VERIFY(is_open(*menu));
353 if (menu == m_current_menu) {
354 return;
355 }
356
357 m_current_menu = menu;
358}
359
360Menu* MenuManager::previous_menu(Menu* current)
361{
362 auto& wm = WindowManager::the();
363 if (!wm.window_with_active_menu())
364 return nullptr;
365 Menu* found = nullptr;
366 Menu* previous = nullptr;
367 wm.window_with_active_menu()->menubar().for_each_menu([&](Menu& menu) {
368 if (current == &menu) {
369 found = previous;
370 return IterationDecision::Break;
371 }
372 previous = &menu;
373 return IterationDecision::Continue;
374 });
375 return found;
376}
377
378Menu* MenuManager::next_menu(Menu* current)
379{
380 Menu* found = nullptr;
381 bool is_next = false;
382 auto& wm = WindowManager::the();
383 if (!wm.window_with_active_menu())
384 return nullptr;
385 wm.window_with_active_menu()->menubar().for_each_menu([&](Menu& menu) {
386 if (is_next) {
387 found = &menu;
388 return IterationDecision::Break;
389 }
390 if (current == &menu)
391 is_next = true;
392 return IterationDecision::Continue;
393 });
394 return found;
395}
396
397void MenuManager::did_change_theme()
398{
399 ++m_theme_index;
400 refresh();
401}
402
403}