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
77const Gfx::Font& MenuManager::menu_font() const
78{
79 return Gfx::Font::default_font();
80}
81
82const Gfx::Font& MenuManager::app_menu_font() const
83{
84 return Gfx::Font::default_bold_font();
85}
86
87void MenuManager::draw()
88{
89 auto& wm = WindowManager::the();
90 auto palette = wm.palette();
91 auto menubar_rect = this->menubar_rect();
92
93 if (m_needs_window_resize) {
94 m_window->set_rect(menubar_rect);
95 AppletManager::the().calculate_applet_rects(window());
96 m_needs_window_resize = false;
97 }
98
99 Gfx::Painter painter(*window().backing_store());
100
101 painter.fill_rect(menubar_rect, palette.window());
102 painter.draw_line({ 0, menubar_rect.bottom() }, { menubar_rect.right(), menubar_rect.bottom() }, palette.threed_shadow1());
103 int index = 0;
104 for_each_active_menubar_menu([&](Menu& menu) {
105 Color text_color = palette.window_text();
106 if (is_open(menu)) {
107 painter.fill_rect(menu.rect_in_menubar(), palette.menu_selection());
108 painter.draw_rect(menu.rect_in_menubar(), palette.menu_selection().darkened());
109 text_color = palette.menu_selection_text();
110 }
111 painter.draw_text(
112 menu.text_rect_in_menubar(),
113 menu.name(),
114 index == 1 ? app_menu_font() : menu_font(),
115 Gfx::TextAlignment::CenterLeft,
116 text_color);
117 ++index;
118 return IterationDecision::Continue;
119 });
120
121 AppletManager::the().draw();
122}
123
124void MenuManager::refresh()
125{
126 if (!m_window)
127 return;
128 draw();
129 window().invalidate();
130}
131
132void MenuManager::event(Core::Event& event)
133{
134 if (WindowManager::the().active_window_is_modal())
135 return Core::Object::event(event);
136
137 if (static_cast<Event&>(event).is_mouse_event()) {
138 handle_mouse_event(static_cast<MouseEvent&>(event));
139 return;
140 }
141
142 if (static_cast<Event&>(event).is_key_event()) {
143 auto& key_event = static_cast<const KeyEvent&>(event);
144
145 if (key_event.type() == Event::KeyUp && key_event.key() == Key_Escape) {
146 close_everyone();
147 return;
148 }
149
150 if (event.type() == Event::KeyDown) {
151 for_each_active_menubar_menu([&](Menu& menu) {
152 if (is_open(menu))
153 menu.dispatch_event(event);
154 return IterationDecision::Continue;
155 });
156 }
157 }
158
159 return Core::Object::event(event);
160}
161
162void MenuManager::handle_mouse_event(MouseEvent& mouse_event)
163{
164 bool handled_menubar_event = false;
165 for_each_active_menubar_menu([&](Menu& menu) {
166 if (menu.rect_in_menubar().contains(mouse_event.position())) {
167 handle_menu_mouse_event(menu, mouse_event);
168 handled_menubar_event = true;
169 return IterationDecision::Break;
170 }
171 return IterationDecision::Continue;
172 });
173 if (handled_menubar_event)
174 return;
175
176 if (has_open_menu()) {
177 auto* topmost_menu = m_open_menu_stack.last().ptr();
178 ASSERT(topmost_menu);
179 auto* window = topmost_menu->menu_window();
180 ASSERT(window);
181 ASSERT(window->is_visible());
182
183 bool event_is_inside_current_menu = window->rect().contains(mouse_event.position());
184 if (event_is_inside_current_menu) {
185 WindowManager::the().set_hovered_window(window);
186 auto translated_event = mouse_event.translated(-window->position());
187 WindowManager::the().deliver_mouse_event(*window, translated_event);
188 return;
189 }
190
191 if (topmost_menu->hovered_item())
192 topmost_menu->clear_hovered_item();
193 if (mouse_event.type() == Event::MouseDown || mouse_event.type() == Event::MouseUp) {
194 auto* window_menu_of = topmost_menu->window_menu_of();
195 if (window_menu_of) {
196 bool event_is_inside_taskbar_button = window_menu_of->taskbar_rect().contains(mouse_event.position());
197 if (event_is_inside_taskbar_button && !topmost_menu->is_window_menu_open()) {
198 topmost_menu->set_window_menu_open(true);
199 return;
200 }
201 }
202
203 if (mouse_event.type() == Event::MouseDown) {
204 close_bar();
205 topmost_menu->set_window_menu_open(false);
206 }
207 }
208
209 if (mouse_event.type() == Event::MouseMove) {
210 for (auto& menu : m_open_menu_stack) {
211 if (!menu)
212 continue;
213 if (!menu->menu_window()->rect().contains(mouse_event.position()))
214 continue;
215 WindowManager::the().set_hovered_window(menu->menu_window());
216 auto translated_event = mouse_event.translated(-menu->menu_window()->position());
217 WindowManager::the().deliver_mouse_event(*menu->menu_window(), translated_event);
218 break;
219 }
220 }
221 return;
222 }
223
224 AppletManager::the().dispatch_event(static_cast<Event&>(mouse_event));
225}
226
227void MenuManager::handle_menu_mouse_event(Menu& menu, const MouseEvent& event)
228{
229 bool is_hover_with_any_menu_open = event.type() == MouseEvent::MouseMove
230 && has_open_menu()
231 && (m_open_menu_stack.first()->menubar() || m_open_menu_stack.first() == m_system_menu.ptr());
232 bool is_mousedown_with_left_button = event.type() == MouseEvent::MouseDown && event.button() == MouseButton::Left;
233 bool should_open_menu = &menu != m_current_menu && (is_hover_with_any_menu_open || is_mousedown_with_left_button);
234
235 if (is_mousedown_with_left_button)
236 m_bar_open = !m_bar_open;
237
238 if (should_open_menu && m_bar_open) {
239 open_menu(menu);
240 return;
241 }
242
243 if (!m_bar_open)
244 close_everyone();
245}
246
247void MenuManager::set_needs_window_resize()
248{
249 m_needs_window_resize = true;
250}
251
252void MenuManager::close_all_menus_from_client(Badge<ClientConnection>, ClientConnection& client)
253{
254 if (!has_open_menu())
255 return;
256 if (m_open_menu_stack.first()->client() != &client)
257 return;
258 close_everyone();
259}
260
261void MenuManager::close_everyone()
262{
263 for (auto& menu : m_open_menu_stack) {
264 if (menu && menu->menu_window())
265 menu->menu_window()->set_visible(false);
266 menu->clear_hovered_item();
267 }
268 m_open_menu_stack.clear();
269 m_current_menu = nullptr;
270 refresh();
271}
272
273void MenuManager::close_everyone_not_in_lineage(Menu& menu)
274{
275 Vector<Menu*> menus_to_close;
276 for (auto& open_menu : m_open_menu_stack) {
277 if (!open_menu)
278 continue;
279 if (&menu == open_menu.ptr() || open_menu->is_menu_ancestor_of(menu))
280 continue;
281 menus_to_close.append(open_menu);
282 }
283 close_menus(menus_to_close);
284}
285
286void MenuManager::close_menus(const Vector<Menu*>& menus)
287{
288 for (auto& menu : menus) {
289 if (menu == m_current_menu)
290 m_current_menu = nullptr;
291 if (menu->menu_window())
292 menu->menu_window()->set_visible(false);
293 menu->clear_hovered_item();
294 m_open_menu_stack.remove_first_matching([&](auto& entry) {
295 return entry == menu;
296 });
297 }
298 refresh();
299}
300
301static void collect_menu_subtree(Menu& menu, Vector<Menu*>& menus)
302{
303 menus.append(&menu);
304 for (int i = 0; i < menu.item_count(); ++i) {
305 auto& item = menu.item(i);
306 if (!item.is_submenu())
307 continue;
308 collect_menu_subtree(*const_cast<MenuItem&>(item).submenu(), menus);
309 }
310}
311
312void MenuManager::close_menu_and_descendants(Menu& menu)
313{
314 Vector<Menu*> menus_to_close;
315 collect_menu_subtree(menu, menus_to_close);
316 close_menus(menus_to_close);
317}
318
319void MenuManager::toggle_menu(Menu& menu)
320{
321 if (is_open(menu)) {
322 close_menu_and_descendants(menu);
323 return;
324 }
325 open_menu(menu);
326}
327
328void MenuManager::open_menu(Menu& menu)
329{
330 if (is_open(menu))
331 return;
332 if (!menu.is_empty()) {
333 menu.redraw_if_theme_changed();
334 auto& menu_window = menu.ensure_menu_window();
335 menu_window.move_to({ menu.rect_in_menubar().x(), menu.rect_in_menubar().bottom() + 2 });
336 menu_window.set_visible(true);
337 }
338 set_current_menu(&menu);
339 refresh();
340}
341
342void MenuManager::set_current_menu(Menu* menu, bool is_submenu)
343{
344 if (menu == m_current_menu)
345 return;
346
347 if (!is_submenu) {
348 if (menu)
349 close_everyone_not_in_lineage(*menu);
350 else
351 close_everyone();
352 }
353
354 if (!menu) {
355 m_current_menu = nullptr;
356 return;
357 }
358
359 m_current_menu = menu->make_weak_ptr();
360 if (m_open_menu_stack.find([menu](auto& other) { return menu == other.ptr(); }).is_end())
361 m_open_menu_stack.append(menu->make_weak_ptr());
362}
363
364void MenuManager::close_bar()
365{
366 close_everyone();
367 m_bar_open = false;
368}
369
370Gfx::Rect MenuManager::menubar_rect() const
371{
372 return { 0, 0, Screen::the().rect().width(), 18 };
373}
374
375void MenuManager::set_current_menubar(MenuBar* menubar)
376{
377 if (menubar)
378 m_current_menubar = menubar->make_weak_ptr();
379 else
380 m_current_menubar = nullptr;
381#ifdef DEBUG_MENUS
382 dbg() << "[WM] Current menubar is now " << menubar;
383#endif
384 Gfx::Point next_menu_location { MenuManager::menubar_menu_margin() / 2, 0 };
385 int index = 0;
386 for_each_active_menubar_menu([&](Menu& menu) {
387 int text_width = index == 1 ? Gfx::Font::default_bold_font().width(menu.name()) : Gfx::Font::default_font().width(menu.name());
388 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 });
389 menu.set_text_rect_in_menubar({ next_menu_location, { text_width, menubar_rect().height() } });
390 next_menu_location.move_by(menu.rect_in_menubar().width(), 0);
391 ++index;
392 return IterationDecision::Continue;
393 });
394 refresh();
395}
396
397void MenuManager::close_menubar(MenuBar& menubar)
398{
399 if (current_menubar() == &menubar)
400 set_current_menubar(nullptr);
401}
402
403void MenuManager::set_system_menu(Menu& menu)
404{
405 m_system_menu = menu.make_weak_ptr();
406 set_current_menubar(m_current_menubar);
407}
408
409void MenuManager::did_change_theme()
410{
411 ++m_theme_index;
412 refresh();
413}
414
415}