Serenity Operating System
1/*
2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
3 * Copyright (c) 2020, Shannon Booth <shannon.ml.booth@gmail.com>
4 * All rights reserved.
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions are met:
8 *
9 * 1. Redistributions of source code must retain the above copyright notice, this
10 * list of conditions and the following disclaimer.
11 *
12 * 2. Redistributions in binary form must reproduce the above copyright notice,
13 * this list of conditions and the following disclaimer in the documentation
14 * and/or other materials provided with the distribution.
15 *
16 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 */
27
28#include <AK/Badge.h>
29#include <AK/FileSystemPath.h>
30#include <AK/QuickSort.h>
31#include <LibCore/DirIterator.h>
32#include <LibGfx/Font.h>
33#include <LibGfx/Painter.h>
34#include <WindowServer/AppletManager.h>
35#include <WindowServer/MenuManager.h>
36#include <WindowServer/Screen.h>
37#include <WindowServer/WindowManager.h>
38#include <unistd.h>
39
40//#define DEBUG_MENUS
41
42namespace WindowServer {
43
44static MenuManager* s_the;
45
46MenuManager& MenuManager::the()
47{
48 ASSERT(s_the);
49 return *s_the;
50}
51
52MenuManager::MenuManager()
53{
54 s_the = this;
55 m_needs_window_resize = true;
56
57 // NOTE: This ensures that the system menu has the correct dimensions.
58 set_current_menubar(nullptr);
59
60 m_window = Window::construct(*this, WindowType::Menubar);
61 m_window->set_rect(menubar_rect());
62}
63
64MenuManager::~MenuManager()
65{
66}
67
68bool MenuManager::is_open(const Menu& menu) const
69{
70 for (size_t i = 0; i < m_open_menu_stack.size(); ++i) {
71 if (&menu == m_open_menu_stack[i].ptr())
72 return true;
73 }
74 return false;
75}
76
77void MenuManager::draw()
78{
79 auto& wm = WindowManager::the();
80 auto palette = wm.palette();
81 auto menubar_rect = this->menubar_rect();
82
83 if (m_needs_window_resize) {
84 m_window->set_rect(menubar_rect);
85 AppletManager::the().calculate_applet_rects(window());
86 m_needs_window_resize = false;
87 }
88
89 Gfx::Painter painter(*window().backing_store());
90
91 painter.fill_rect(menubar_rect, palette.window());
92 painter.draw_line({ 0, menubar_rect.bottom() }, { menubar_rect.right(), menubar_rect.bottom() }, palette.threed_shadow1());
93
94 for_each_active_menubar_menu([&](Menu& menu) {
95 Color text_color = palette.window_text();
96 if (is_open(menu)) {
97 painter.fill_rect(menu.rect_in_menubar(), palette.menu_selection());
98 painter.draw_rect(menu.rect_in_menubar(), palette.menu_selection().darkened());
99 text_color = palette.menu_selection_text();
100 }
101 painter.draw_text(
102 menu.text_rect_in_menubar(),
103 menu.name(),
104 menu.title_font(),
105 Gfx::TextAlignment::CenterLeft,
106 text_color);
107 return IterationDecision::Continue;
108 });
109
110 AppletManager::the().draw();
111}
112
113void MenuManager::refresh()
114{
115 if (!m_window)
116 return;
117 draw();
118 window().invalidate();
119}
120
121void MenuManager::event(Core::Event& event)
122{
123 auto* active_window = WindowManager::the().active_window();
124 if (active_window && active_window->is_modal() && has_open_menu()) {
125 auto* topmost_menu = m_open_menu_stack.last().ptr();
126 ASSERT(topmost_menu);
127 // Always allow window menu interaction, even while a modal window is active.
128 if (!topmost_menu->window_menu_of())
129 return Core::Object::event(event);
130 }
131
132 if (static_cast<Event&>(event).is_mouse_event()) {
133 handle_mouse_event(static_cast<MouseEvent&>(event));
134 return;
135 }
136
137 if (static_cast<Event&>(event).is_key_event()) {
138 auto& key_event = static_cast<const KeyEvent&>(event);
139
140 if (key_event.type() == Event::KeyUp && key_event.key() == Key_Escape) {
141 close_everyone();
142 return;
143 }
144
145 if (event.type() == Event::KeyDown) {
146 for_each_active_menubar_menu([&](Menu& menu) {
147 if (is_open(menu))
148 menu.dispatch_event(event);
149 return IterationDecision::Continue;
150 });
151 }
152 }
153
154 return Core::Object::event(event);
155}
156
157void MenuManager::handle_mouse_event(MouseEvent& mouse_event)
158{
159 bool handled_menubar_event = false;
160 for_each_active_menubar_menu([&](Menu& menu) {
161 if (menu.rect_in_menubar().contains(mouse_event.position())) {
162 handle_menu_mouse_event(menu, mouse_event);
163 handled_menubar_event = true;
164 return IterationDecision::Break;
165 }
166 return IterationDecision::Continue;
167 });
168 if (handled_menubar_event)
169 return;
170
171 if (has_open_menu()) {
172 auto* topmost_menu = m_open_menu_stack.last().ptr();
173 ASSERT(topmost_menu);
174 auto* window = topmost_menu->menu_window();
175 if (!window) {
176 dbg() << "MenuManager::handle_mouse_event: No menu window";
177 return;
178 }
179 ASSERT(window->is_visible());
180
181 bool event_is_inside_current_menu = window->rect().contains(mouse_event.position());
182 if (event_is_inside_current_menu) {
183 WindowManager::the().set_hovered_window(window);
184 auto translated_event = mouse_event.translated(-window->position());
185 WindowManager::the().deliver_mouse_event(*window, translated_event);
186 return;
187 }
188
189 if (topmost_menu->hovered_item())
190 topmost_menu->clear_hovered_item();
191 if (mouse_event.type() == Event::MouseDown || mouse_event.type() == Event::MouseUp) {
192 auto* window_menu_of = topmost_menu->window_menu_of();
193 if (window_menu_of) {
194 bool event_is_inside_taskbar_button = window_menu_of->taskbar_rect().contains(mouse_event.position());
195 if (event_is_inside_taskbar_button && !topmost_menu->is_window_menu_open()) {
196 topmost_menu->set_window_menu_open(true);
197 return;
198 }
199 }
200
201 if (mouse_event.type() == Event::MouseDown) {
202 close_bar();
203 topmost_menu->set_window_menu_open(false);
204 }
205 }
206
207 if (mouse_event.type() == Event::MouseMove) {
208 for (auto& menu : m_open_menu_stack) {
209 if (!menu)
210 continue;
211 if (!menu->menu_window()->rect().contains(mouse_event.position()))
212 continue;
213 WindowManager::the().set_hovered_window(menu->menu_window());
214 auto translated_event = mouse_event.translated(-menu->menu_window()->position());
215 WindowManager::the().deliver_mouse_event(*menu->menu_window(), translated_event);
216 break;
217 }
218 }
219 return;
220 }
221
222 AppletManager::the().dispatch_event(static_cast<Event&>(mouse_event));
223}
224
225void MenuManager::handle_menu_mouse_event(Menu& menu, const MouseEvent& event)
226{
227 bool is_hover_with_any_menu_open = event.type() == MouseEvent::MouseMove
228 && has_open_menu()
229 && (m_open_menu_stack.first()->menubar() || m_open_menu_stack.first() == m_system_menu.ptr());
230 bool is_mousedown_with_left_button = event.type() == MouseEvent::MouseDown && event.button() == MouseButton::Left;
231 bool should_open_menu = &menu != m_current_menu && (is_hover_with_any_menu_open || is_mousedown_with_left_button);
232
233 if (is_mousedown_with_left_button)
234 m_bar_open = !m_bar_open;
235
236 if (should_open_menu && m_bar_open) {
237 open_menu(menu);
238 return;
239 }
240
241 if (!m_bar_open)
242 close_everyone();
243}
244
245void MenuManager::set_needs_window_resize()
246{
247 m_needs_window_resize = true;
248}
249
250void MenuManager::close_all_menus_from_client(Badge<ClientConnection>, ClientConnection& client)
251{
252 if (!has_open_menu())
253 return;
254 if (m_open_menu_stack.first()->client() != &client)
255 return;
256 close_everyone();
257}
258
259void MenuManager::close_everyone()
260{
261 for (auto& menu : m_open_menu_stack) {
262 if (menu && menu->menu_window())
263 menu->menu_window()->set_visible(false);
264 menu->clear_hovered_item();
265 }
266 m_open_menu_stack.clear();
267 m_current_menu = nullptr;
268 refresh();
269}
270
271void MenuManager::close_everyone_not_in_lineage(Menu& menu)
272{
273 Vector<Menu*> menus_to_close;
274 for (auto& open_menu : m_open_menu_stack) {
275 if (!open_menu)
276 continue;
277 if (&menu == open_menu.ptr() || open_menu->is_menu_ancestor_of(menu))
278 continue;
279 menus_to_close.append(open_menu);
280 }
281 close_menus(menus_to_close);
282}
283
284void MenuManager::close_menus(const Vector<Menu*>& menus)
285{
286 for (auto& menu : menus) {
287 if (menu == m_current_menu)
288 m_current_menu = nullptr;
289 if (menu->menu_window())
290 menu->menu_window()->set_visible(false);
291 menu->clear_hovered_item();
292 m_open_menu_stack.remove_first_matching([&](auto& entry) {
293 return entry == menu;
294 });
295 }
296 refresh();
297}
298
299static void collect_menu_subtree(Menu& menu, Vector<Menu*>& menus)
300{
301 menus.append(&menu);
302 for (int i = 0; i < menu.item_count(); ++i) {
303 auto& item = menu.item(i);
304 if (!item.is_submenu())
305 continue;
306 collect_menu_subtree(*const_cast<MenuItem&>(item).submenu(), menus);
307 }
308}
309
310void MenuManager::close_menu_and_descendants(Menu& menu)
311{
312 Vector<Menu*> menus_to_close;
313 collect_menu_subtree(menu, menus_to_close);
314 close_menus(menus_to_close);
315}
316
317void MenuManager::toggle_menu(Menu& menu)
318{
319 if (is_open(menu)) {
320 close_menu_and_descendants(menu);
321 return;
322 }
323 open_menu(menu);
324}
325
326void MenuManager::open_menu(Menu& menu)
327{
328 if (is_open(menu))
329 return;
330 if (!menu.is_empty()) {
331 menu.redraw_if_theme_changed();
332 auto& menu_window = menu.ensure_menu_window();
333 menu_window.move_to({ menu.rect_in_menubar().x(), menu.rect_in_menubar().bottom() + 2 });
334 menu_window.set_visible(true);
335 }
336 set_current_menu(&menu);
337 refresh();
338}
339
340void MenuManager::set_current_menu(Menu* menu, bool is_submenu)
341{
342 if (menu == m_current_menu)
343 return;
344
345 if (!is_submenu) {
346 if (menu)
347 close_everyone_not_in_lineage(*menu);
348 else
349 close_everyone();
350 }
351
352 if (!menu) {
353 m_current_menu = nullptr;
354 return;
355 }
356
357 m_current_menu = menu->make_weak_ptr();
358 if (m_open_menu_stack.find([menu](auto& other) { return menu == other.ptr(); }).is_end())
359 m_open_menu_stack.append(menu->make_weak_ptr());
360}
361
362void MenuManager::close_bar()
363{
364 close_everyone();
365 m_bar_open = false;
366}
367
368Gfx::Rect MenuManager::menubar_rect() const
369{
370 return { 0, 0, Screen::the().rect().width(), 18 };
371}
372
373void MenuManager::set_current_menubar(MenuBar* menubar)
374{
375 if (menubar)
376 m_current_menubar = menubar->make_weak_ptr();
377 else
378 m_current_menubar = nullptr;
379#ifdef DEBUG_MENUS
380 dbg() << "[WM] Current menubar is now " << menubar;
381#endif
382 Gfx::Point next_menu_location { MenuManager::menubar_menu_margin() / 2, 0 };
383 for_each_active_menubar_menu([&](Menu& menu) {
384 int text_width = menu.title_font().width(menu.name());
385 menu.set_rect_in_menubar({ next_menu_location.x() - MenuManager::menubar_menu_margin() / 2, 0, text_width + MenuManager::menubar_menu_margin(), menubar_rect().height() - 1 });
386 menu.set_text_rect_in_menubar({ next_menu_location, { text_width, menubar_rect().height() } });
387 next_menu_location.move_by(menu.rect_in_menubar().width(), 0);
388 return IterationDecision::Continue;
389 });
390 refresh();
391}
392
393void MenuManager::close_menubar(MenuBar& menubar)
394{
395 if (current_menubar() == &menubar)
396 set_current_menubar(nullptr);
397}
398
399void MenuManager::set_system_menu(Menu& menu)
400{
401 m_system_menu = menu.make_weak_ptr();
402 set_current_menubar(m_current_menubar);
403}
404
405void MenuManager::did_change_theme()
406{
407 ++m_theme_index;
408 refresh();
409}
410
411}